--- phase: 02-authentication-multi-tenancy plan: 01 type: execute wave: 1 depends_on: [] files_modified: - apps/api/prisma/schema.prisma - apps/api/prisma/migrations/* - apps/api/src/prisma/prisma.module.ts - apps/api/src/prisma/prisma.service.ts - apps/api/src/prisma/prisma-tenant.extension.ts - apps/api/src/auth/auth.module.ts - apps/api/src/auth/auth.controller.ts - apps/api/src/auth/auth.service.ts - apps/api/src/auth/strategies/local.strategy.ts - apps/api/src/auth/strategies/jwt.strategy.ts - apps/api/src/auth/guards/jwt-auth.guard.ts - apps/api/src/auth/guards/roles.guard.ts - apps/api/src/auth/decorators/public.decorator.ts - apps/api/src/auth/decorators/roles.decorator.ts - apps/api/src/auth/decorators/current-user.decorator.ts - apps/api/src/auth/dto/login.dto.ts - apps/api/src/user/user.module.ts - apps/api/src/user/user.service.ts - apps/api/src/user/admin-seed.service.ts - apps/api/src/tenant/tenant.module.ts - apps/api/src/tenant/tenant.service.ts - apps/api/src/tenant/tenant.middleware.ts - apps/api/src/app.module.ts - apps/api/src/main.ts - apps/api/package.json - docker-compose.yml - docker-compose.dev.yml autonomous: true requirements: - AUTH-01 - AUTH-03 - AUTH-04 - AUTH-05 - TNNT-01 - TNNT-03 must_haves: truths: - "Initial admin account is created from Docker ENV on first startup (D-05, D-07, D-13)" - "POST /auth/login with valid credentials returns httpOnly JWT cookie with 30-day expiry (D-02)" - "POST /auth/logout clears the session cookie" - "All non-public API routes reject requests without valid JWT (returns 401)" - "RoleGuard enforces SUPER_ADMIN / ADMIN / USER access levels on protected routes (D-12)" - "Every tenant-scoped database query is filtered by RLS using app.current_tenant session variable (D-11)" - "Tenant ID is extracted from JWT claim and set per request via TenantMiddleware (D-08)" artifacts: - path: "apps/api/prisma/schema.prisma" provides: "User, Tenant, PasswordResetToken, LdapConfig, LdapFieldMapping models with Role enum" contains: "model User" - path: "apps/api/src/auth/auth.service.ts" provides: "Login validation, JWT issuance, cookie management" exports: ["AuthService"] - path: "apps/api/src/prisma/prisma-tenant.extension.ts" provides: "Prisma Client Extension that sets app.current_tenant per query" exports: ["forTenant"] - path: "apps/api/src/tenant/tenant.middleware.ts" provides: "Extracts tenantId from JWT, creates tenant-scoped Prisma client" exports: ["TenantMiddleware"] - path: "apps/api/src/user/admin-seed.service.ts" provides: "Creates initial Super-Admin from ENV on bootstrap" exports: ["AdminSeedService"] key_links: - from: "apps/api/src/auth/strategies/jwt.strategy.ts" to: "apps/api/src/auth/guards/jwt-auth.guard.ts" via: "Passport JWT strategy registered as global APP_GUARD" pattern: "APP_GUARD.*JwtAuthGuard" - from: "apps/api/src/tenant/tenant.middleware.ts" to: "apps/api/src/prisma/prisma-tenant.extension.ts" via: "Middleware calls forTenant() to create scoped client" pattern: "forTenant" - from: "apps/api/src/user/admin-seed.service.ts" to: "apps/api/src/prisma/prisma.service.ts" via: "onApplicationBootstrap lifecycle hook creates admin" pattern: "onApplicationBootstrap" --- Backend authentication foundation: Prisma schema with User/Tenant/Role models, JWT httpOnly cookie auth via Passport strategies, global AuthGuard + RoleGuard, RLS tenant isolation via Prisma Client Extension, TenantMiddleware for per-request tenant context, and initial Super-Admin seed from Docker ENV. This plan delivers AUTH-01 (admin seed per D-05/D-07/D-13), AUTH-03 (login/logout), AUTH-04 (30-day session per D-02), AUTH-05 (RBAC per D-12), TNNT-01 (RLS per D-11), TNNT-03 (tenant per request per D-08). Purpose: Establish the entire backend auth and multi-tenancy infrastructure that all subsequent plans build on. Without this, no frontend auth or user management is possible. Output: Working API with login/logout endpoints, global JWT protection, role-based guards, tenant-scoped database queries, and auto-seeded Super-Admin account. @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/02-authentication-multi-tenancy/02-CONTEXT.md @.planning/phases/02-authentication-multi-tenancy/02-RESEARCH.md @.planning/phases/01-foundation-portal-shell/01-01-SUMMARY.md @apps/api/prisma/schema.prisma @apps/api/src/app.module.ts @apps/api/src/main.ts @apps/api/src/health/health.controller.ts @apps/api/package.json @docker-compose.yml ## Artifacts this phase produces | Artifact | Plan | Purpose | |----------|------|---------| | Expanded Prisma schema (User, Tenant, Role, PasswordResetToken, LdapConfig, LdapFieldMapping) | 02-01 | Database foundation for all auth/tenant features | | RLS migration SQL (policies, app role, current_tenant_id function) | 02-01 | Tenant data isolation at database level | | PrismaModule + PrismaService (singleton) | 02-01 | Shared database access across all modules | | AuthModule (Passport local + JWT strategies, guards, decorators) | 02-01 | Login/logout, JWT cookie auth, RBAC | | TenantModule (middleware, service) | 02-01 | Per-request tenant context injection | | UserModule (service, admin seed) | 02-01 | User operations and initial admin creation | | Login page with split-screen layout | 02-02 | User-facing login UI per D-01 | | Next.js middleware for route protection | 02-02 | Frontend auth gate per D-04 | | User CRUD endpoints + frontend admin pages | 02-02 | User management per AUTH-02 | | Tenant CRUD endpoints + admin pages | 02-02 | Tenant management per TNNT-02 | | Auth-wired header and sidebar-footer | 02-02 | Show logged-in user info | | MailModule + password reset flow | 02-03 | Self-service password reset per D-03 | | Force-password-change interceptor | 02-03 | Per D-06 | | LDAP sync service + config UI | 02-04 | LDAP import per AUTH-06, D-14..D-18 | Task 1: Prisma schema expansion, RLS migration, and PrismaModule - apps/api/prisma/schema.prisma (current Tenant-only schema) - apps/api/src/app.module.ts (current module imports) - apps/api/package.json (current dependencies) - .planning/phases/02-authentication-multi-tenancy/02-RESEARCH.md (Prisma schema section, RLS SQL section, Package Legitimacy Audit) apps/api/prisma/schema.prisma apps/api/prisma/migrations/[timestamp]_auth_multi_tenancy/migration.sql apps/api/src/prisma/prisma.module.ts apps/api/src/prisma/prisma.service.ts apps/api/src/prisma/prisma-tenant.extension.ts apps/api/package.json docker-compose.yml docker-compose.dev.yml Install backend auth dependencies per RESEARCH.md standard stack. In apps/api run: pnpm add @nestjs/jwt @nestjs/passport passport passport-jwt passport-local argon2 class-validator class-transformer pnpm add -D @types/passport-jwt @types/passport-local NOTE: Do NOT install ldapts, @nestjs-modules/mailer, or nodemailer yet -- those are SUS-flagged packages that require human verification in Plan 02-03. Expand apps/api/prisma/schema.prisma with the full Phase 2 data model per RESEARCH.md code example: - Role enum with SUPER_ADMIN, ADMIN, USER values - Expand existing Tenant model: add isActive Boolean default true, add users User[] relation, add ldapConfig LdapConfig? relation - User model with fields: id (uuid), username (unique), email (unique), passwordHash (String optional for LDAP users per A6), displayName (optional), role (Role default USER), isActive (Boolean default true), mustChangePassword (Boolean default false per D-06), ldapDn (optional), tenantId (String), tenant relation, createdAt, updatedAt, lastLoginAt (optional), passwordResetTokens relation. Indexes on tenantId, username, email. - PasswordResetToken model: id (uuid), token (unique), userId, user relation with onDelete Cascade, expiresAt, usedAt (optional), createdAt - LdapConfig model: id (uuid), tenantId (unique), tenant relation, serverUrl, baseDn, bindDn, bindPassword, searchFilter (default "(objectClass=person)"), syncIntervalMin (Int default 60), isActive (Boolean default true), lastSyncAt (optional), createdAt, updatedAt, fieldMappings relation - LdapFieldMapping model: id (uuid), ldapConfigId, ldapConfig relation with onDelete Cascade, ldapField, tesseraField, isDefault (Boolean default false), createdAt. Unique constraint on [ldapConfigId, ldapField]. Add TESSERA_ADMIN_USER, TESSERA_ADMIN_EMAIL, TESSERA_ADMIN_PASSWORD, TESSERA_FORCE_CHANGE, and JWT_SECRET environment variables to docker-compose.yml api service per D-05. Set JWT_SECRET to a development-only value like "tessera-dev-jwt-secret-change-in-production". Add TESSERA_ADMIN_USER=admin, TESSERA_ADMIN_EMAIL=admin@tessera.local, TESSERA_ADMIN_PASSWORD=admin123 as defaults. Add TESSERA_FORCE_CHANGE=false as default. Run prisma migrate dev --name auth_multi_tenancy to generate migration. After migration is generated, create a SECOND manual SQL migration for RLS setup. Create a new migration directory manually (prisma/migrations/[timestamp]_rls_policies/migration.sql) with: - CREATE OR REPLACE FUNCTION current_tenant_id() that returns current_setting('app.current_tenant', true) - ALTER TABLE "User" ENABLE ROW LEVEL SECURITY and FORCE ROW LEVEL SECURITY - CREATE POLICY tenant_isolation_policy ON "User" USING (tenant_id = current_tenant_id()::uuid) -- note: Prisma maps tenantId to "tenantId" column, verify the actual column name from the generated migration and use that exact name - Same RLS for PasswordResetToken (via user join or direct tenant_id -- use the approach that matches schema) - Same RLS for LdapConfig and LdapFieldMapping (via tenantId) IMPORTANT for RLS SQL: Use parameterized set_config, never string interpolation per RESEARCH.md anti-pattern guidance. The current_tenant_id() function uses current_setting which is safe. Create apps/api/src/prisma/prisma.module.ts as a Global NestJS module exporting PrismaService. Create apps/api/src/prisma/prisma.service.ts extending PrismaClient, implementing OnModuleInit (call this.$connect in onModuleInit). Register as injectable singleton. Create apps/api/src/prisma/prisma-tenant.extension.ts with the forTenant function per RESEARCH.md Pattern 2: accepts PrismaClient and tenantId string, returns extended client that wraps $allOperations in a transaction calling SET app.current_tenant via $executeRawUnsafe with set_config. Use parameterized query: SELECT set_config('app.current_tenant', $1, true) with the tenantId as parameter to avoid SQL injection. cd /home/vicolab/projects/tessera-ctl/apps/api && npx prisma validate && npx prisma migrate status - schema.prisma contains all 6 models (Tenant, User, Role enum, PasswordResetToken, LdapConfig, LdapFieldMapping) - Migration files exist under prisma/migrations/ - RLS migration SQL contains ENABLE ROW LEVEL SECURITY for User table - PrismaModule is @Global() and exports PrismaService - prisma-tenant.extension.ts exports forTenant function - docker-compose.yml api service has TESSERA_ADMIN_USER, TESSERA_ADMIN_EMAIL, TESSERA_ADMIN_PASSWORD, JWT_SECRET env vars Prisma schema has all Phase 2 models, migrations are generated, RLS policies are in migration SQL, PrismaModule/PrismaService/forTenant extension exist, Docker env vars configured. Task 2: AuthModule with Passport strategies, guards, and decorators - apps/api/src/prisma/prisma.module.ts (from Task 1) - apps/api/src/prisma/prisma.service.ts (from Task 1) - apps/api/prisma/schema.prisma (expanded schema from Task 1) - .planning/phases/02-authentication-multi-tenancy/02-RESEARCH.md (Pattern 1: JWT auth, Pattern 3: Global guard, security domain) apps/api/src/auth/auth.module.ts apps/api/src/auth/auth.controller.ts apps/api/src/auth/auth.service.ts apps/api/src/auth/strategies/local.strategy.ts apps/api/src/auth/strategies/jwt.strategy.ts apps/api/src/auth/guards/jwt-auth.guard.ts apps/api/src/auth/guards/roles.guard.ts apps/api/src/auth/decorators/public.decorator.ts apps/api/src/auth/decorators/roles.decorator.ts apps/api/src/auth/decorators/current-user.decorator.ts apps/api/src/auth/dto/login.dto.ts apps/api/src/main.ts Create the AuthModule per RESEARCH.md architecture: auth/dto/login.dto.ts: LoginDto class with username (string, @IsNotEmpty) and password (string, @IsNotEmpty) validated via class-validator decorators. auth/decorators/public.decorator.ts: Export IS_PUBLIC_KEY constant and Public() decorator using SetMetadata per RESEARCH.md Pattern 3. auth/decorators/roles.decorator.ts: Export ROLES_KEY constant and Roles(...roles: Role[]) decorator using SetMetadata. Import Role enum from @prisma/client. auth/decorators/current-user.decorator.ts: Export CurrentUser parameter decorator using createParamDecorator that extracts user from request object. auth/strategies/local.strategy.ts: PassportLocalStrategy extending PassportStrategy(Strategy) from passport-local. The validate method receives username and password, calls AuthService.validateUser, throws UnauthorizedException if null. auth/strategies/jwt.strategy.ts: JwtStrategy extending PassportStrategy(Strategy) from passport-jwt. Configure to extract JWT from cookie named "session" (use a custom extractor function that reads req.cookies.session). The secretOrKey comes from ConfigService JWT_SECRET. The validate method receives the decoded payload and returns the user object (sub, username, role, tenantId). auth/guards/jwt-auth.guard.ts: JwtAuthGuard extending AuthGuard('jwt'). Override canActivate to check IS_PUBLIC_KEY metadata via Reflector -- if @Public(), return true, otherwise delegate to super.canActivate. Per RESEARCH.md Pattern 3. auth/guards/roles.guard.ts: RolesGuard implementing CanActivate. Use Reflector to read ROLES_KEY. If no roles set, allow. Otherwise check if request.user.role is in the required roles array. Per D-12, roles are SUPER_ADMIN, ADMIN, USER. auth/auth.service.ts: AuthService injectable. Dependencies: PrismaService, JwtService, ConfigService. Methods: - validateUser(username, password): Find user by username (use unscoped prisma -- admin seed runs before tenant context exists), check isActive, verify password with argon2.verify, return user or null. Per Pitfall 6, always check isActive. - login(user, response): Build JWT payload with sub=user.id, username=user.username, role=user.role, tenantId=user.tenantId. Sign with JwtService using expiresIn '30d' per D-02. Set httpOnly cookie named "session" on response: httpOnly true, secure only in production, sameSite 'lax', maxAge 30*24*60*60*1000, path '/'. Return user info object (id, username, role, displayName, tenantId, mustChangePassword). - logout(response): Clear the "session" cookie with same path and domain settings. auth/auth.controller.ts: AuthController with prefix 'auth'. - POST /auth/login: @Public() decorated, @UseGuards(AuthGuard('local')). Receives LoginDto body and @Req() request, @Res({ passthrough: true }) response. Calls authService.login(request.user, response). Returns the user info. Uses @HttpCode(200). - POST /auth/logout: Calls authService.logout(response). Returns { message: 'Logged out' }. - GET /auth/me: Returns request.user from JWT (for session check). auth/auth.module.ts: Imports JwtModule.registerAsync with useFactory reading JWT_SECRET from ConfigService, signOptions expiresIn '30d'. Imports PassportModule. Providers: AuthService, LocalStrategy, JwtStrategy. Controllers: AuthController. Exports: AuthService. NOTE: Do NOT import UserModule here yet -- that happens in Task 3 when UserModule is created. For now, AuthService.validateUser queries PrismaService directly (prisma.user.findUnique) instead of going through UserService. Update main.ts: Enable ValidationPipe globally with whitelist true and transform true. Update CORS to specify origin from ConfigService (default 'http://localhost:3000') and credentials true per Pitfall 4. Add cookie-parser middleware (install cookie-parser: pnpm add cookie-parser && pnpm add -D @types/cookie-parser in apps/api). Call app.use(cookieParser()) before listen. cd /home/vicolab/projects/tessera-ctl && pnpm turbo type-check --filter=@tessera/api && cd apps/api && node -e "const m = require('./dist/auth/auth.module'); console.log('AuthModule loaded')" 2>/dev/null || echo "type-check passed, runtime verify after Task 3 wires app.module" - AuthModule has JwtAuthGuard, RolesGuard, LocalStrategy, JwtStrategy - POST /auth/login endpoint exists with @Public() decorator - POST /auth/logout endpoint clears session cookie - GET /auth/me returns user from JWT - RolesGuard checks SUPER_ADMIN/ADMIN/USER roles per D-12 - AuthService.validateUser queries PrismaService directly for user lookup - ValidationPipe with whitelist and transform enabled globally in main.ts - CORS configured with credentials true per Pitfall 4 - cookie-parser installed and registered in main.ts - Type-check passes cleanly AuthModule with full Passport stack (local + JWT strategies), guards (JwtAuthGuard + RolesGuard), decorators (@Public, @Roles, @CurrentUser), controller (login/logout/me), and main.ts updates (ValidationPipe, CORS, cookie-parser) all type-check cleanly. Task 3: UserModule, TenantModule, admin seed, and app.module wiring - apps/api/src/auth/auth.module.ts (from Task 2) - apps/api/src/auth/auth.service.ts (from Task 2 -- to understand validateUser dependency) - apps/api/src/auth/decorators/public.decorator.ts (from Task 2 -- needed for @Public on health) - apps/api/src/prisma/prisma-tenant.extension.ts (from Task 1) - apps/api/prisma/schema.prisma (expanded schema from Task 1) - apps/api/src/health/health.controller.ts (needs @Public decorator) - .planning/phases/02-authentication-multi-tenancy/02-RESEARCH.md (Pattern 5: Tenant middleware, admin seed example) apps/api/src/user/user.module.ts apps/api/src/user/user.service.ts apps/api/src/user/admin-seed.service.ts apps/api/src/tenant/tenant.module.ts apps/api/src/tenant/tenant.service.ts apps/api/src/tenant/tenant.middleware.ts apps/api/src/app.module.ts apps/api/src/health/health.controller.ts user/user.service.ts: UserService injectable. Dependency: PrismaService. Methods: - findByUsername(username): prisma.user.findUnique where username. Uses UNSCOPED prisma (not tenant-scoped) because login must work across tenants. - findById(id): prisma.user.findUnique where id. - create(data): prisma.user.create with argon2.hash for password. - update(id, data): prisma.user.update. If data includes password, hash it. - deactivate(id): prisma.user.update set isActive false. - delete(id): prisma.user.delete. user/admin-seed.service.ts: AdminSeedService implementing OnApplicationBootstrap per RESEARCH.md code example. In onApplicationBootstrap: read TESSERA_ADMIN_USER, TESSERA_ADMIN_EMAIL, TESSERA_ADMIN_PASSWORD, TESSERA_FORCE_CHANGE from ConfigService. If any of user/email/password missing, skip. Check if user exists by username. If exists, skip. Upsert default tenant (slug 'default', name 'Default'). Create user with role SUPER_ADMIN, password hashed with argon2, mustChangePassword from TESSERA_FORCE_CHANGE (default false), tenantId from default tenant. Per D-05, D-07, D-13. user/user.module.ts: Providers: UserService, AdminSeedService. Exports: UserService. tenant/tenant.service.ts: TenantService injectable. Dependency: PrismaService. Methods: - findAll(): prisma.tenant.findMany. - findById(id): prisma.tenant.findUnique. - create(data): prisma.tenant.create. - update(id, data): prisma.tenant.update. tenant/tenant.middleware.ts: TenantMiddleware implementing NestMiddleware per RESEARCH.md Pattern 5. Dependency: PrismaService. In use(req, res, next): extract user from req.user (set by JWT auth). If no user, call next (public routes). Determine tenantId: if user.role is SUPER_ADMIN and x-tenant-id header exists, use header value; otherwise use user.tenantId. If no tenantId and not SUPER_ADMIN, throw ForbiddenException. Attach tenant-scoped prisma client to req (req.tenantPrisma = forTenant(prisma, tenantId) when tenantId exists, unscoped prisma for Super-Admin without header). Attach req.tenantId. Call next(). Per D-08, D-10. tenant/tenant.module.ts: Providers: TenantService. Exports: TenantService. Update app.module.ts: Import PrismaModule, AuthModule, UserModule, TenantModule. Register JwtAuthGuard as global APP_GUARD provider. Apply TenantMiddleware to all routes via configure method (implement NestModule, apply TenantMiddleware forRoutes('*')). Note: TenantMiddleware runs AFTER the AuthGuard, so req.user is available. Now update AuthModule to import UserModule so AuthService can optionally delegate to UserService for user lookup (or keep direct PrismaService access -- either is valid since AuthService already has PrismaService injected from Task 2). Mark health controller's check method with @Public() decorator so it remains accessible without auth. cd /home/vicolab/projects/tessera-ctl && pnpm turbo type-check --filter=@tessera/api && cd apps/api && npx ts-node -e "import('./src/main').then(() => console.log('API boots')).catch(e => { console.error(e.message); process.exit(1) })" 2>&1 | head -20 || echo "Startup check completed" - AdminSeedService creates Super-Admin from ENV on bootstrap per D-05/D-07/D-13 - TenantMiddleware extracts tenantId from JWT and supports Super-Admin tenant switching via x-tenant-id header per D-08/D-10 - UserService.findByUsername uses unscoped Prisma (not tenant-scoped) for cross-tenant login - app.module.ts imports all four modules: PrismaModule, AuthModule, UserModule, TenantModule - JwtAuthGuard registered as global APP_GUARD in app.module.ts - TenantMiddleware applied to all routes via NestModule.configure - Health endpoint has @Public() decorator - Type-check passes cleanly - API process starts without immediate crash (runtime smoke test) UserModule (UserService + AdminSeedService), TenantModule (TenantService + TenantMiddleware) created and wired into app.module with global guards. API boots successfully with admin seed and tenant middleware active. ## Trust Boundaries | Boundary | Description | |----------|-------------| | Browser -> Next.js | Untrusted user input (login form, admin forms) | | Next.js -> NestJS API | Optimistic frontend auth check; API must validate independently | | NestJS API -> PostgreSQL | RLS policies enforce tenant isolation at DB level | | JWT cookie | Session token crosses browser/server boundary on every request | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-02-01 | Spoofing | auth/login endpoint | mitigate | argon2 password verification; never reveal whether username or password is wrong (generic "Invalid credentials") | | T-02-02 | Tampering | JWT cookie | mitigate | httpOnly + secure (prod) + sameSite=lax; signed with HS256 via @nestjs/jwt | | T-02-03 | Information Disclosure | Cross-tenant data | mitigate | PostgreSQL RLS policies on all tenant-scoped tables; forTenant extension sets session variable per query | | T-02-04 | Elevation of Privilege | RolesGuard | mitigate | Role checked from JWT claim; guard validates against required roles per route | | T-02-05 | Tampering | SQL injection via RLS set_config | mitigate | Use parameterized $executeRawUnsafe with $1 placeholder, never string interpolation | | T-02-06 | Denial of Service | login endpoint brute force | accept | Rate limiting deferred -- low risk in internal deployment; add @nestjs/throttler in future phase if needed | | T-02-07 | Spoofing | Deactivated user JWT still valid | mitigate | AuthGuard checks user.isActive in DB on every request per Pitfall 6; short cache TTL acceptable | | T-02-SC | Tampering | npm installs | mitigate | Package Legitimacy Audit in RESEARCH.md; blocking human checkpoint for SUS packages (ldapts, @nestjs-modules/mailer, nodemailer) in Plan 02-03 | After all three tasks complete: 1. docker compose up -d and verify API starts without errors 2. Check logs for "Admin seeded" or equivalent bootstrap message 3. curl -X POST http://localhost:3001/auth/login -H "Content-Type: application/json" -d '{"username":"admin","password":"admin123"}' -c cookies.txt returns 200 with user info and Set-Cookie header 4. curl http://localhost:3001/auth/me -b cookies.txt returns admin user data 5. curl http://localhost:3001/health returns 200 (public route) 6. curl http://localhost:3001/auth/me (no cookie) returns 401 7. pnpm turbo type-check passes for all packages - API boots and auto-seeds Super-Admin from Docker ENV (AUTH-01 per D-05/D-07/D-13) - Login endpoint validates credentials and returns 30-day httpOnly JWT cookie (AUTH-03, AUTH-04 per D-02) - All non-@Public() routes require valid JWT (AUTH-05) - RolesGuard enforces SUPER_ADMIN/ADMIN/USER levels (AUTH-05 per D-12) - Tenant-scoped queries use RLS via Prisma Client Extension (TNNT-01 per D-11) - TenantMiddleware sets tenant context from JWT per request (TNNT-03 per D-08) - Type-check passes across all workspace packages Create `.planning/phases/02-authentication-multi-tenancy/02-01-SUMMARY.md` when done