clean routes

This commit is contained in:
2026-01-03 02:42:47 +01:00
parent 07985fb8f1
commit 2f21dc4595
107 changed files with 7596 additions and 3401 deletions

View File

@@ -6,6 +6,9 @@ import { IPasswordHashingService } from '@core/identity/domain/services/Password
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';
@@ -24,7 +27,7 @@ export type DemoLoginApplicationError = ApplicationErrorCode<DemoLoginErrorCode,
*
* 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> {
@@ -33,6 +36,7 @@ export class DemoLoginUseCase implements UseCase<DemoLoginInput, void, DemoLogin
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>> {
@@ -47,13 +51,13 @@ export class DemoLoginUseCase implements UseCase<DemoLoginInput, void, DemoLogin
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 },
'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];
@@ -102,6 +106,45 @@ export class DemoLoginUseCase implements UseCase<DemoLoginInput, void, DemoLogin
});
}
// 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);
@@ -121,4 +164,4 @@ export class DemoLoginUseCase implements UseCase<DemoLoginInput, void, DemoLogin
});
}
}
}
}

View File

@@ -53,6 +53,7 @@ export class GetDashboardStatsUseCase {
try {
// Get actor (current user)
const actor = await this.adminUserRepo.findById(UserId.fromString(input.actorId));
if (!actor) {
return Result.err({
code: 'AUTHORIZATION_ERROR',
@@ -177,4 +178,4 @@ export class GetDashboardStatsUseCase {
});
}
}
}
}

View File

@@ -1,5 +1,6 @@
import { Module } from '@nestjs/common';
import { IdentityPersistenceModule } from '../../persistence/identity/IdentityPersistenceModule';
import { InMemoryAdminPersistenceModule } from '../../persistence/inmemory/InMemoryAdminPersistenceModule';
import { AuthService } from './AuthService';
import { AuthController } from './AuthController';
import { AuthProviders } from './AuthProviders';
@@ -8,7 +9,7 @@ import { AuthorizationGuard } from './AuthorizationGuard';
import { AuthorizationService } from './AuthorizationService';
@Module({
imports: [IdentityPersistenceModule],
imports: [IdentityPersistenceModule, InMemoryAdminPersistenceModule],
controllers: [AuthController],
providers: [AuthService, ...AuthProviders, AuthenticationGuard, AuthorizationService, AuthorizationGuard],
exports: [AuthService, AuthenticationGuard, AuthorizationService, AuthorizationGuard],

View File

@@ -19,6 +19,7 @@ import type { ForgotPasswordResult } from '@core/identity/application/use-cases/
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,
@@ -26,6 +27,7 @@ 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';
@@ -143,7 +145,8 @@ export const AuthProviders: Provider[] = [
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],
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

@@ -225,6 +225,7 @@ describe('AuthService - New Methods', () => {
userId: 'demo-user-123',
email: 'demo.driver@example.com',
displayName: 'Alex Johnson',
role: 'driver',
},
});
});

View File

@@ -89,13 +89,13 @@ export class AuthService {
const coreSession = await this.identitySessionPort.getCurrentSession();
if (!coreSession) return null;
// TODO!!
return {
token: coreSession.token,
user: {
userId: coreSession.user.id,
email: coreSession.user.email ?? '',
displayName: coreSession.user.displayName,
role: coreSession.user.role as any,
},
};
}
@@ -307,6 +307,7 @@ export class AuthService {
id: sessionId,
displayName: user.getDisplayName(),
email: user.getEmail() ?? '',
role: params.role,
},
sessionOptions
);
@@ -315,6 +316,7 @@ export class AuthService {
userId: user.getId().value,
email: user.getEmail() ?? '',
displayName: user.getDisplayName(),
role: params.role,
};
if (primaryDriverId !== undefined) {
@@ -326,4 +328,4 @@ export class AuthService {
user: userDTO,
};
}
}
}

View File

@@ -3,7 +3,7 @@ import type { IdentitySessionPort } from '@core/identity/application/ports/Ident
import { IDENTITY_SESSION_PORT_TOKEN } from './AuthProviders';
type AuthenticatedRequest = {
user?: { userId: string };
user?: { userId: string; role?: string | undefined };
};
@Injectable()
@@ -22,7 +22,10 @@ export class AuthenticationGuard implements CanActivate {
const session = await this.sessionPort.getCurrentSession();
if (session?.user?.id) {
request.user = { userId: session.user.id };
request.user = {
userId: session.user.id,
role: session.user.role
};
}
return true;

View File

@@ -6,7 +6,7 @@ import { REQUIRE_AUTHENTICATED_USER_METADATA_KEY } from './RequireAuthenticatedU
import { REQUIRE_ROLES_METADATA_KEY, RequireRolesMetadata } from './RequireRoles';
type AuthenticatedRequest = {
user?: { userId: string };
user?: { userId: string; role?: string | undefined };
};
@Injectable()
@@ -16,7 +16,7 @@ export class AuthorizationGuard implements CanActivate {
private readonly authorizationService: AuthorizationService,
) {}
canActivate(context: ExecutionContext): boolean {
async canActivate(context: ExecutionContext): Promise<boolean> {
const handler = context.getHandler();
const controllerClass = context.getClass();
@@ -55,8 +55,24 @@ export class AuthorizationGuard implements CanActivate {
void requiresAuth;
if (rolesMetadata && rolesMetadata.anyOf.length > 0) {
const userRoles = this.authorizationService.getRolesForUser(userId);
const hasAnyRole = rolesMetadata.anyOf.some((r) => userRoles.includes(r));
let userRoles = this.authorizationService.getRolesForUser(userId);
// If no roles from service, check request for demo login roles
if (userRoles.length === 0 && request.user?.role) {
userRoles = [request.user.role];
}
// Map demo login roles to API expected roles
const mappedRoles = userRoles.map(role => {
if (role === 'league-admin') return 'admin';
if (role === 'league-owner') return 'owner';
if (role === 'league-steward') return 'steward';
if (role === 'system-owner') return 'owner';
if (role === 'super-admin') return 'admin';
return role;
});
const hasAnyRole = rolesMetadata.anyOf.some((r) => mappedRoles.includes(r));
if (!hasAnyRole) {
throw new ForbiddenException('Forbidden');
}

View File

@@ -3,10 +3,11 @@ import { getHttpRequestContext } from '@adapters/http/RequestContext';
export type Actor = {
userId: string;
driverId: string;
role?: string | undefined;
};
type AuthenticatedRequest = {
user?: { userId: string };
user?: { userId: string; role?: string };
};
export function getActorFromRequestContext(): Actor {
@@ -21,5 +22,6 @@ export function getActorFromRequestContext(): Actor {
// Current canonical mapping:
// - The authenticated session identity is `userId`.
// - In the current system, that `userId` is also treated as the performer `driverId`.
return { userId, driverId: userId };
// - Include role from session if available
return { userId, driverId: userId, role: req.user?.role };
}

View File

@@ -10,6 +10,11 @@ export async function requireLeagueAdminOrOwner(
): Promise<void> {
const actor = getActorFromRequestContext();
// Check for demo session roles - bypass database check
if (actor.role && ['league-admin', 'league-owner', 'super-admin', 'system-owner'].includes(actor.role)) {
return; // Allow access for demo admin roles
}
const permissionResult = await getLeagueAdminPermissionsUseCase.execute({
leagueId,
performerDriverId: actor.driverId,

View File

@@ -1,9 +1,10 @@
import { Controller, Get, Post, Put, Body, HttpCode, HttpStatus, Param, Query, Inject } from '@nestjs/common';
import { Controller, Get, Post, Put, Body, HttpCode, HttpStatus, Param, Query, Inject, UseGuards } from '@nestjs/common';
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
import { Public } from '../auth/Public';
import { RequireAuthenticatedUser } from '../auth/RequireAuthenticatedUser';
import { RequireRoles } from '../auth/RequireRoles';
import { RequireCapability } from '../policy/RequireCapability';
import { AuthorizationGuard } from '../auth/AuthorizationGuard';
import { SponsorService } from './SponsorService';
import { GetEntitySponsorshipPricingResultDTO } from './dtos/GetEntitySponsorshipPricingResultDTO';
import { GetSponsorsOutputDTO } from './dtos/GetSponsorsOutputDTO';
@@ -32,6 +33,7 @@ import type { RejectSponsorshipRequestResult } from '@core/racing/application/us
@ApiTags('sponsors')
@Controller('sponsors')
@UseGuards(AuthorizationGuard)
export class SponsorController {
constructor(@Inject(SponsorService) private readonly sponsorService: SponsorService) {}
@@ -78,7 +80,7 @@ export class SponsorController {
@Get('dashboard/:sponsorId')
@RequireAuthenticatedUser()
@RequireRoles('admin')
@RequireRoles('admin', 'sponsor')
@RequireCapability('sponsors.portal', 'view')
@ApiOperation({ summary: 'Get sponsor dashboard metrics and sponsored leagues' })
@ApiResponse({
@@ -97,7 +99,7 @@ export class SponsorController {
@Get(':sponsorId/sponsorships')
@RequireAuthenticatedUser()
@RequireRoles('admin')
@RequireRoles('admin', 'sponsor')
@RequireCapability('sponsors.portal', 'view')
@ApiOperation({
summary: 'Get all sponsorships for a given sponsor',
@@ -193,7 +195,7 @@ export class SponsorController {
@Get('billing/:sponsorId')
@RequireAuthenticatedUser()
@RequireRoles('admin')
@RequireRoles('admin', 'sponsor')
@RequireCapability('sponsors.portal', 'view')
@ApiOperation({ summary: 'Get sponsor billing information' })
@ApiResponse({ status: 200, description: 'Sponsor billing data', type: Object })
@@ -209,7 +211,7 @@ export class SponsorController {
@Get('leagues/available')
@RequireAuthenticatedUser()
@RequireRoles('admin')
@RequireRoles('admin', 'sponsor')
@RequireCapability('sponsors.portal', 'view')
@ApiOperation({ summary: 'Get available leagues for sponsorship' })
@ApiResponse({
@@ -224,7 +226,7 @@ export class SponsorController {
@Get('leagues/:leagueId/detail')
@RequireAuthenticatedUser()
@RequireRoles('admin')
@RequireRoles('admin', 'sponsor')
@RequireCapability('sponsors.portal', 'view')
@ApiOperation({ summary: 'Get detailed league information for sponsors' })
@ApiResponse({ status: 200, description: 'League detail data', type: Object })

View File

@@ -283,6 +283,17 @@ export class SponsorService {
throw new Error('Sponsor billing not found');
}
const billingData = result.unwrap();
this.sponsorBillingPresenter.present({
paymentMethods: billingData.paymentMethods,
invoices: billingData.invoices,
stats: {
...billingData.stats,
nextPaymentDate: billingData.stats.nextPaymentDate ?? '',
nextPaymentAmount: billingData.stats.nextPaymentAmount ?? 0,
},
});
return this.sponsorBillingPresenter.viewModel;
}