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(notapps/web). - API server:
gaicc-app/Servers(notapps/api).
User-facing surface
| Item | Location |
|---|---|
| Sign-up page component | gaicc-app/Clients/src/presentation/pages/RegisterAdmin.tsx |
| Route | path: "/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— ifauth.tokenis 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
| Concern | Location |
|---|---|
| Mutation hook | gaicc-app/Clients/src/application/queries/auth.queries.ts — useRegisterAdmin |
| HTTP call | gaicc-app/Clients/src/infrastructure/repositories/auth.repository.ts — registerAdmin() → POST /api/auth/register/admin |
| DTO type | gaicc-app/Clients/src/domain/types/auth.types.ts — RegisterAdminDto |
| Auth state after success | setAuth({ token, user, workspaces }) in useRegisterAdmin onSuccess; reducer gaicc-app/Clients/src/application/slices/authSlice.ts |
| HTTP client | gaicc-app/Clients/src/infrastructure/axiosInstance.ts — baseURL 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 banner | getApiErrorMessage 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.ts→authController.registerAdmin - Mount:
app.use("/api", authRoutes)ingaicc-app/Servers/src/index.ts→ full path/api/auth/register/admin
Request body (JSON)
| Field | Rules |
|---|---|
email | String, trimmed, lowercased, valid email (Zod) |
password | strongPassword in auth.controller.ts: min 8 chars, at least one letter, one digit, one non-alphanumeric |
Responses
| Status | When |
|---|---|
| 201 | User + org created; body is auth payload (see below). |
| 400 | registerAdminSchema.safeParse fails → { message: "Validation failed", errors: ZodIssue[] }. |
| 409 | Service throws CONFLICT → e.g. { message: "An account with this email already exists" }. |
| 500 | Unhandled 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 allOrganizationMemberrows for that user.
JWT payload type: gaicc-app/Servers/src/domain.layer/interfaces/auth.interfaces.ts — JwtPayload: 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
- Normalize email;
prisma.user.findUnique({ where: { email } })— if found, errorAn account with this email already existswith codeCONFLICT. bcrypt.hash(password, 12)(SALT_ROUNDS = 12).- Six-digit code:
String(Math.floor(100000 + Math.random() * 900000)). prisma.$transaction:- Create
Organization—name: "My Organization",slug: \org-$1778099587968`` (unique slug). - Create
User— emptyname, email,passwordHash,role: Admin,activeOrganizationId,onboardingStep: 0,emailVerifyCode,emailVerifyCodeSentAt: new Date(). - Update
Organization.createdByUserIdto new user id. - Create
OrganizationMemberlinking user to org asAdmin.
- Create
- After transaction:
enqueueEmailOrSend({ type: "verificationCode", payload: { to: normalizedEmail, code: verifyCode } })fromgaicc-app/Servers/src/jobs/publishEmailJob.ts.- Tries
publishEmailJob(BullMQ). On enqueue failure,sendEmailDirectsends viasendVerificationCodeEmailinemail.service.ts.
- Tries
logger.info("Admin registered", { userId }).- 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:
| Model | Sign-up writes |
|---|---|
Organization | Insert; then update createdByUserId |
User | Insert |
OrganizationMember | Insert (@@id([userId, organizationId])) |
Email
- Template:
sendVerificationCodeEmailingaicc-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) fromemailVerifyCodeSentAtinauth.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→ 400UNAUTHORIZED→ 401FORBIDDEN→ 403CONFLICT→ 409
registerAdmin primarily uses CONFLICT for duplicate email.
Testing pointers (signup only)
- Client: component tests for
RegisterAdminstep 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).