Developer reference: email / password login
This document covers only the Govern365 login flow (work email + password → JWT in client). It does not cover signup, invites, OAuth, or post-login app features.
Repository layout
- Web client:
gaicc-app/Clients - API server:
gaicc-app/Servers
UI surface
| Item | Location |
|---|---|
| Login page | gaicc-app/Clients/src/presentation/pages/Login.tsx |
| Route | path: "/login" in gaicc-app/Clients/src/App.tsx, wrapped in PublicOnlyRoute |
| Public gate | gaicc-app/Clients/src/presentation/components/PublicOnlyRoute.tsx — if auth.token is already set, user is redirected away from /login (onboarding, optional ?next=, or /dashboard) |
UX pattern (two steps, same as register)
- Email step:
Work emailinput, Continue → client validation. - Password step: read-only email + Change,
Passwordinput, Show/Hide password, link Forgot password? →Linkto/forgot-password, Sign in submits login mutation.
Not implemented on this screen
- Remember me — no checkbox or long-lived session flag in
Login.tsx(grep acrossClientsshows norememberusage). Persistence is via redux-persist +localStoragefor the auth slice (see below), not a separate "remember" control.
Visible cross-links
Forgot password?→/forgot-password(ForgotPassword.tsx).- Create an account →
navigate("/register")(button, not tested in login-only scope).
Client data flow
| Concern | Location |
|---|---|
| Login mutation | gaicc-app/Clients/src/application/queries/auth.queries.ts — useLogin |
| HTTP | gaicc-app/Clients/src/infrastructure/repositories/auth.repository.ts — login() → POST /api/auth/login |
| DTO | gaicc-app/Clients/src/domain/types/auth.types.ts — LoginDto |
| Auth state | useLogin onSuccess → setAuth({ token, user, workspaces }) in auth.queries.ts |
| Reducer | gaicc-app/Clients/src/application/slices/authSlice.ts |
| Persistence | gaicc-app/Clients/src/application/store.ts — redux-persist key govern365-root, whitelist: ["auth"], localStorage adapter |
| Rehydration gate | gaicc-app/Clients/src/main.tsx — PersistGate with loading={null} around <App /> |
| Session refresh after rehydrate | gaicc-app/Clients/src/presentation/components/AuthBootstrap.tsx — when token exists, calls GET /api/auth/session and setAuth with fresh token / presigned URLs |
HTTP client
gaicc-app/Clients/src/infrastructure/axiosInstance.ts— attachesAuthorization: Bearer <token>when present./api/auth/loginis listed as an "auth endpoint" so a 401 on login does not trigger the global interceptor that clears auth and redirects to/login(that path is for expired session on other APIs).
Success navigation (Login.tsx onSuccess)
Order of checks:
?next=— ifsearchParams.get("next")resolves to a safe internal path (safeAppInternalPath),navigate(next).- Onboarding —
onboardingStepfrom response (default3if missing). IfhubProvisionedand step< 1, treat step as1. Ifstep < 3, logic for onboarding is handled byRequireAuth/RequireOnboardingon protected routes; successful login still navigates to/dashboardwhenstep >= 3unless workspace rule applies. - No workspaces — if
step >= 3andworkspaces.length === 0, navigate to/create-workspace. - Else
/dashboard.
(Exact onboarding redirects for incomplete setup are also enforced when hitting protected routes; see RequireAuth.tsx.)
API
| Method | Path | Auth |
|---|---|---|
POST | /api/auth/login | None |
Controller — gaicc-app/Servers/src/controllers/auth.controller.ts
- Zod
loginSchema:email(trim, lower, valid email),password(min length 1). - Success:
res.json(result)(default 200). - Validation failure: 400
{ message: "Validation failed", errors }. - Service
UNAUTHORIZED: 401{ message }.
Service — gaicc-app/Servers/src/services/auth.service.ts login
- Normalize email; load
Userby email;bcrypt.comparepassword. - Bad user or bad password →
Invalid email or password(UNAUTHORIZED) — single message for unknown email vs wrong password. - If
deletedAtset (and not healed) →Account is deactivated(UNAUTHORIZED). - Success →
buildAuthResponse(user)(JWT, user payload, workspaces list) — same helper as register.
JWT
- Signed in
signJwt(auth.service.ts) withJWT_SECRET, payloaduserId,organizationId,role(JwtPayloadindomain.layer/interfaces/auth.interfaces.ts). - Expiry:
JWT_EXPIRES_INenv (default7din code).
Middleware — authenticateJWT in middleware/auth.middleware.ts for protected routes; login does not use it.
Related: forgot password (linked from login)
- UI:
gaicc-app/Clients/src/presentation/pages/ForgotPassword.tsx - API:
POST /api/auth/forgot-password→auth.service.tsforgotPassword— always returnsIf that email is registered, a reset link has been sent.to avoid email enumeration.
Session expiry
- JWT expiry is controlled by
JWT_EXPIRES_INon the server. After expiry, API calls return 401; the axios response interceptor clears auth and setswindow.location.href = "/login"for non-auth endpoints (seeaxiosInstance.ts).