import { Inject } from '@nestjs/common'; import type { Logger } from '@core/shared/domain/Logger'; import { ForgotPasswordUseCase, type ForgotPasswordApplicationError, type ForgotPasswordInput, } from '@core/identity/application/use-cases/ForgotPasswordUseCase'; import { LoginUseCase, type LoginApplicationError, type LoginInput, } from '@core/identity/application/use-cases/LoginUseCase'; import { LogoutUseCase, type LogoutApplicationError } from '@core/identity/application/use-cases/LogoutUseCase'; import { ResetPasswordUseCase, type ResetPasswordApplicationError, type ResetPasswordInput, } from '@core/identity/application/use-cases/ResetPasswordUseCase'; import { SignupSponsorUseCase, type SignupSponsorApplicationError, type SignupSponsorInput, } from '@core/identity/application/use-cases/SignupSponsorUseCase'; import { SignupUseCase, type SignupApplicationError, type SignupInput, } from '@core/identity/application/use-cases/SignupUseCase'; import type { IdentitySessionPort } from '@core/identity/application/ports/IdentitySessionPort'; import { AUTH_SESSION_OUTPUT_PORT_TOKEN, COMMAND_RESULT_OUTPUT_PORT_TOKEN, FORGOT_PASSWORD_OUTPUT_PORT_TOKEN, FORGOT_PASSWORD_USE_CASE_TOKEN, IDENTITY_SESSION_PORT_TOKEN, LOGGER_TOKEN, LOGIN_USE_CASE_TOKEN, LOGOUT_USE_CASE_TOKEN, RESET_PASSWORD_OUTPUT_PORT_TOKEN, RESET_PASSWORD_USE_CASE_TOKEN, SIGNUP_SPONSOR_USE_CASE_TOKEN, SIGNUP_USE_CASE_TOKEN, } from './AuthProviders'; import type { AuthSessionDTO } from './dtos/AuthDto'; import { LoginParamsDTO, SignupParamsDTO, SignupSponsorParamsDTO } from './dtos/AuthDto'; import { AuthSessionPresenter } from './presenters/AuthSessionPresenter'; import type { CommandResultDTO } from './presenters/CommandResultPresenter'; import { CommandResultPresenter } from './presenters/CommandResultPresenter'; import { ForgotPasswordPresenter } from './presenters/ForgotPasswordPresenter'; import { ResetPasswordPresenter } from './presenters/ResetPasswordPresenter'; function mapApplicationErrorToMessage(error: { details?: { message?: string } } | undefined, fallback: string): string { return error?.details?.message ?? fallback; } function inferDemoRoleFromEmail(email: string): AuthSessionDTO['user']['role'] | undefined { const normalized = email.trim().toLowerCase(); if (normalized === 'demo.driver@example.com') return 'driver'; if (normalized === 'demo.sponsor@example.com') return 'sponsor'; if (normalized === 'demo.owner@example.com') return 'league-owner'; if (normalized === 'demo.steward@example.com') return 'league-steward'; if (normalized === 'demo.admin@example.com') return 'league-admin'; if (normalized === 'demo.systemowner@example.com') return 'system-owner'; if (normalized === 'demo.superadmin@example.com') return 'super-admin'; return undefined; } export class AuthService { constructor( @Inject(LOGGER_TOKEN) private readonly logger: Logger, @Inject(IDENTITY_SESSION_PORT_TOKEN) private readonly identitySessionPort: IdentitySessionPort, @Inject(LOGIN_USE_CASE_TOKEN) private readonly loginUseCase: LoginUseCase, @Inject(SIGNUP_USE_CASE_TOKEN) private readonly signupUseCase: SignupUseCase, @Inject(SIGNUP_SPONSOR_USE_CASE_TOKEN) private readonly signupSponsorUseCase: SignupSponsorUseCase, @Inject(LOGOUT_USE_CASE_TOKEN) private readonly logoutUseCase: LogoutUseCase, @Inject(FORGOT_PASSWORD_USE_CASE_TOKEN) private readonly forgotPasswordUseCase: ForgotPasswordUseCase, @Inject(RESET_PASSWORD_USE_CASE_TOKEN) private readonly resetPasswordUseCase: ResetPasswordUseCase, // TODO presenters must not be injected @Inject(AUTH_SESSION_OUTPUT_PORT_TOKEN) private readonly authSessionPresenter: AuthSessionPresenter, @Inject(COMMAND_RESULT_OUTPUT_PORT_TOKEN) private readonly commandResultPresenter: CommandResultPresenter, @Inject(FORGOT_PASSWORD_OUTPUT_PORT_TOKEN) private readonly forgotPasswordPresenter: ForgotPasswordPresenter, @Inject(RESET_PASSWORD_OUTPUT_PORT_TOKEN) private readonly resetPasswordPresenter: ResetPasswordPresenter, ) {} async getCurrentSession(): Promise { // TODO must call a use case this.logger.debug('[AuthService] Attempting to get current session.'); const coreSession = await this.identitySessionPort.getCurrentSession(); if (!coreSession) return null; const userRole = coreSession.user.role; const role = userRole ? (userRole as AuthSessionDTO['user']['role']) : undefined; return { token: coreSession.token, user: { userId: coreSession.user.id, email: coreSession.user.email ?? '', displayName: coreSession.user.displayName, ...(role !== undefined ? { role } : {}), }, }; } async signupWithEmail(params: SignupParamsDTO): Promise { this.logger.debug(`[AuthService] Attempting signup for email: ${params.email}`); const input: SignupInput = { email: params.email, password: params.password, displayName: params.displayName, }; const result = await this.signupUseCase.execute(input); if (result.isErr()) { const error = result.unwrapErr() as SignupApplicationError; throw new Error(mapApplicationErrorToMessage(error, 'Signup failed')); } const signupResult = result.unwrap(); this.authSessionPresenter.present(signupResult); const userDTO = this.authSessionPresenter.responseModel; const inferredRole = inferDemoRoleFromEmail(userDTO.email); const session = await this.identitySessionPort.createSession({ id: userDTO.userId, displayName: userDTO.displayName, email: userDTO.email, ...(inferredRole ? { role: inferredRole } : {}), }); return { token: session.token, user: userDTO, }; } async signupSponsor(params: SignupSponsorParamsDTO): Promise { this.logger.debug(`[AuthService] Attempting sponsor signup for email: ${params.email}`); const input: SignupSponsorInput = { email: params.email, password: params.password, displayName: params.displayName, companyName: params.companyName, }; const result = await this.signupSponsorUseCase.execute(input); if (result.isErr()) { const error = result.unwrapErr() as SignupSponsorApplicationError; throw new Error(mapApplicationErrorToMessage(error, 'Sponsor signup failed')); } const signupResult = result.unwrap(); this.authSessionPresenter.present(signupResult); const userDTO = this.authSessionPresenter.responseModel; const inferredRole = inferDemoRoleFromEmail(userDTO.email); const session = await this.identitySessionPort.createSession({ id: userDTO.userId, displayName: userDTO.displayName, email: userDTO.email, ...(inferredRole ? { role: inferredRole } : {}), }); return { token: session.token, user: userDTO, }; } async loginWithEmail(params: LoginParamsDTO): Promise { this.logger.debug(`[AuthService] Attempting login for email: ${params.email}`); const input: LoginInput = { email: params.email, password: params.password, }; const result = await this.loginUseCase.execute(input); if (result.isErr()) { const error = result.unwrapErr() as LoginApplicationError; throw new Error(mapApplicationErrorToMessage(error, 'Login failed')); } const loginResult = result.unwrap(); this.authSessionPresenter.present(loginResult); const userDTO = this.authSessionPresenter.responseModel; const sessionOptions = params.rememberMe !== undefined ? { rememberMe: params.rememberMe } : undefined; const inferredRole = inferDemoRoleFromEmail(userDTO.email); const session = await this.identitySessionPort.createSession( { id: userDTO.userId, displayName: userDTO.displayName, email: userDTO.email, ...(inferredRole ? { role: inferredRole } : {}), }, sessionOptions ); return { token: session.token, user: userDTO, }; } async logout(): Promise { this.logger.debug('[AuthService] Attempting logout.'); const result = await this.logoutUseCase.execute(); if (result.isErr()) { const error = result.unwrapErr() as LogoutApplicationError; throw new Error(mapApplicationErrorToMessage(error, 'Logout failed')); } const logoutResult = result.unwrap(); this.commandResultPresenter.present(logoutResult); return this.commandResultPresenter.responseModel; } /** * Start iRacing OAuth flow. * * NOTE: This is a placeholder implementation for the current alpha build. * A production implementation should delegate to a dedicated iRacing OAuth port * and persist/validate state server-side. */ async startIracingAuth(returnTo?: string): Promise { this.logger.debug('[AuthService] Starting iRacing auth flow', { returnTo }); const state = Math.random().toString(36).slice(2); const base = 'https://example.com/iracing/auth'; const query = new URLSearchParams(); query.set('state', state); if (returnTo) { query.set('returnTo', returnTo); } return `${base}?${query.toString()}`; } /** * Handle iRacing OAuth callback. * * NOTE: Placeholder implementation that creates a demo session. */ async iracingCallback(code: string, state: string, returnTo?: string): Promise { this.logger.debug('[AuthService] iRacing callback received', { hasCode: !!code, state, returnTo }); const userId = `iracing-${state || code}`.slice(0, 64); const session = await this.identitySessionPort.createSession({ id: userId, displayName: 'iRacing User', email: '', }); return { token: session.token, user: { userId, email: '', displayName: 'iRacing User', }, }; } async forgotPassword(params: { email: string }): Promise<{ message: string; magicLink?: string }> { this.logger.debug(`[AuthService] Attempting forgot password for email: ${params.email}`); const input: ForgotPasswordInput = { email: params.email, }; const executeResult = await this.forgotPasswordUseCase.execute(input); if (executeResult.isErr()) { const error = executeResult.unwrapErr() as ForgotPasswordApplicationError; throw new Error(mapApplicationErrorToMessage(error, 'Forgot password failed')); } const forgotPasswordResult = executeResult.unwrap(); this.forgotPasswordPresenter.present(forgotPasswordResult); const response = this.forgotPasswordPresenter.responseModel; const result: { message: string; magicLink?: string } = { message: response.message, }; if (response.magicLink) { result.magicLink = response.magicLink; } return result; } async resetPassword(params: { token: string; newPassword: string }): Promise<{ message: string }> { this.logger.debug('[AuthService] Attempting reset password'); const input: ResetPasswordInput = { token: params.token, newPassword: params.newPassword, }; const result = await this.resetPasswordUseCase.execute(input); if (result.isErr()) { const error = result.unwrapErr() as ResetPasswordApplicationError; throw new Error(mapApplicationErrorToMessage(error, 'Reset password failed')); } const resetResult = result.unwrap(); this.resetPasswordPresenter.present(resetResult); return this.resetPasswordPresenter.responseModel; } }