diff --git a/apps/backend/src/modules/auth/decorators/roles.decorator.ts b/apps/backend/src/modules/auth/decorators/roles.decorator.ts new file mode 100644 index 00000000..e038e168 --- /dev/null +++ b/apps/backend/src/modules/auth/decorators/roles.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common'; + +export const ROLES_KEY = 'roles'; +export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles); diff --git a/apps/backend/src/modules/auth/guards/roles.guard.ts b/apps/backend/src/modules/auth/guards/roles.guard.ts new file mode 100644 index 00000000..6d567dee --- /dev/null +++ b/apps/backend/src/modules/auth/guards/roles.guard.ts @@ -0,0 +1,29 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { ROLES_KEY } from '../decorators/roles.decorator'; + +@Injectable() +export class RolesGuard implements CanActivate { + constructor(private reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const requiredRoles = this.reflector.getAllAndOverride(ROLES_KEY, [ + context.getHandler(), + context.getClass(), + ]); + + if (!requiredRoles) { + return true; + } + + const { user } = context.switchToHttp().getRequest(); + + if (!user) { + return false; + } + + // Check if user has any of the required roles + const userRoles = user.roles || []; + return requiredRoles.some((role) => userRoles.includes(role)); + } +} diff --git a/apps/backend/src/modules/commissions/services/index.ts b/apps/backend/src/modules/commissions/services/index.ts index 6d48e80f..0cbb7d65 100644 --- a/apps/backend/src/modules/commissions/services/index.ts +++ b/apps/backend/src/modules/commissions/services/index.ts @@ -1,5 +1,12 @@ -export * from './schemes.service'; -export * from './assignments.service'; -export * from './entries.service'; -export * from './periods.service'; -export * from './commissions-dashboard.service'; +// Export pagination types only from schemes.service to avoid duplicates +export { + SchemesService, + PaginatedResult, + PaginationOptions +} from './schemes.service'; + +// Export other services without pagination types (they re-declare the same interfaces) +export { AssignmentsService } from './assignments.service'; +export { EntriesService } from './entries.service'; +export { PeriodsService } from './periods.service'; +export { CommissionsDashboardService } from './commissions-dashboard.service'; diff --git a/apps/backend/src/modules/sales/services/index.ts b/apps/backend/src/modules/sales/services/index.ts index efc2348a..6cd2796b 100644 --- a/apps/backend/src/modules/sales/services/index.ts +++ b/apps/backend/src/modules/sales/services/index.ts @@ -1,5 +1,13 @@ -export * from './leads.service'; -export * from './opportunities.service'; -export * from './pipeline.service'; -export * from './activities.service'; -export * from './sales-dashboard.service'; +// Export pagination types only from leads.service to avoid duplicates +export { + LeadsService, + LeadFilters, + PaginatedResult, + PaginationOptions +} from './leads.service'; + +// Export other services without pagination types (they re-declare the same interfaces) +export { OpportunitiesService, OpportunityFilters } from './opportunities.service'; +export { PipelineService } from './pipeline.service'; +export { ActivitiesService, ActivityFilters } from './activities.service'; +export { SalesDashboardService } from './sales-dashboard.service'; diff --git a/apps/backend/src/modules/superadmin/__tests__/superadmin.service.spec.ts b/apps/backend/src/modules/superadmin/__tests__/superadmin.service.spec.ts index adba8acc..b0cd7678 100644 --- a/apps/backend/src/modules/superadmin/__tests__/superadmin.service.spec.ts +++ b/apps/backend/src/modules/superadmin/__tests__/superadmin.service.spec.ts @@ -180,7 +180,7 @@ describe('SuperadminService', () => { name: 'New Company', slug: 'new-company', domain: 'new.example.com', - status: 'trial', + status: 'pending' as const, }; it('should create a new tenant', async () => { diff --git a/apps/backend/src/modules/superadmin/dto/index.ts b/apps/backend/src/modules/superadmin/dto/index.ts index a868404f..2bd97f9a 100644 --- a/apps/backend/src/modules/superadmin/dto/index.ts +++ b/apps/backend/src/modules/superadmin/dto/index.ts @@ -1,6 +1,9 @@ import { IsString, IsOptional, IsEnum, IsNumber, Min, Max, IsUUID } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +// Type that matches Tenant entity status enum +export type TenantStatus = 'pending' | 'active' | 'suspended' | 'cancelled'; + export class CreateTenantDto { @ApiProperty({ description: 'Tenant name' }) @IsString() @@ -25,10 +28,10 @@ export class CreateTenantDto { @IsOptional() plan_id?: string; - @ApiPropertyOptional({ description: 'Initial status', enum: ['active', 'trial', 'suspended'] }) - @IsEnum(['active', 'trial', 'suspended']) + @ApiPropertyOptional({ description: 'Initial status', enum: ['pending', 'active', 'suspended', 'cancelled'] }) + @IsEnum(['pending', 'active', 'suspended', 'cancelled']) @IsOptional() - status?: string; + status?: TenantStatus; } export class UpdateTenantDto { @@ -62,9 +65,9 @@ export class UpdateTenantDto { } export class UpdateTenantStatusDto { - @ApiProperty({ description: 'New status', enum: ['active', 'suspended', 'trial', 'canceled'] }) - @IsEnum(['active', 'suspended', 'trial', 'canceled']) - status: string; + @ApiProperty({ description: 'New status', enum: ['pending', 'active', 'suspended', 'cancelled'] }) + @IsEnum(['pending', 'active', 'suspended', 'cancelled']) + status: TenantStatus; @ApiPropertyOptional({ description: 'Reason for status change' }) @IsString() @@ -91,10 +94,10 @@ export class ListTenantsQueryDto { @IsOptional() search?: string; - @ApiPropertyOptional({ description: 'Filter by status', enum: ['active', 'suspended', 'trial', 'canceled'] }) - @IsEnum(['active', 'suspended', 'trial', 'canceled']) + @ApiPropertyOptional({ description: 'Filter by status', enum: ['pending', 'active', 'suspended', 'cancelled'] }) + @IsEnum(['pending', 'active', 'suspended', 'cancelled']) @IsOptional() - status?: string; + status?: TenantStatus; @ApiPropertyOptional({ description: 'Sort by field', enum: ['name', 'created_at', 'status'] }) @IsString() diff --git a/apps/backend/src/modules/superadmin/superadmin.service.ts b/apps/backend/src/modules/superadmin/superadmin.service.ts index 1e5337e9..37a255a8 100644 --- a/apps/backend/src/modules/superadmin/superadmin.service.ts +++ b/apps/backend/src/modules/superadmin/superadmin.service.ts @@ -347,7 +347,7 @@ export class SuperadminService { } async getStatusDistribution(): Promise<{ status: string; count: number; percentage: number }[]> { - const statuses = ['active', 'pending', 'suspended', 'cancelled']; + const statuses: Array<'active' | 'pending' | 'suspended' | 'cancelled'> = ['active', 'pending', 'suspended', 'cancelled']; const total = await this.tenantRepository.count(); const result = await Promise.all( diff --git a/apps/backend/src/modules/tenants/decorators/current-tenant.decorator.ts b/apps/backend/src/modules/tenants/decorators/current-tenant.decorator.ts new file mode 100644 index 00000000..9b5ae5b8 --- /dev/null +++ b/apps/backend/src/modules/tenants/decorators/current-tenant.decorator.ts @@ -0,0 +1,2 @@ +// Re-export from auth module for backwards compatibility +export { CurrentTenant } from '../../auth/decorators/tenant.decorator'; diff --git a/apps/backend/src/modules/tenants/decorators/tenant-id.decorator.ts b/apps/backend/src/modules/tenants/decorators/tenant-id.decorator.ts new file mode 100644 index 00000000..d289ba45 --- /dev/null +++ b/apps/backend/src/modules/tenants/decorators/tenant-id.decorator.ts @@ -0,0 +1,2 @@ +// Re-export CurrentTenant as TenantId for backwards compatibility +export { CurrentTenant as TenantId } from '../../auth/decorators/tenant.decorator';