# JWT authentication

Auth types are imported directly from the shared subpath:

    import type { AuthUser, LoginRequest, AuthResponse } from "@preventive-maintenance/shared/auth";

packages/shared/auth.ts exports the types itself. No auth re-export from
index.ts is needed. The package exports ./auth and builds each source file;
pnpm dev watches shared automatically. API TypeScript uses Node16 module
resolution to read package exports while remaining CommonJS.

## Endpoints and behavior

- POST /api/v1/auth/login: email, password, optional rememberMe.
- GET /api/v1/auth/me: current user or 401.
- POST /api/v1/auth/logout: clear the JWT cookie.

The backend signs and verifies HS256 JWTs with jsonwebtoken. Verification checks the
signature, algorithm, issuer, audience, subject, issued-at, expiry, and JWT ID.
Each protected request also reads the current User so inactive/deleted
accounts are rejected and role changes take effect.

JWTs are stored in an HttpOnly, SameSite=Lax cookie named pm_access_token,
with Path=/api and Secure in production. The JSON response contains safe
user fields only. Tokens and password hashes are not stored in localStorage.
Passwords remain salted scrypt hashes.

Default validity is 1 hour, or 24 hours with Remember me. Without Remember me
the browser cookie is non-persistent; the JWT still expires after 1 hour.
There is no refresh token and no Session database lookup or write.

Logout clears the browser cookie. A copied JWT remains valid until expiry;
there is no individual token revocation without server-side state. Rotating
JWT_SECRET invalidates all JWTs. Disabling/deleting a user denies access even
before expiry. Re-enabling that user permits their unexpired tokens again.

## Database schema

Settings now provides atomic profile/password updates at PATCH /api/v1/settings/profile.
An optional password change requires the current password and matching new password
confirmation (8-128 characters). Existing JWTs are not revoked by password changes.
ADMIN can create accounts with an initial password and manage roles/active status
through /api/v1/settings/users. Initial passwords are communicated outside the app;
there is no email invitation or administrator password-reset endpoint. Accounts are
deactivated rather than deleted. Self-deactivation/role changes and removal of the
last active ADMIN are rejected. See apps/api/src/services/settings.md.

The frontend uses the auth service for login, route guards, and sign-out.
The Prisma schema no longer contains Session or User.sessions.
No database migration was run by the assistant. For a fresh database, use
this schema for the initial migration. For an existing database, review the
generated migration before dropping the former Session table and its data.
## Environment

The API requires JWT_SECRET: a random 64-character hexadecimal string.
For each environment, generate a separate value and set JWT_SECRET in
apps/api/.env or your deployment secret configuration:

    node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))"

apps/api/.env.sample documents the other settings:

    APP_ORIGIN=http://localhost:3000
    JWT_ISSUER=preventive-maintenance-api
    JWT_AUDIENCE=preventive-maintenance-app
    JWT_TTL_SECONDS=3600
    JWT_REMEMBER_TTL_SECONDS=86400

apps/app/.env:

    VITE_API_BASE_URL=
    API_PROXY_TARGET=http://localhost:5000

VITE_API_BASE_URL is the API origin (for example http://localhost:5000), not a versioned path. Leave it empty for the same-origin Vite /api proxy; the client appends /api/v1. Do not set it to /api/v1 or that path will be duplicated.

Use localhost consistently and restart servers after environment changes.
Keep the JWT secret exclusively on the API; never use a VITE_ variable for it.

## Database and first admin

Configure DATABASE_URL for MySQL and create the development database first.
For a fresh development database, run:

    pnpm --filter express-api-starter-ts exec prisma migrate dev --name init
    pnpm prisma:generate

There is no default password or public registration endpoint. From root in
PowerShell:

    $env:AUTH_ADMIN_EMAIL = Read-Host 'Admin email'
    $env:AUTH_ADMIN_NAME = Read-Host 'Admin name'
    $secret = Read-Host 'Admin password (8-128 characters)' -AsSecureString
    $credential = New-Object System.Management.Automation.PSCredential('admin', $secret)
    try {
      $env:AUTH_ADMIN_PASSWORD = $credential.GetNetworkCredential().Password
      pnpm prisma:seed
    } finally {
      Remove-Item Env:AUTH_ADMIN_PASSWORD -ErrorAction SilentlyContinue
      Remove-Item Env:AUTH_ADMIN_EMAIL -ErrorAction SilentlyContinue
      Remove-Item Env:AUTH_ADMIN_NAME -ErrorAction SilentlyContinue
      $credential = $null
      $secret.Dispose()
    }

The seeder creates an active ADMIN only when the email does not exist.
Existing names, passwords, roles, and active status remain unchanged.
Seeders are in apps/api/prisma/seeders and registered in prisma.config.ts.
Run pnpm prisma:seed explicitly after migrations. Start with pnpm dev.

## Deployment and verification

Use HTTPS and same-site frontend/API hosts, for example app.example.com and
api.example.com. Set APP_ORIGIN to the exact frontend origin. Auth endpoints
check browser origins and are not cached. The login limiter allows 10 failed
attempts per IP per 15 minutes; multiple API instances need a shared store.
Configure trust proxy only for your actual proxy topology.

Use requireAuth on future private API routes and add role checks as needed.
Frontend route guards do not replace backend authorization; demo endpoints
remain public.

Verification commands:

    pnpm typecheck
    pnpm build
    pnpm test
    pnpm --filter express-api-starter-ts test:docs

Tests use Prisma fixtures but exercise real password hashing, JWT verification, HTTP endpoints and the frontend Axios auth service. The frontend HTTP integration test checks login/current-user/logout and Indonesian validation against the changed API on a temporary local port. It emulates browser cookie storage in Node. This is not a browser automation or live database test; no migration is required.

JWT library documentation: https://github.com/auth0/node-jsonwebtoken


## Coordinated envelope release and rollback

AuthResponse remains the shared payload { user }. Import ApiSuccess,
ApiFailure or ApiResponse from @preventive-maintenance/shared for HTTP bodies.

| Contract | Before | After |
| --- | --- | --- |
| Login/current-user success | { user } | { success: true, data: { user }, meta: { requestId } } |
| Failure | { message } | { success: false, error: { code, message, details? }, meta: { requestId } } |
| Invalid login fields | 400 | 422 VALIDATION_ERROR with safe field details |
| Logout success | 204 with no body | 204 with no body |

Axios callers now read response.data.data.user. AuthServiceError preserves
stable code/status and optional safe details; display error.message, but
never branch on Indonesian message text. Current-user 401 returns null.
Network and malformed-response fallbacks are Indonesian.

Treat the API, shared types and frontend consumer as one release unit.
Do not release only the API or only the frontend: old/new success decoding
is incompatible. Build and verify the three together, then switch the API
and frontend artifacts together (or keep the app unavailable during the
transition). Account for already-open clients and cached bundles by requiring
a reload/version refresh during the cutover.

Rollback must restore the matching API and frontend artifacts and shared
contract revision together. Rolling back only one side is prohibited.
This envelope change has no database migration or database rollback.
Cookie name, security attributes, token/account checks, origin protection,
no-store and bodyless logout remain the same.

Before release, run the commands above and review the scenario/validation
record under apps/api/openspec/changes/establish-core-api-architecture/verification.md.
