clean routes
This commit is contained in:
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -225,6 +225,7 @@ describe('AuthService - New Methods', () => {
|
||||
userId: 'demo-user-123',
|
||||
email: 'demo.driver@example.com',
|
||||
displayName: 'Alex Johnson',
|
||||
role: 'driver',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user