Back to archive
III. Platform, DevOps & Securityshowcasesoloclient anonymised

Admin v2 — Edge-RBAC + STRIDE Hardening

Production-grade multi-tenant admin panel (Restaurant chain (UZ) + Restaurant chain (UZ)) on Next.js 16 / React 19 over a legacy Laravel backend, with a strong security focus: edge-RBAC, TOTP MFA, audit log, strict CSP.

Status
active
Period
2026-05-20 → 2026-05-21
AI sessions
Stack
Languages
TypeScript
Frameworks · Infra
Next.js 16 (App RouterTurbopack)React 19better-authdrizzle-ormTailwind CSS v4shadcn/uiTanStack Query/Tablenext-safe-actionzod v4
§01

Overview

  • What it is: Hardened admin panel for two brands from a single codebase — branding and API base are picked at runtime via the TENANT env. Manages catalog (products, modifiers, categories, menu), sales and discount rules, locations (cities/organizations/terminals), marketing (Telegram broadcasts, templates, SMS, news, sliders), and system settings (roles, languages, order statuses, push events). The frontend is a wrapper over an existing Laravel API (OTP-by-phone auth, /api/v2/*).
  • Type / status / role: web-app · active (fresh, active development) · solo (Davron Yuldashev).
  • Activity window: 2026-05-20 → 2026-05-21. Very young: bootstrap scaffold + one large feat commit (237 files, +27,468 lines) on day one, then a security fix the next day.
§02

Stack

  • Languages: TypeScript (strict + noUncheckedIndexedAccess).
  • Frameworks/libraries: Next.js 16 (App Router, Turbopack), React 19.2, better-auth 1.6 (Drizzle adapter, SQLite session store), drizzle-orm + better-sqlite3, TanStack Query v5 + TanStack Table v8, next-safe-action v8 (type-safe server actions), react-hook-form + zod v4 resolvers, nuqs (URL state), shadcn/ui (new-york) + Tailwind v4 (CSS-first, no tailwind.config.js), lucide-react, sonner, @t3-oss/env-nextjs (env validation), hashids, input-otp, react-google-recaptcha-v3.
  • Infra/deploy: Bun (bun.lock lockfile, bun dev/build scripts), ESLint flat config + eslint-plugin-security. CI/hosting not pinned in the repo. @upstash/ratelimit + @upstash/redis are wired in but flagged as a stub (rate limiting not yet active).
  • Data: SQLite (local.db) — only for better-auth sessions/MFA/audit; business data lives in Laravel. Drizzle-kit for migrations.
  • Notable tooling: bun smoke — a custom smoke test of the Laravel client via --conditions react-server. README "banned dependencies": react-quill (XSS), next-auth (replaced by better-auth), axios (native fetch only).
§03

What was shipped

Timeline (git log, 3 commits):

  • Initial bootstrap — Next 16 + shadcn + better-auth scaffold (61 files, +5154). Author bootstrap@admin-v2-build.local (automated scaffolding tool).
  • `dfc8884` feat: auth + 22 dashboard pages + broadcasts parity — (2026-05-20, Davron Yuldashev). The bulk: 237 files, +27,468 / −181. This is where the entire core is built: OTP login via Laravel, TOTP MFA, a type-safe Laravel client with zod schemas across ~30 resources, 22+ CRUD dashboard pages, and the broadcasts feature (Telegram broadcasts) with full lifecycle.
  • `28d31e3` fix(proxy): close cookie-presence auth bypass — (2026-05-21, Davron Yuldashev). Security fix from a self-audit (finding C1): a forged session cookie with a junk payload was returning 200 on protected routes because middleware was "letting through" requests when getCookieCache → null. Fixed: cookie present + parse failed ⇒ redirect to /login + delete both cookies.
  • Volume: 3 commits over 2 days; a "zero-to-product" burst in one go, then targeted security work. Doesn't look abandoned — fresh, active.
§04

Technical challenges

All confirmed against diffs/files:

  • Edge-RBAC without DB in middleware (proxy.ts): roles are checked directly in the Edge runtime by reading the signed better-auth cookie cache (getCookieCache, HMAC over BETTER_AUTH_SECRET) — no SQLite hit. Implemented: per-request CSP-nonce generation, absolute 12-hour session cap (session.createdAt + 12h), admin-gated prefixes (/users, /configs, /admin), normalizing roles from both an array and a JSON string. → Demonstrates understanding of Edge runtime constraints and auth engineering.
  • Closing the auth bypass (commit 28d31e3): the classic "cookie present ⇒ let through" mistake, found by a self-driven STRIDE audit and fixed; RBAC and cap checks were moved out of the if (cache) branch after the early-return, since cache is now guaranteed non-null. → Real security mindset, not a checkbox.
  • TOTP MFA with at-rest secret encryption (lib/auth/mfa.ts): generates a 160-bit base32 secret (RFC 6238), encrypts with AES-GCM via symmetricEncrypt (key MFA_ENCRYPTION_KEY ⇒ fallback BETTER_AUTH_SECRET), otpauth:// URI, verification with a ±1 window (tolerant of ±30s skew), strict code normalization. → Knowledge of cryptography and TOTP standards.
  • DB-backed brute-force lockout (lib/auth/lockout.ts): 5 failed TOTP attempts in 15 min ⇒ 15-min lock; state stored in the user row (mfa_failed_attempts/mfa_locked_until), not memory — survives restarts and is consistent across workers. Atomic increment via sql\${col} + 1\` with .returning()`. Public errors are intentionally generic. → Production-grade rate limiting.
  • Type-safe Laravel client (lib/api/laravel.ts, 405 lines): a single entry point laravelFetch — centralizes structured JSON logging (never logs the token/body), bearer forwarding, the tenant X-Project-Code header, auto Idempotency-Key (UUID) on all mutations, AbortSignal + 15s timeout merging, status-to-typed-error mapping (LaravelUnauthorized/Forbidden/Validation/...), and optional zod response validation. Lazy dynamic-import of the session stack so skipAuth calls don't load the DB. → Excellent integration-layer architecture.
  • Quirks of integrating with legacy Laravel (lib/auth/laravel.ts, lib/api/decode.ts): the weird OTP contract is reproduced "as-is" (/api/keldi → base64 CSRF, /api/send_otp with a wtf header = recaptcha, /api/auth_otp → base64 JSON or the literal "false"). Helpers decode double-encoded base64 envelopes. → The ability to tame someone else's non-standard API without changing it.
  • Server-action factory (lib/forms/serverAction.ts): defineCrudAction({input, method, path, output, revalidate}) codifies the repeating CRUD pattern over next-safe-action; Laravel 422 errors are translated back into form field errors via returnValidationErrors; handleServerError collapses any non-Laravel errors into a generic string (no leaked stack traces). path can be a function of the validated input. → DRY + type safety as a systemic solution.
  • Multipart workaround (broadcasts/actions.ts): since laravelFetch is JSON-only, broadcast image uploads do a direct fetch with FormData, deliberately not setting Content-Type (so fetch sets the multipart boundary itself), while reusing the same auth/tenant headers. Client-side type/size validation (≤10 MB). Broadcast lifecycle (schedule/pause/resume/cancel/cancel-and-delete/reset-to-draft/duplicate) + live stats from Redis via polling.
§05

AI-assisted development

  • Sessions found: 0 — there is no ~/.claude/projects/*admin_v2* directory.
  • What was done with AI: both user commits carry Co-Authored-By: Claude Opus 4.7, so AI was used during development, but local Claude Code session logs for this repo weren't saved (possibly run from a different path/tool). The bootstrap commit itself is an automated scaffolding agent (bootstrap@admin-v2-build.local), which is also hinted at by AGENTS.md containing agent rules for Next 16.
  • AI workflow patterns: the agentic setup is visible indirectly (AGENTS.md, the "auth-builder finalizes schema" mention in README, references to a "security audit 2026-05-21" with finding codes) — structured work with AI agents and self-driven code review/audit. No direct traces.
§06

Achievements & metrics

  • Scale: 22+ dashboard pages (broadcasts, broadcast-templates, catalog, categories, cities, configs, delivery-pricings, events-system, langs, menu-items, menu-types, mobile-push-events, modifiers, news, order-statuses, organizations, products, roles, sales, sales-rules, sliders, sms-templates, terminals).
  • ~30 zod resource schemas in lib/api/schemas/** (catalog/sales/locations/people/marketing/settings).
  • +27,468 lines in the main feat commit in a single day.
  • Security: strict CSP with per-request nonce infrastructure, the full set of security headers (HSTS preload, COOP/CORP, X-Frame DENY, Permissions-Policy), STRIDE threat model in ARCHITECTURE.md (§6), append-only audit log, idempotency keys on mutations.
  • Multi-tenancy: a single codebase → 2 brands (Restaurant chain (UZ)/Restaurant chain (UZ)) via the TENANT env.
§07

Contributors

git shortlog · all branches

  1. Dave932
  2. bootstrap1
2 contributors3 commits total
Currently

Open to Senior / Staff engineering roles and selective freelance — production AI, platform, and full-stack work.

Get in touch