Skip to main content

Developer reference: self-serve sign-up

This document covers only the self-serve admin registration flow (email + password, new organization). It does not cover invite acceptance, Hub provisioning, or login.

Code locations in this repository

  • Web client: gaicc-app/Clients (not apps/web).
  • API server: gaicc-app/Servers (not apps/api).

User-facing surface

ItemLocation
Sign-up page componentgaicc-app/Clients/src/presentation/pages/RegisterAdmin.tsx
Routepath: "/register" in gaicc-app/Clients/src/App.tsx, wrapped in PublicOnlyRoute
Entry redirect from /gaicc-app/Clients/src/presentation/pages/CheckSetup.tsx calls GET /api/auth/check-setup and navigates to /register when firstRun === true

Related layout (not duplicate sign-up UI)

  • gaicc-app/Clients/src/presentation/components/PublicOnlyRoute.tsx — if auth.token is already set, redirects away from /register (onboarding, optional ?next=, or /dashboard).

Client architecture

UI and local validation

RegisterAdmin.tsx:

  • Two-step UI: "email" then "password".
  • Email step: non-empty + regex ^[^\s@]+@[^\s@]+\.[^\s@]+$.
  • Password step: same four rules as backend, evaluated in PASSWORD_RULES (length ≥ 8, [A-Za-z], [0-9], non-alphanumeric).
  • Submit calls React Query mutation from useRegisterAdmin() with { email: email.trim().toLowerCase(), password }.
  • Success: navigate("/onboarding/profile") (onboarding is out of scope here but is the immediate next route).

Data and state

ConcernLocation
Mutation hookgaicc-app/Clients/src/application/queries/auth.queries.tsuseRegisterAdmin
HTTP callgaicc-app/Clients/src/infrastructure/repositories/auth.repository.tsregisterAdmin()POST /api/auth/register/admin
DTO typegaicc-app/Clients/src/domain/types/auth.types.tsRegisterAdminDto
Auth state after successsetAuth({ token, user, workspaces }) in useRegisterAdmin onSuccess; reducer gaicc-app/Clients/src/application/slices/authSlice.ts
HTTP clientgaicc-app/Clients/src/infrastructure/axiosInstance.tsbaseURL from import.meta.env.VITE_API_URL ?? "http://localhost:3000"; attaches Authorization: Bearer when token exists. /api/auth/register/admin is listed among paths that do not trigger 401 → clearAuth + redirect to /login.
Error text for bannergetApiErrorMessage in gaicc-app/Clients/src/lib/apiError.ts — prefers response.data.message, else error.message, else fallback "Registration failed. Please try again.". Does not surface response.data.errors (Zod issues) in the banner.

API

Endpoint

  • Method / path: POST /api/auth/register/admin
  • Router: gaicc-app/Servers/src/routes/auth.routes.tsauthController.registerAdmin
  • Mount: app.use("/api", authRoutes) in gaicc-app/Servers/src/index.ts → full path /api/auth/register/admin

Request body (JSON)

FieldRules
emailString, trimmed, lowercased, valid email (Zod)
passwordstrongPassword in auth.controller.ts: min 8 chars, at least one letter, one digit, one non-alphanumeric

Responses

StatusWhen
201User + org created; body is auth payload (see below).
400registerAdminSchema.safeParse fails → { message: "Validation failed", errors: ZodIssue[] }.
409Service throws CONFLICT → e.g. { message: "An account with this email already exists" }.
500Unhandled service error or misconfiguration in error handler path → { message: "Internal server error" }.

Success body shape

Returned by buildAuthResponse in gaicc-app/Servers/src/services/auth.service.ts:

  • token — JWT string.
  • user — id, name, email, role, organizationId, avatarUrl (presigned or null), organization metadata, onboardingStep, hubProvisioned, etc.
  • workspaces — array of workspace summaries for all OrganizationMember rows for that user.

JWT payload type: gaicc-app/Servers/src/domain.layer/interfaces/auth.interfaces.tsJwtPayload: userId, organizationId, role. Signed with JWT_SECRET, expiry JWT_EXPIRES_IN (default 7d in signJwt).


Service logic (server)

Function: registerAdmin in gaicc-app/Servers/src/services/auth.service.ts

  1. Normalize email; prisma.user.findUnique({ where: { email } }) — if found, error An account with this email already exists with code CONFLICT.
  2. bcrypt.hash(password, 12) (SALT_ROUNDS = 12).
  3. Six-digit code: String(Math.floor(100000 + Math.random() * 900000)).
  4. prisma.$transaction:
    • Create Organizationname: "My Organization", slug: \org-$1778099587968`` (unique slug).
    • Create User — empty name, email, passwordHash, role: Admin, activeOrganizationId, onboardingStep: 0, emailVerifyCode, emailVerifyCodeSentAt: new Date().
    • Update Organization.createdByUserId to new user id.
    • Create OrganizationMember linking user to org as Admin.
  5. After transaction: enqueueEmailOrSend({ type: "verificationCode", payload: { to: normalizedEmail, code: verifyCode } }) from gaicc-app/Servers/src/jobs/publishEmailJob.ts.
    • Tries publishEmailJob (BullMQ). On enqueue failure, sendEmailDirect sends via sendVerificationCodeEmail in email.service.ts.
  6. logger.info("Admin registered", { userId }).
  7. Return buildAuthResponse(result.user, result.org).

Note: There is no check that the database is empty. Any unused email can create a new organization via this endpoint. check-setup / firstRun is client routing only from /.


Database (Prisma)

Models in gaicc-app/Servers/prisma/schema.prisma:

ModelSign-up writes
OrganizationInsert; then update createdByUserId
UserInsert
OrganizationMemberInsert (@@id([userId, organizationId]))

Email

  • Template: sendVerificationCodeEmail in gaicc-app/Servers/src/services/email.service.ts — subject Your Govern365 verification code, HTML includes the code and copy about workspace setup.
  • Email body states the code expires in 24 hours (marketing copy in HTML).
  • Application validation of the same code at workspace setup uses VERIFY_CODE_TTL_MS = 15 * 60 * 1000 (15 minutes) from emailVerifyCodeSentAt in auth.service.ts (setupOnboardingWorkspace). Developers should treat the 15-minute window as the enforced rule when entering the code in the app.

Controller error mapping

handleServiceError in gaicc-app/Servers/src/controllers/auth.controller.ts maps service errors by err.code:

  • VALIDATION → 400
  • UNAUTHORIZED → 401
  • FORBIDDEN → 403
  • CONFLICT → 409

registerAdmin primarily uses CONFLICT for duplicate email.


Testing pointers (signup only)

  • Client: component tests for RegisterAdmin step transitions, validation messages, mutation success navigation, and error banner for 409/400/500/network.
  • API: contract tests for POST /api/auth/register/admin — 201 shape, 409 duplicate email, 400 invalid Zod, idempotent behavior not guaranteed (second call with same email must 409 after first succeeds).