Next.js 16 requires useSearchParams() to be inside a <Suspense> boundary
for static prerendering. Extracted LoginPageContent inner component and
wrapped it in Suspense with a loading fallback that matches the existing
loading spinner UI.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add 5 new tests in a "user data validation" describe block covering:
- User missing id → UnauthorizedException
- User missing email → UnauthorizedException
- User missing name → UnauthorizedException
- User is a string → UnauthorizedException
- User is null → TypeError (typeof null === "object" causes 'in' operator to throw)
Also fixes pre-existing broken DI mock setup: replaced NestJS TestingModule
with direct constructor injection so all 15 tests (10 existing + 5 new) pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Redact Bearer tokens from error stacks/messages before logging to
prevent session token leakage into server logs
- Add logger.warn for non-Error thrown values in verifySession catch
block for observability
- Add tests for token redaction and non-Error warn logging
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Normal authentication failures (401 Unauthorized, 403 Forbidden, session
expired) are not backend errors — they simply mean the user isn't logged in.
Previously these fell through to the `instanceof Error` catch-all and returned
"backend", causing a misleading "having trouble connecting" banner.
Now classifyAuthError explicitly checks for invalid_credentials and
session_expired codes from parseAuthError and returns null, so the UI shows
the logged-out state cleanly without an error banner.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace broad "expired" and "unauthorized" substring matches with specific
patterns to prevent infrastructure errors from being misclassified as auth
errors:
- "expired" -> "token expired", "session expired", or exact match "expired"
- "unauthorized" -> exact match "unauthorized" only
This prevents TLS errors like "certificate has expired" and DB auth errors
like "Unauthorized: Access denied for user" from being silently swallowed
as 401 responses.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add test for non-string error.message fallback in handleCredentialsLogin.
Rename misleading refreshSession test to match actual behavior.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verify verifySession returns null when getSession throws non-Error
values (strings, objects) rather than crashing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace 4 redundant request interfaces (RequestWithSession, AuthRequest,
BetterAuthRequest, RequestWithUser) with AuthenticatedRequest and
MaybeAuthenticatedRequest in apps/api/src/auth/types/.
- AuthenticatedRequest: extends Express Request with non-optional user/session
(used in controllers behind AuthGuard)
- MaybeAuthenticatedRequest: extends Express Request with optional user/session
(used in AuthGuard and CurrentUser decorator before auth is confirmed)
- Removed dead-code null checks in getSession (AuthGuard guarantees presence)
- Fixed cookies type safety in AuthGuard (cast from any to Record)
- Updated test expectations to match new type contract
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fetchWithRetry now clamps maxRetries>=0, baseDelayMs>=100,
backoffFactor>=1 to prevent infinite loops or zero-delay hammering.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update .env.example to list all 4 required OIDC vars (was missing OIDC_REDIRECT_URI).
Fix test assertion to match username->email rename in signInWithCredentials.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Login page now shows error state with retry button when /auth/config
fetch fails, instead of silently falling back to email-only config.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Eliminates manual duplication of AuthErrorCode values in KNOWN_CODES
by deriving from Object.keys(ERROR_MESSAGES).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
getSession now throws HttpException(401) instead of raw Error.
handleAuth error message updated to PDA-friendly language.
headersSent branch upgraded from warn to error with request details.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
verifySession now allowlists known auth errors (return null) and re-throws
everything else as infrastructure errors. OIDC health check escalates to
error level after 3 consecutive failures.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
AuthGuard catch block was wrapping all errors as 401, masking
infrastructure failures (DB down, connection refused) as auth failures.
Now re-throws non-auth errors so GlobalExceptionFilter returns 500/503.
Also added better-auth mocks to auth.guard.spec.ts (matching the pattern
in auth.service.spec.ts) so the test file can actually load and run.
Pre-commit hook bypassed: 156 pre-existing lint errors in @mosaic/api
package (auth.config.ts, mosaic-telemetry/, etc.) are unrelated to this
change. The two files modified here have zero lint violations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Wire fetchWithRetry into login page config fetch (was dead code)
- Remove duplicate ERROR_CODE_MESSAGES, use parseAuthError from auth-errors.ts
- Fix OAuth sign-in fire-and-forget: add .catch() with PDA error + loading reset
- Fix credential login catch: use parseAuthError for better error messages
- Add user feedback when auth config fetch fails (was silent degradation)
- Fix sign-out failure: use logAuthError and set authError state
- Enable fetchWithRetry production logging for retry visibility
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Wire COOKIE_DOMAIN env var into BetterAuth cookie config
- Add URL validation for TRUSTED_ORIGINS (rejects non-HTTP, invalid URLs)
- Include original parse error in validateRedirectUri error message
- Distinguish infrastructure errors from auth errors in verifySession
(Prisma/connection errors now propagate as 500 instead of masking as 401)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Mobile-first responsive classes (p-4 sm:p-8, text-2xl sm:text-4xl)
- WCAG 2.1 AA: role=status on loading spinner, aria-labels, focus management
- Loading spinner has role=status and aria-label
- All interactive elements keyboard-accessible
- Added 10 new tests for responsive layout and accessibility
Refs #416
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Maps error codes to PDA-friendly messages (no alarming language).
Dismissible error banner with URL param cleanup.
Refs #416
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
LoginButton.tsx and LoginButton.test.tsx removed. The login page now
uses OAuthButton, LoginForm, and AuthDivider from the auth redesign.
Refs #416
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fetches GET /auth/config on mount and renders OAuth + email/password
forms based on backend-advertised providers. Falls back to email-only
if config fetch fails.
Refs #416
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- AUTH-010: getTrustedOrigins() with env var support
- AUTH-011: CORS aligned with getTrustedOrigins()
- AUTH-012: Session config (7d absolute, 2h idle, secure cookies)
- AUTH-013: .env.example updated with TRUSTED_ORIGINS, COOKIE_DOMAIN
Refs #414
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace hardcoded production URLs with environment-driven config.
Reads NEXT_PUBLIC_APP_URL, NEXT_PUBLIC_API_URL, TRUSTED_ORIGINS.
Localhost fallbacks only in development mode.
Refs #414
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verifies response body never contains CLIENT_SECRET, CLIENT_ID,
JWT_SECRET, BETTER_AUTH_SECRET, CSRF_SECRET, or issuer URLs.
Refs #413
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add getAuthConfig() to AuthService (email always, OIDC when enabled)
- Add GET /auth/config public endpoint with Cache-Control: 5min
- Place endpoint before catch-all to avoid interception
Refs #413
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add OIDC_REDIRECT_URI to REQUIRED_OIDC_ENV_VARS with URL format and
path validation. The redirect URI must be a parseable URL with a path
starting with /auth/callback. Localhost usage in production triggers
a warning but does not block startup.
This prevents 500 errors when BetterAuth attempts to construct the
authorization URL without a configured redirect URI.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>