CODEX // oaoisme wiki

§ unswayed-backend · Admin panel & RBAC (Phase 12)

Admin access hardening — CORS, email-domain allowlist & admin:create

updated 2026-06-21

This page explains three related changes that harden how admins reach and are created in the back-office: enabling CORS (so the browser dashboard can call the API at all), fencing admin emails to RALLi domains, and a CLI to bootstrap admin accounts.

1. CORS — why admin login showed a "CORS error"

The admin dashboard is a separate web app served from a different origin than the API. Browsers guard cross-origin requests with a preflight (OPTIONS) and require the response to carry Access-Control-Allow-* headers. The HTTP app never called enableCors()configureApp() only set the global /api prefix and trust proxy. So every cross-origin call (admin login included) failed the preflight, and the browser surfaced it as a generic "CORS error" even though the endpoint itself worked (a server-side curl login succeeded the whole time).

The fix — configureApp() + buildCorsOptions()

CORS is enabled in src/configure-app.ts, which runs in both main.ts and the e2e bootstrap (test/support.ts), so tests exercise the same contract production does.

app.enableCors(buildCorsOptions());

buildCorsOptions(env) is a pure, unit-tested function:

  • CORS_ORIGINS — optional, comma-separated list of exact origins (e.g. https://admin.rallitechnologies.online,https://app.unswayed.com). When set, only those origins are reflected.
  • Unset / blank ⇒ origin: true — reflect the request origin. A permissive default so any dashboard works out of the box; tighten later by setting the env var. No code change needed to lock down.
  • Always: credentials: true, the verbs GET/HEAD/PUT/PATCH/POST/DELETE/OPTIONS, and the headers Content-Type, Authorization, Accept, X-Requested-With. Authorization matters because the admin flow is bearer-token, not cookies.

CORS_ORIGINS is declared optional in env.validation.ts and documented in .env.example.

Ops gotcha: the running staging build predates this, so the fix only takes effect after a redeploy (build + restart). CORS being absent on the live build is the expected "before" state.

2. Admin email-domain allowlist

Back-office identities are staff-only, so admin account creation/edit is fenced to RALLi-controlled domains. (Login is not gated — existing admins on any domain keep working.)

src/admin/auth/dto/admin-email.decorator.ts exports:

  • ALLOWED_ADMIN_EMAIL_DOMAINSrallitechnologies.online, rallitechnologies.com, unswayed.com.
  • isAllowedAdminEmailDomain(email) — predicate used both inside the validation pipeline and off it (the CLI). It takes the substring after the last @, lower-cases it, and does an exact set match. Two deliberate properties:
    • last-@ ⇒ a crafted local part like a@unswayed.com@evil.com can't smuggle an approved domain past it;
    • exact (not suffix) match ⇒ look-alikes like evil-unswayed.com or unswayed.com.evil.com are rejected.
  • IsAllowedAdminEmailDomain() — the class-validator decorator wrapping the predicate.
  • IsAdminEmail() — the composite policy (IsString + IsEmail + MaxLength(100) + the domain rule) applied to CreateAdminDto.email and UpdateAdminDto.email.

A well-formed but off-domain email is now a keyed 422 { email: ['…approved RALLi domain…'] } at the wire.

3. admin:create CLI — bootstrapping admins

POST /api/admin/users requires an existing Super-Admin token — a chicken-and-egg for the first real admins. The admin:create command (src/cli/admin-create.{service,command}.ts, wired into CliModule) is the bootstrap path. It runs standalone (nest-commander), not in the HTTP app.

npx ts-node -r tsconfig-paths/register src/cli.ts admin:create \
  --email kemdi.ifeanyi@rallitechnologies.online \
  --name "Kemdi Ifeanyi" \
  [--role super-admin|admin]   # defaults to super-admin
  [--password '<explicit>']     # omit to auto-generate

What it guarantees:

  • Same fences as HTTP — rejects off-domain emails (isAllowedAdminEmailDomain) and weak explicit passwords (isStrongAdminPassword, the predicate form of the admin password policy).
  • Role — upserts Super Admin/Admin under the admin guard and links the assignment (idempotent). Note: the Admin role currently carries no permissions — only Super Admin (which bypasses all permission checks in PermissionsGuard) can actually operate the console.
  • Password — with no --password, it mints a 16-char password from a CSPRNG (crypto.randomInt, guaranteed to satisfy the policy) and sets mustResetPassword: true, so the admin must rotate it on first login. The temp password is printed once. An explicit --password is used verbatim with mustResetPassword: false.
  • Idempotent — an existing email is never recreated or repassworded; its role assignment is just re-ensured.

First two real admins

Created on staging via this command, both as Super Admin with forced first-login rotation, and both verified logging in against live staging:

  • kemdi.ifeanyi@rallitechnologies.online
  • AbiolaEmmanuel.Ojo@rallitechnologies.online

Tests

TDD throughout: unit specs for the validator, buildCorsOptions, and the CLI service/command; e2e for the CORS contract (test/cors.e2e-spec.ts) and the domain rule (test/admin-directory.e2e-spec.ts). The full npm run verify gate is green (coverage ~99%).