diff --git a/apps/bff/src/integrations/whmcs/connection/services/whmcs-http-client.service.ts b/apps/bff/src/integrations/whmcs/connection/services/whmcs-http-client.service.ts index 21199ffa..11fecace 100644 --- a/apps/bff/src/integrations/whmcs/connection/services/whmcs-http-client.service.ts +++ b/apps/bff/src/integrations/whmcs/connection/services/whmcs-http-client.service.ts @@ -59,8 +59,7 @@ export class WhmcsHttpClientService { params: redactForLogs(params), responseTime: Date.now() - startTime, }, - "WHMCS HTTP request failed [%s]", - action + "WHMCS HTTP request failed" ); throw error; @@ -146,8 +145,7 @@ export class WhmcsHttpClientService { statusText: response.statusText, snippet, }, - "WHMCS non-OK response body snippet [%s]", - action + "WHMCS non-OK response body snippet" ); } } @@ -255,8 +253,7 @@ export class WhmcsHttpClientService { parseError: extractErrorMessage(parseError), params: redactForLogs(params), }, - "Invalid JSON response from WHMCS API [%s]", - action + "Invalid JSON response from WHMCS API" ); throw new WhmcsOperationException("Invalid JSON response from WHMCS API"); } @@ -272,8 +269,7 @@ export class WhmcsHttpClientService { : { responseText: responseText.slice(0, 500) }), params: redactForLogs(params), }, - "WHMCS API returned invalid response structure [%s]", - action + "WHMCS API returned invalid response structure" ); throw new WhmcsOperationException("Invalid response structure from WHMCS API"); } @@ -291,12 +287,12 @@ export class WhmcsHttpClientService { // classifies severity and the business layer logs the final outcome. this.logger.debug( { + action, errorMessage, errorCode, params: redactForLogs(params), }, - "WHMCS API returned error [%s]", - action + "WHMCS API returned error" ); // Return error response for the orchestrator to handle with proper exception types diff --git a/apps/bff/src/modules/auth/application/auth-login.service.ts b/apps/bff/src/modules/auth/application/auth-login.service.ts index 42082937..1f5e617e 100644 --- a/apps/bff/src/modules/auth/application/auth-login.service.ts +++ b/apps/bff/src/modules/auth/application/auth-login.service.ts @@ -10,7 +10,7 @@ import { UsersService } from "@bff/modules/users/application/users.service.js"; import { AuditService, AuditAction } from "@bff/infra/audit/audit.service.js"; import { extractErrorMessage } from "@bff/core/utils/error.util.js"; -export interface ValidatedUser { +interface ValidatedUser { id: string; email: string; role: string; diff --git a/apps/bff/src/modules/auth/application/auth-orchestrator.service.ts b/apps/bff/src/modules/auth/application/auth-orchestrator.service.ts index 58d33ca5..5325af73 100644 --- a/apps/bff/src/modules/auth/application/auth-orchestrator.service.ts +++ b/apps/bff/src/modules/auth/application/auth-orchestrator.service.ts @@ -14,8 +14,6 @@ import type { User as PrismaUser } from "@prisma/client"; import type { Request } from "express"; import { TokenBlacklistService } from "../infra/token/token-blacklist.service.js"; import { AuthTokenService } from "../infra/token/token.service.js"; -import { AuthLoginService } from "./auth-login.service.js"; - /** * Auth Orchestrator * @@ -36,18 +34,9 @@ export class AuthOrchestrator { private readonly auditService: AuditService, private readonly tokenBlacklistService: TokenBlacklistService, private readonly tokenService: AuthTokenService, - private readonly loginService: AuthLoginService, @Inject(Logger) private readonly logger: Logger ) {} - async validateUser( - email: string, - password: string, - request?: Request - ): Promise<{ id: string; email: string; role: string } | null> { - return this.loginService.validateUser(email, password, request); - } - /** * Complete login after credential validation (and OTP verification if required) * Generates tokens and updates user state @@ -171,29 +160,11 @@ export class AuthOrchestrator { * Create SSO link to WHMCS for general access */ async createSsoLink(userId: string, destination?: string): Promise { - try { - this.logger.log("Creating SSO link request"); + const whmcsClientId = await this.mappingsService.getWhmcsClientIdOrThrow(userId); + const ssoDestination = "sso:custom_redirect"; + const ssoRedirectPath = sanitizeWhmcsRedirectPath(destination); - const whmcsClientId = await this.mappingsService.getWhmcsClientIdOrThrow(userId); - const ssoDestination = "sso:custom_redirect"; - const ssoRedirectPath = sanitizeWhmcsRedirectPath(destination); - - const result = await this.whmcsSsoService.createSsoToken( - whmcsClientId, - ssoDestination, - ssoRedirectPath - ); - - this.logger.log("SSO link created successfully"); - - return result; - } catch (error) { - this.logger.error("SSO link creation failed", { - errorType: error instanceof Error ? error.constructor.name : "Unknown", - message: extractErrorMessage(error), - }); - throw error; - } + return this.whmcsSsoService.createSsoToken(whmcsClientId, ssoDestination, ssoRedirectPath); } async refreshTokens( diff --git a/apps/bff/src/modules/auth/auth.types.ts b/apps/bff/src/modules/auth/auth.types.ts index 13e03e0c..f3e30870 100644 --- a/apps/bff/src/modules/auth/auth.types.ts +++ b/apps/bff/src/modules/auth/auth.types.ts @@ -2,6 +2,14 @@ import type { User, UserAuth } from "@customer-portal/domain/customer"; import type { Request } from "express"; import type { AuthTokens } from "@customer-portal/domain/auth"; +/** + * Express Request with typed cookies. + * Used across auth controllers and cookie utilities. + */ +export type RequestWithCookies = Omit & { + cookies?: Record; +}; + export type RequestWithUser = Request & { user: User }; /** diff --git a/apps/bff/src/modules/auth/constants/auth-errors.ts b/apps/bff/src/modules/auth/constants/auth-errors.ts deleted file mode 100644 index 8aee8e2a..00000000 --- a/apps/bff/src/modules/auth/constants/auth-errors.ts +++ /dev/null @@ -1,115 +0,0 @@ -/** - * Centralized Authentication Error Catalog - * - * Provides consistent error codes and messages across the auth module. - * Use these constants instead of hardcoding error messages. - */ - -export const AUTH_ERRORS = { - // Token errors - TOKEN_INVALID: { - code: "TOKEN_INVALID", - message: "Invalid token", - }, - TOKEN_EXPIRED: { - code: "TOKEN_EXPIRED", - message: "Token has expired", - }, - TOKEN_REVOKED: { - code: "TOKEN_REVOKED", - message: "Token has been revoked", - }, - TOKEN_MISSING: { - code: "TOKEN_MISSING", - message: "Missing token", - }, - TOKEN_INVALID_TYPE: { - code: "TOKEN_INVALID_TYPE", - message: "Invalid access token", - }, - TOKEN_INVALID_PAYLOAD: { - code: "TOKEN_INVALID_PAYLOAD", - message: "Invalid token payload", - }, - TOKEN_MISSING_EXPIRATION: { - code: "TOKEN_MISSING_EXPIRATION", - message: "Token missing expiration claim", - }, - TOKEN_EXPIRING_SOON: { - code: "TOKEN_EXPIRING_SOON", - message: "Token expired or expiring soon", - }, - TOKEN_SUBJECT_MISMATCH: { - code: "TOKEN_SUBJECT_MISMATCH", - message: "Token subject does not match user record", - }, - - // User errors - USER_NOT_FOUND: { - code: "USER_NOT_FOUND", - message: "User not found", - }, - ACCOUNT_EXISTS: { - code: "ACCOUNT_EXISTS", - message: "Account already exists", - }, - INVALID_CREDENTIALS: { - code: "INVALID_CREDENTIALS", - message: "Invalid credentials", - }, - EMAIL_NOT_VERIFIED: { - code: "EMAIL_NOT_VERIFIED", - message: "Email not verified", - }, - ACCOUNT_DISABLED: { - code: "ACCOUNT_DISABLED", - message: "Account has been disabled", - }, - - // Password errors - PASSWORD_TOO_WEAK: { - code: "PASSWORD_TOO_WEAK", - message: "Password does not meet requirements", - }, - PASSWORD_MISMATCH: { - code: "PASSWORD_MISMATCH", - message: "Current password is incorrect", - }, - PASSWORD_RESET_EXPIRED: { - code: "PASSWORD_RESET_EXPIRED", - message: "Password reset token has expired", - }, - PASSWORD_RESET_INVALID: { - code: "PASSWORD_RESET_INVALID", - message: "Invalid password reset token", - }, - - // Session errors - SESSION_EXPIRED: { - code: "SESSION_EXPIRED", - message: "Session has expired", - }, - SESSION_INVALID: { - code: "SESSION_INVALID", - message: "Invalid session", - }, - - // Request context errors - INVALID_REQUEST_CONTEXT: { - code: "INVALID_REQUEST_CONTEXT", - message: "Invalid request context", - }, - - // Service errors - SERVICE_UNAVAILABLE: { - code: "SERVICE_UNAVAILABLE", - message: "Authentication temporarily unavailable", - }, - RATE_LIMITED: { - code: "RATE_LIMITED", - message: "Too many requests. Please try again later.", - }, -} as const; - -export type AuthErrorCode = keyof typeof AUTH_ERRORS; -export type AuthError = (typeof AUTH_ERRORS)[AuthErrorCode]; diff --git a/apps/bff/src/modules/auth/infra/login/login-session.service.ts b/apps/bff/src/modules/auth/infra/login/login-session.service.ts index e08399fe..c3c3ba23 100644 --- a/apps/bff/src/modules/auth/infra/login/login-session.service.ts +++ b/apps/bff/src/modules/auth/infra/login/login-session.service.ts @@ -91,7 +91,7 @@ export class LoginSessionService { * @param sessionToken - The session token (UUID) * @returns Session data if found and valid, null otherwise */ - async get(sessionToken: string): Promise { + private async get(sessionToken: string): Promise { const sessionData = await this.cache.get(this.buildKey(sessionToken)); if (!sessionData) { diff --git a/apps/bff/src/modules/auth/infra/otp/get-started-session.service.ts b/apps/bff/src/modules/auth/infra/otp/get-started-session.service.ts index 554872bf..c9ece409 100644 --- a/apps/bff/src/modules/auth/infra/otp/get-started-session.service.ts +++ b/apps/bff/src/modules/auth/infra/otp/get-started-session.service.ts @@ -173,25 +173,6 @@ export class GetStartedSessionService { this.logger.debug({ sessionId: sessionToken }, "Get-started session invalidated"); } - /** - * Validate that a session exists and email is verified - */ - async validateVerifiedSession(sessionToken: string): Promise { - const session = await this.get(sessionToken); - - if (!session) { - this.logger.warn({ sessionId: sessionToken }, "Session not found"); - return null; - } - - if (!session.emailVerified) { - this.logger.warn({ sessionId: sessionToken }, "Session email not verified"); - return null; - } - - return session; - } - // ============================================================================ // Guest Handoff Token Methods // ============================================================================ diff --git a/apps/bff/src/modules/auth/infra/otp/otp.service.ts b/apps/bff/src/modules/auth/infra/otp/otp.service.ts index 83d2852e..e4cb0c77 100644 --- a/apps/bff/src/modules/auth/infra/otp/otp.service.ts +++ b/apps/bff/src/modules/auth/infra/otp/otp.service.ts @@ -136,16 +136,6 @@ export class OtpService { return result; } - /** - * Check if an OTP exists for the given email (without consuming attempts) - */ - async exists(email: string): Promise { - const normalizedEmail = this.normalizeEmail(email); - const key = this.buildKey(normalizedEmail); - const otpData = await this.cache.get(key); - return otpData !== null; - } - /** * Delete any existing OTP for the given email */ diff --git a/apps/bff/src/modules/auth/infra/rate-limiting/auth-rate-limit.service.ts b/apps/bff/src/modules/auth/infra/rate-limiting/auth-rate-limit.service.ts index 492fdca8..598dbada 100644 --- a/apps/bff/src/modules/auth/infra/rate-limiting/auth-rate-limit.service.ts +++ b/apps/bff/src/modules/auth/infra/rate-limiting/auth-rate-limit.service.ts @@ -9,7 +9,6 @@ import { ConfigService } from "@nestjs/config"; import { Logger } from "nestjs-pino"; import type { Request } from "express"; import { RateLimiterRedis, RateLimiterRes } from "rate-limiter-flexible"; -import { createHash } from "crypto"; import type { Redis } from "ioredis"; import { extractErrorMessage } from "@bff/core/utils/error.util.js"; import { @@ -29,10 +28,6 @@ export interface RateLimitOutcome { export class AuthRateLimitService { private readonly loginLimiter: RateLimiterRedis; - private readonly refreshLimiter: RateLimiterRedis; - - private readonly signupLimiter: RateLimiterRedis; - private readonly passwordResetLimiter: RateLimiterRedis; private readonly loginCaptchaThreshold: number; @@ -47,29 +42,21 @@ export class AuthRateLimitService { const loginLimit = this.configService.get("LOGIN_RATE_LIMIT_LIMIT", 20); const loginTtlMs = this.configService.get("LOGIN_RATE_LIMIT_TTL", 300000); - const signupLimit = this.configService.get("SIGNUP_RATE_LIMIT_LIMIT", 5); - const signupTtlMs = this.configService.get("SIGNUP_RATE_LIMIT_TTL", 900000); - const passwordResetLimit = this.configService.get("PASSWORD_RESET_RATE_LIMIT_LIMIT", 5); const passwordResetTtlMs = this.configService.get( "PASSWORD_RESET_RATE_LIMIT_TTL", 900000 ); - const refreshLimit = this.configService.get("AUTH_REFRESH_RATE_LIMIT_LIMIT", 10); - const refreshTtlMs = this.configService.get("AUTH_REFRESH_RATE_LIMIT_TTL", 300000); - this.loginCaptchaThreshold = this.configService.get("LOGIN_CAPTCHA_AFTER_ATTEMPTS", 5); this.captchaAlwaysOn = this.configService.get("AUTH_CAPTCHA_ALWAYS_ON", "false") === "true"; this.loginLimiter = this.createLimiter("auth-login", loginLimit, loginTtlMs); - this.signupLimiter = this.createLimiter("auth-signup", signupLimit, signupTtlMs); this.passwordResetLimiter = this.createLimiter( "auth-password-reset", passwordResetLimit, passwordResetTtlMs ); - this.refreshLimiter = this.createLimiter("auth-refresh", refreshLimit, refreshTtlMs); } async consumeLoginAttempt(request: Request): Promise { @@ -86,31 +73,12 @@ export class AuthRateLimitService { await this.deleteKey(this.loginLimiter, key, "login"); } - async consumeSignupAttempt(request: Request): Promise { - const key = this.buildKey("signup", request); - const outcome = await this.consume(this.signupLimiter, key, "signup"); - return { ...outcome, needsCaptcha: false }; - } - async consumePasswordReset(request: Request): Promise { const key = this.buildKey("password-reset", request); const outcome = await this.consume(this.passwordResetLimiter, key, "password-reset"); return { ...outcome, needsCaptcha: false }; } - async consumeRefreshAttempt(request: Request, refreshToken: string): Promise { - const tokenHash = this.hashToken(refreshToken); - const key = this.buildKey("refresh", request, tokenHash); - return this.consume(this.refreshLimiter, key, "refresh"); - } - - getCaptchaHeaderValue(needsCaptcha: boolean): string { - if (needsCaptcha || this.captchaAlwaysOn) { - return "required"; - } - return "optional"; - } - private requiresCaptcha(consumedPoints: number): boolean { if (this.loginCaptchaThreshold <= 0) { return false; @@ -208,8 +176,4 @@ export class AuthRateLimitService { const timeMessage = remainingSeconds ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`; return `Too many ${context} attempts. Try again in ${timeMessage}.`; } - - private hashToken(token: string): string { - return createHash("sha256").update(token).digest("hex"); - } } diff --git a/apps/bff/src/modules/auth/infra/token/index.ts b/apps/bff/src/modules/auth/infra/token/index.ts index cf1bcfd5..2eac24a0 100644 --- a/apps/bff/src/modules/auth/infra/token/index.ts +++ b/apps/bff/src/modules/auth/infra/token/index.ts @@ -5,6 +5,7 @@ */ export type { RefreshTokenPayload, DeviceInfo } from "./token.types.js"; +export { ACCESS_TOKEN_EXPIRY, REFRESH_TOKEN_EXPIRY } from "./token.constants.js"; export { AuthTokenService } from "./token.service.js"; export { TokenGeneratorService } from "./token-generator.service.js"; export { TokenRefreshService } from "./token-refresh.service.js"; diff --git a/apps/bff/src/modules/auth/infra/token/password-reset-token.service.ts b/apps/bff/src/modules/auth/infra/token/password-reset-token.service.ts index 8b6fb8e2..7dab83a3 100644 --- a/apps/bff/src/modules/auth/infra/token/password-reset-token.service.ts +++ b/apps/bff/src/modules/auth/infra/token/password-reset-token.service.ts @@ -145,31 +145,6 @@ export class PasswordResetTokenService { return { userId: payload.sub }; } - /** - * Invalidate a password reset token without using it - * - * Useful for cleanup or when a user requests a new token - * - * @param tokenId - The token ID to invalidate - */ - async invalidate(tokenId: string): Promise { - await this.cache.del(this.buildKey(tokenId)); - this.logger.log({ tokenId }, "Password reset token invalidated"); - } - - /** - * Invalidate all password reset tokens for a user - * - * Useful when password is changed through another method - * - * @param userId - The user ID to invalidate tokens for - */ - async invalidateAllForUser(userId: string): Promise { - // Use pattern-based deletion - await this.cache.delPattern(`${this.REDIS_PREFIX}*`); - this.logger.log({ userId }, "All password reset tokens invalidated for user"); - } - /** * Build Redis key for token storage */ diff --git a/apps/bff/src/modules/auth/infra/token/token-blacklist.service.ts b/apps/bff/src/modules/auth/infra/token/token-blacklist.service.ts index e514371d..cdef36fa 100644 --- a/apps/bff/src/modules/auth/infra/token/token-blacklist.service.ts +++ b/apps/bff/src/modules/auth/infra/token/token-blacklist.service.ts @@ -1,8 +1,8 @@ import { Injectable, Inject, ServiceUnavailableException } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; -import { createHash } from "crypto"; import { Redis } from "ioredis"; import { Logger } from "nestjs-pino"; +import { hashToken } from "../../utils/hash-token.util.js"; import { parseJwtExpiry } from "../../utils/jwt-expiry.util.js"; import { JoseJwtService } from "./jose-jwt.service.js"; @@ -108,10 +108,6 @@ export class TokenBlacklistService { } private buildBlacklistKey(token: string): string { - return `blacklist:${this.hashToken(token)}`; - } - - private hashToken(token: string): string { - return createHash("sha256").update(token).digest("hex"); + return `blacklist:${hashToken(token)}`; } } diff --git a/apps/bff/src/modules/auth/infra/token/token-generator.service.ts b/apps/bff/src/modules/auth/infra/token/token-generator.service.ts index 075f9c61..a09411d7 100644 --- a/apps/bff/src/modules/auth/infra/token/token-generator.service.ts +++ b/apps/bff/src/modules/auth/infra/token/token-generator.service.ts @@ -6,10 +6,13 @@ import { } from "@nestjs/common"; import { Redis } from "ioredis"; import { Logger } from "nestjs-pino"; -import { randomBytes, createHash } from "crypto"; +import { randomBytes } from "crypto"; import type { AuthTokens } from "@customer-portal/domain/auth"; import type { UserRole } from "@customer-portal/domain/customer"; +import { parseJwtExpiry, parseJwtExpiryMs } from "../../utils/jwt-expiry.util.js"; +import { hashToken } from "../../utils/hash-token.util.js"; import type { RefreshTokenPayload, DeviceInfo } from "./token.types.js"; +import { ACCESS_TOKEN_EXPIRY, REFRESH_TOKEN_EXPIRY } from "./token.constants.js"; import { JoseJwtService } from "./jose-jwt.service.js"; import { TokenStorageService } from "./token-storage.service.js"; @@ -22,9 +25,6 @@ const ERROR_SERVICE_UNAVAILABLE = "Authentication service temporarily unavailabl */ @Injectable() export class TokenGeneratorService { - readonly ACCESS_TOKEN_EXPIRY = "15m"; - readonly REFRESH_TOKEN_EXPIRY = "7d"; - constructor( private readonly jwtService: JoseJwtService, private readonly storage: TokenStorageService, @@ -77,12 +77,12 @@ export class TokenGeneratorService { type: "refresh", }; - const accessToken = await this.jwtService.sign(accessPayload, this.ACCESS_TOKEN_EXPIRY); + const accessToken = await this.jwtService.sign(accessPayload, ACCESS_TOKEN_EXPIRY); - const refreshExpirySeconds = this.parseExpiryToSeconds(this.REFRESH_TOKEN_EXPIRY); + const refreshExpirySeconds = parseJwtExpiry(REFRESH_TOKEN_EXPIRY); const refreshToken = await this.jwtService.sign(refreshPayload, refreshExpirySeconds); - const refreshTokenHash = this.hashToken(refreshToken); + const refreshTokenHash = hashToken(refreshToken); const refreshAbsoluteExpiresAt = new Date( Date.now() + refreshExpirySeconds * 1000 ).toISOString(); @@ -112,7 +112,7 @@ export class TokenGeneratorService { } const accessExpiresAt = new Date( - Date.now() + this.parseExpiryToMs(this.ACCESS_TOKEN_EXPIRY) + Date.now() + parseJwtExpiryMs(ACCESS_TOKEN_EXPIRY) ).toISOString(); const refreshExpiresAt = refreshAbsoluteExpiresAt; @@ -161,9 +161,9 @@ export class TokenGeneratorService { type: "refresh", }; - const newAccessToken = await this.jwtService.sign(accessPayload, this.ACCESS_TOKEN_EXPIRY); + const newAccessToken = await this.jwtService.sign(accessPayload, ACCESS_TOKEN_EXPIRY); const newRefreshToken = await this.jwtService.sign(newRefreshPayload, remainingSeconds); - const newRefreshTokenHash = this.hashToken(newRefreshToken); + const newRefreshTokenHash = hashToken(newRefreshToken); return { newAccessToken, newRefreshToken, newRefreshTokenHash }; } @@ -171,30 +171,4 @@ export class TokenGeneratorService { generateTokenId(): string { return randomBytes(32).toString("hex"); } - - hashToken(token: string): string { - return createHash("sha256").update(token).digest("hex"); - } - - parseExpiryToMs(expiry: string): number { - const unit = expiry.slice(-1); - const value = Number.parseInt(expiry.slice(0, -1)); - - switch (unit) { - case "s": - return value * 1000; - case "m": - return value * 60 * 1000; - case "h": - return value * 60 * 60 * 1000; - case "d": - return value * 24 * 60 * 60 * 1000; - default: - return 15 * 60 * 1000; - } - } - - parseExpiryToSeconds(expiry: string): number { - return Math.floor(this.parseExpiryToMs(expiry) / 1000); - } } diff --git a/apps/bff/src/modules/auth/infra/token/token-migration.service.ts b/apps/bff/src/modules/auth/infra/token/token-migration.service.ts deleted file mode 100644 index ca6f1809..00000000 --- a/apps/bff/src/modules/auth/infra/token/token-migration.service.ts +++ /dev/null @@ -1,452 +0,0 @@ -import { Injectable, Inject, ServiceUnavailableException } from "@nestjs/common"; -import { Logger } from "nestjs-pino"; -import { Redis } from "ioredis"; -import { extractErrorMessage } from "@bff/core/utils/error.util.js"; - -export interface MigrationStats { - totalKeysScanned: number; - familiesFound: number; - familiesMigrated: number; - tokensFound: number; - tokensMigrated: number; - orphanedTokens: number; - errors: number; - duration: number; -} - -interface ParsedFamily { - userId: string; -} - -interface ParsedToken { - userId: string; - familyId: string; -} - -@Injectable() -export class TokenMigrationService { - private readonly REFRESH_TOKEN_FAMILY_PREFIX = "refresh_family:"; - private readonly REFRESH_TOKEN_PREFIX = "refresh_token:"; - private readonly REFRESH_USER_SET_PREFIX = "refresh_user:"; - - constructor( - @Inject("REDIS_CLIENT") private readonly redis: Redis, - @Inject(Logger) private readonly logger: Logger - ) {} - - /** - * Migrate existing refresh tokens to the new per-user token set structure - */ - async migrateExistingTokens(dryRun = true): Promise { - const startTime = Date.now(); - const stats: MigrationStats = { - totalKeysScanned: 0, - familiesFound: 0, - familiesMigrated: 0, - tokensFound: 0, - tokensMigrated: 0, - orphanedTokens: 0, - errors: 0, - duration: 0, - }; - - this.logger.log("Starting token migration", { dryRun }); - - if (this.redis.status !== "ready") { - throw new ServiceUnavailableException("Redis is not ready for migration"); - } - - try { - // First, scan for all refresh token families - await this.migrateFamilies(stats, dryRun); - - // Then, scan for orphaned tokens (tokens without families) - await this.migrateOrphanedTokens(stats, dryRun); - - stats.duration = Date.now() - startTime; - - this.logger.log("Token migration completed", { - dryRun, - stats, - }); - - return stats; - } catch (error) { - stats.duration = Date.now() - startTime; - stats.errors++; - - this.logger.error("Token migration failed", { - error: extractErrorMessage(error), - stats, - }); - - throw error; - } - } - - /** - * Migrate refresh token families to per-user sets - */ - private async migrateFamilies(stats: MigrationStats, dryRun: boolean): Promise { - let cursor = "0"; - const pattern = `${this.REFRESH_TOKEN_FAMILY_PREFIX}*`; - - do { - // eslint-disable-next-line no-await-in-loop -- Redis SCAN requires sequential cursor iteration - const [nextCursor, keys] = await this.redis.scan(cursor, "MATCH", pattern, "COUNT", 100); - cursor = nextCursor; - stats.totalKeysScanned += keys.length; - - if (keys && keys.length > 0) { - // eslint-disable-next-line no-await-in-loop -- Batch processing within SCAN loop - const results = await Promise.allSettled( - keys.map(async key => this.migrateSingleFamily(key, stats, dryRun)) - ); - this.countSettledErrors(results, stats, "Failed to migrate family"); - } - } while (cursor !== "0"); - } - - /** - * Migrate a single refresh token family - */ - private async migrateSingleFamily( - familyKey: string, - stats: MigrationStats, - dryRun: boolean - ): Promise { - const familyData = await this.redis.get(familyKey); - if (!familyData) { - return; - } - - stats.familiesFound++; - - const family = this.parseFamilyData(familyKey, familyData, stats); - if (!family) { - return; - } - - const familyId = familyKey.replace(this.REFRESH_TOKEN_FAMILY_PREFIX, ""); - const userFamilySetKey = `${this.REFRESH_USER_SET_PREFIX}${family.userId}`; - - // Check if this family is already in the user's set - const isAlreadyMigrated = await this.redis.sismember(userFamilySetKey, familyId); - - if (isAlreadyMigrated) { - this.logger.debug("Family already migrated", { familyKey, userId: family.userId }); - return; - } - - if (!dryRun) { - // Add family to user's token set - const pipeline = this.redis.pipeline(); - pipeline.sadd(userFamilySetKey, familyId); - - // Set expiration on the user set (use the same TTL as the family) - const ttl = await this.redis.ttl(familyKey); - if (ttl > 0) { - pipeline.expire(userFamilySetKey, ttl); - } - - await pipeline.exec(); - } - - stats.familiesMigrated++; - - this.logger.debug("Migrated family to user set", { - familyKey, - userId: family.userId, - dryRun, - }); - } - - /** - * Migrate orphaned tokens (tokens without corresponding families) - */ - private async migrateOrphanedTokens(stats: MigrationStats, dryRun: boolean): Promise { - let cursor = "0"; - const pattern = `${this.REFRESH_TOKEN_PREFIX}*`; - - do { - // eslint-disable-next-line no-await-in-loop -- Redis SCAN requires sequential cursor iteration - const [nextCursor, keys] = await this.redis.scan(cursor, "MATCH", pattern, "COUNT", 100); - cursor = nextCursor; - stats.totalKeysScanned += keys.length; - - if (keys && keys.length > 0) { - // eslint-disable-next-line no-await-in-loop -- Batch processing within SCAN loop - const results = await Promise.allSettled( - keys.map(async key => this.migrateSingleToken(key, stats, dryRun)) - ); - this.countSettledErrors(results, stats, "Failed to migrate token"); - } - } while (cursor !== "0"); - } - - /** - * Migrate a single refresh token - */ - private async migrateSingleToken( - tokenKey: string, - stats: MigrationStats, - dryRun: boolean - ): Promise { - const tokenData = await this.redis.get(tokenKey); - if (!tokenData) { - return; - } - - stats.tokensFound++; - - const token = this.parseTokenData(tokenKey, tokenData, stats); - if (!token) { - return; - } - - // Check if the corresponding family exists - const familyKey = `${this.REFRESH_TOKEN_FAMILY_PREFIX}${token.familyId}`; - const familyExists = await this.redis.exists(familyKey); - - if (!familyExists) { - stats.orphanedTokens++; - this.logger.warn("Found orphaned token (no corresponding family)", { - tokenKey, - familyId: token.familyId, - userId: token.userId, - }); - - if (!dryRun) { - // Remove orphaned token - await this.redis.del(tokenKey); - this.logger.debug("Removed orphaned token", { tokenKey }); - } - return; - } - - // Check if this token's family is already in the user's set - const userFamilySetKey = `${this.REFRESH_USER_SET_PREFIX}${token.userId}`; - const isAlreadyMigrated = await this.redis.sismember(userFamilySetKey, token.familyId); - - if (isAlreadyMigrated) { - stats.tokensMigrated++; - return; - } - - if (!dryRun) { - // Add family to user's token set - const pipeline = this.redis.pipeline(); - pipeline.sadd(userFamilySetKey, token.familyId); - - // Set expiration on the user set (use the same TTL as the token) - const ttl = await this.redis.ttl(tokenKey); - if (ttl > 0) { - pipeline.expire(userFamilySetKey, ttl); - } - - await pipeline.exec(); - } - - stats.tokensMigrated++; - - this.logger.debug("Migrated token family to user set", { - tokenKey, - familyId: token.familyId, - userId: token.userId, - dryRun, - }); - } - - /** - * Clean up orphaned tokens and expired families - */ - async cleanupOrphanedTokens(dryRun = true): Promise<{ removed: number; errors: number }> { - const stats = { removed: 0, errors: 0 }; - - this.logger.log("Starting orphaned token cleanup", { dryRun }); - - let cursor = "0"; - const pattern = `${this.REFRESH_TOKEN_PREFIX}*`; - - do { - // eslint-disable-next-line no-await-in-loop -- Redis SCAN requires sequential cursor iteration - const [nextCursor, keys] = await this.redis.scan(cursor, "MATCH", pattern, "COUNT", 100); - cursor = nextCursor; - - if (keys && keys.length > 0) { - // eslint-disable-next-line no-await-in-loop -- Batch processing within SCAN loop - const results = await Promise.allSettled( - keys.map(async key => this.cleanupSingleToken(key, stats, dryRun)) - ); - this.countSettledErrors(results, stats, "Failed to cleanup token"); - } - } while (cursor !== "0"); - - this.logger.log("Orphaned token cleanup completed", { dryRun, stats }); - return stats; - } - - /** - * Clean up a single orphaned token - */ - private async cleanupSingleToken( - key: string, - stats: { removed: number; errors: number }, - dryRun: boolean - ): Promise { - const tokenData = await this.redis.get(key); - if (!tokenData) { - return; - } - - const token = this.parseTokenData(key, tokenData, stats); - if (!token) { - return; - } - - const familyKey = `${this.REFRESH_TOKEN_FAMILY_PREFIX}${token.familyId}`; - const familyExists = await this.redis.exists(familyKey); - - if (familyExists) { - return; - } - - if (!dryRun) { - await this.redis.del(key); - } - stats.removed++; - - this.logger.debug("Removed orphaned token", { - tokenKey: key, - familyId: token.familyId, - dryRun, - }); - } - - /** - * Get migration status and statistics - */ - async getMigrationStatus(): Promise<{ - totalFamilies: number; - totalTokens: number; - migratedUsers: number; - orphanedTokens: number; - needsMigration: boolean; - }> { - const [totalFamilies, totalTokens, migratedUsers] = await Promise.all([ - this.countKeysByPattern(`${this.REFRESH_TOKEN_FAMILY_PREFIX}*`), - this.countKeysByPattern(`${this.REFRESH_TOKEN_PREFIX}*`), - this.countKeysByPattern(`${this.REFRESH_USER_SET_PREFIX}*`), - ]); - - return { - totalFamilies, - totalTokens, - migratedUsers, - orphanedTokens: 0, - needsMigration: totalFamilies > 0 && migratedUsers === 0, - }; - } - - /** - * Count keys matching a pattern using Redis SCAN - */ - private async countKeysByPattern(pattern: string): Promise { - let count = 0; - let cursor = "0"; - - do { - // eslint-disable-next-line no-await-in-loop -- Redis SCAN requires sequential cursor iteration - const [nextCursor, keys] = await this.redis.scan(cursor, "MATCH", pattern, "COUNT", 100); - cursor = nextCursor; - count += keys.length; - } while (cursor !== "0"); - - return count; - } - - private parseFamilyData( - familyKey: string, - raw: string, - tracker: { errors: number } - ): ParsedFamily | null { - let parsed: unknown; - try { - parsed = JSON.parse(raw); - } catch (error) { - tracker.errors++; - this.logger.error("Failed to parse family data", { - familyKey, - error: extractErrorMessage(error), - }); - return null; - } - - if (!parsed || typeof parsed !== "object") { - this.logger.warn("Invalid family structure, skipping", { familyKey }); - return null; - } - - const record = parsed as Record; - const userId = record["userId"]; - - if (typeof userId !== "string" || userId.length === 0) { - this.logger.warn("Invalid family structure, skipping", { familyKey }); - return null; - } - - return { userId }; - } - - private parseTokenData( - tokenKey: string, - raw: string, - tracker: { errors: number } - ): ParsedToken | null { - let parsed: unknown; - try { - parsed = JSON.parse(raw); - } catch (error) { - tracker.errors++; - this.logger.error("Failed to parse token data", { - tokenKey, - error: extractErrorMessage(error), - }); - return null; - } - - if (!parsed || typeof parsed !== "object") { - this.logger.warn("Invalid token structure, skipping", { tokenKey }); - return null; - } - - const record = parsed as Record; - const userId = record["userId"]; - const familyId = record["familyId"]; - - if (typeof userId !== "string" || typeof familyId !== "string") { - this.logger.warn("Invalid token structure, skipping", { tokenKey }); - return null; - } - - return { userId, familyId }; - } - - /** - * Count and log errors from Promise.allSettled results - */ - private countSettledErrors( - results: PromiseSettledResult[], - stats: { errors: number }, - message: string - ): void { - for (const result of results) { - if (result.status === "rejected") { - stats.errors++; - this.logger.error(message, { - error: extractErrorMessage(result.reason), - }); - } - } - } -} diff --git a/apps/bff/src/modules/auth/infra/token/token-refresh.service.ts b/apps/bff/src/modules/auth/infra/token/token-refresh.service.ts index dd4066bf..f966fb0c 100644 --- a/apps/bff/src/modules/auth/infra/token/token-refresh.service.ts +++ b/apps/bff/src/modules/auth/infra/token/token-refresh.service.ts @@ -11,7 +11,10 @@ import type { AuthTokens } from "@customer-portal/domain/auth"; import type { UserAuth } from "@customer-portal/domain/customer"; import { UsersService } from "@bff/modules/users/application/users.service.js"; import { mapPrismaUserToDomain } from "@bff/infra/mappers/index.js"; +import { parseJwtExpiry, parseJwtExpiryMs } from "../../utils/jwt-expiry.util.js"; +import { hashToken } from "../../utils/hash-token.util.js"; import type { RefreshTokenPayload, DeviceInfo } from "./token.types.js"; +import { ACCESS_TOKEN_EXPIRY, REFRESH_TOKEN_EXPIRY } from "./token.constants.js"; import { TokenGeneratorService } from "./token-generator.service.js"; import { TokenStorageService } from "./token-storage.service.js"; import { TokenRevocationService } from "./token-revocation.service.js"; @@ -87,7 +90,7 @@ export class TokenRefreshService { ): Promise { const payload = await this.verifyRefreshTokenPayload(refreshToken); const familyId = this.extractFamilyId(payload); - const refreshTokenHash = this.generator.hashToken(refreshToken); + const refreshTokenHash = hashToken(refreshToken); await this.validateStoredToken(refreshTokenHash, familyId, payload); const { remainingSeconds, absoluteExpiresAt, createdAt } = await this.calculateTokenLifetime( @@ -235,7 +238,7 @@ export class TokenRefreshService { const tokenTtl = await this.storage.getTtl(tokenKey); return typeof tokenTtl === "number" && tokenTtl > 0 ? tokenTtl - : this.generator.parseExpiryToSeconds(this.generator.REFRESH_TOKEN_EXPIRY); + : parseJwtExpiry(REFRESH_TOKEN_EXPIRY); } private async performTokenRotation( @@ -283,7 +286,7 @@ export class TokenRefreshService { } const accessExpiresAt = new Date( - Date.now() + this.generator.parseExpiryToMs(this.generator.ACCESS_TOKEN_EXPIRY) + Date.now() + parseJwtExpiryMs(ACCESS_TOKEN_EXPIRY) ).toISOString(); this.logger.debug("Refreshed token pair", { userId: context.payload.userId }); diff --git a/apps/bff/src/modules/auth/infra/token/token-revocation.service.ts b/apps/bff/src/modules/auth/infra/token/token-revocation.service.ts index 194bde77..e1142ae7 100644 --- a/apps/bff/src/modules/auth/infra/token/token-revocation.service.ts +++ b/apps/bff/src/modules/auth/infra/token/token-revocation.service.ts @@ -1,6 +1,6 @@ import { Injectable, Inject } from "@nestjs/common"; import { Logger } from "nestjs-pino"; -import { createHash } from "crypto"; +import { hashToken } from "../../utils/hash-token.util.js"; import { TokenStorageService } from "./token-storage.service.js"; /** @@ -23,7 +23,7 @@ export class TokenRevocationService { */ async revokeRefreshToken(refreshToken: string): Promise { try { - const tokenHash = this.hashToken(refreshToken); + const tokenHash = hashToken(refreshToken); const tokenRecord = await this.storage.getTokenByHash(tokenHash); if (tokenRecord) { @@ -212,8 +212,4 @@ export class TokenRevocationService { return []; } } - - private hashToken(token: string): string { - return createHash("sha256").update(token).digest("hex"); - } } diff --git a/apps/bff/src/modules/auth/infra/token/token-storage.service.ts b/apps/bff/src/modules/auth/infra/token/token-storage.service.ts index 1ed0ce6c..7604be5b 100644 --- a/apps/bff/src/modules/auth/infra/token/token-storage.service.ts +++ b/apps/bff/src/modules/auth/infra/token/token-storage.service.ts @@ -27,16 +27,6 @@ export interface StoreRefreshTokenParams { absoluteExpiresAt?: string; } -export interface UpdateFamilyParams { - familyId: string; - userId: string; - newTokenHash: string; - deviceInfo?: DeviceInfo; - createdAt: string; - absoluteExpiresAt: string; - ttlSeconds: number; -} - export interface AtomicTokenRotationParams { oldTokenHash: string; newTokenHash: string; @@ -223,19 +213,6 @@ export class TokenStorageService { return { token, family }; } - /** - * Mark a token as invalid (used during rotation) - */ - async markTokenInvalid( - tokenHash: string, - familyId: string, - userId: string, - ttlSeconds: number - ): Promise { - const key = `${this.REFRESH_TOKEN_PREFIX}${tokenHash}`; - await this.redis.setex(key, ttlSeconds, JSON.stringify({ familyId, userId, valid: false })); - } - /** * Atomically validate and rotate a refresh token. * Uses Redis Lua script to prevent race conditions during concurrent refresh requests. @@ -338,35 +315,6 @@ export class TokenStorageService { return { success: false, error: result[1] }; } - /** - * Update token family with new token - */ - async updateFamily(params: UpdateFamilyParams): Promise { - const { familyId, userId, newTokenHash, deviceInfo, createdAt, absoluteExpiresAt, ttlSeconds } = - params; - const familyKey = `${this.REFRESH_TOKEN_FAMILY_PREFIX}${familyId}`; - const tokenKey = `${this.REFRESH_TOKEN_PREFIX}${newTokenHash}`; - const userFamilySetKey = `${this.REFRESH_USER_SET_PREFIX}${userId}`; - - const pipeline = this.redis.pipeline(); - pipeline.setex( - familyKey, - ttlSeconds, - JSON.stringify({ - userId, - tokenHash: newTokenHash, - deviceId: deviceInfo?.deviceId, - userAgent: deviceInfo?.userAgent, - createdAt, - absoluteExpiresAt, - }) - ); - pipeline.setex(tokenKey, ttlSeconds, JSON.stringify({ familyId, userId, valid: true })); - pipeline.sadd(userFamilySetKey, familyId); - pipeline.expire(userFamilySetKey, ttlSeconds); - await pipeline.exec(); - } - /** * Delete a token by hash */ diff --git a/apps/bff/src/modules/auth/infra/token/token.service.ts b/apps/bff/src/modules/auth/infra/token/token.service.ts index 8201a265..05f218b3 100644 --- a/apps/bff/src/modules/auth/infra/token/token.service.ts +++ b/apps/bff/src/modules/auth/infra/token/token.service.ts @@ -84,28 +84,9 @@ export class AuthTokenService { } /** - * 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 + * Revoke all refresh tokens for a user. + * Intentionally skips `checkServiceAvailability()` — logout must succeed + * even during maintenance windows so users are never locked into a session. */ async revokeAllUserTokens(userId: string): Promise { return this.revocation.revokeAllUserTokens(userId); diff --git a/apps/bff/src/modules/auth/infra/workflows/steps/create-whmcs-client.step.ts b/apps/bff/src/modules/auth/infra/workflows/steps/create-whmcs-client.step.ts index d2b11158..892f39fd 100644 --- a/apps/bff/src/modules/auth/infra/workflows/steps/create-whmcs-client.step.ts +++ b/apps/bff/src/modules/auth/infra/workflows/steps/create-whmcs-client.step.ts @@ -38,26 +38,32 @@ export interface CreateWhmcsClientResult { */ @Injectable() export class CreateWhmcsClientStep { + private readonly customerNumberFieldId: string | undefined; + private readonly dobFieldId: string | undefined; + private readonly genderFieldId: string | undefined; + private readonly nationalityFieldId: string | undefined; + constructor( private readonly whmcsClientService: WhmcsClientService, - private readonly configService: ConfigService, + configService: ConfigService, @Inject(Logger) private readonly logger: Logger - ) {} + ) { + this.customerNumberFieldId = configService.get("WHMCS_CUSTOMER_NUMBER_FIELD_ID"); + this.dobFieldId = configService.get("WHMCS_DOB_FIELD_ID"); + this.genderFieldId = configService.get("WHMCS_GENDER_FIELD_ID"); + this.nationalityFieldId = configService.get("WHMCS_NATIONALITY_FIELD_ID"); + } async execute(params: CreateWhmcsClientParams): Promise { - const customerNumberFieldId = this.configService.get("WHMCS_CUSTOMER_NUMBER_FIELD_ID"); - const dobFieldId = this.configService.get("WHMCS_DOB_FIELD_ID"); - const genderFieldId = this.configService.get("WHMCS_GENDER_FIELD_ID"); - const nationalityFieldId = this.configService.get("WHMCS_NATIONALITY_FIELD_ID"); - const customfieldsMap: Record = {}; - if (customerNumberFieldId && params.customerNumber) { - customfieldsMap[customerNumberFieldId] = params.customerNumber; + if (this.customerNumberFieldId && params.customerNumber) { + customfieldsMap[this.customerNumberFieldId] = params.customerNumber; } - if (dobFieldId && params.dateOfBirth) customfieldsMap[dobFieldId] = params.dateOfBirth; - if (genderFieldId && params.gender) customfieldsMap[genderFieldId] = params.gender; - if (nationalityFieldId && params.nationality) - customfieldsMap[nationalityFieldId] = params.nationality; + if (this.dobFieldId && params.dateOfBirth) + customfieldsMap[this.dobFieldId] = params.dateOfBirth; + if (this.genderFieldId && params.gender) customfieldsMap[this.genderFieldId] = params.gender; + if (this.nationalityFieldId && params.nationality) + customfieldsMap[this.nationalityFieldId] = params.nationality; this.logger.log( { @@ -102,9 +108,7 @@ export class CreateWhmcsClientStep { }, "Failed to create WHMCS client" ); - throw new BadRequestException( - `Failed to create billing account: ${extractErrorMessage(whmcsError)}` - ); + throw new BadRequestException("Failed to create billing account"); } } diff --git a/apps/bff/src/modules/auth/presentation/http/auth.controller.ts b/apps/bff/src/modules/auth/presentation/http/auth.controller.ts index 2acb89f2..b2cf2df7 100644 --- a/apps/bff/src/modules/auth/presentation/http/auth.controller.ts +++ b/apps/bff/src/modules/auth/presentation/http/auth.controller.ts @@ -22,7 +22,7 @@ import { import { LoginResultInterceptor } from "./interceptors/login-result.interceptor.js"; import { Public, OptionalAuth } from "../../decorators/public.decorator.js"; import { createZodDto, ZodResponse } from "nestjs-zod"; -import type { RequestWithUser } from "@bff/modules/auth/auth.types.js"; +import type { RequestWithUser, RequestWithCookies } from "@bff/modules/auth/auth.types.js"; import { JoseJwtService } from "../../infra/token/jose-jwt.service.js"; import { LoginOtpWorkflowService } from "../../infra/workflows/login-otp-workflow.service.js"; import { PasswordWorkflowService } from "../../infra/workflows/password-workflow.service.js"; @@ -59,11 +59,6 @@ import { loginVerifyOtpRequestSchema, } from "@customer-portal/domain/auth"; -type CookieValue = string | undefined; -type RequestWithCookies = Omit & { - cookies?: Record; -}; - // Re-export for backward compatibility with tests export { ACCESS_COOKIE_PATH, REFRESH_COOKIE_PATH, TOKEN_TYPE }; diff --git a/apps/bff/src/modules/auth/presentation/http/guards/local-auth.guard.ts b/apps/bff/src/modules/auth/presentation/http/guards/local-auth.guard.ts index 3e0ae98f..172dca60 100644 --- a/apps/bff/src/modules/auth/presentation/http/guards/local-auth.guard.ts +++ b/apps/bff/src/modules/auth/presentation/http/guards/local-auth.guard.ts @@ -1,12 +1,12 @@ import { Injectable, UnauthorizedException } from "@nestjs/common"; import type { CanActivate, ExecutionContext } from "@nestjs/common"; import type { Request } from "express"; -import { AuthOrchestrator } from "@bff/modules/auth/application/auth-orchestrator.service.js"; +import { AuthLoginService } from "@bff/modules/auth/application/auth-login.service.js"; import { ErrorCode } from "@customer-portal/domain/common"; @Injectable() export class LocalAuthGuard implements CanActivate { - constructor(private readonly authOrchestrator: AuthOrchestrator) {} + constructor(private readonly loginService: AuthLoginService) {} async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); @@ -22,7 +22,7 @@ export class LocalAuthGuard implements CanActivate { }); } - const user = await this.authOrchestrator.validateUser(email, password, request); + const user = await this.loginService.validateUser(email, password, request); if (!user) { throw new UnauthorizedException({ message: "Invalid credentials", diff --git a/apps/bff/src/modules/auth/presentation/http/utils/trusted-device-cookie.util.ts b/apps/bff/src/modules/auth/presentation/http/utils/trusted-device-cookie.util.ts index 1020b254..ab021024 100644 --- a/apps/bff/src/modules/auth/presentation/http/utils/trusted-device-cookie.util.ts +++ b/apps/bff/src/modules/auth/presentation/http/utils/trusted-device-cookie.util.ts @@ -1,4 +1,5 @@ -import type { Request, Response } from "express"; +import type { Response } from "express"; +import type { RequestWithCookies } from "@bff/modules/auth/auth.types.js"; import { getSecureCookie } from "./secure-cookie.util.js"; /** @@ -67,13 +68,6 @@ export function clearTrustedDeviceCookie(res: Response): void { } } -/** - * Request type with cookies - */ -type RequestWithCookies = Omit & { - cookies?: Record; -}; - /** * Get the trusted device token from the request cookies * diff --git a/apps/bff/src/modules/auth/presentation/index.ts b/apps/bff/src/modules/auth/presentation/index.ts deleted file mode 100644 index 0dce6867..00000000 --- a/apps/bff/src/modules/auth/presentation/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./http/auth.controller.js"; diff --git a/apps/bff/src/modules/auth/tokens/tokens.module.ts b/apps/bff/src/modules/auth/tokens/tokens.module.ts index 0137baa1..85043582 100644 --- a/apps/bff/src/modules/auth/tokens/tokens.module.ts +++ b/apps/bff/src/modules/auth/tokens/tokens.module.ts @@ -8,7 +8,6 @@ import { TokenRevocationService } from "../infra/token/token-revocation.service. import { TokenBlacklistService } from "../infra/token/token-blacklist.service.js"; import { AuthTokenService } from "../infra/token/token.service.js"; import { PasswordResetTokenService } from "../infra/token/password-reset-token.service.js"; -import { TokenMigrationService } from "../infra/token/token-migration.service.js"; @Module({ imports: [UsersModule], @@ -21,15 +20,7 @@ import { TokenMigrationService } from "../infra/token/token-migration.service.js TokenBlacklistService, AuthTokenService, PasswordResetTokenService, - TokenMigrationService, - ], - exports: [ - JoseJwtService, - AuthTokenService, - TokenBlacklistService, - TokenRefreshService, - PasswordResetTokenService, - TokenMigrationService, ], + exports: [JoseJwtService, AuthTokenService, TokenBlacklistService, PasswordResetTokenService], }) export class TokensModule {} diff --git a/apps/bff/src/modules/auth/utils/hash-token.util.ts b/apps/bff/src/modules/auth/utils/hash-token.util.ts new file mode 100644 index 00000000..6feb1d72 --- /dev/null +++ b/apps/bff/src/modules/auth/utils/hash-token.util.ts @@ -0,0 +1,9 @@ +import { createHash } from "crypto"; + +/** + * SHA-256 hash a token string to a hex digest. + * Used consistently for refresh token hashing and blacklist keys. + */ +export function hashToken(token: string): string { + return createHash("sha256").update(token).digest("hex"); +} diff --git a/apps/bff/src/modules/auth/utils/jwt-expiry.util.ts b/apps/bff/src/modules/auth/utils/jwt-expiry.util.ts index ab227a98..49d720b2 100644 --- a/apps/bff/src/modules/auth/utils/jwt-expiry.util.ts +++ b/apps/bff/src/modules/auth/utils/jwt-expiry.util.ts @@ -51,12 +51,9 @@ export const parseJwtExpiry = (expiresIn: string | number | undefined | null): n } }; -export const calculateExpiryDate = ( - expiresIn: string | number | undefined | null, - referenceDate: number = Date.now() -): string => { - const seconds = parseJwtExpiry(expiresIn); - return new Date(referenceDate + seconds * 1000).toISOString(); -}; - -export const JWT_EXPIRY_DEFAULT_SECONDS = DEFAULT_JWT_EXPIRY_SECONDS; +/** + * Parse a JWT expiry configuration string into milliseconds. + * Convenience wrapper over `parseJwtExpiry`. + */ +export const parseJwtExpiryMs = (expiresIn: string | number | undefined | null): number => + parseJwtExpiry(expiresIn) * 1000;