refactor
This commit is contained in:
42
apps/api/src/domain/auth/AuthController.ts
Normal file
42
apps/api/src/domain/auth/AuthController.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Controller, Get, Post, Body, Query, Res, Redirect, HttpStatus } from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
import { AuthService } from './AuthService';
|
||||
import { LoginParams, SignupParams, LoginWithIracingCallbackParams } from './dto/AuthDto';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Post('signup')
|
||||
async signup(@Body() params: SignupParams) {
|
||||
return this.authService.signupWithEmail(params);
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
async login(@Body() params: LoginParams) {
|
||||
return this.authService.loginWithEmail(params);
|
||||
}
|
||||
|
||||
@Get('session')
|
||||
async getSession() {
|
||||
return this.authService.getCurrentSession();
|
||||
}
|
||||
|
||||
@Post('logout')
|
||||
async logout() {
|
||||
return this.authService.logout();
|
||||
}
|
||||
|
||||
@Get('iracing/start')
|
||||
async startIracingAuthRedirect(@Query('returnTo') returnTo?: string, @Res() res?: Response) {
|
||||
const { redirectUrl, state } = await this.authService.startIracingAuthRedirect(returnTo);
|
||||
// In real application, you might want to store 'state' in a secure cookie or session.
|
||||
// For this example, we'll just redirect.
|
||||
res.redirect(HttpStatus.FOUND, redirectUrl);
|
||||
}
|
||||
|
||||
@Get('iracing/callback')
|
||||
async loginWithIracingCallback(@Query('code') code: string, @Query('state') state: string, @Query('returnTo') returnTo?: string) {
|
||||
return this.authService.loginWithIracingCallback({ code, state, returnTo });
|
||||
}
|
||||
}
|
||||
11
apps/api/src/domain/auth/AuthModule.ts
Normal file
11
apps/api/src/domain/auth/AuthModule.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthService } from './AuthService';
|
||||
import { AuthController } from './AuthController';
|
||||
import { AuthProviders } from './AuthProviders';
|
||||
|
||||
@Module({
|
||||
controllers: [AuthController],
|
||||
providers: AuthProviders,
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
64
apps/api/src/domain/auth/AuthProviders.ts
Normal file
64
apps/api/src/domain/auth/AuthProviders.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { AuthService } from './AuthService';
|
||||
|
||||
// Import interfaces and concrete implementations
|
||||
import type { IAuthRepository } from '@core/identity/domain/repositories/IAuthRepository';
|
||||
import { IUserRepository, StoredUser } from '@core/identity/domain/repositories/IUserRepository';
|
||||
import type { IPasswordHashingService } from '@core/identity/domain/services/PasswordHashingService';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
import { InMemoryAuthRepository } from '@adapters/identity/persistence/inmemory/InMemoryAuthRepository';
|
||||
import { InMemoryUserRepository } from '@adapters/identity/persistence/inmemory/InMemoryUserRepository';
|
||||
import { InMemoryPasswordHashingService } from '@adapters/identity/services/InMemoryPasswordHashingService';
|
||||
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
|
||||
import { IdentitySessionPort } from '@core/identity/application/ports/IdentitySessionPort';
|
||||
import { CookieIdentitySessionAdapter } from '@adapters/identity/session/CookieIdentitySessionAdapter';
|
||||
|
||||
// Define the tokens for dependency injection
|
||||
export const AUTH_REPOSITORY_TOKEN = 'IAuthRepository';
|
||||
export const USER_REPOSITORY_TOKEN = 'IUserRepository';
|
||||
export const PASSWORD_HASHING_SERVICE_TOKEN = 'IPasswordHashingService';
|
||||
export const LOGGER_TOKEN = 'Logger';
|
||||
export const IDENTITY_SESSION_PORT_TOKEN = 'IdentitySessionPort';
|
||||
|
||||
export const AuthProviders: Provider[] = [
|
||||
AuthService, // Provide the service itself
|
||||
{
|
||||
provide: AUTH_REPOSITORY_TOKEN,
|
||||
useFactory: (userRepository: IUserRepository, passwordHashingService: IPasswordHashingService, logger: Logger) => {
|
||||
// Seed initial users for InMemoryUserRepository
|
||||
const initialUsers: StoredUser[] = [
|
||||
// Example user (replace with actual test users as needed)
|
||||
{
|
||||
id: 'user-1',
|
||||
email: 'test@example.com',
|
||||
passwordHash: 'demo_salt_moc.elpmaxe@tset', // "test@example.com" reversed
|
||||
displayName: 'Test User',
|
||||
salt: '', // Handled by hashing service
|
||||
createdAt: new Date(),
|
||||
},
|
||||
];
|
||||
const inMemoryUserRepository = new InMemoryUserRepository(logger, initialUsers);
|
||||
return new InMemoryAuthRepository(inMemoryUserRepository, passwordHashingService, logger);
|
||||
},
|
||||
inject: [USER_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: USER_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryUserRepository(logger), // Factory for InMemoryUserRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: PASSWORD_HASHING_SERVICE_TOKEN,
|
||||
useClass: InMemoryPasswordHashingService,
|
||||
},
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
{
|
||||
provide: IDENTITY_SESSION_PORT_TOKEN,
|
||||
useFactory: (logger: Logger) => new CookieIdentitySessionAdapter(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
];
|
||||
140
apps/api/src/domain/auth/AuthService.ts
Normal file
140
apps/api/src/domain/auth/AuthService.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { Injectable, Inject, InternalServerErrorException } from '@nestjs/common';
|
||||
import type { AuthenticatedUserDTO, AuthSessionDTO, SignupParams, LoginParams, IracingAuthRedirectResult, LoginWithIracingCallbackParams } from './dto/AuthDto';
|
||||
|
||||
// Core Use Cases
|
||||
import { LoginUseCase } from '@core/identity/application/use-cases/LoginUseCase';
|
||||
import { SignupUseCase } from '@core/identity/application/use-cases/SignupUseCase';
|
||||
import { GetCurrentSessionUseCase } from '@core/identity/application/use-cases/GetCurrentSessionUseCase';
|
||||
import { LogoutUseCase } from '@core/identity/application/use-cases/LogoutUseCase';
|
||||
import { StartIracingAuthRedirectUseCase } from '@core/identity/application/use-cases/StartIracingAuthRedirectUseCase';
|
||||
import { LoginWithIracingCallbackUseCase } from '@core/identity/application/use-cases/LoginWithIracingCallbackUseCase';
|
||||
|
||||
// Core Interfaces and Tokens
|
||||
import { AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN, IDENTITY_SESSION_PORT_TOKEN, USER_REPOSITORY_TOKEN } from './AuthProviders';
|
||||
import type { IAuthRepository } from '@core/identity/domain/repositories/IAuthRepository';
|
||||
import type { IPasswordHashingService } from '@core/identity/domain/services/PasswordHashingService';
|
||||
import type { Logger } from "@core/shared/application";
|
||||
import { IdentitySessionPort } from '@core/identity/application/ports/IdentitySessionPort';
|
||||
import { UserId } from '@core/identity/domain/value-objects/UserId';
|
||||
import { User } from '@core/identity/domain/entities/User';
|
||||
import type { IUserRepository } from '@core/identity/domain/repositories/IUserRepository';
|
||||
import { AuthenticatedUserDTO as CoreAuthenticatedUserDTO } from '@core/identity/application/dto/AuthenticatedUserDTO';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
private readonly loginUseCase: LoginUseCase;
|
||||
private readonly signupUseCase: SignupUseCase;
|
||||
private readonly getCurrentSessionUseCase: GetCurrentSessionUseCase;
|
||||
private readonly logoutUseCase: LogoutUseCase;
|
||||
private readonly startIracingAuthRedirectUseCase: StartIracingAuthRedirectUseCase;
|
||||
private readonly loginWithIracingCallbackUseCase: LoginWithIracingCallbackUseCase;
|
||||
|
||||
constructor(
|
||||
@Inject(AUTH_REPOSITORY_TOKEN) private authRepository: IAuthRepository,
|
||||
@Inject(PASSWORD_HASHING_SERVICE_TOKEN) private passwordHashingService: IPasswordHashingService,
|
||||
@Inject(LOGGER_TOKEN) private logger: Logger,
|
||||
@Inject(IDENTITY_SESSION_PORT_TOKEN) private identitySessionPort: IdentitySessionPort,
|
||||
@Inject(USER_REPOSITORY_TOKEN) private userRepository: IUserRepository, // Inject IUserRepository here
|
||||
) {
|
||||
this.loginUseCase = new LoginUseCase(this.authRepository, this.passwordHashingService);
|
||||
this.signupUseCase = new SignupUseCase(this.authRepository, this.passwordHashingService);
|
||||
this.getCurrentSessionUseCase = new GetCurrentSessionUseCase(); // Doesn't have constructor parameters normally
|
||||
this.logoutUseCase = new LogoutUseCase(this.identitySessionPort);
|
||||
this.startIracingAuthRedirectUseCase = new StartIracingAuthRedirectUseCase();
|
||||
this.loginWithIracingCallbackUseCase = new LoginWithIracingCallbackUseCase();
|
||||
}
|
||||
|
||||
private mapUserToAuthenticatedUserDTO(user: User): AuthenticatedUserDTO {
|
||||
return {
|
||||
userId: user.getId().value,
|
||||
email: user.getEmail() ?? '',
|
||||
displayName: user.getDisplayName() ?? '',
|
||||
// Map other fields as necessary
|
||||
iracingCustomerId: user.getIracingCustomerId() ?? undefined,
|
||||
primaryDriverId: user.getPrimaryDriverId() ?? undefined,
|
||||
avatarUrl: user.getAvatarUrl() ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async getCurrentSession(): Promise<AuthSessionDTO | null> {
|
||||
this.logger.debug('[AuthService] Attempting to get current session.');
|
||||
const coreSession = await this.identitySessionPort.getCurrentSession();
|
||||
if (!coreSession) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findById(coreSession.user.id); // Use userRepository to fetch full user
|
||||
if (!user) {
|
||||
// If session exists but user doesn't in DB, perhaps clear session?
|
||||
this.logger.warn(`[AuthService] Session found for user ID ${coreSession.user.id}, but user not found in repository.`);
|
||||
await this.identitySessionPort.clearSession(); // Clear potentially stale session
|
||||
return null;
|
||||
}
|
||||
|
||||
const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(User.fromStored(user));
|
||||
|
||||
return {
|
||||
token: coreSession.token,
|
||||
user: authenticatedUserDTO,
|
||||
};
|
||||
}
|
||||
|
||||
async signupWithEmail(params: SignupParams): Promise<AuthSessionDTO> {
|
||||
this.logger.debug(`[AuthService] Attempting signup for email: ${params.email}`);
|
||||
const user = await this.signupUseCase.execute(params.email, params.password, params.displayName);
|
||||
|
||||
// Create session after successful signup
|
||||
const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(user);
|
||||
const session = await this.identitySessionPort.createSession(authenticatedUserDTO as CoreAuthenticatedUserDTO);
|
||||
|
||||
return {
|
||||
token: session.token,
|
||||
user: authenticatedUserDTO,
|
||||
};
|
||||
}
|
||||
|
||||
async loginWithEmail(params: LoginParams): Promise<AuthSessionDTO> {
|
||||
this.logger.debug(`[AuthService] Attempting login for email: ${params.email}`);
|
||||
try {
|
||||
const user = await this.loginUseCase.execute(params.email, params.password);
|
||||
// Create session after successful login
|
||||
const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(user);
|
||||
const session = await this.identitySessionPort.createSession(authenticatedUserDTO as CoreAuthenticatedUserDTO);
|
||||
|
||||
return {
|
||||
token: session.token,
|
||||
user: authenticatedUserDTO,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`[AuthService] Login failed for email ${params.email}:`, error instanceof Error ? error : new Error(String(error)));
|
||||
throw new InternalServerErrorException('Login failed due to invalid credentials or server error.');
|
||||
}
|
||||
}
|
||||
|
||||
async startIracingAuthRedirect(returnTo?: string): Promise<IracingAuthRedirectResult> {
|
||||
this.logger.debug('[AuthService] Starting iRacing auth redirect.');
|
||||
// Note: The StartIracingAuthRedirectUseCase takes optional returnTo, but the DTO doesnt
|
||||
const result = await this.startIracingAuthRedirectUseCase.execute(returnTo);
|
||||
// Map core IracingAuthRedirectResult to AuthDto's IracingAuthRedirectResult
|
||||
return { redirectUrl: result.redirectUrl, state: result.state };
|
||||
}
|
||||
|
||||
async loginWithIracingCallback(params: LoginWithIracingCallbackParams): Promise<AuthSessionDTO> {
|
||||
this.logger.debug(`[AuthService] Handling iRacing callback for code: ${params.code}`);
|
||||
const user = await this.loginWithIracingCallbackUseCase.execute(params); // Pass params as is
|
||||
|
||||
// Create session after successful iRacing login
|
||||
const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(user);
|
||||
const session = await this.identitySessionPort.createSession(authenticatedUserDTO as CoreAuthenticatedUserDTO);
|
||||
|
||||
return {
|
||||
token: session.token,
|
||||
user: authenticatedUserDTO,
|
||||
};
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
this.logger.debug('[AuthService] Attempting logout.');
|
||||
await this.logoutUseCase.execute();
|
||||
}
|
||||
}
|
||||
55
apps/api/src/domain/auth/dto/AuthDto.ts
Normal file
55
apps/api/src/domain/auth/dto/AuthDto.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class AuthenticatedUserDTO {
|
||||
@ApiProperty()
|
||||
userId!: string;
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
@ApiProperty()
|
||||
displayName!: string;
|
||||
}
|
||||
|
||||
export class AuthSessionDTO {
|
||||
@ApiProperty()
|
||||
token!: string;
|
||||
@ApiProperty()
|
||||
user!: AuthenticatedUserDTO;
|
||||
}
|
||||
|
||||
export class SignupParams {
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
@ApiProperty()
|
||||
password!: string;
|
||||
@ApiProperty()
|
||||
displayName!: string;
|
||||
@ApiProperty({ required: false })
|
||||
iracingCustomerId?: string;
|
||||
@ApiProperty({ required: false })
|
||||
primaryDriverId?: string;
|
||||
@ApiProperty({ required: false })
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
export class LoginParams {
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
@ApiProperty()
|
||||
password!: string;
|
||||
}
|
||||
|
||||
export class IracingAuthRedirectResult {
|
||||
@ApiProperty()
|
||||
redirectUrl!: string;
|
||||
@ApiProperty()
|
||||
state!: string;
|
||||
}
|
||||
|
||||
export class LoginWithIracingCallbackParams {
|
||||
@ApiProperty()
|
||||
code!: string;
|
||||
@ApiProperty()
|
||||
state!: string;
|
||||
@ApiProperty({ required: false })
|
||||
returnTo?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user