--- phase: 02-authentication-multi-tenancy plan: 02 type: execute wave: 2 depends_on: - 02-01 files_modified: - apps/web/src/app/(auth)/layout.tsx - apps/web/src/app/(auth)/login/page.tsx - apps/web/src/app/(portal)/layout.tsx - apps/web/src/app/(portal)/page.tsx - apps/web/src/app/(portal)/admin/users/page.tsx - apps/web/src/app/(portal)/admin/tenants/page.tsx - apps/web/src/app/layout.tsx - apps/web/src/middleware.ts - apps/web/src/lib/session.ts - apps/web/src/lib/auth-actions.ts - apps/web/src/lib/stores/auth-store.ts - apps/web/src/components/layout/header.tsx - apps/web/src/components/layout/sidebar-footer.tsx - apps/web/src/components/layout/sidebar.tsx - apps/web/src/messages/de.json - apps/web/src/messages/en.json - apps/web/package.json - apps/api/src/user/user.controller.ts - apps/api/src/user/user.module.ts - apps/api/src/user/dto/create-user.dto.ts - apps/api/src/user/dto/update-user.dto.ts - apps/api/src/tenant/tenant.controller.ts - apps/api/src/tenant/tenant.module.ts - apps/api/src/tenant/dto/create-tenant.dto.ts autonomous: true requirements: - AUTH-02 - AUTH-03 - AUTH-04 - TNNT-02 must_haves: truths: - "User sees a split-screen login page with branding left and form right (D-01)" - "Login page has no sidebar or header -- standalone layout (D-04)" - "User can log in with valid credentials and is redirected to dashboard" - "Logged-in user sees their name and role in header and sidebar footer" - "Unauthenticated browser navigation to any portal page redirects to /login" - "Admin can create, edit, and delete users with role assignment via admin/users page (D-12)" - "Super-Admin can create and manage tenants via admin/tenants page (D-10)" - "Remember-me checkbox on login controls session duration (D-02)" artifacts: - path: "apps/web/src/app/(auth)/login/page.tsx" provides: "Split-screen login page with branding and form" min_lines: 50 - path: "apps/web/src/app/(auth)/layout.tsx" provides: "Standalone auth layout without sidebar/header" min_lines: 10 - path: "apps/web/src/middleware.ts" provides: "JWT validation for route protection" min_lines: 20 - path: "apps/web/src/app/(portal)/admin/users/page.tsx" provides: "User management admin page" min_lines: 50 - path: "apps/web/src/app/(portal)/admin/tenants/page.tsx" provides: "Tenant management Super-Admin page" min_lines: 40 - path: "apps/api/src/user/user.controller.ts" provides: "User CRUD REST endpoints" exports: ["UserController"] - path: "apps/api/src/tenant/tenant.controller.ts" provides: "Tenant CRUD REST endpoints" exports: ["TenantController"] key_links: - from: "apps/web/src/middleware.ts" to: "apps/web/src/lib/session.ts" via: "JWT verification using jose" pattern: "jwtVerify" - from: "apps/web/src/app/(auth)/login/page.tsx" to: "apps/api/src/auth/auth.controller.ts" via: "POST /auth/login fetch with credentials include" pattern: "fetch.*auth/login" - from: "apps/web/src/components/layout/header.tsx" to: "apps/web/src/lib/stores/auth-store.ts" via: "Zustand store for client-side user state" pattern: "useAuthStore" --- Frontend authentication flow and user/tenant management: Split-screen login page (D-01, D-04), Next.js middleware for route protection, auth-wired header and sidebar, User CRUD API + admin page (AUTH-02), Tenant CRUD API + admin page (TNNT-02), and Zustand auth store for client-side user state. This plan delivers the vertical slice where a user can log in, see their identity in the portal, and admins can manage users and tenants. Purpose: Connect the backend auth (Plan 02-01) to the frontend portal shell (Phase 1), making the portal a real authenticated application. Output: Working login flow, authenticated portal with user info displayed, admin pages for user and tenant management. @$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-02-SUMMARY.md @.planning/phases/02-authentication-multi-tenancy/02-01-SUMMARY.md @apps/web/src/app/layout.tsx @apps/web/src/components/layout/header.tsx @apps/web/src/components/layout/sidebar-footer.tsx @apps/web/src/components/layout/app-shell.tsx @apps/web/src/messages/de.json @apps/web/src/messages/en.json @apps/web/package.json ## Artifacts this phase produces See Plan 02-01 for the full artifacts table. Task 1: Login page, auth layout, Next.js middleware, auth store, and auth-wired portal components - apps/web/src/app/layout.tsx (root layout to understand provider structure) - apps/web/src/components/layout/header.tsx (user avatar placeholder to wire) - apps/web/src/components/layout/sidebar-footer.tsx (user info placeholder to wire) - apps/web/src/components/layout/app-shell.tsx (AppShell wrapper for portal routes) - apps/web/src/lib/stores/sidebar-store.ts (existing Zustand store pattern to follow) - apps/web/src/messages/de.json (existing i18n keys to extend) - apps/web/src/messages/en.json (existing i18n keys to extend) - apps/web/src/app/globals.css (design tokens for styling login page) - .planning/phases/02-authentication-multi-tenancy/02-RESEARCH.md (Pattern 4: Next.js middleware, session.ts pattern, DAL pattern) apps/web/src/app/(auth)/layout.tsx apps/web/src/app/(auth)/login/page.tsx apps/web/src/app/(portal)/layout.tsx apps/web/src/app/(portal)/page.tsx apps/web/src/middleware.ts apps/web/src/lib/session.ts apps/web/src/lib/auth-actions.ts apps/web/src/lib/stores/auth-store.ts apps/web/src/components/layout/header.tsx apps/web/src/components/layout/sidebar-footer.tsx apps/web/src/messages/de.json apps/web/src/messages/en.json apps/web/package.json apps/web/src/app/layout.tsx Install frontend auth dependencies: cd apps/web and pnpm add jose zod Create route group structure for auth separation per D-04: - Move existing page.tsx content into apps/web/src/app/(portal)/page.tsx (dashboard page) - Create apps/web/src/app/(portal)/layout.tsx that wraps children with AppShell component (import from @/components/layout/app-shell). This ensures all portal routes get header + sidebar. - Create apps/web/src/app/(auth)/layout.tsx as a STANDALONE layout: no sidebar, no header. Simple centered layout with min-h-screen bg-background. Per D-04. - Update apps/web/src/app/layout.tsx to remove AppShell wrapping if it was there (root layout should only have ThemeProvider and NextIntlClientProvider -- the route group layouts handle structural differences). Create apps/web/src/lib/session.ts: - Export a function verifySession(token: string) that uses jose.jwtVerify with the JWT_SECRET env var (read from process.env.JWT_SECRET or SESSION_SECRET). Returns the decoded payload or null on failure. Use HS256 algorithm. - Export a function getSessionFromCookies(cookies) that reads the "session" cookie value. Create apps/web/src/middleware.ts per RESEARCH.md Pattern 4: - Define publicRoutes array: ['/login', '/reset-password'] - On every request, check if path starts with a public route prefix -- if yes, NextResponse.next() - Also skip _next/static, _next/image, favicon.ico, and API routes - Read "session" cookie from request - If no cookie, redirect to /login - Verify JWT with jose.jwtVerify using the secret from process.env.JWT_SECRET - If verification fails, redirect to /login (clear stale cookie) - If user payload has mustChangePassword true, redirect to /change-password (except if already on that page) - Export matcher config per RESEARCH.md: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'] Create apps/web/src/lib/auth-actions.ts with server-side functions: - login(formData): Extract username and password. POST to API_URL/auth/login with credentials include. On success, the API sets the cookie directly (since both are behind Traefik in production). In development, handle the Set-Cookie from the API response and forward it. Return success/error. - logout(): POST to API_URL/auth/logout. Clear cookies. Redirect to /login. - fetchCurrentUser(): GET API_URL/auth/me with credentials include. Return user or null. NOTE: API_URL should be read from NEXT_PUBLIC_API_URL env var (already set in docker-compose.yml as http://localhost:3001). Create apps/web/src/lib/stores/auth-store.ts following the existing sidebar-store.ts pattern: - Zustand store with user state (id, username, displayName, role, tenantId) or null - Actions: setUser, clearUser - No persist middleware (user state comes from API, not localStorage) Create apps/web/src/app/(auth)/login/page.tsx per D-01 split-screen design: - 'use client' component - Full-screen split layout: LEFT side (hidden on mobile, flex-1 on md+) shows Tessera branding with primary yellow (#ffed00 / var(--primary)) background, large "Tessera" text, and tagline. RIGHT side (full width mobile, flex-1 desktop) shows login form on white/dark background. - Form fields: username input, password input, "Angemeldet bleiben" (Remember me) checkbox per D-02 - Submit button with primary color - Error message display area - Form submission calls the login action, which POSTs to /auth/login on the API. On success, redirect to '/' (dashboard). On error, show error message. - All strings through useTranslations('auth') hook per UI-03 pattern - Include i18n keys for: auth.login, auth.username, auth.password, auth.rememberMe, auth.submit, auth.error.invalidCredentials, auth.branding.tagline Update apps/web/src/components/layout/header.tsx: - Replace the user avatar placeholder div with a real user dropdown component - Import useAuthStore to get current user - Show user initial (first letter of displayName or username) in the avatar circle - On click, show a dropdown with: user display name, role badge, "Abmelden" (Logout) button - Logout button calls the logout action - Keep the existing hamburger, logo, breadcrumb, and ThemeToggle structure intact Update apps/web/src/components/layout/sidebar-footer.tsx: - Replace user info placeholder with real user data from useAuthStore - Show user display name (or username) and role - When sidebar is collapsed, show only avatar initial Add i18n keys to both de.json and en.json for all new strings: - auth namespace: login, username, password, rememberMe, submit, error.invalidCredentials, branding.tagline - header namespace: logout, role.SUPER_ADMIN, role.ADMIN, role.USER - admin namespace: users.title, users.create, users.edit, users.delete, users.name, users.email, users.role, users.status, users.actions, tenants.title, tenants.create, tenants.name, tenants.slug, tenants.status, tenants.actions cd /home/vicolab/projects/tessera-ctl && pnpm turbo type-check --filter=@tessera/web - (auth) route group has standalone layout without AppShell per D-04 - (portal) route group wraps children with AppShell (header + sidebar) - Login page is split-screen with branding left and form right per D-01 - Login form has username, password, and remember-me checkbox per D-02 - Next.js middleware validates JWT cookie and redirects unauthenticated to /login - middleware.ts allows /login and /reset-password without auth - Header shows real user initial and dropdown with logout per auth store - Sidebar footer shows real user name and role per auth store - All new UI strings use i18n t() function (no hardcoded text) - Both de.json and en.json have all new auth/admin translation keys - Type-check passes for @tessera/web Login page renders split-screen layout; unauthenticated users are redirected to /login; authenticated users see their name in header and sidebar; all strings are internationalized. Task 2: User CRUD API + admin page and Tenant CRUD API + admin page - apps/api/src/user/user.service.ts (from Plan 02-01, user operations) - apps/api/src/user/user.module.ts (from Plan 02-01) - apps/api/src/tenant/tenant.service.ts (from Plan 02-01, tenant operations) - apps/api/src/tenant/tenant.module.ts (from Plan 02-01) - apps/api/src/auth/decorators/roles.decorator.ts (Roles decorator) - apps/api/src/auth/decorators/current-user.decorator.ts (CurrentUser decorator) - apps/api/src/auth/guards/roles.guard.ts (RolesGuard) - .planning/phases/02-authentication-multi-tenancy/02-RESEARCH.md (architecture diagram, project structure) apps/api/src/user/user.controller.ts apps/api/src/user/user.module.ts apps/api/src/user/dto/create-user.dto.ts apps/api/src/user/dto/update-user.dto.ts apps/api/src/tenant/tenant.controller.ts apps/api/src/tenant/tenant.module.ts apps/api/src/tenant/dto/create-tenant.dto.ts apps/web/src/app/(portal)/admin/users/page.tsx apps/web/src/app/(portal)/admin/tenants/page.tsx apps/web/src/components/layout/sidebar.tsx Create user DTOs: - create-user.dto.ts: CreateUserDto with fields: username (@IsString, @IsNotEmpty), email (@IsEmail), password (@IsString, @MinLength(8)), displayName (@IsString, @IsOptional), role (@IsEnum(Role), @IsOptional, default USER). Use class-validator decorators. - update-user.dto.ts: UpdateUserDto with all fields optional (PartialType of CreateUserDto using @nestjs/mapped-types). Add isActive (@IsBoolean, @IsOptional). Create apps/api/src/user/user.controller.ts: - Prefix 'users' - GET /users: @Roles(Role.ADMIN, Role.SUPER_ADMIN) @UseGuards(RolesGuard). Returns list of users. For ADMIN, filter by tenant (use req.tenantPrisma if available, otherwise filter by user's tenantId). For SUPER_ADMIN, return all users. - GET /users/:id: @Roles(Role.ADMIN, Role.SUPER_ADMIN). Return single user. - POST /users: @Roles(Role.ADMIN, Role.SUPER_ADMIN). Body: CreateUserDto. Create user via UserService. ADMIN can only create users in own tenant; SUPER_ADMIN can specify tenantId. - PATCH /users/:id: @Roles(Role.ADMIN, Role.SUPER_ADMIN). Body: UpdateUserDto. Update user. ADMIN can only update users in own tenant and cannot set role to SUPER_ADMIN. - DELETE /users/:id: @Roles(Role.ADMIN, Role.SUPER_ADMIN). Delete user. ADMIN can only delete users in own tenant and cannot delete self. Update user.module.ts to add UserController to controllers array. Create tenant DTOs: - create-tenant.dto.ts: CreateTenantDto with fields: name (@IsString, @IsNotEmpty), slug (@IsString, @IsNotEmpty, @Matches /^[a-z0-9-]+$/). Create apps/api/src/tenant/tenant.controller.ts: - Prefix 'tenants' - GET /tenants: @Roles(Role.SUPER_ADMIN) @UseGuards(RolesGuard). Returns all tenants. Per D-10, only Super-Admin manages tenants. - GET /tenants/:id: @Roles(Role.SUPER_ADMIN). Return single tenant with user count. - POST /tenants: @Roles(Role.SUPER_ADMIN). Body: CreateTenantDto. Create tenant. - PATCH /tenants/:id: @Roles(Role.SUPER_ADMIN). Update tenant name, isActive. - DELETE /tenants/:id: @Roles(Role.SUPER_ADMIN). Delete tenant (only if no active users). Update tenant.module.ts to add TenantController to controllers array. Create apps/web/src/app/(portal)/admin/users/page.tsx: - 'use client' component - Fetch users from GET /users API endpoint on mount - Display table with columns: Username, Email, Display Name, Role, Status (Active/Inactive), Actions - Create user button opens a form dialog/modal with CreateUserDto fields - Edit button opens pre-filled form dialog - Delete button with confirmation dialog - Role displayed as colored badge (SUPER_ADMIN = red, ADMIN = blue, USER = gray) - All strings through useTranslations('admin') per i18n pattern - Only visible to ADMIN and SUPER_ADMIN roles (check auth store) Create apps/web/src/app/(portal)/admin/tenants/page.tsx: - 'use client' component - Fetch tenants from GET /tenants API endpoint - Display table with columns: Name, Slug, Status, User Count, Actions - Create tenant button opens form dialog - Edit and deactivate actions - Only visible to SUPER_ADMIN role (check auth store, show "Access denied" otherwise) - All strings through useTranslations('admin') Update apps/web/src/components/layout/sidebar.tsx: - Add admin section to sidebar navigation: "Verwaltung" (Administration) category - Under Verwaltung: "Benutzer" (Users) link to /admin/users, "Mandanten" (Tenants) link to /admin/tenants - Verwaltung section only visible when user role is ADMIN or SUPER_ADMIN - Tenants link only visible when role is SUPER_ADMIN - Use auth store to check current user role - Add i18n keys: sidebar.admin, sidebar.users, sidebar.tenants cd /home/vicolab/projects/tessera-ctl && pnpm turbo type-check - User CRUD endpoints exist at /users with proper role guards (ADMIN + SUPER_ADMIN) - ADMIN can only manage users in own tenant; SUPER_ADMIN can manage all - Tenant CRUD endpoints exist at /tenants with SUPER_ADMIN-only access per D-10 - Admin users page shows user table with create/edit/delete actions - Tenants page shows tenant table with create/edit actions (SUPER_ADMIN only) - Sidebar shows "Verwaltung" section for ADMIN/SUPER_ADMIN roles - All new UI strings internationalized in de.json and en.json - Type-check passes for both API and web packages Admin can create/edit/delete users with role assignment (AUTH-02); Super-Admin can create/manage tenants (TNNT-02); admin navigation visible in sidebar for authorized roles. ## Trust Boundaries | Boundary | Description | |----------|-------------| | Browser -> Login form | Untrusted credentials input | | Next.js middleware -> JWT | Optimistic session check; API validates independently | | Admin UI -> API | Role-based access must be enforced server-side, not just UI-hidden | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-02-08 | Elevation of Privilege | User CRUD endpoints | mitigate | RolesGuard enforces ADMIN/SUPER_ADMIN; ADMIN cannot escalate to SUPER_ADMIN or modify other tenants | | T-02-09 | Tampering | Tenant CRUD | mitigate | SUPER_ADMIN-only via RolesGuard; tenant deletion blocked if active users exist | | T-02-10 | Information Disclosure | User list | mitigate | ADMIN sees only own-tenant users; SUPER_ADMIN sees all; enforced at controller level | | T-02-11 | Spoofing | Login form CSRF | mitigate | SameSite=lax cookie prevents cross-origin POST; form submission is same-origin | | T-02-SC | Tampering | npm installs (jose, zod) | accept | Both packages are VERIFIED in legitimacy audit with 87M+ and 201M+ weekly downloads | After both tasks complete: 1. Navigate to http://localhost:3000 -- should redirect to /login 2. Login with admin/admin123 -- should redirect to dashboard, header shows "admin" user 3. Navigate to /admin/users -- should show user table with the admin account 4. Create a new user via the admin page -- should appear in the table 5. Navigate to /admin/tenants -- should show "Default" tenant 6. Click logout -- should redirect to /login 7. Try accessing /admin/users directly without login -- should redirect to /login 8. pnpm turbo type-check passes for all packages - User can log in via split-screen login page and see their identity in portal (AUTH-03, D-01, D-04) - Session persists across browser refresh via JWT cookie (AUTH-04, D-02) - Admin can CRUD users with role assignment (AUTH-02, D-12) - Super-Admin can CRUD tenants (TNNT-02, D-10) - Unauthenticated access redirects to /login - All new strings internationalized (DE/EN) Create `.planning/phases/02-authentication-multi-tenancy/02-02-SUMMARY.md` when done