diff --git a/.planning/phases/02-authentication-multi-tenancy/02-01-PLAN.md b/.planning/phases/02-authentication-multi-tenancy/02-01-PLAN.md
index df15410..958e346 100644
--- a/.planning/phases/02-authentication-multi-tenancy/02-01-PLAN.md
+++ b/.planning/phases/02-authentication-multi-tenancy/02-01-PLAN.md
@@ -199,13 +199,12 @@ Output: Working API with login/logout endpoints, global JWT protection, role-bas
- Task 2: AuthModule with Passport strategies, guards, and admin seed
+ 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/src/prisma/prisma-tenant.extension.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, Pattern 5: Tenant middleware, admin seed example, security domain)
+ - .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
@@ -219,13 +218,6 @@ Output: Working API with login/logout endpoints, global JWT protection, role-bas
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
@@ -254,8 +246,50 @@ Output: Working API with login/logout endpoints, global JWT protection, role-bas
- 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. Imports UserModule. Providers: AuthService, LocalStrategy, JwtStrategy. Controllers: AuthController. Exports: AuthService.
+ 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.
@@ -280,28 +314,25 @@ Output: Working API with login/logout endpoints, global JWT protection, role-bas
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.
- 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). Call app.use(cookieParser()) before listen.
+ 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 /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"
- - AuthModule registers JwtAuthGuard as global APP_GUARD
- - POST /auth/login endpoint exists with @Public() decorator
- - POST /auth/logout endpoint clears session cookie
- - GET /auth/me returns user from JWT
- 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
- - RolesGuard checks SUPER_ADMIN/ADMIN/USER roles per D-12
- UserService.findByUsername uses unscoped Prisma (not tenant-scoped) for cross-tenant login
- - ValidationPipe with whitelist and transform enabled globally
- - CORS configured with credentials true per Pitfall 4
+ - 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)
- Full backend auth stack operational: login returns JWT cookie, global guard protects all routes except @Public(), admin auto-seeded from ENV, tenant context injected per request via RLS.
+ 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.
@@ -331,7 +362,7 @@ Output: Working API with login/logout endpoints, global JWT protection, role-bas
-After both tasks complete:
+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
diff --git a/.planning/phases/02-authentication-multi-tenancy/02-02-PLAN.md b/.planning/phases/02-authentication-multi-tenancy/02-02-PLAN.md
index af4fa33..b29aad4 100644
--- a/.planning/phases/02-authentication-multi-tenancy/02-02-PLAN.md
+++ b/.planning/phases/02-authentication-multi-tenancy/02-02-PLAN.md
@@ -10,8 +10,6 @@ files_modified:
- 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
@@ -30,6 +28,8 @@ files_modified:
- 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
autonomous: true
requirements:
- AUTH-02
@@ -122,33 +122,23 @@ 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
+ Task 1: Auth infrastructure -- route groups, middleware, session, auth-actions, and auth store
- 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/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/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
@@ -184,14 +174,50 @@ See Plan 02-01 for the full artifacts table.
- 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)
+
+
+ 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)
+ - Next.js middleware validates JWT cookie and redirects unauthenticated to /login
+ - middleware.ts allows /login and /reset-password without auth
+ - session.ts exports verifySession using jose
+ - auth-actions.ts exports login, logout, fetchCurrentUser
+ - auth-store.ts exports useAuthStore Zustand hook
+ - Type-check passes for @tessera/web
+
+ Route groups created (auth standalone, portal with AppShell); Next.js middleware protects routes via JWT verification; auth-actions provide login/logout/fetchCurrentUser; Zustand auth store created.
+
+
+ Task 2: Login page UI, header/sidebar auth wiring, and i18n keys
+
+ - apps/web/src/app/(auth)/layout.tsx (from Task 1 -- standalone layout)
+ - apps/web/src/lib/auth-actions.ts (from Task 1 -- login action to call)
+ - apps/web/src/lib/stores/auth-store.ts (from Task 1 -- store to populate)
+ - 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/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)
+
+
+ apps/web/src/app/(auth)/login/page.tsx
+ 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
+
+
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.
+ - Form submission calls the login action from auth-actions.ts, 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
@@ -200,7 +226,7 @@ See Plan 02-01 for the full artifacts table.
- 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
+ - Logout button calls the logout action from auth-actions.ts
- Keep the existing hamburger, logo, breadcrumb, and ThemeToggle structure intact
Update apps/web/src/components/layout/sidebar-footer.tsx:
@@ -217,23 +243,19 @@ See Plan 02-01 for the full artifacts table.
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
+ - Both de.json and en.json have all new auth/header/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.
+ Login page renders split-screen layout per D-01; header shows user dropdown with logout; sidebar footer shows user info; all strings internationalized in DE/EN.
- Task 2: User CRUD API + admin page and Tenant CRUD API + admin page
+ Task 3: 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)
@@ -349,7 +371,7 @@ See Plan 02-01 for the full artifacts table.
-After both tasks complete:
+After all three 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