--- phase: 05-visibility-and-client-portal plan: 02 subsystem: portal tags: [portal, auth, subscriber, nextauth, credentials, invoices, payments, account] # Dependency graph requires: - phase: 01-foundation provides: NextAuth v4 with JWT strategy, withTenantContext, middleware auth exclusions - phase: 02-subscriber-and-billing-core provides: Subscriber, Invoice, Payment, ServicePlan models provides: - Portal credentials provider (accountNumber + password login) - PortalService with subscriber-scoped data retrieval - Portal API routes (account, invoices, payments) - withPortalAuth middleware for portal endpoint authorization affects: [05-03 (portal UI consumes these APIs), 05-05 (e2e tests)] # Tech tracking tech-stack: added: [] patterns: - "Dual NextAuth credentials provider (staff + portal) on same instance" - "subscriberId in JWT/session for portal user identification" - "withPortalAuth HOF for portal-only endpoint authorization" key-files: created: - src/lib/services/portal-service.ts - src/lib/middleware/portal-auth.ts - src/app/api/portal/account/route.ts - src/app/api/portal/invoices/route.ts - src/app/api/portal/payments/route.ts - src/lib/__tests__/portal-service.test.ts modified: - prisma/schema.prisma - src/lib/auth-options.ts - src/types/next-auth.d.ts - src/middleware.ts # Decisions decisions: - id: "portal-dual-provider" description: "Two CredentialsProviders on same NextAuth instance (id: credentials, id: portal-credentials) -- additive, no change to staff auth" - id: "portal-password-nullable" description: "Subscriber.passwordHash is nullable -- only subscribers with a set password can log in to portal" - id: "portal-subscriberId-jwt" description: "subscriberId persisted in JWT token and session -- distinguishes portal users from staff users without DB lookup" # Metrics metrics: duration: "4 min" completed: "2026-03-05" tasks: 2 tests: 5 --- # Phase 05 Plan 02: Subscriber Portal Authentication and API Summary **Portal auth via account number + password with subscriber-scoped read-only API endpoints, 5 tests passing** ## What Was Done ### Task 1: Portal Authentication - Added `passwordHash` (nullable String) to Subscriber model in Prisma schema - Added second CredentialsProvider (`portal-credentials`) to NextAuth config accepting accountNumber + password + tenantId - Provider looks up subscriber by @@unique([tenantId, accountNumber]) with non-null passwordHash - JWT callback persists `subscriberId` into token; session callback exposes it on session.user - Extended next-auth type definitions (Session, User, JWT) with optional `subscriberId` - Updated middleware matcher to exclude `/portal/login` and `/api/portal/auth` from auth requirement ### Task 2: PortalService and API Routes - Created `PortalService` with three subscriber-scoped methods: - `getPortalAccount(db, subscriberId)` -- returns profile, plan details, credit balance, billing day - `getPortalInvoices(db, subscriberId, options)` -- paginated invoices with line items, ordered by periodStart DESC - `getPortalPayments(db, subscriberId, options)` -- paginated payments, ordered by createdAt DESC - Created `withPortalAuth` middleware HOF that validates subscriberId in session (401 if no session, 403 if not portal user) - Created three portal API routes: - `GET /api/portal/account` -- subscriber profile + plan - `GET /api/portal/invoices` -- paginated invoices (query: page, limit) - `GET /api/portal/payments` -- paginated payments (query: page, limit) ### Integration Tests (5/5 passing) 1. `getPortalAccount returns subscriber with plan details` -- verifies all fields including plan name, speed, price, billingType 2. `getPortalInvoices returns paginated invoices` -- creates 3 invoices, verifies page 1 limit 2 returns 2 with total 3 3. `getPortalPayments returns paginated payment history` -- creates 2 payments, verifies list and subscriber scoping 4. `getPortalAccount scoped to subscriberId only` -- creates 2 subscribers, verifies each sees only own data 5. `getPortalInvoices includes invoice line items` -- verifies lines array populated with description and lineTotal ## Deviations from Plan None -- plan executed exactly as written. ## Commits | Hash | Message | |------|---------| | 539564d | feat(05-02): portal authentication via subscriber account number | | 7222dbe | feat(05-02): portal service, API routes, and integration tests | ## Requirements Satisfied - **PORT-01**: Subscriber can log in with account number and password - **PORT-02**: Subscriber can view current bill and outstanding balance (via invoices endpoint + account creditBalance) - **PORT-04**: Subscriber can view full payment history (via payments endpoint) - Account overview includes plan details, balance, and billing day - Invoice and payment history are paginated - Portal API endpoints scoped to logged-in subscriber only