import { Injectable, Inject, ServiceUnavailableException } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { Redis } from "ioredis"; import { Logger } from "nestjs-pino"; import type { JWTPayload } from "jose"; import type { AuthTokens } from "@customer-portal/domain/auth"; import type { UserAuth, UserRole } from "@customer-portal/domain/customer"; import { TokenGeneratorService } from "./token-generator.service.js"; import { TokenRefreshService } from "./token-refresh.service.js"; import { TokenRevocationService } from "./token-revocation.service.js"; export interface RefreshTokenPayload extends JWTPayload { userId: string; /** * Refresh token family identifier (stable across rotations). * Present on newly issued tokens; legacy tokens used `tokenId` for this value. */ familyId?: string | undefined; /** * Refresh token identifier (unique per token). Used for replay/reuse detection. * For legacy tokens, this was equal to the family id. */ tokenId: string; deviceId?: string | undefined; userAgent?: string | undefined; type: "refresh"; } export interface DeviceInfo { deviceId?: string | undefined; userAgent?: string | undefined; } const ERROR_SERVICE_UNAVAILABLE = "Authentication service temporarily unavailable"; /** * Auth Token Service * * Thin orchestrator that delegates to focused services: * - TokenGeneratorService: token creation * - TokenRefreshService: refresh + rotation logic * - TokenRevocationService: token revocation * * Preserves the existing public API so consumers don't need changes. */ @Injectable() export class AuthTokenService { private readonly requireRedisForTokens: boolean; private readonly maintenanceMode: boolean; private readonly maintenanceMessage: string; constructor( private readonly generator: TokenGeneratorService, private readonly refreshService: TokenRefreshService, private readonly revocation: TokenRevocationService, configService: ConfigService, @Inject("REDIS_CLIENT") private readonly redis: Redis, @Inject(Logger) private readonly logger: Logger ) { this.requireRedisForTokens = configService.get("AUTH_REQUIRE_REDIS_FOR_TOKENS", "false") === "true"; this.maintenanceMode = configService.get("AUTH_MAINTENANCE_MODE", "false") === "true"; this.maintenanceMessage = configService.get( "AUTH_MAINTENANCE_MESSAGE", "Authentication service is temporarily unavailable for maintenance. Please try again later." ); } private checkServiceAvailability(): void { if (this.maintenanceMode) { this.logger.warn("Authentication service in maintenance mode", { maintenanceMessage: this.maintenanceMessage, }); throw new ServiceUnavailableException(this.maintenanceMessage); } if (this.requireRedisForTokens && this.redis.status !== "ready") { this.logger.error("Redis required for token operations but not available", { redisStatus: this.redis.status, requireRedisForTokens: this.requireRedisForTokens, }); throw new ServiceUnavailableException(ERROR_SERVICE_UNAVAILABLE); } } /** * Generate a new token pair with refresh token rotation */ async generateTokenPair( user: { id: string; email: string; role?: UserRole }, deviceInfo?: DeviceInfo ): Promise { this.checkServiceAvailability(); return this.generator.generateTokenPair(user, deviceInfo); } /** * Refresh access token using refresh token rotation */ async refreshTokens( refreshToken: string, deviceInfo?: DeviceInfo ): Promise<{ tokens: AuthTokens; user: UserAuth }> { this.checkServiceAvailability(); return this.refreshService.refreshTokens(refreshToken, deviceInfo); } /** * Revoke a specific refresh token */ async revokeRefreshToken(refreshToken: string): Promise { return this.revocation.revokeRefreshToken(refreshToken); } /** * Get all active refresh token families for a user */ async getUserRefreshTokenFamilies(userId: string): Promise< Array<{ familyId: string; deviceId?: string | undefined; userAgent?: string | undefined; createdAt?: string | undefined; }> > { return this.revocation.getUserRefreshTokenFamilies(userId); } /** * Revoke all refresh tokens for a user */ async revokeAllUserTokens(userId: string): Promise { return this.revocation.revokeAllUserTokens(userId); } }