remove demo code

This commit is contained in:
2026-01-03 11:38:51 +01:00
parent 2f21dc4595
commit 9a7efa496f
38 changed files with 1535 additions and 1157 deletions

View File

@@ -60,15 +60,6 @@
}
}
},
"/auth/demo-login": {
"post": {
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/auth/forgot-password": {
"post": {
"responses": {
@@ -2157,20 +2148,6 @@
"success"
]
},
"DemoLoginDTO": {
"type": "object",
"properties": {
"role": {
"type": "string"
},
"rememberMe": {
"type": "boolean"
}
},
"required": [
"role"
]
},
"DriverDTO": {
"type": "object",
"properties": {

View File

@@ -1,167 +0,0 @@
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';
import type { IAdminUserRepository } from '@core/admin/domain/repositories/IAdminUserRepository';
import { AdminUser } from '@core/admin/domain/entities/AdminUser';
import { Email } from '@core/admin/domain/value-objects/Email';
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>,
private readonly adminUserRepo?: IAdminUserRepository,
) {}
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, adminRole: null },
'sponsor': { email: 'demo.sponsor@example.com', name: 'Jane Sponsor', primaryDriverId: false, adminRole: null },
'league-owner': { email: 'demo.owner@example.com', name: 'Alex Owner', primaryDriverId: true, adminRole: 'owner' },
'league-steward': { email: 'demo.steward@example.com', name: 'Sam Steward', primaryDriverId: true, adminRole: 'admin' },
'league-admin': { email: 'demo.admin@example.com', name: 'Taylor Admin', primaryDriverId: true, adminRole: 'admin' },
'system-owner': { email: 'demo.systemowner@example.com', name: 'System Owner', primaryDriverId: true, adminRole: 'owner' },
'super-admin': { email: 'demo.superadmin@example.com', name: 'Super Admin', primaryDriverId: true, adminRole: 'admin' },
};
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);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const userProps: any = {
id: userId,
displayName: config.name,
email: config.email,
passwordHash,
};
// Always set primaryDriverId for demo users to ensure dashboard works
userProps.primaryDriverId = `demo-${input.role}-${userId.value}`;
if (config.primaryDriverId) {
// 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
});
}
// Also create admin user if this role requires admin access
if (config.adminRole && this.adminUserRepo) {
const existingAdmin = await this.adminUserRepo.findByEmail(Email.create(config.email));
if (!existingAdmin) {
this.logger.info('[DemoLoginUseCase] Creating admin user for demo', { role: config.adminRole });
const adminProps: {
id: string;
email: string;
roles: string[];
status: string;
displayName: string;
createdAt?: Date;
updatedAt?: Date;
lastLoginAt?: Date;
primaryDriverId?: string;
} = {
id: user.getId().value,
email: config.email,
displayName: config.name,
roles: [config.adminRole],
status: 'active',
createdAt: new Date(),
updatedAt: new Date(),
lastLoginAt: new Date(),
};
const primaryDriverId = user.getPrimaryDriverId();
if (primaryDriverId) {
adminProps.primaryDriverId = primaryDriverId;
}
const adminUser = AdminUser.create(adminProps);
await this.adminUserRepo.create(adminUser);
}
}
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 },
});
}
}
}

View File

@@ -1,10 +1,9 @@
import { Controller, Get, Post, Body, Query, Inject, Res } from '@nestjs/common';
import { Public } from './Public';
import { AuthService } from './AuthService';
import { LoginParamsDTO, SignupParamsDTO, AuthSessionDTO, ForgotPasswordDTO, ResetPasswordDTO, DemoLoginDTO } from './dtos/AuthDto';
import { LoginParamsDTO, SignupParamsDTO, AuthSessionDTO, ForgotPasswordDTO, ResetPasswordDTO } 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')
@@ -58,13 +57,4 @@ export class AuthController {
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);
}
}
}

View File

@@ -6,7 +6,6 @@ import { LogoutUseCase } from '@core/identity/application/use-cases/LogoutUseCas
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';
@@ -17,9 +16,7 @@ import type { LogoutResult } from '@core/identity/application/use-cases/LogoutUs
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 type { IAdminUserRepository } from '@core/admin/domain/repositories/IAdminUserRepository';
import {
AUTH_REPOSITORY_TOKEN,
@@ -27,13 +24,11 @@ import {
USER_REPOSITORY_TOKEN,
MAGIC_LINK_REPOSITORY_TOKEN,
} from '../../persistence/identity/IdentityPersistenceTokens';
import { ADMIN_USER_REPOSITORY_TOKEN } from '../../persistence/admin/AdminPersistenceTokens';
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
@@ -45,13 +40,11 @@ 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[] = [
@@ -98,7 +91,6 @@ export const AuthProviders: Provider[] = [
},
ForgotPasswordPresenter,
ResetPasswordPresenter,
DemoLoginPresenter,
{
provide: FORGOT_PASSWORD_OUTPUT_PORT_TOKEN,
useExisting: ForgotPasswordPresenter,
@@ -107,10 +99,6 @@ export const AuthProviders: Provider[] = [
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),
@@ -138,15 +126,4 @@ export const AuthProviders: Provider[] = [
) => 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>,
adminUserRepo: IAdminUserRepository,
) => new DemoLoginUseCase(authRepo, passwordHashing, logger, output, adminUserRepo),
inject: [AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN, DEMO_LOGIN_OUTPUT_PORT_TOKEN, ADMIN_USER_REPOSITORY_TOKEN],
},
];
];

View File

@@ -42,16 +42,6 @@ class FakeResetPasswordPresenter {
}
}
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 () => {
@@ -71,12 +61,10 @@ describe('AuthService - New Methods', () => {
{ execute: vi.fn() } as any,
forgotPasswordUseCase 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' });
@@ -97,12 +85,10 @@ describe('AuthService - New Methods', () => {
{ 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');
@@ -127,12 +113,10 @@ describe('AuthService - New Methods', () => {
{ 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({
@@ -148,6 +132,7 @@ describe('AuthService - New Methods', () => {
});
it('should throw error on use case failure', async () => {
const resetPasswordPresenter = new FakeResetPasswordPresenter();
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,
@@ -156,12 +141,10 @@ describe('AuthService - New Methods', () => {
{ 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,
resetPasswordPresenter as any,
);
await expect(
@@ -169,85 +152,4 @@ describe('AuthService - New Methods', () => {
).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: () => 'Alex Johnson',
getEmail: () => 'demo.driver@example.com',
getPrimaryDriverId: () => undefined,
};
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: 'Alex Johnson',
email: 'demo.driver@example.com',
},
undefined
);
expect(result).toEqual({
token: 'demo-token-123',
user: {
userId: 'demo-user-123',
email: 'demo.driver@example.com',
displayName: 'Alex Johnson',
role: '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');
});
});
});

View File

@@ -40,12 +40,10 @@ describe('AuthService', () => {
{ 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();
@@ -66,12 +64,10 @@ describe('AuthService', () => {
{ 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({
@@ -102,12 +98,10 @@ describe('AuthService', () => {
{ 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({
@@ -138,12 +132,10 @@ describe('AuthService', () => {
{ 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(
@@ -173,12 +165,10 @@ describe('AuthService', () => {
{ 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({
@@ -206,12 +196,10 @@ describe('AuthService', () => {
{ 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');
@@ -226,12 +214,10 @@ describe('AuthService', () => {
{ 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');
@@ -254,12 +240,10 @@ describe('AuthService', () => {
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 });
@@ -274,14 +258,12 @@ describe('AuthService', () => {
{ 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');
});
});
});

View File

@@ -23,11 +23,6 @@ import {
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';
@@ -36,7 +31,6 @@ import {
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,
@@ -44,16 +38,14 @@ import {
SIGNUP_USE_CASE_TOKEN,
FORGOT_PASSWORD_USE_CASE_TOKEN,
RESET_PASSWORD_USE_CASE_TOKEN,
DEMO_LOGIN_USE_CASE_TOKEN,
} from './AuthProviders';
import type { AuthSessionDTO, AuthenticatedUserDTO } from './dtos/AuthDto';
import type { AuthSessionDTO } 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;
@@ -69,7 +61,6 @@ export class AuthService {
@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,
@@ -79,8 +70,6 @@ export class AuthService {
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> {
@@ -89,13 +78,16 @@ export class AuthService {
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: coreSession.user.role as any,
...(role !== undefined ? { role } : {}),
},
};
}
@@ -275,57 +267,4 @@ export class AuthService {
return this.resetPasswordPresenter.responseModel;
}
async demoLogin(params: { role: 'driver' | 'sponsor' | 'league-owner' | 'league-steward' | 'league-admin' | 'system-owner' | 'super-admin', rememberMe?: boolean }): 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 sessionOptions = params.rememberMe !== undefined
? { rememberMe: params.rememberMe }
: undefined;
const session = await this.identitySessionPort.createSession(
{
id: sessionId,
displayName: user.getDisplayName(),
email: user.getEmail() ?? '',
role: params.role,
},
sessionOptions
);
const userDTO: AuthenticatedUserDTO = {
userId: user.getId().value,
email: user.getEmail() ?? '',
displayName: user.getDisplayName(),
role: params.role,
};
if (primaryDriverId !== undefined) {
userDTO.primaryDriverId = primaryDriverId;
}
return {
token: session.token,
user: userDTO,
};
}
}

View File

@@ -1,18 +0,0 @@
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;
}
}

View File

@@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsString, MinLength, IsIn, IsOptional } from 'class-validator';
import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
export class AuthenticatedUserDTO {
@ApiProperty()
@@ -98,15 +98,4 @@ export class ResetPasswordDTO {
@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';
@ApiProperty({ required: false, default: false })
@IsOptional()
rememberMe?: boolean;
}
}

View File

@@ -1,23 +0,0 @@
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;
}
}

View File

@@ -50,18 +50,29 @@ describe('BootstrapModule Postgres racing seed gating (unit)', () => {
return { SeedRacingData };
});
// Mock SeedDemoUsers to avoid constructor issues
vi.doMock('../../../../../adapters/bootstrap/SeedDemoUsers', () => {
class SeedDemoUsers {
execute = vi.fn(async () => undefined);
}
return { SeedDemoUsers };
});
const { BootstrapModule } = await import('./BootstrapModule');
const ensureExecute = vi.fn(async () => undefined);
const leagueCountAll = vi.fn(async () => leaguesCount);
const seedDemoUsersExecute = vi.fn(async () => undefined);
const bootstrapModule = new BootstrapModule(
{ execute: ensureExecute } as any,
{ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() } as any,
{
leagueRepository: { countAll: leagueCountAll },
} as any,
{ execute: seedDemoUsersExecute } as any,
);
await bootstrapModule.onModuleInit();

View File

@@ -0,0 +1,179 @@
import 'reflect-metadata';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
type SetupOptions = {
persistence: 'postgres' | 'inmemory';
nodeEnv: string | undefined;
bootstrapEnabled: boolean;
leaguesCount: number;
forceReseed?: boolean;
};
describe('BootstrapModule demo user seed integration (unit)', () => {
const originalEnv = { ...process.env };
beforeEach(() => {
vi.resetModules();
process.env = { ...originalEnv };
});
afterEach(() => {
process.env = originalEnv;
vi.restoreAllMocks();
});
async function setup({
persistence,
nodeEnv,
bootstrapEnabled,
leaguesCount,
forceReseed = false,
}: SetupOptions): Promise<{
seedRacingExecute: ReturnType<typeof vi.fn>;
seedDemoUsersExecute: ReturnType<typeof vi.fn>;
ensureExecute: ReturnType<typeof vi.fn>;
leagueCountAll: ReturnType<typeof vi.fn>;
}> {
process.env.NODE_ENV = nodeEnv;
process.env.GRIDPILOT_API_PERSISTENCE = persistence;
process.env.GRIDPILOT_API_BOOTSTRAP = bootstrapEnabled ? 'true' : 'false';
if (forceReseed) {
process.env.GRIDPILOT_API_FORCE_RESEED = 'true';
} else {
delete process.env.GRIDPILOT_API_FORCE_RESEED;
}
vi.doMock('../../env', async () => {
const actual = await vi.importActual<typeof import('../../env')>('../../env');
return {
...actual,
getApiPersistence: () => persistence,
getEnableBootstrap: () => bootstrapEnabled,
getForceReseed: () => forceReseed,
};
});
const seedRacingExecute = vi.fn(async () => undefined);
const seedDemoUsersExecute = vi.fn(async () => undefined);
vi.doMock('../../../../../adapters/bootstrap/SeedRacingData', () => {
class SeedRacingData {
execute = seedRacingExecute;
}
return { SeedRacingData };
});
vi.doMock('../../../../../adapters/bootstrap/SeedDemoUsers', () => {
class SeedDemoUsers {
execute = seedDemoUsersExecute;
}
return { SeedDemoUsers };
});
const { BootstrapModule } = await import('./BootstrapModule');
const ensureExecute = vi.fn(async () => undefined);
const leagueCountAll = vi.fn(async () => leaguesCount);
const bootstrapModule = new BootstrapModule(
{ execute: ensureExecute } as any,
{ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() } as any,
{
leagueRepository: { countAll: leagueCountAll },
} as any,
{ execute: seedDemoUsersExecute } as any,
);
await bootstrapModule.onModuleInit();
return { seedRacingExecute, seedDemoUsersExecute, ensureExecute, leagueCountAll };
}
it('seeds demo users when inmemory + bootstrap enabled', async () => {
const { seedDemoUsersExecute, ensureExecute } = await setup({
persistence: 'inmemory',
nodeEnv: 'test',
bootstrapEnabled: true,
leaguesCount: 123,
});
expect(ensureExecute).toHaveBeenCalledTimes(1);
expect(seedDemoUsersExecute).toHaveBeenCalledTimes(1);
});
it('seeds demo users when postgres + development + bootstrap enabled', async () => {
const { seedDemoUsersExecute, ensureExecute } = await setup({
persistence: 'postgres',
nodeEnv: 'development',
bootstrapEnabled: true,
leaguesCount: 1,
});
expect(ensureExecute).toHaveBeenCalledTimes(1);
expect(seedDemoUsersExecute).toHaveBeenCalledTimes(1);
});
it('does not seed demo users when postgres + production', async () => {
const { seedDemoUsersExecute, ensureExecute } = await setup({
persistence: 'postgres',
nodeEnv: 'production',
bootstrapEnabled: true,
leaguesCount: 0,
});
expect(ensureExecute).toHaveBeenCalledTimes(1);
expect(seedDemoUsersExecute).toHaveBeenCalledTimes(0);
});
it('does not seed demo users when bootstrap disabled', async () => {
const { seedDemoUsersExecute, ensureExecute } = await setup({
persistence: 'inmemory',
nodeEnv: 'test',
bootstrapEnabled: false,
leaguesCount: 0,
});
expect(ensureExecute).toHaveBeenCalledTimes(0);
expect(seedDemoUsersExecute).toHaveBeenCalledTimes(0);
});
it('seeds demo users with force reseed enabled', async () => {
const { seedDemoUsersExecute, ensureExecute } = await setup({
persistence: 'postgres',
nodeEnv: 'development',
bootstrapEnabled: true,
leaguesCount: 1,
forceReseed: true,
});
expect(ensureExecute).toHaveBeenCalledTimes(1);
expect(seedDemoUsersExecute).toHaveBeenCalledTimes(1);
});
it('seeds both racing data and demo users when both are needed', async () => {
const { seedRacingExecute, seedDemoUsersExecute, ensureExecute } = await setup({
persistence: 'inmemory',
nodeEnv: 'test',
bootstrapEnabled: true,
leaguesCount: 0,
});
expect(ensureExecute).toHaveBeenCalledTimes(1);
expect(seedRacingExecute).toHaveBeenCalledTimes(1);
expect(seedDemoUsersExecute).toHaveBeenCalledTimes(1);
});
it('skips racing seed but seeds demo users when database has data', async () => {
const { seedRacingExecute, seedDemoUsersExecute, ensureExecute } = await setup({
persistence: 'postgres',
nodeEnv: 'development',
bootstrapEnabled: true,
leaguesCount: 120,
});
expect(ensureExecute).toHaveBeenCalledTimes(1);
expect(seedRacingExecute).toHaveBeenCalledTimes(0);
expect(seedDemoUsersExecute).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,16 +1,18 @@
import type { Logger } from '@core/shared/application';
import type { EnsureInitialData } from '../../../../../adapters/bootstrap/EnsureInitialData';
import { SeedRacingData, type RacingSeedDependencies } from '../../../../../adapters/bootstrap/SeedRacingData';
import { SeedDemoUsers } from '../../../../../adapters/bootstrap/SeedDemoUsers';
import { Inject, Module, OnModuleInit } from '@nestjs/common';
import { getApiPersistence, getEnableBootstrap, getForceReseed } from '../../env';
import { RacingPersistenceModule } from '../../persistence/racing/RacingPersistenceModule';
import { SocialPersistenceModule } from '../../persistence/social/SocialPersistenceModule';
import { AchievementPersistenceModule } from '../../persistence/achievement/AchievementPersistenceModule';
import { IdentityPersistenceModule } from '../../persistence/identity/IdentityPersistenceModule';
import { BootstrapProviders, ENSURE_INITIAL_DATA_TOKEN } from './BootstrapProviders';
import { AdminPersistenceModule } from '../../persistence/admin/AdminPersistenceModule';
import { BootstrapProviders, ENSURE_INITIAL_DATA_TOKEN, SEED_DEMO_USERS_TOKEN } from './BootstrapProviders';
@Module({
imports: [RacingPersistenceModule, SocialPersistenceModule, AchievementPersistenceModule, IdentityPersistenceModule],
imports: [RacingPersistenceModule, SocialPersistenceModule, AchievementPersistenceModule, IdentityPersistenceModule, AdminPersistenceModule],
providers: BootstrapProviders,
})
export class BootstrapModule implements OnModuleInit {
@@ -18,6 +20,7 @@ export class BootstrapModule implements OnModuleInit {
@Inject(ENSURE_INITIAL_DATA_TOKEN) private readonly ensureInitialData: EnsureInitialData,
@Inject('Logger') private readonly logger: Logger,
@Inject('RacingSeedDependencies') private readonly seedDeps: RacingSeedDependencies,
@Inject(SEED_DEMO_USERS_TOKEN) private readonly seedDemoUsers: SeedDemoUsers,
) {}
async onModuleInit() {
@@ -34,6 +37,11 @@ export class BootstrapModule implements OnModuleInit {
await new SeedRacingData(this.logger, this.seedDeps).execute();
}
// Seed demo users (only in dev/test, respects bootstrap enable flag)
if (await this.shouldSeedDemoUsers()) {
await this.seedDemoUsers.execute();
}
console.log('[Bootstrap] Application data initialized successfully');
} catch (error) {
console.error('[Bootstrap] Failed to initialize application data:', error);
@@ -65,6 +73,31 @@ export class BootstrapModule implements OnModuleInit {
return true;
}
private async shouldSeedDemoUsers(): Promise<boolean> {
const persistence = getApiPersistence();
// Demo users are only seeded in dev/test environments
if (process.env.NODE_ENV === 'production') {
return false;
}
// Demo users can be seeded in both postgres and inmemory
if (persistence === 'postgres' || persistence === 'inmemory') {
// Check for force reseed flag
const forceReseed = getForceReseed();
if (forceReseed) {
this.logger.info('[Bootstrap] Demo users force reseed enabled');
return true;
}
// The SeedDemoUsers class handles its own existence checks
// We just need to determine if we should call it
return true;
}
return false;
}
private async isRacingDatabaseEmpty(): Promise<boolean> {
const count = await this.seedDeps.leagueRepository.countAll?.();
if (typeof count === 'number') return count === 0;

View File

@@ -3,6 +3,7 @@ import { SOCIAL_FEED_REPOSITORY_TOKEN, SOCIAL_GRAPH_REPOSITORY_TOKEN } from '../
import { ACHIEVEMENT_REPOSITORY_TOKEN } from '../../persistence/achievement/AchievementPersistenceTokens';
import { EnsureInitialData } from '../../../../../adapters/bootstrap/EnsureInitialData';
import type { RacingSeedDependencies } from '../../../../../adapters/bootstrap/SeedRacingData';
import { SeedDemoUsers } from '../../../../../adapters/bootstrap/SeedDemoUsers';
import { SignupWithEmailUseCase, type SignupWithEmailResult } from '@core/identity/application/use-cases/SignupWithEmailUseCase';
import {
CreateAchievementUseCase,
@@ -14,7 +15,8 @@ import type { IdentitySessionPort } from '@core/identity/application/ports/Ident
import type { Logger } from '@core/shared/application';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import { CookieIdentitySessionAdapter } from '../../../../../adapters/identity/session/CookieIdentitySessionAdapter';
import { USER_REPOSITORY_TOKEN as IDENTITY_USER_REPOSITORY_TOKEN } from '../../persistence/identity/IdentityPersistenceTokens';
import { USER_REPOSITORY_TOKEN as IDENTITY_USER_REPOSITORY_TOKEN, AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN } from '../../persistence/identity/IdentityPersistenceTokens';
import { ADMIN_USER_REPOSITORY_TOKEN } from '../../persistence/admin/AdminPersistenceTokens';
// Define tokens
export const USER_REPOSITORY_TOKEN = 'IUserRepository_Bootstrap';
@@ -25,6 +27,7 @@ export const CREATE_ACHIEVEMENT_USE_CASE_TOKEN = 'CreateAchievementUseCase_Boots
export const RACING_SEED_DEPENDENCIES_TOKEN = 'RacingSeedDependencies';
export const ENSURE_INITIAL_DATA_TOKEN = 'EnsureInitialData_Bootstrap';
export const SEED_DEMO_USERS_TOKEN = 'SeedDemoUsers';
// Adapter classes for output ports
class SignupWithEmailOutputAdapter implements UseCaseOutputPort<SignupWithEmailResult> {
@@ -68,6 +71,9 @@ export const BootstrapProviders: Provider[] = [
driverStatsRepository: RacingSeedDependencies['driverStatsRepository'],
teamStatsRepository: RacingSeedDependencies['teamStatsRepository'],
mediaRepository: RacingSeedDependencies['mediaRepository'],
authRepository: RacingSeedDependencies['authRepository'],
passwordHashingService: RacingSeedDependencies['passwordHashingService'],
adminUserRepository: RacingSeedDependencies['adminUserRepository'],
): RacingSeedDependencies => ({
driverRepository,
leagueRepository,
@@ -92,6 +98,9 @@ export const BootstrapProviders: Provider[] = [
driverStatsRepository,
teamStatsRepository,
mediaRepository,
authRepository,
passwordHashingService,
adminUserRepository,
}),
inject: [
'IDriverRepository',
@@ -117,6 +126,9 @@ export const BootstrapProviders: Provider[] = [
'IDriverStatsRepository',
'ITeamStatsRepository',
'IMediaRepository',
AUTH_REPOSITORY_TOKEN,
PASSWORD_HASHING_SERVICE_TOKEN,
ADMIN_USER_REPOSITORY_TOKEN,
],
},
{
@@ -171,4 +183,14 @@ export const BootstrapProviders: Provider[] = [
},
inject: [SIGNUP_USE_CASE_TOKEN, CREATE_ACHIEVEMENT_USE_CASE_TOKEN, 'Logger'],
},
{
provide: SEED_DEMO_USERS_TOKEN,
useFactory: (
logger: Logger,
seedDeps: RacingSeedDependencies,
) => {
return new SeedDemoUsers(logger, seedDeps.authRepository, seedDeps.passwordHashingService, seedDeps.adminUserRepository);
},
inject: ['Logger', RACING_SEED_DEPENDENCIES_TOKEN],
},
];

View File

@@ -11,7 +11,6 @@ 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: [] });
@@ -23,7 +22,6 @@ 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');
@@ -34,9 +32,8 @@ 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');
});
});
});

View File

@@ -5,13 +5,11 @@ 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 (standalone to avoid circular imports)
import {
DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN,
DASHBOARD_OVERVIEW_USE_CASE_TOKEN,
IMAGE_SERVICE_TOKEN,
LOGGER_TOKEN,
} from './DashboardTokens';
@@ -21,27 +19,11 @@ 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()) {
@@ -52,185 +34,4 @@ 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: [],
};
}
}
}

View File

@@ -1,11 +1,14 @@
/**
* Admin Persistence Module
*
* Abstract module interface for admin persistence.
* Both InMemory and TypeORM implementations should export this.
*/
import { Module } from '@nestjs/common';
@Module({})
import { getApiPersistence } from '../../env';
import { InMemoryAdminPersistenceModule } from '../inmemory/InMemoryAdminPersistenceModule';
import { PostgresAdminPersistenceModule } from '../postgres/PostgresAdminPersistenceModule';
const selectedPersistenceModule =
getApiPersistence() === 'postgres' ? PostgresAdminPersistenceModule : InMemoryAdminPersistenceModule;
@Module({
imports: [selectedPersistenceModule],
exports: [selectedPersistenceModule],
})
export class AdminPersistenceModule {}

View File

@@ -12,9 +12,7 @@ import {
LogIn,
AlertCircle,
Flag,
Gamepad2,
Shield,
ChevronRight,
} from 'lucide-react';
import Card from '@/components/ui/Card';
@@ -97,31 +95,6 @@ export default function LoginPage() {
},
});
const handleDemoLogin = async () => {
try {
const { ServiceFactory } = await import('@/lib/services/ServiceFactory');
const serviceFactory = new ServiceFactory(process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001');
const authService = serviceFactory.createAuthService();
// Get rememberMe value safely
const rememberMe = formState.fields.rememberMe?.value ?? false;
await authService.demoLogin({
role: 'driver',
rememberMe,
});
await new Promise(resolve => setTimeout(resolve, 500));
router.push(returnTo);
} catch (error) {
setFormError('Demo login failed. Please try again.');
logErrorWithContext(error, {
component: 'LoginPage',
action: 'demo-login',
});
}
};
return (
<main className="min-h-screen bg-deep-graphite flex">
{/* Background Pattern */}
@@ -164,8 +137,7 @@ export default function LoginPage() {
<span>Secure login</span>
</div>
<div className="flex items-center gap-2">
<Gamepad2 className="w-4 h-4" />
<span>iRacing verified</span>
<span className="text-sm">iRacing verified</span>
</div>
</div>
</div>
@@ -317,20 +289,6 @@ export default function LoginPage() {
</div>
</div>
{/* Demo Login */}
<motion.button
type="button"
onClick={handleDemoLogin}
disabled={formState.isSubmitting}
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.99 }}
className="w-full flex items-center justify-center gap-3 px-4 py-3 rounded-lg bg-gradient-to-r from-deep-graphite to-iron-gray border border-charcoal-outline text-gray-300 hover:border-primary-blue/30 transition-all disabled:opacity-50 group"
>
<Gamepad2 className="w-5 h-5 text-primary-blue" />
<span>Demo Login</span>
<ChevronRight className="w-4 h-4 text-gray-500 group-hover:translate-x-0.5 transition-transform" />
</motion.button>
{/* Sign Up Link */}
<p className="mt-6 text-center text-sm text-gray-400">
Don't have an account?{' '}

View File

@@ -15,13 +15,11 @@ import {
User,
Check,
X,
Gamepad2,
Loader2,
Car,
Users,
Trophy,
Shield,
ChevronRight,
Sparkles,
} from 'lucide-react';
@@ -239,26 +237,6 @@ export default function SignupPage() {
}
};
const handleDemoLogin = async () => {
setLoading(true);
try {
const { ServiceFactory } = await import('@/lib/services/ServiceFactory');
const serviceFactory = new ServiceFactory(process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001');
const authService = serviceFactory.createAuthService();
await authService.demoLogin({ role: 'driver' });
await new Promise(resolve => setTimeout(resolve, 500));
// Always redirect to dashboard after demo login
router.push('/dashboard');
} catch {
setErrors({
submit: 'Demo login failed. Please try again.',
});
setLoading(false);
}
};
// Show loading while checking auth
if (checkingAuth) {
return (
@@ -344,7 +322,6 @@ export default function SignupPage() {
<span>Secure signup</span>
</div>
<div className="flex items-center gap-2">
<Gamepad2 className="w-4 h-4" />
<span>iRacing integration</span>
</div>
</div>
@@ -596,24 +573,10 @@ export default function SignupPage() {
<div className="w-full border-t border-charcoal-outline" />
</div>
<div className="relative flex justify-center text-xs">
<span className="px-4 bg-iron-gray text-gray-500">or sign up with</span>
<span className="px-4 bg-iron-gray text-gray-500">or continue with</span>
</div>
</div>
{/* Demo Login */}
<motion.button
type="button"
onClick={handleDemoLogin}
disabled={loading}
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.99 }}
className="w-full flex items-center justify-center gap-3 px-4 py-3 rounded-lg bg-gradient-to-r from-deep-graphite to-iron-gray border border-charcoal-outline text-gray-300 hover:border-primary-blue/30 transition-all disabled:opacity-50 group"
>
<Gamepad2 className="w-5 h-5 text-primary-blue" />
<span>Demo Login</span>
<ChevronRight className="w-4 h-4 text-gray-500 group-hover:translate-x-0.5 transition-transform" />
</motion.button>
{/* Login Link */}
<p className="mt-6 text-center text-sm text-gray-400">
Already have an account?{' '}

View File

@@ -140,29 +140,6 @@ export default function SponsorSignupPage() {
const [errors, setErrors] = useState<Record<string, string>>({});
const [submitting, setSubmitting] = useState(false);
const handleDemoLogin = async () => {
setSubmitting(true);
try {
// Use the demo login API instead of setting cookies
const response = await fetch('/api/auth/demo-login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ role: 'sponsor' }),
});
if (!response.ok) {
throw new Error('Demo login failed');
}
router.push('/sponsor/dashboard');
} catch (error) {
console.error('Demo login failed:', error);
alert('Demo login failed. Please check the API server status.');
} finally {
setSubmitting(false);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -206,22 +183,47 @@ export default function SponsorSignupPage() {
setSubmitting(true);
try {
// For demo purposes, use the demo login API with sponsor role
// In production, this would create a real sponsor account
const response = await fetch('/api/auth/demo-login', {
// Create a sponsor account using the normal signup flow
// The backend will handle creating the sponsor user with the appropriate role
const response = await fetch('/api/auth/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ role: 'sponsor' }),
body: JSON.stringify({
email: formData.contactEmail,
password: formData.password,
displayName: formData.companyName,
// Additional sponsor-specific data
sponsorData: {
companyName: formData.companyName,
websiteUrl: formData.websiteUrl,
interests: formData.interests,
},
}),
});
if (!response.ok) {
throw new Error('Signup failed');
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || 'Signup failed');
}
// Auto-login after successful signup
const loginResponse = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: formData.contactEmail,
password: formData.password,
}),
});
if (!loginResponse.ok) {
throw new Error('Auto-login failed');
}
router.push('/sponsor/dashboard');
} catch (err) {
console.error('Sponsor signup failed:', err);
alert('Registration failed. Try again.');
alert('Registration failed. ' + (err instanceof Error ? err.message : 'Try again.'));
} finally {
setSubmitting(false);
}
@@ -263,17 +265,6 @@ export default function SponsorSignupPage() {
<ArrowRight className="w-5 h-5 ml-2" />
</Button>
</div>
{/* Demo Login */}
<div className="mt-6">
<button
onClick={handleDemoLogin}
disabled={submitting}
className="text-sm text-gray-500 hover:text-gray-400 transition-colors disabled:opacity-50"
>
{submitting ? 'Loading...' : 'Try demo sponsor account →'}
</button>
</div>
</SponsorHero>
{/* Platform Stats */}
@@ -529,13 +520,6 @@ export default function SponsorSignupPage() {
Create one
</button>
</p>
<button
onClick={handleDemoLogin}
disabled={submitting}
className="w-full text-sm text-gray-500 hover:text-gray-400 text-center"
>
Or try the demo account
</button>
</div>
</Card>
</div>

View File

@@ -3,7 +3,7 @@
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
import { useNotifications } from '@/components/notifications/NotificationProvider';
import type { NotificationVariant } from '@/components/notifications/notificationTypes';
import { Wrench, ChevronDown, ChevronUp, X, MessageSquare, Activity, LogIn, AlertTriangle } from 'lucide-react';
import { Wrench, ChevronDown, ChevronUp, X, MessageSquare, Activity, AlertTriangle } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { ApiConnectionMonitor } from '@/lib/api/base/ApiConnectionMonitor';
@@ -16,10 +16,9 @@ import { NotificationTypeSection } from './sections/NotificationTypeSection';
import { UrgencySection } from './sections/UrgencySection';
import { NotificationSendSection } from './sections/NotificationSendSection';
import { APIStatusSection } from './sections/APIStatusSection';
import { LoginSection } from './sections/LoginSection';
// Import types
import type { DemoNotificationType, DemoUrgency, LoginMode } from './types';
import type { DemoNotificationType, DemoUrgency } from './types';
export default function DevToolbar() {
const router = useRouter();
@@ -30,8 +29,6 @@ export default function DevToolbar() {
const [selectedUrgency, setSelectedUrgency] = useState<DemoUrgency>('toast');
const [sending, setSending] = useState(false);
const [lastSent, setLastSent] = useState<string | null>(null);
const [loginMode, setLoginMode] = useState<LoginMode>('none');
const [loggingIn, setLoggingIn] = useState(false);
// API Status Monitoring State
const [apiStatus, setApiStatus] = useState(() => ApiConnectionMonitor.getInstance().getStatus());
@@ -47,77 +44,6 @@ export default function DevToolbar() {
const currentDriverId = useEffectiveDriverId();
// Sync login mode with actual session state on mount
useEffect(() => {
if (typeof document !== 'undefined') {
// Check for actual session cookie first
const cookies = document.cookie.split(';');
const sessionCookie = cookies.find(c => c.trim().startsWith('gp_session='));
if (sessionCookie) {
// User has a session cookie, check if it's valid by calling the API
fetch('/api/auth/session', {
method: 'GET',
credentials: 'include'
})
.then(res => {
if (res.ok) {
return res.json();
}
throw new Error('No valid session');
})
.then(session => {
if (session && session.user) {
// Determine login mode based on user email patterns
const email = session.user.email?.toLowerCase() || '';
const displayName = session.user.displayName?.toLowerCase() || '';
const role = (session.user as any).role;
let mode: LoginMode = 'none';
// First check session.role if available
if (role) {
if (role === 'sponsor') mode = 'sponsor';
else if (role === 'league-owner') mode = 'league-owner';
else if (role === 'league-steward') mode = 'league-steward';
else if (role === 'league-admin') mode = 'league-admin';
else if (role === 'system-owner') mode = 'system-owner';
else if (role === 'super-admin') mode = 'super-admin';
else if (role === 'driver') mode = 'driver';
}
// Fallback to email patterns
else if (email.includes('sponsor') || displayName.includes('sponsor')) {
mode = 'sponsor';
} else if (email.includes('league-owner') || displayName.includes('owner')) {
mode = 'league-owner';
} else if (email.includes('league-steward') || displayName.includes('steward')) {
mode = 'league-steward';
} else if (email.includes('league-admin') || displayName.includes('admin')) {
mode = 'league-admin';
} else if (email.includes('system-owner') || displayName.includes('system owner')) {
mode = 'system-owner';
} else if (email.includes('super-admin') || displayName.includes('super admin')) {
mode = 'super-admin';
} else if (email.includes('driver') || displayName.includes('demo')) {
mode = 'driver';
}
setLoginMode(mode);
} else {
setLoginMode('none');
}
})
.catch(() => {
// Session invalid or expired
setLoginMode('none');
});
} else {
// No session cookie means not logged in
setLoginMode('none');
}
}
}, []);
// API Status Monitoring Effects
useEffect(() => {
const monitor = ApiConnectionMonitor.getInstance();
@@ -235,54 +161,6 @@ export default function DevToolbar() {
}
};
const handleDemoLogin = async (role: LoginMode) => {
if (role === 'none') return;
setLoggingIn(true);
try {
// Use the demo login API
const response = await fetch('/api/auth/demo-login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ role }),
});
if (!response.ok) {
throw new Error('Demo login failed');
}
setLoginMode(role);
// Navigate based on role
if (role === 'sponsor') {
window.location.href = '/sponsor/dashboard';
} else {
// For driver and league roles, go to dashboard
window.location.href = '/dashboard';
}
} catch (error) {
alert('Demo login failed. Please check the API server status.');
} finally {
setLoggingIn(false);
}
};
const handleLogout = async () => {
setLoggingIn(true);
try {
// Call logout API
await fetch('/api/auth/logout', { method: 'POST' });
setLoginMode('none');
// Refresh to update all components
window.location.href = '/';
} catch (error) {
alert('Logout failed. Please check the API server status.');
} finally {
setLoggingIn(false);
}
};
// Only show in development
if (process.env.NODE_ENV === 'production') {
return null;
@@ -435,21 +313,6 @@ export default function DevToolbar() {
/>
</Accordion>
{/* Login Section - Accordion */}
<Accordion
title="Demo Login"
icon={<LogIn className="w-4 h-4 text-gray-400" />}
isOpen={openAccordion === 'login'}
onToggle={() => setOpenAccordion(openAccordion === 'login' ? null : 'login')}
>
<LoginSection
loginMode={loginMode}
loggingIn={loggingIn}
onDemoLogin={handleDemoLogin}
onLogout={handleLogout}
/>
</Accordion>
{/* Error Stats Section - Accordion */}
<Accordion
title="Error Stats"

View File

@@ -1,79 +0,0 @@
'use client';
import { LogIn, LogOut, User, Shield, Building2 } from 'lucide-react';
import type { LoginMode } from '../types';
interface LoginSectionProps {
loginMode: LoginMode;
loggingIn: boolean;
onDemoLogin: (role: LoginMode) => void;
onLogout: () => void;
}
export function LoginSection({ loginMode, loggingIn, onDemoLogin, onLogout }: LoginSectionProps) {
const loginOptions = [
{ mode: 'driver' as LoginMode, label: 'Driver', icon: User, color: 'primary-blue', emoji: null },
{ mode: 'league-owner' as LoginMode, label: 'League Owner', icon: null, color: 'purple-500', emoji: '👑' },
{ mode: 'league-steward' as LoginMode, label: 'Steward', icon: Shield, color: 'amber-500', emoji: null },
{ mode: 'league-admin' as LoginMode, label: 'Admin', icon: null, color: 'red-500', emoji: '⚙️' },
{ mode: 'sponsor' as LoginMode, label: 'Sponsor', icon: Building2, color: 'performance-green', emoji: null },
{ mode: 'system-owner' as LoginMode, label: 'System Owner', icon: null, color: 'indigo-500', emoji: '👑' },
{ mode: 'super-admin' as LoginMode, label: 'Super Admin', icon: null, color: 'pink-500', emoji: '⚡' },
];
return (
<div>
<div className="flex items-center gap-2 mb-3">
<LogIn className="w-4 h-4 text-gray-400" />
<span className="text-xs font-semibold text-gray-400 uppercase tracking-wide">
Demo Login
</span>
</div>
<div className="space-y-2">
{loginOptions.map((option) => {
const Icon = option.icon;
const isSelected = loginMode === option.mode;
return (
<button
key={option.mode}
onClick={() => onDemoLogin(option.mode)}
disabled={loggingIn || isSelected}
className={`
w-full flex items-center gap-2 px-3 py-2 rounded-lg border text-sm font-medium transition-all
${isSelected
? `bg-${option.color}/20 border-${option.color}/50 text-${option.color}`
: 'bg-iron-gray/30 border-charcoal-outline text-gray-300 hover:bg-iron-gray/50'
}
disabled:opacity-50 disabled:cursor-not-allowed
`}
>
{option.emoji ? (
<span className="text-xs">{option.emoji}</span>
) : Icon ? (
<Icon className="w-4 h-4" />
) : null}
{isSelected ? `${option.label}` : `Login as ${option.label}`}
</button>
);
})}
{loginMode !== 'none' && (
<button
onClick={onLogout}
disabled={loggingIn}
className="w-full flex items-center gap-2 px-3 py-2 rounded-lg border border-red-500/30 bg-red-500/10 text-red-400 text-sm font-medium hover:bg-red-500/20 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
<LogOut className="w-4 h-4" />
Logout
</button>
)}
</div>
<p className="text-[10px] text-gray-600 mt-2">
Test different user roles for demo purposes. Dashboard works for all roles.
</p>
</div>
);
}

View File

@@ -2,7 +2,6 @@ import type { NotificationVariant } from '@/components/notifications/notificatio
export type DemoNotificationType = 'protest_filed' | 'defense_requested' | 'vote_required' | 'race_performance_summary' | 'race_final_results';
export type DemoUrgency = 'silent' | 'toast' | 'modal';
export type LoginMode = 'none' | 'driver' | 'sponsor' | 'league-owner' | 'league-steward' | 'league-admin' | 'system-owner' | 'super-admin';
export interface NotificationOption {
type: DemoNotificationType;

View File

@@ -4,7 +4,6 @@ import { LoginParamsDTO } from '../../types/generated/LoginParamsDTO';
import { SignupParamsDTO } from '../../types/generated/SignupParamsDTO';
import { ForgotPasswordDTO } from '../../types/generated/ForgotPasswordDTO';
import { ResetPasswordDTO } from '../../types/generated/ResetPasswordDTO';
import { DemoLoginDTO } from '../../types/generated/DemoLoginDTO';
/**
* Auth API Client
@@ -43,9 +42,4 @@ export class AuthApiClient extends BaseApiClient {
resetPassword(params: ResetPasswordDTO): Promise<{ message: string }> {
return this.post<{ message: string }>('/auth/reset-password', params);
}
/** Demo login (development only) */
demoLogin(params: DemoLoginDTO): Promise<AuthSessionDTO> {
return this.post<AuthSessionDTO>('/auth/demo-login', params);
}
}
}

View File

@@ -83,7 +83,6 @@ export function getPublicRoutes(): readonly string[] {
'/api/auth/login',
'/api/auth/forgot-password',
'/api/auth/reset-password',
'/api/auth/demo-login',
'/api/auth/session',
'/api/auth/logout',
'/auth/login',

View File

@@ -4,7 +4,6 @@ import type { LoginParamsDTO } from '../../types/generated/LoginParamsDTO';
import type { SignupParamsDTO } from '../../types/generated/SignupParamsDTO';
import type { ForgotPasswordDTO } from '../../types/generated/ForgotPasswordDTO';
import type { ResetPasswordDTO } from '../../types/generated/ResetPasswordDTO';
import type { DemoLoginDTO } from '../../types/generated/DemoLoginDTO';
/**
* Auth Service
@@ -73,16 +72,4 @@ export class AuthService {
throw error;
}
}
/**
* Demo login (development only)
*/
async demoLogin(params: DemoLoginDTO): Promise<SessionViewModel> {
try {
const dto = await this.apiClient.demoLogin(params);
return new SessionViewModel(dto.user);
} catch (error) {
throw error;
}
}
}

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/DemoLoginDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

@@ -1,11 +0,0 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/
export interface DemoLoginDTO {
role: string;
rememberMe?: boolean;
}

View File

@@ -46,7 +46,6 @@ export type { DashboardRaceSummaryDTO } from './DashboardRaceSummaryDTO';
export type { DashboardRecentResultDTO } from './DashboardRecentResultDTO';
export type { DeleteMediaOutputDTO } from './DeleteMediaOutputDTO';
export type { DeletePrizeResultDTO } from './DeletePrizeResultDTO';
export type { DemoLoginDTO } from './DemoLoginDTO';
export type { DriverDTO } from './DriverDTO';
export type { DriverLeaderboardItemDTO } from './DriverLeaderboardItemDTO';
export type { DriverProfileAchievementDTO } from './DriverProfileAchievementDTO';
@@ -247,4 +246,4 @@ export type { WizardErrorsDTO } from './WizardErrorsDTO';
export type { WizardErrorsScoringDTO } from './WizardErrorsScoringDTO';
export type { WizardErrorsStructureDTO } from './WizardErrorsStructureDTO';
export type { WizardErrorsTimingsDTO } from './WizardErrorsTimingsDTO';
export type { WizardStepDTO } from './WizardStepDTO';
export type { WizardStepDTO } from './WizardStepDTO';