§ unswayed-backend · Admin panel & RBAC (Phase 12)
Admin access hardening — CORS, email-domain allowlist & admin:create
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 verbsGET/HEAD/PUT/PATCH/POST/DELETE/OPTIONS, and the headersContent-Type, Authorization, Accept, X-Requested-With.Authorizationmatters 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_DOMAINS—rallitechnologies.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 likea@unswayed.com@evil.comcan't smuggle an approved domain past it; - exact (not suffix) match ⇒ look-alikes like
evil-unswayed.comorunswayed.com.evil.comare rejected.
- last-
IsAllowedAdminEmailDomain()— the class-validator decorator wrapping the predicate.IsAdminEmail()— the composite policy (IsString+IsEmail+MaxLength(100)+ the domain rule) applied toCreateAdminDto.emailandUpdateAdminDto.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/Adminunder theadminguard and links the assignment (idempotent). Note: theAdminrole currently carries no permissions — onlySuper Admin(which bypasses all permission checks inPermissionsGuard) 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 setsmustResetPassword: true, so the admin must rotate it on first login. The temp password is printed once. An explicit--passwordis used verbatim withmustResetPassword: 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.onlineAbiolaEmmanuel.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%).