auth
This commit is contained in:
122
apps/api/src/development/use-cases/DemoLoginUseCase.ts
Normal file
122
apps/api/src/development/use-cases/DemoLoginUseCase.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { EmailAddress } from '@core/identity/domain/value-objects/EmailAddress';
|
||||
import { UserId } from '@core/identity/domain/value-objects/UserId';
|
||||
import { User } from '@core/identity/domain/entities/User';
|
||||
import { IAuthRepository } from '@core/identity/domain/repositories/IAuthRepository';
|
||||
import { IPasswordHashingService } from '@core/identity/domain/services/PasswordHashingService';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { UseCaseOutputPort, Logger, UseCase } from '@core/shared/application';
|
||||
|
||||
export type DemoLoginInput = {
|
||||
role: 'driver' | 'sponsor' | 'league-owner' | 'league-steward' | 'league-admin' | 'system-owner' | 'super-admin';
|
||||
};
|
||||
|
||||
export type DemoLoginResult = {
|
||||
user: User;
|
||||
};
|
||||
|
||||
export type DemoLoginErrorCode = 'DEMO_NOT_ALLOWED' | 'REPOSITORY_ERROR';
|
||||
|
||||
export type DemoLoginApplicationError = ApplicationErrorCode<DemoLoginErrorCode, { message: string }>;
|
||||
|
||||
/**
|
||||
* Application Use Case: DemoLoginUseCase
|
||||
*
|
||||
* Provides demo login functionality for development environments.
|
||||
* Creates demo users with predefined credentials.
|
||||
*
|
||||
* ⚠️ DEVELOPMENT ONLY - Should be disabled in production
|
||||
*/
|
||||
export class DemoLoginUseCase implements UseCase<DemoLoginInput, void, DemoLoginErrorCode> {
|
||||
constructor(
|
||||
private readonly authRepo: IAuthRepository,
|
||||
private readonly passwordService: IPasswordHashingService,
|
||||
private readonly logger: Logger,
|
||||
private readonly output: UseCaseOutputPort<DemoLoginResult>,
|
||||
) {}
|
||||
|
||||
async execute(input: DemoLoginInput): Promise<Result<void, DemoLoginApplicationError>> {
|
||||
// Security check: Only allow in development
|
||||
if (process.env.NODE_ENV !== 'development' && process.env.ALLOW_DEMO_LOGIN !== 'true') {
|
||||
return Result.err({
|
||||
code: 'DEMO_NOT_ALLOWED',
|
||||
details: { message: 'Demo login is only available in development environment' },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Generate demo user email and display name based on role
|
||||
const roleConfig = {
|
||||
'driver': { email: 'demo.driver@example.com', name: 'John Demo', primaryDriverId: true },
|
||||
'sponsor': { email: 'demo.sponsor@example.com', name: 'Jane Sponsor', primaryDriverId: false },
|
||||
'league-owner': { email: 'demo.owner@example.com', name: 'Alex Owner', primaryDriverId: true },
|
||||
'league-steward': { email: 'demo.steward@example.com', name: 'Sam Steward', primaryDriverId: true },
|
||||
'league-admin': { email: 'demo.admin@example.com', name: 'Taylor Admin', primaryDriverId: true },
|
||||
'system-owner': { email: 'demo.systemowner@example.com', name: 'System Owner', primaryDriverId: true },
|
||||
'super-admin': { email: 'demo.superadmin@example.com', name: 'Super Admin', primaryDriverId: true },
|
||||
};
|
||||
|
||||
const config = roleConfig[input.role];
|
||||
const emailVO = EmailAddress.create(config.email);
|
||||
|
||||
// Check if demo user already exists
|
||||
let user = await this.authRepo.findByEmail(emailVO);
|
||||
|
||||
if (!user) {
|
||||
// Create new demo user
|
||||
this.logger.info('[DemoLoginUseCase] Creating new demo user', { role: input.role });
|
||||
|
||||
const userId = UserId.create();
|
||||
|
||||
// Use a fixed demo password and hash it
|
||||
const demoPassword = 'Demo1234!';
|
||||
const hashedPassword = await this.passwordService.hash(demoPassword);
|
||||
|
||||
// Import PasswordHash and create proper object
|
||||
const passwordHashModule = await import('@core/identity/domain/value-objects/PasswordHash');
|
||||
const passwordHash = passwordHashModule.PasswordHash.fromHash(hashedPassword);
|
||||
|
||||
const userProps: any = {
|
||||
id: userId,
|
||||
displayName: config.name,
|
||||
email: config.email,
|
||||
passwordHash,
|
||||
};
|
||||
|
||||
if (config.primaryDriverId) {
|
||||
userProps.primaryDriverId = `demo-${input.role}-${userId.value}`;
|
||||
// Add avatar URL for demo users with primary driver
|
||||
// Use the same format as seeded drivers: /media/default/neutral-default-avatar
|
||||
userProps.avatarUrl = '/media/default/neutral-default-avatar';
|
||||
}
|
||||
|
||||
user = User.create(userProps);
|
||||
|
||||
await this.authRepo.save(user);
|
||||
} else {
|
||||
this.logger.info('[DemoLoginUseCase] Using existing demo user', {
|
||||
role: input.role,
|
||||
userId: user.getId().value
|
||||
});
|
||||
}
|
||||
|
||||
this.output.present({ user });
|
||||
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error && error.message
|
||||
? error.message
|
||||
: 'Failed to execute DemoLoginUseCase';
|
||||
|
||||
this.logger.error('DemoLoginUseCase.execute failed', error instanceof Error ? error : undefined, {
|
||||
input,
|
||||
});
|
||||
|
||||
return Result.err({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: { message },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Controller, Get, Post, Body, Query, Inject, Res } from '@nestjs/common';
|
||||
import { Public } from './Public';
|
||||
import { AuthService } from './AuthService';
|
||||
import { LoginParamsDTO, SignupParamsDTO, AuthSessionDTO } from './dtos/AuthDto';
|
||||
import { LoginParamsDTO, SignupParamsDTO, AuthSessionDTO, ForgotPasswordDTO, ResetPasswordDTO, DemoLoginDTO } from './dtos/AuthDto';
|
||||
import type { CommandResultDTO } from './presenters/CommandResultPresenter';
|
||||
import type { Response } from 'express';
|
||||
// ProductionGuard will be added if needed - for now we'll use environment check directly
|
||||
|
||||
@Public()
|
||||
@Controller('auth')
|
||||
@@ -47,4 +48,23 @@ export class AuthController {
|
||||
): Promise<AuthSessionDTO> {
|
||||
return this.authService.iracingCallback(code, state, returnTo);
|
||||
}
|
||||
|
||||
@Post('forgot-password')
|
||||
async forgotPassword(@Body() params: ForgotPasswordDTO): Promise<{ message: string; magicLink?: string }> {
|
||||
return this.authService.forgotPassword(params);
|
||||
}
|
||||
|
||||
@Post('reset-password')
|
||||
async resetPassword(@Body() params: ResetPasswordDTO): Promise<{ message: string }> {
|
||||
return this.authService.resetPassword(params);
|
||||
}
|
||||
|
||||
@Post('demo-login')
|
||||
async demoLogin(@Body() params: DemoLoginDTO): Promise<AuthSessionDTO> {
|
||||
// Manual production check
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
throw new Error('Demo login is not available in production');
|
||||
}
|
||||
return this.authService.demoLogin(params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,22 +4,35 @@ import { CookieIdentitySessionAdapter } from '@adapters/identity/session/CookieI
|
||||
import { LoginUseCase } from '@core/identity/application/use-cases/LoginUseCase';
|
||||
import { LogoutUseCase } from '@core/identity/application/use-cases/LogoutUseCase';
|
||||
import { SignupUseCase } from '@core/identity/application/use-cases/SignupUseCase';
|
||||
import { ForgotPasswordUseCase } from '@core/identity/application/use-cases/ForgotPasswordUseCase';
|
||||
import { ResetPasswordUseCase } from '@core/identity/application/use-cases/ResetPasswordUseCase';
|
||||
import { DemoLoginUseCase } from '../../development/use-cases/DemoLoginUseCase';
|
||||
import type { IdentitySessionPort } from '@core/identity/application/ports/IdentitySessionPort';
|
||||
import type { IAuthRepository } from '@core/identity/domain/repositories/IAuthRepository';
|
||||
import type { IMagicLinkRepository } from '@core/identity/domain/repositories/IMagicLinkRepository';
|
||||
import type { IPasswordHashingService } from '@core/identity/domain/services/PasswordHashingService';
|
||||
import type { IMagicLinkNotificationPort } from '@core/identity/domain/ports/IMagicLinkNotificationPort';
|
||||
import type { LoginResult } from '@core/identity/application/use-cases/LoginUseCase';
|
||||
import type { LogoutResult } from '@core/identity/application/use-cases/LogoutUseCase';
|
||||
import type { SignupResult } from '@core/identity/application/use-cases/SignupUseCase';
|
||||
import type { ForgotPasswordResult } from '@core/identity/application/use-cases/ForgotPasswordUseCase';
|
||||
import type { ResetPasswordResult } from '@core/identity/application/use-cases/ResetPasswordUseCase';
|
||||
import type { DemoLoginResult } from '../../development/use-cases/DemoLoginUseCase';
|
||||
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
|
||||
|
||||
import {
|
||||
AUTH_REPOSITORY_TOKEN,
|
||||
PASSWORD_HASHING_SERVICE_TOKEN,
|
||||
USER_REPOSITORY_TOKEN,
|
||||
MAGIC_LINK_REPOSITORY_TOKEN,
|
||||
} from '../../persistence/identity/IdentityPersistenceTokens';
|
||||
|
||||
import { AuthSessionPresenter } from './presenters/AuthSessionPresenter';
|
||||
import { CommandResultPresenter } from './presenters/CommandResultPresenter';
|
||||
import { ForgotPasswordPresenter } from './presenters/ForgotPasswordPresenter';
|
||||
import { ResetPasswordPresenter } from './presenters/ResetPasswordPresenter';
|
||||
import { DemoLoginPresenter } from './presenters/DemoLoginPresenter';
|
||||
import { ConsoleMagicLinkNotificationAdapter } from '@adapters/notifications/ports/ConsoleMagicLinkNotificationAdapter';
|
||||
|
||||
// Define the tokens for dependency injection
|
||||
export { AUTH_REPOSITORY_TOKEN, USER_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN };
|
||||
@@ -28,9 +41,16 @@ export const IDENTITY_SESSION_PORT_TOKEN = 'IdentitySessionPort';
|
||||
export const LOGIN_USE_CASE_TOKEN = 'LoginUseCase';
|
||||
export const SIGNUP_USE_CASE_TOKEN = 'SignupUseCase';
|
||||
export const LOGOUT_USE_CASE_TOKEN = 'LogoutUseCase';
|
||||
export const FORGOT_PASSWORD_USE_CASE_TOKEN = 'ForgotPasswordUseCase';
|
||||
export const RESET_PASSWORD_USE_CASE_TOKEN = 'ResetPasswordUseCase';
|
||||
export const DEMO_LOGIN_USE_CASE_TOKEN = 'DemoLoginUseCase';
|
||||
|
||||
export const AUTH_SESSION_OUTPUT_PORT_TOKEN = 'AuthSessionOutputPort';
|
||||
export const COMMAND_RESULT_OUTPUT_PORT_TOKEN = 'CommandResultOutputPort';
|
||||
export const FORGOT_PASSWORD_OUTPUT_PORT_TOKEN = 'ForgotPasswordOutputPort';
|
||||
export const RESET_PASSWORD_OUTPUT_PORT_TOKEN = 'ResetPasswordOutputPort';
|
||||
export const DEMO_LOGIN_OUTPUT_PORT_TOKEN = 'DemoLoginOutputPort';
|
||||
export const MAGIC_LINK_NOTIFICATION_PORT_TOKEN = 'MagicLinkNotificationPort';
|
||||
|
||||
export const AuthProviders: Provider[] = [
|
||||
{
|
||||
@@ -80,4 +100,65 @@ export const AuthProviders: Provider[] = [
|
||||
new LogoutUseCase(sessionPort, logger, output),
|
||||
inject: [IDENTITY_SESSION_PORT_TOKEN, LOGGER_TOKEN, COMMAND_RESULT_OUTPUT_PORT_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: ForgotPasswordPresenter,
|
||||
useClass: ForgotPasswordPresenter,
|
||||
},
|
||||
{
|
||||
provide: ResetPasswordPresenter,
|
||||
useClass: ResetPasswordPresenter,
|
||||
},
|
||||
{
|
||||
provide: DemoLoginPresenter,
|
||||
useClass: DemoLoginPresenter,
|
||||
},
|
||||
{
|
||||
provide: FORGOT_PASSWORD_OUTPUT_PORT_TOKEN,
|
||||
useExisting: ForgotPasswordPresenter,
|
||||
},
|
||||
{
|
||||
provide: RESET_PASSWORD_OUTPUT_PORT_TOKEN,
|
||||
useExisting: ResetPasswordPresenter,
|
||||
},
|
||||
{
|
||||
provide: DEMO_LOGIN_OUTPUT_PORT_TOKEN,
|
||||
useExisting: DemoLoginPresenter,
|
||||
},
|
||||
{
|
||||
provide: MAGIC_LINK_NOTIFICATION_PORT_TOKEN,
|
||||
useFactory: (logger: Logger) => new ConsoleMagicLinkNotificationAdapter(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: FORGOT_PASSWORD_USE_CASE_TOKEN,
|
||||
useFactory: (
|
||||
authRepo: IAuthRepository,
|
||||
magicLinkRepo: IMagicLinkRepository,
|
||||
notificationPort: IMagicLinkNotificationPort,
|
||||
logger: Logger,
|
||||
output: UseCaseOutputPort<ForgotPasswordResult>,
|
||||
) => new ForgotPasswordUseCase(authRepo, magicLinkRepo, notificationPort, logger, output),
|
||||
inject: [AUTH_REPOSITORY_TOKEN, MAGIC_LINK_REPOSITORY_TOKEN, MAGIC_LINK_NOTIFICATION_PORT_TOKEN, LOGGER_TOKEN, FORGOT_PASSWORD_OUTPUT_PORT_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: RESET_PASSWORD_USE_CASE_TOKEN,
|
||||
useFactory: (
|
||||
authRepo: IAuthRepository,
|
||||
magicLinkRepo: IMagicLinkRepository,
|
||||
passwordHashing: IPasswordHashingService,
|
||||
logger: Logger,
|
||||
output: UseCaseOutputPort<ResetPasswordResult>,
|
||||
) => new ResetPasswordUseCase(authRepo, magicLinkRepo, passwordHashing, logger, output),
|
||||
inject: [AUTH_REPOSITORY_TOKEN, MAGIC_LINK_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN, RESET_PASSWORD_OUTPUT_PORT_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: DEMO_LOGIN_USE_CASE_TOKEN,
|
||||
useFactory: (
|
||||
authRepo: IAuthRepository,
|
||||
passwordHashing: IPasswordHashingService,
|
||||
logger: Logger,
|
||||
output: UseCaseOutputPort<DemoLoginResult>,
|
||||
) => new DemoLoginUseCase(authRepo, passwordHashing, logger, output),
|
||||
inject: [AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN, DEMO_LOGIN_OUTPUT_PORT_TOKEN],
|
||||
},
|
||||
];
|
||||
|
||||
248
apps/api/src/domain/auth/AuthService.new.test.ts
Normal file
248
apps/api/src/domain/auth/AuthService.new.test.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { AuthService } from './AuthService';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
|
||||
class FakeAuthSessionPresenter {
|
||||
private model: any = null;
|
||||
reset() { this.model = null; }
|
||||
present(model: any) { this.model = model; }
|
||||
get responseModel() {
|
||||
if (!this.model) throw new Error('Presenter not presented');
|
||||
return this.model;
|
||||
}
|
||||
}
|
||||
|
||||
class FakeCommandResultPresenter {
|
||||
private model: any = null;
|
||||
reset() { this.model = null; }
|
||||
present(model: any) { this.model = model; }
|
||||
get responseModel() {
|
||||
if (!this.model) throw new Error('Presenter not presented');
|
||||
return this.model;
|
||||
}
|
||||
}
|
||||
|
||||
class FakeForgotPasswordPresenter {
|
||||
private model: any = null;
|
||||
reset() { this.model = null; }
|
||||
present(model: any) { this.model = model; }
|
||||
get responseModel() {
|
||||
if (!this.model) throw new Error('Presenter not presented');
|
||||
return this.model;
|
||||
}
|
||||
}
|
||||
|
||||
class FakeResetPasswordPresenter {
|
||||
private model: any = null;
|
||||
reset() { this.model = null; }
|
||||
present(model: any) { this.model = model; }
|
||||
get responseModel() {
|
||||
if (!this.model) throw new Error('Presenter not presented');
|
||||
return this.model;
|
||||
}
|
||||
}
|
||||
|
||||
class FakeDemoLoginPresenter {
|
||||
private model: any = null;
|
||||
reset() { this.model = null; }
|
||||
present(model: any) { this.model = model; }
|
||||
get responseModel() {
|
||||
if (!this.model) throw new Error('Presenter not presented');
|
||||
return this.model;
|
||||
}
|
||||
}
|
||||
|
||||
describe('AuthService - New Methods', () => {
|
||||
describe('forgotPassword', () => {
|
||||
it('should execute forgot password use case and return result', async () => {
|
||||
const forgotPasswordPresenter = new FakeForgotPasswordPresenter();
|
||||
const forgotPasswordUseCase = {
|
||||
execute: vi.fn(async () => {
|
||||
forgotPasswordPresenter.present({ message: 'Reset link sent', magicLink: 'http://example.com/reset?token=abc123' });
|
||||
return Result.ok(undefined);
|
||||
}),
|
||||
};
|
||||
|
||||
const service = new AuthService(
|
||||
{ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() } as any,
|
||||
{ getCurrentSession: vi.fn(), createSession: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeCommandResultPresenter() as any,
|
||||
forgotPasswordPresenter as any,
|
||||
new FakeResetPasswordPresenter() as any,
|
||||
new FakeDemoLoginPresenter() as any,
|
||||
);
|
||||
|
||||
const result = await service.forgotPassword({ email: 'test@example.com' });
|
||||
|
||||
expect(forgotPasswordUseCase.execute).toHaveBeenCalledWith({ email: 'test@example.com' });
|
||||
expect(result).toEqual({
|
||||
message: 'Reset link sent',
|
||||
magicLink: 'http://example.com/reset?token=abc123',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error on use case failure', async () => {
|
||||
const service = new AuthService(
|
||||
{ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() } as any,
|
||||
{ getCurrentSession: vi.fn(), createSession: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn(async () => Result.err({ code: 'RATE_LIMIT_EXCEEDED', details: { message: 'Too many attempts' } })) } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeCommandResultPresenter() as any,
|
||||
new FakeForgotPasswordPresenter() as any,
|
||||
new FakeResetPasswordPresenter() as any,
|
||||
new FakeDemoLoginPresenter() as any,
|
||||
);
|
||||
|
||||
await expect(service.forgotPassword({ email: 'test@example.com' })).rejects.toThrow('Too many attempts');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetPassword', () => {
|
||||
it('should execute reset password use case and return result', async () => {
|
||||
const resetPasswordPresenter = new FakeResetPasswordPresenter();
|
||||
const resetPasswordUseCase = {
|
||||
execute: vi.fn(async () => {
|
||||
resetPasswordPresenter.present({ message: 'Password reset successfully' });
|
||||
return Result.ok(undefined);
|
||||
}),
|
||||
};
|
||||
|
||||
const service = new AuthService(
|
||||
{ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() } as any,
|
||||
{ getCurrentSession: vi.fn(), createSession: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
resetPasswordUseCase as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeCommandResultPresenter() as any,
|
||||
new FakeForgotPasswordPresenter() as any,
|
||||
resetPasswordPresenter as any,
|
||||
new FakeDemoLoginPresenter() as any,
|
||||
);
|
||||
|
||||
const result = await service.resetPassword({
|
||||
token: 'abc123',
|
||||
newPassword: 'NewPass123!',
|
||||
});
|
||||
|
||||
expect(resetPasswordUseCase.execute).toHaveBeenCalledWith({
|
||||
token: 'abc123',
|
||||
newPassword: 'NewPass123!',
|
||||
});
|
||||
expect(result).toEqual({ message: 'Password reset successfully' });
|
||||
});
|
||||
|
||||
it('should throw error on use case failure', async () => {
|
||||
const service = new AuthService(
|
||||
{ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() } as any,
|
||||
{ getCurrentSession: vi.fn(), createSession: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn(async () => Result.err({ code: 'INVALID_TOKEN', details: { message: 'Invalid token' } })) } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeCommandResultPresenter() as any,
|
||||
new FakeForgotPasswordPresenter() as any,
|
||||
new FakeResetPasswordPresenter() as any,
|
||||
new FakeDemoLoginPresenter() as any,
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.resetPassword({ token: 'invalid', newPassword: 'NewPass123!' })
|
||||
).rejects.toThrow('Invalid token');
|
||||
});
|
||||
});
|
||||
|
||||
describe('demoLogin', () => {
|
||||
it('should execute demo login use case and create session', async () => {
|
||||
const demoLoginPresenter = new FakeDemoLoginPresenter();
|
||||
const mockUser = {
|
||||
getId: () => ({ value: 'demo-user-123' }),
|
||||
getDisplayName: () => 'Demo Driver',
|
||||
getEmail: () => 'demo.driver@example.com',
|
||||
};
|
||||
|
||||
const demoLoginUseCase = {
|
||||
execute: vi.fn(async () => {
|
||||
demoLoginPresenter.present({ user: mockUser });
|
||||
return Result.ok(undefined);
|
||||
}),
|
||||
};
|
||||
|
||||
const identitySessionPort = {
|
||||
getCurrentSession: vi.fn(),
|
||||
createSession: vi.fn(async () => ({ token: 'demo-token-123' })),
|
||||
};
|
||||
|
||||
const service = new AuthService(
|
||||
{ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() } as any,
|
||||
identitySessionPort as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
demoLoginUseCase as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeCommandResultPresenter() as any,
|
||||
new FakeForgotPasswordPresenter() as any,
|
||||
new FakeResetPasswordPresenter() as any,
|
||||
demoLoginPresenter as any,
|
||||
);
|
||||
|
||||
const result = await service.demoLogin({ role: 'driver' });
|
||||
|
||||
expect(demoLoginUseCase.execute).toHaveBeenCalledWith({ role: 'driver' });
|
||||
expect(identitySessionPort.createSession).toHaveBeenCalledWith({
|
||||
id: 'demo-user-123',
|
||||
displayName: 'Demo Driver',
|
||||
email: 'demo.driver@example.com',
|
||||
});
|
||||
expect(result).toEqual({
|
||||
token: 'demo-token-123',
|
||||
user: {
|
||||
userId: 'demo-user-123',
|
||||
email: 'demo.driver@example.com',
|
||||
displayName: 'Demo Driver',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error on use case failure', async () => {
|
||||
const service = new AuthService(
|
||||
{ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() } as any,
|
||||
{ getCurrentSession: vi.fn(), createSession: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn(async () => Result.err({ code: 'DEMO_NOT_ALLOWED', details: { message: 'Demo not allowed' } })) } as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeCommandResultPresenter() as any,
|
||||
new FakeForgotPasswordPresenter() as any,
|
||||
new FakeResetPasswordPresenter() as any,
|
||||
new FakeDemoLoginPresenter() as any,
|
||||
);
|
||||
|
||||
await expect(service.demoLogin({ role: 'driver' })).rejects.toThrow('Demo not allowed');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -38,8 +38,14 @@ describe('AuthService', () => {
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeCommandResultPresenter() as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
);
|
||||
|
||||
await expect(service.getCurrentSession()).resolves.toBeNull();
|
||||
@@ -58,8 +64,14 @@ describe('AuthService', () => {
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeCommandResultPresenter() as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
);
|
||||
|
||||
await expect(service.getCurrentSession()).resolves.toEqual({
|
||||
@@ -88,8 +100,14 @@ describe('AuthService', () => {
|
||||
{ execute: vi.fn() } as any,
|
||||
signupUseCase as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
authSessionPresenter as any,
|
||||
new FakeCommandResultPresenter() as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
);
|
||||
|
||||
const session = await service.signupWithEmail({
|
||||
@@ -118,8 +136,14 @@ describe('AuthService', () => {
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR' } as any)) } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeCommandResultPresenter() as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
);
|
||||
|
||||
await expect(
|
||||
@@ -147,8 +171,14 @@ describe('AuthService', () => {
|
||||
loginUseCase as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
authSessionPresenter as any,
|
||||
new FakeCommandResultPresenter() as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
);
|
||||
|
||||
await expect(service.loginWithEmail({ email: 'e3', password: 'p3' } as any)).resolves.toEqual({
|
||||
@@ -171,8 +201,14 @@ describe('AuthService', () => {
|
||||
{ execute: vi.fn(async () => Result.err({ code: 'INVALID_CREDENTIALS', details: { message: 'Bad login' } })) } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeCommandResultPresenter() as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
);
|
||||
|
||||
await expect(service.loginWithEmail({ email: 'e', password: 'p' } as any)).rejects.toThrow('Bad login');
|
||||
@@ -185,8 +221,14 @@ describe('AuthService', () => {
|
||||
{ execute: vi.fn(async () => Result.err({ code: 'INVALID_CREDENTIALS' } as any)) } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeCommandResultPresenter() as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
);
|
||||
|
||||
await expect(service.loginWithEmail({ email: 'e', password: 'p' } as any)).rejects.toThrow('Login failed');
|
||||
@@ -207,8 +249,14 @@ describe('AuthService', () => {
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
logoutUseCase as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
commandResultPresenter as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
);
|
||||
|
||||
await expect(service.logout()).resolves.toEqual({ success: true });
|
||||
@@ -221,8 +269,14 @@ describe('AuthService', () => {
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR' } as any)) } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
{ execute: vi.fn() } as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeCommandResultPresenter() as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
new FakeAuthSessionPresenter() as any,
|
||||
);
|
||||
|
||||
await expect(service.logout()).rejects.toThrow('Logout failed');
|
||||
|
||||
@@ -13,23 +13,47 @@ import {
|
||||
type SignupApplicationError,
|
||||
type SignupInput,
|
||||
} from '@core/identity/application/use-cases/SignupUseCase';
|
||||
import {
|
||||
ForgotPasswordUseCase,
|
||||
type ForgotPasswordApplicationError,
|
||||
type ForgotPasswordInput,
|
||||
} from '@core/identity/application/use-cases/ForgotPasswordUseCase';
|
||||
import {
|
||||
ResetPasswordUseCase,
|
||||
type ResetPasswordApplicationError,
|
||||
type ResetPasswordInput,
|
||||
} from '@core/identity/application/use-cases/ResetPasswordUseCase';
|
||||
import {
|
||||
DemoLoginUseCase,
|
||||
type DemoLoginApplicationError,
|
||||
type DemoLoginInput,
|
||||
} from '../../development/use-cases/DemoLoginUseCase';
|
||||
|
||||
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,
|
||||
RESET_PASSWORD_OUTPUT_PORT_TOKEN,
|
||||
DEMO_LOGIN_OUTPUT_PORT_TOKEN,
|
||||
IDENTITY_SESSION_PORT_TOKEN,
|
||||
LOGGER_TOKEN,
|
||||
LOGIN_USE_CASE_TOKEN,
|
||||
LOGOUT_USE_CASE_TOKEN,
|
||||
SIGNUP_USE_CASE_TOKEN,
|
||||
FORGOT_PASSWORD_USE_CASE_TOKEN,
|
||||
RESET_PASSWORD_USE_CASE_TOKEN,
|
||||
DEMO_LOGIN_USE_CASE_TOKEN,
|
||||
} from './AuthProviders';
|
||||
import type { AuthSessionDTO } from './dtos/AuthDto';
|
||||
import type { AuthSessionDTO, AuthenticatedUserDTO } from './dtos/AuthDto';
|
||||
import { LoginParamsDTO, SignupParamsDTO } 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';
|
||||
import { DemoLoginPresenter } from './presenters/DemoLoginPresenter';
|
||||
|
||||
function mapApplicationErrorToMessage(error: { details?: { message?: string } } | undefined, fallback: string): string {
|
||||
return error?.details?.message ?? fallback;
|
||||
@@ -43,11 +67,20 @@ export class AuthService {
|
||||
@Inject(LOGIN_USE_CASE_TOKEN) private readonly loginUseCase: LoginUseCase,
|
||||
@Inject(SIGNUP_USE_CASE_TOKEN) private readonly signupUseCase: SignupUseCase,
|
||||
@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,
|
||||
@Inject(DEMO_LOGIN_USE_CASE_TOKEN) private readonly demoLoginUseCase: DemoLoginUseCase,
|
||||
// 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,
|
||||
@Inject(DEMO_LOGIN_OUTPUT_PORT_TOKEN)
|
||||
private readonly demoLoginPresenter: DemoLoginPresenter,
|
||||
) {}
|
||||
|
||||
async getCurrentSession(): Promise<AuthSessionDTO | null> {
|
||||
@@ -189,4 +222,94 @@ export class AuthService {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async forgotPassword(params: { email: string }): Promise<{ message: string; magicLink?: string }> {
|
||||
this.logger.debug(`[AuthService] Attempting forgot password for email: ${params.email}`);
|
||||
|
||||
this.forgotPasswordPresenter.reset();
|
||||
|
||||
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 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');
|
||||
|
||||
this.resetPasswordPresenter.reset();
|
||||
|
||||
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'));
|
||||
}
|
||||
|
||||
return this.resetPasswordPresenter.responseModel;
|
||||
}
|
||||
|
||||
async demoLogin(params: { role: 'driver' | 'sponsor' | 'league-owner' | 'league-steward' | 'league-admin' | 'system-owner' | 'super-admin' }): Promise<AuthSessionDTO> {
|
||||
this.logger.debug(`[AuthService] Attempting demo login for role: ${params.role}`);
|
||||
|
||||
this.demoLoginPresenter.reset();
|
||||
|
||||
const input: DemoLoginInput = {
|
||||
role: params.role,
|
||||
};
|
||||
|
||||
const result = await this.demoLoginUseCase.execute(input);
|
||||
|
||||
if (result.isErr()) {
|
||||
const error = result.unwrapErr() as DemoLoginApplicationError;
|
||||
throw new Error(mapApplicationErrorToMessage(error, 'Demo login failed'));
|
||||
}
|
||||
|
||||
const user = this.demoLoginPresenter.responseModel.user;
|
||||
const primaryDriverId = user.getPrimaryDriverId();
|
||||
|
||||
// Use primaryDriverId for session if available, otherwise fall back to userId
|
||||
const sessionId = primaryDriverId ?? user.getId().value;
|
||||
|
||||
const session = await this.identitySessionPort.createSession({
|
||||
id: sessionId,
|
||||
displayName: user.getDisplayName(),
|
||||
email: user.getEmail() ?? '',
|
||||
});
|
||||
|
||||
const userDTO: AuthenticatedUserDTO = {
|
||||
userId: user.getId().value,
|
||||
email: user.getEmail() ?? '',
|
||||
displayName: user.getDisplayName(),
|
||||
};
|
||||
|
||||
if (primaryDriverId !== undefined) {
|
||||
userDTO.primaryDriverId = primaryDriverId;
|
||||
}
|
||||
|
||||
return {
|
||||
token: session.token,
|
||||
user: userDTO,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
18
apps/api/src/domain/auth/ProductionGuard.ts
Normal file
18
apps/api/src/domain/auth/ProductionGuard.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { CanActivate, ExecutionContext, Injectable, ForbiddenException } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class ProductionGuard implements CanActivate {
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const path = request.path;
|
||||
|
||||
// Block demo login in production
|
||||
if (path === '/auth/demo-login' || path === '/api/auth/demo-login') {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
throw new ForbiddenException('Demo login is not available in production');
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEmail, IsString, MinLength, IsIn } from 'class-validator';
|
||||
|
||||
export class AuthenticatedUserDTO {
|
||||
@ApiProperty()
|
||||
@@ -7,6 +8,10 @@ export class AuthenticatedUserDTO {
|
||||
email!: string;
|
||||
@ApiProperty()
|
||||
displayName!: string;
|
||||
@ApiProperty({ required: false })
|
||||
primaryDriverId?: string;
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
avatarUrl?: string | null;
|
||||
}
|
||||
|
||||
export class AuthSessionDTO {
|
||||
@@ -53,3 +58,27 @@ export class LoginWithIracingCallbackParamsDTO {
|
||||
@ApiProperty({ required: false })
|
||||
returnTo?: string;
|
||||
}
|
||||
|
||||
export class ForgotPasswordDTO {
|
||||
@ApiProperty()
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
}
|
||||
|
||||
export class ResetPasswordDTO {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
token!: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
newPassword!: string;
|
||||
}
|
||||
|
||||
export class DemoLoginDTO {
|
||||
@ApiProperty({ enum: ['driver', 'sponsor', 'league-owner', 'league-steward', 'league-admin', 'system-owner', 'super-admin'] })
|
||||
@IsString()
|
||||
@IsIn(['driver', 'sponsor', 'league-owner', 'league-steward', 'league-admin', 'system-owner', 'super-admin'])
|
||||
role!: 'driver' | 'sponsor' | 'league-owner' | 'league-steward' | 'league-admin' | 'system-owner' | 'super-admin';
|
||||
}
|
||||
|
||||
@@ -13,10 +13,14 @@ export class AuthSessionPresenter implements UseCaseOutputPort<AuthSessionResult
|
||||
}
|
||||
|
||||
present(result: AuthSessionResult): void {
|
||||
const primaryDriverId = result.user.getPrimaryDriverId();
|
||||
const avatarUrl = result.user.getAvatarUrl();
|
||||
this.model = {
|
||||
userId: result.user.getId().value,
|
||||
email: result.user.getEmail() ?? '',
|
||||
displayName: result.user.getDisplayName() ?? '',
|
||||
...(primaryDriverId !== undefined ? { primaryDriverId } : {}),
|
||||
...(avatarUrl !== undefined ? { avatarUrl } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
23
apps/api/src/domain/auth/presenters/DemoLoginPresenter.ts
Normal file
23
apps/api/src/domain/auth/presenters/DemoLoginPresenter.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UseCaseOutputPort } from '@core/shared/application';
|
||||
import { DemoLoginResult } from '../../../development/use-cases/DemoLoginUseCase';
|
||||
|
||||
@Injectable()
|
||||
export class DemoLoginPresenter implements UseCaseOutputPort<DemoLoginResult> {
|
||||
private _responseModel: DemoLoginResult | null = null;
|
||||
|
||||
present(result: DemoLoginResult): void {
|
||||
this._responseModel = result;
|
||||
}
|
||||
|
||||
get responseModel(): DemoLoginResult {
|
||||
if (!this._responseModel) {
|
||||
throw new Error('DemoLoginPresenter: No response model available');
|
||||
}
|
||||
return this._responseModel;
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this._responseModel = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UseCaseOutputPort } from '@core/shared/application';
|
||||
import { ForgotPasswordResult } from '@core/identity/application/use-cases/ForgotPasswordUseCase';
|
||||
|
||||
@Injectable()
|
||||
export class ForgotPasswordPresenter implements UseCaseOutputPort<ForgotPasswordResult> {
|
||||
private _responseModel: ForgotPasswordResult | null = null;
|
||||
|
||||
present(result: ForgotPasswordResult): void {
|
||||
this._responseModel = result;
|
||||
}
|
||||
|
||||
get responseModel(): ForgotPasswordResult {
|
||||
if (!this._responseModel) {
|
||||
throw new Error('ForgotPasswordPresenter: No response model available');
|
||||
}
|
||||
return this._responseModel;
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this._responseModel = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UseCaseOutputPort } from '@core/shared/application';
|
||||
import { ResetPasswordResult } from '@core/identity/application/use-cases/ResetPasswordUseCase';
|
||||
|
||||
@Injectable()
|
||||
export class ResetPasswordPresenter implements UseCaseOutputPort<ResetPasswordResult> {
|
||||
private _responseModel: ResetPasswordResult | null = null;
|
||||
|
||||
present(result: ResetPasswordResult): void {
|
||||
this._responseModel = result;
|
||||
}
|
||||
|
||||
get responseModel(): ResetPasswordResult {
|
||||
if (!this._responseModel) {
|
||||
throw new Error('ResetPasswordPresenter: No response model available');
|
||||
}
|
||||
return this._responseModel;
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this._responseModel = null;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { DashboardOverviewUseCase } from '@core/racing/application/use-cases/Das
|
||||
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
|
||||
import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
|
||||
import { DashboardOverviewPresenter } from './presenters/DashboardOverviewPresenter';
|
||||
import { DashboardService } from './DashboardService';
|
||||
|
||||
// Define injection tokens
|
||||
export const LOGGER_TOKEN = 'Logger';
|
||||
@@ -92,4 +93,19 @@ export const DashboardProviders: Provider[] = [
|
||||
DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: DashboardService,
|
||||
useFactory: (
|
||||
logger: Logger,
|
||||
dashboardOverviewUseCase: DashboardOverviewUseCase,
|
||||
presenter: DashboardOverviewPresenter,
|
||||
imageService: ImageServicePort,
|
||||
) => new DashboardService(logger, dashboardOverviewUseCase, presenter, imageService),
|
||||
inject: [
|
||||
LOGGER_TOKEN,
|
||||
DASHBOARD_OVERVIEW_USE_CASE_TOKEN,
|
||||
DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN,
|
||||
IMAGE_SERVICE_TOKEN,
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -11,6 +11,7 @@ describe('DashboardService', () => {
|
||||
{ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() } as any,
|
||||
useCase as any,
|
||||
presenter as any,
|
||||
{ getDriverAvatar: vi.fn(() => '/media/avatar/test') } as any,
|
||||
);
|
||||
|
||||
await expect(service.getDashboardOverview('d1')).resolves.toEqual({ feed: [] });
|
||||
@@ -22,6 +23,7 @@ describe('DashboardService', () => {
|
||||
{ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() } as any,
|
||||
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } })) } as any,
|
||||
{ getResponseModel: vi.fn() } as any,
|
||||
{ getDriverAvatar: vi.fn(() => '/media/avatar/test') } as any,
|
||||
);
|
||||
|
||||
await expect(service.getDashboardOverview('d1')).rejects.toThrow('Failed to get dashboard overview: boom');
|
||||
@@ -32,6 +34,7 @@ describe('DashboardService', () => {
|
||||
{ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() } as any,
|
||||
{ execute: vi.fn(async () => Result.err({ code: 'REPOSITORY_ERROR' } as any)) } as any,
|
||||
{ getResponseModel: vi.fn() } as any,
|
||||
{ getDriverAvatar: vi.fn(() => '/media/avatar/test') } as any,
|
||||
);
|
||||
|
||||
await expect(service.getDashboardOverview('d1')).rejects.toThrow('Failed to get dashboard overview: Unknown error');
|
||||
|
||||
@@ -5,9 +5,10 @@ import { DashboardOverviewPresenter } from './presenters/DashboardOverviewPresen
|
||||
|
||||
// Core imports
|
||||
import type { Logger } from '@core/shared/application/Logger';
|
||||
import type { ImageServicePort } from '@core/media/application/ports/ImageServicePort';
|
||||
|
||||
// Tokens
|
||||
import { DASHBOARD_OVERVIEW_USE_CASE_TOKEN, LOGGER_TOKEN, DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN } from './DashboardProviders';
|
||||
import { DASHBOARD_OVERVIEW_USE_CASE_TOKEN, LOGGER_TOKEN, DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN, IMAGE_SERVICE_TOKEN } from './DashboardProviders';
|
||||
|
||||
@Injectable()
|
||||
export class DashboardService {
|
||||
@@ -15,11 +16,27 @@ export class DashboardService {
|
||||
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
|
||||
@Inject(DASHBOARD_OVERVIEW_USE_CASE_TOKEN) private readonly dashboardOverviewUseCase: DashboardOverviewUseCase,
|
||||
@Inject(DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN) private readonly presenter: DashboardOverviewPresenter,
|
||||
@Inject(IMAGE_SERVICE_TOKEN) private readonly imageService: ImageServicePort,
|
||||
) {}
|
||||
|
||||
async getDashboardOverview(driverId: string): Promise<DashboardOverviewDTO> {
|
||||
this.logger.debug('[DashboardService] Getting dashboard overview:', { driverId });
|
||||
|
||||
// Check if this is a demo user
|
||||
const isDemoUser = driverId.startsWith('demo-driver-') ||
|
||||
driverId.startsWith('demo-sponsor-') ||
|
||||
driverId.startsWith('demo-league-owner-') ||
|
||||
driverId.startsWith('demo-league-steward-') ||
|
||||
driverId.startsWith('demo-league-admin-') ||
|
||||
driverId.startsWith('demo-system-owner-') ||
|
||||
driverId.startsWith('demo-super-admin-');
|
||||
|
||||
if (isDemoUser) {
|
||||
// Return mock dashboard data for demo users
|
||||
this.logger.info('[DashboardService] Returning mock data for demo user', { driverId });
|
||||
return await this.getMockDashboardData(driverId);
|
||||
}
|
||||
|
||||
const result = await this.dashboardOverviewUseCase.execute({ driverId });
|
||||
|
||||
if (result.isErr()) {
|
||||
@@ -30,4 +47,185 @@ export class DashboardService {
|
||||
|
||||
return this.presenter.getResponseModel();
|
||||
}
|
||||
|
||||
private async getMockDashboardData(driverId: string): Promise<DashboardOverviewDTO> {
|
||||
// Determine role from driverId prefix
|
||||
const isSponsor = driverId.startsWith('demo-sponsor-');
|
||||
const isLeagueOwner = driverId.startsWith('demo-league-owner-');
|
||||
const isLeagueSteward = driverId.startsWith('demo-league-steward-');
|
||||
const isLeagueAdmin = driverId.startsWith('demo-league-admin-');
|
||||
const isSystemOwner = driverId.startsWith('demo-system-owner-');
|
||||
const isSuperAdmin = driverId.startsWith('demo-super-admin-');
|
||||
|
||||
// Get avatar URL using the image service (same as real drivers)
|
||||
const avatarUrl = this.imageService.getDriverAvatar(driverId);
|
||||
|
||||
// Mock sponsor dashboard
|
||||
if (isSponsor) {
|
||||
return {
|
||||
currentDriver: null,
|
||||
myUpcomingRaces: [],
|
||||
otherUpcomingRaces: [],
|
||||
upcomingRaces: [],
|
||||
activeLeaguesCount: 0,
|
||||
nextRace: null,
|
||||
recentResults: [],
|
||||
leagueStandingsSummaries: [],
|
||||
feedSummary: {
|
||||
notificationCount: 0,
|
||||
items: [],
|
||||
},
|
||||
friends: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Mock league admin/owner/steward dashboard (similar to driver but with more leagues)
|
||||
if (isLeagueOwner || isLeagueSteward || isLeagueAdmin) {
|
||||
const roleTitle = isLeagueOwner ? 'League Owner' : isLeagueSteward ? 'League Steward' : 'League Admin';
|
||||
return {
|
||||
currentDriver: {
|
||||
id: driverId,
|
||||
name: `Demo ${roleTitle}`,
|
||||
country: 'US',
|
||||
avatarUrl,
|
||||
rating: 1600,
|
||||
globalRank: 15,
|
||||
totalRaces: 8,
|
||||
wins: 3,
|
||||
podiums: 5,
|
||||
consistency: 90,
|
||||
},
|
||||
myUpcomingRaces: [],
|
||||
otherUpcomingRaces: [],
|
||||
upcomingRaces: [],
|
||||
activeLeaguesCount: 2,
|
||||
nextRace: null,
|
||||
recentResults: [],
|
||||
leagueStandingsSummaries: [],
|
||||
feedSummary: {
|
||||
notificationCount: 2,
|
||||
items: [
|
||||
{
|
||||
id: 'feed-1',
|
||||
type: 'league_update',
|
||||
headline: 'New league season starting',
|
||||
body: 'Your league "Demo League" is about to start a new season',
|
||||
timestamp: new Date().toISOString(),
|
||||
ctaLabel: 'View League',
|
||||
ctaHref: '/leagues',
|
||||
},
|
||||
],
|
||||
},
|
||||
friends: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Mock system owner dashboard (highest privileges)
|
||||
if (isSystemOwner) {
|
||||
return {
|
||||
currentDriver: {
|
||||
id: driverId,
|
||||
name: 'System Owner',
|
||||
country: 'US',
|
||||
avatarUrl,
|
||||
rating: 2000,
|
||||
globalRank: 1,
|
||||
totalRaces: 50,
|
||||
wins: 25,
|
||||
podiums: 40,
|
||||
consistency: 95,
|
||||
},
|
||||
myUpcomingRaces: [],
|
||||
otherUpcomingRaces: [],
|
||||
upcomingRaces: [],
|
||||
activeLeaguesCount: 10,
|
||||
nextRace: null,
|
||||
recentResults: [],
|
||||
leagueStandingsSummaries: [],
|
||||
feedSummary: {
|
||||
notificationCount: 5,
|
||||
items: [
|
||||
{
|
||||
id: 'feed-1',
|
||||
type: 'system_alert',
|
||||
headline: 'System maintenance scheduled',
|
||||
body: 'Platform will undergo maintenance in 24 hours',
|
||||
timestamp: new Date().toISOString(),
|
||||
ctaLabel: 'View Details',
|
||||
ctaHref: '/admin/system',
|
||||
},
|
||||
],
|
||||
},
|
||||
friends: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Mock super admin dashboard (all access)
|
||||
if (isSuperAdmin) {
|
||||
return {
|
||||
currentDriver: {
|
||||
id: driverId,
|
||||
name: 'Super Admin',
|
||||
country: 'US',
|
||||
avatarUrl,
|
||||
rating: 1800,
|
||||
globalRank: 5,
|
||||
totalRaces: 30,
|
||||
wins: 15,
|
||||
podiums: 25,
|
||||
consistency: 92,
|
||||
},
|
||||
myUpcomingRaces: [],
|
||||
otherUpcomingRaces: [],
|
||||
upcomingRaces: [],
|
||||
activeLeaguesCount: 5,
|
||||
nextRace: null,
|
||||
recentResults: [],
|
||||
leagueStandingsSummaries: [],
|
||||
feedSummary: {
|
||||
notificationCount: 3,
|
||||
items: [
|
||||
{
|
||||
id: 'feed-1',
|
||||
type: 'admin_notification',
|
||||
headline: 'Admin dashboard access granted',
|
||||
body: 'You have full administrative access to all platform features',
|
||||
timestamp: new Date().toISOString(),
|
||||
ctaLabel: 'Admin Panel',
|
||||
ctaHref: '/admin',
|
||||
},
|
||||
],
|
||||
},
|
||||
friends: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Mock driver dashboard (default)
|
||||
return {
|
||||
currentDriver: {
|
||||
id: driverId,
|
||||
name: 'John Demo',
|
||||
country: 'US',
|
||||
avatarUrl,
|
||||
rating: 1500,
|
||||
globalRank: 25,
|
||||
totalRaces: 5,
|
||||
wins: 2,
|
||||
podiums: 3,
|
||||
consistency: 85,
|
||||
},
|
||||
myUpcomingRaces: [],
|
||||
otherUpcomingRaces: [],
|
||||
upcomingRaces: [],
|
||||
activeLeaguesCount: 0,
|
||||
nextRace: null,
|
||||
recentResults: [],
|
||||
leagueStandingsSummaries: [],
|
||||
feedSummary: {
|
||||
notificationCount: 0,
|
||||
items: [],
|
||||
},
|
||||
friends: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export const AUTH_REPOSITORY_TOKEN = 'IAuthRepository';
|
||||
export const USER_REPOSITORY_TOKEN = 'IUserRepository';
|
||||
export const PASSWORD_HASHING_SERVICE_TOKEN = 'IPasswordHashingService';
|
||||
export const PASSWORD_HASHING_SERVICE_TOKEN = 'IPasswordHashingService';
|
||||
export const MAGIC_LINK_REPOSITORY_TOKEN = 'IMagicLinkRepository';
|
||||
@@ -9,8 +9,9 @@ import type { StoredUser } from '@core/identity/domain/repositories/IUserReposit
|
||||
import { InMemoryAuthRepository } from '@adapters/identity/persistence/inmemory/InMemoryAuthRepository';
|
||||
import { InMemoryUserRepository } from '@adapters/identity/persistence/inmemory/InMemoryUserRepository';
|
||||
import { InMemoryPasswordHashingService } from '@adapters/identity/services/InMemoryPasswordHashingService';
|
||||
import { InMemoryMagicLinkRepository } from '@adapters/identity/persistence/inmemory/InMemoryMagicLinkRepository';
|
||||
|
||||
import { AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, USER_REPOSITORY_TOKEN } from '../identity/IdentityPersistenceTokens';
|
||||
import { AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, USER_REPOSITORY_TOKEN, MAGIC_LINK_REPOSITORY_TOKEN } from '../identity/IdentityPersistenceTokens';
|
||||
|
||||
@Module({
|
||||
imports: [LoggingModule],
|
||||
@@ -25,7 +26,6 @@ import { AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, USER_REPOSITORY_
|
||||
email: 'admin@gridpilot.local',
|
||||
passwordHash: 'demo_salt_321nimda', // InMemoryPasswordHashingService: "admin123" reversed.
|
||||
displayName: 'Admin',
|
||||
salt: '',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
];
|
||||
@@ -43,7 +43,12 @@ import { AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, USER_REPOSITORY_
|
||||
provide: PASSWORD_HASHING_SERVICE_TOKEN,
|
||||
useClass: InMemoryPasswordHashingService,
|
||||
},
|
||||
{
|
||||
provide: MAGIC_LINK_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryMagicLinkRepository(logger),
|
||||
inject: ['Logger'],
|
||||
},
|
||||
],
|
||||
exports: [USER_REPOSITORY_TOKEN, AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN],
|
||||
exports: [USER_REPOSITORY_TOKEN, AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, MAGIC_LINK_REPOSITORY_TOKEN],
|
||||
})
|
||||
export class InMemoryIdentityPersistenceModule {}
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule, getDataSourceToken } from '@nestjs/typeorm';
|
||||
import type { DataSource } from 'typeorm';
|
||||
import type { Logger } from '@core/shared/application/Logger';
|
||||
|
||||
import { UserOrmEntity } from '@adapters/identity/persistence/typeorm/entities/UserOrmEntity';
|
||||
import { PasswordResetRequestOrmEntity } from '@adapters/identity/persistence/typeorm/entities/PasswordResetRequestOrmEntity';
|
||||
import { TypeOrmAuthRepository } from '@adapters/identity/persistence/typeorm/repositories/TypeOrmAuthRepository';
|
||||
import { TypeOrmUserRepository } from '@adapters/identity/persistence/typeorm/repositories/TypeOrmUserRepository';
|
||||
import { TypeOrmMagicLinkRepository } from '@adapters/identity/persistence/typeorm/repositories/TypeOrmMagicLinkRepository';
|
||||
import { UserOrmMapper } from '@adapters/identity/persistence/typeorm/mappers/UserOrmMapper';
|
||||
import { InMemoryPasswordHashingService } from '@adapters/identity/services/InMemoryPasswordHashingService';
|
||||
|
||||
@@ -12,9 +15,10 @@ import {
|
||||
AUTH_REPOSITORY_TOKEN,
|
||||
PASSWORD_HASHING_SERVICE_TOKEN,
|
||||
USER_REPOSITORY_TOKEN,
|
||||
MAGIC_LINK_REPOSITORY_TOKEN,
|
||||
} from '../identity/IdentityPersistenceTokens';
|
||||
|
||||
const typeOrmFeatureImports = [TypeOrmModule.forFeature([UserOrmEntity])];
|
||||
const typeOrmFeatureImports = [TypeOrmModule.forFeature([UserOrmEntity, PasswordResetRequestOrmEntity])];
|
||||
|
||||
@Module({
|
||||
imports: [...typeOrmFeatureImports],
|
||||
@@ -34,7 +38,12 @@ const typeOrmFeatureImports = [TypeOrmModule.forFeature([UserOrmEntity])];
|
||||
provide: PASSWORD_HASHING_SERVICE_TOKEN,
|
||||
useClass: InMemoryPasswordHashingService,
|
||||
},
|
||||
{
|
||||
provide: MAGIC_LINK_REPOSITORY_TOKEN,
|
||||
useFactory: (dataSource: DataSource, logger: Logger) => new TypeOrmMagicLinkRepository(dataSource, logger),
|
||||
inject: [getDataSourceToken(), 'Logger'],
|
||||
},
|
||||
],
|
||||
exports: [USER_REPOSITORY_TOKEN, AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN],
|
||||
exports: [USER_REPOSITORY_TOKEN, AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, MAGIC_LINK_REPOSITORY_TOKEN],
|
||||
})
|
||||
export class PostgresIdentityPersistenceModule {}
|
||||
@@ -56,6 +56,7 @@ import {
|
||||
TeamOrmEntity,
|
||||
} from '@adapters/racing/persistence/typeorm/entities/TeamOrmEntities';
|
||||
import { TeamStatsOrmEntity } from '@adapters/racing/persistence/typeorm/entities/TeamStatsOrmEntity';
|
||||
import { DriverStatsOrmEntity } from '@adapters/racing/persistence/typeorm/entities/DriverStatsOrmEntity';
|
||||
|
||||
import { TypeOrmDriverRepository } from '@adapters/racing/persistence/typeorm/repositories/TypeOrmDriverRepository';
|
||||
import { TypeOrmLeagueMembershipRepository } from '@adapters/racing/persistence/typeorm/repositories/TypeOrmLeagueMembershipRepository';
|
||||
@@ -78,13 +79,13 @@ import {
|
||||
import { TypeOrmPenaltyRepository, TypeOrmProtestRepository } from '@adapters/racing/persistence/typeorm/repositories/StewardingTypeOrmRepositories';
|
||||
import { TypeOrmTeamMembershipRepository, TypeOrmTeamRepository } from '@adapters/racing/persistence/typeorm/repositories/TeamTypeOrmRepositories';
|
||||
|
||||
// Import in-memory implementations for new repositories (TypeORM versions not yet implemented)
|
||||
import { InMemoryDriverStatsRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverStatsRepository';
|
||||
import { InMemoryMediaRepository } from '@adapters/racing/persistence/media/InMemoryMediaRepository';
|
||||
|
||||
// Import TypeORM repository for team stats
|
||||
// Import TypeORM repositories
|
||||
import { TypeOrmDriverStatsRepository } from '@adapters/racing/persistence/typeorm/repositories/TypeOrmDriverStatsRepository';
|
||||
import { TypeOrmTeamStatsRepository } from '@adapters/racing/persistence/typeorm/repositories/TypeOrmTeamStatsRepository';
|
||||
|
||||
// Import in-memory implementations for new repositories (TypeORM versions not yet implemented)
|
||||
import { InMemoryMediaRepository } from '@adapters/racing/persistence/media/InMemoryMediaRepository';
|
||||
|
||||
import { DriverOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/DriverOrmMapper';
|
||||
import { LeagueMembershipOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/LeagueMembershipOrmMapper';
|
||||
import { LeagueOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/LeagueOrmMapper';
|
||||
@@ -109,6 +110,7 @@ import { MoneyOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/Mon
|
||||
import { PenaltyOrmMapper, ProtestOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/StewardingOrmMappers';
|
||||
import { TeamMembershipOrmMapper, TeamOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/TeamOrmMappers';
|
||||
import { TeamStatsOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/TeamStatsOrmMapper';
|
||||
import { DriverStatsOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/DriverStatsOrmMapper';
|
||||
|
||||
import { getPointsSystems } from '@adapters/bootstrap/PointsSystems';
|
||||
import type { Logger } from '@core/shared/application/Logger';
|
||||
@@ -131,6 +133,7 @@ const typeOrmFeatureImports = [
|
||||
TeamMembershipOrmEntity,
|
||||
TeamJoinRequestOrmEntity,
|
||||
TeamStatsOrmEntity,
|
||||
DriverStatsOrmEntity,
|
||||
|
||||
PenaltyOrmEntity,
|
||||
ProtestOrmEntity,
|
||||
@@ -161,6 +164,7 @@ const typeOrmFeatureImports = [
|
||||
{ provide: TeamOrmMapper, useFactory: () => new TeamOrmMapper() },
|
||||
{ provide: TeamMembershipOrmMapper, useFactory: () => new TeamMembershipOrmMapper() },
|
||||
{ provide: TeamStatsOrmMapper, useFactory: () => new TeamStatsOrmMapper() },
|
||||
{ provide: DriverStatsOrmMapper, useFactory: () => new DriverStatsOrmMapper() },
|
||||
|
||||
{ provide: PenaltyOrmMapper, useFactory: () => new PenaltyOrmMapper() },
|
||||
{ provide: ProtestOrmMapper, useFactory: () => new ProtestOrmMapper() },
|
||||
@@ -322,8 +326,9 @@ const typeOrmFeatureImports = [
|
||||
},
|
||||
{
|
||||
provide: DRIVER_STATS_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryDriverStatsRepository(logger),
|
||||
inject: ['Logger'],
|
||||
useFactory: (repo: Repository<DriverStatsOrmEntity>, mapper: DriverStatsOrmMapper) =>
|
||||
new TypeOrmDriverStatsRepository(repo, mapper),
|
||||
inject: [getRepositoryToken(DriverStatsOrmEntity), DriverStatsOrmMapper],
|
||||
},
|
||||
{
|
||||
provide: TEAM_STATS_REPOSITORY_TOKEN,
|
||||
|
||||
Reference in New Issue
Block a user