diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts index 0a3d121..a2aa7b7 100644 --- a/apps/server/src/app.module.ts +++ b/apps/server/src/app.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; import { APP_GUARD } from '@nestjs/core'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { EventEmitterModule } from '@nestjs/event-emitter'; @@ -54,6 +54,8 @@ import { ClassroomRentalsModule } from './classroom-rentals/classroom-rentals.mo import { SyncModule } from './sync/sync.module'; import { NotificationsModule } from './notifications/notifications.module'; import { DepartmentsModule } from './departments/departments.module'; +import { CampusScope } from './common/campus-scope'; +import { CampusScopeMiddleware } from './common/campus-scope.middleware'; @Module({ imports: [ @@ -139,9 +141,14 @@ import { DepartmentsModule } from './departments/departments.module'; DepartmentsModule, ], providers: [ + CampusScope, { provide: APP_GUARD, useClass: ThrottlerGuard }, { provide: APP_GUARD, useClass: JwtAuthGuard }, { provide: APP_GUARD, useClass: PermissionGuard }, ], }) -export class AppModule {} +export class AppModule implements NestModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(CampusScopeMiddleware).forRoutes('*'); + } +} diff --git a/apps/server/src/common/campus-scope.middleware.ts b/apps/server/src/common/campus-scope.middleware.ts new file mode 100644 index 0000000..f07f8cd --- /dev/null +++ b/apps/server/src/common/campus-scope.middleware.ts @@ -0,0 +1,13 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; +import { CampusScope, CampusRequest } from './campus-scope'; + +@Injectable() +export class CampusScopeMiddleware implements NestMiddleware { + constructor(private readonly scope: CampusScope) {} + + use(req: Request, _res: Response, next: NextFunction): void { + (req as CampusRequest).campusScope = this.scope; + next(); + } +} diff --git a/apps/server/src/common/campus-scope.ts b/apps/server/src/common/campus-scope.ts new file mode 100644 index 0000000..fc3616e --- /dev/null +++ b/apps/server/src/common/campus-scope.ts @@ -0,0 +1,67 @@ +import { Injectable, Scope, Inject } from '@nestjs/common'; +import { REQUEST } from '@nestjs/core'; +import { In } from 'typeorm'; +import { Request } from 'express'; +import { DepartmentsService } from '../departments/departments.service'; + +export interface CampusRequest extends Request { + user?: { + id: number; + isSuperAdmin?: boolean; + }; + campusScope?: CampusScope; +} + +@Injectable({ scope: Scope.REQUEST }) +export class CampusScope { + constructor( + @Inject(REQUEST) private req: CampusRequest, + private departmentsService: DepartmentsService, + ) {} + + get userId(): number | undefined { + return this.req.user?.id; + } + + get isSuperAdmin(): boolean { + return this.req.user?.isSuperAdmin ?? false; + } + + get currentDepartmentId(): number | null { + const raw = this.req.headers?.['x-campus-id']; + if (raw === undefined) return null; + const str = Array.isArray(raw) ? raw[0] : raw; + if (!str) return null; + const id = parseInt(str, 10); + return Number.isNaN(id) ? null : id; + } + + /** Appends departmentId filter to TypeORM find where conditions */ + async filter>(where: T): Promise { + // Super admin with no campus selected → no filtering + if (this.isSuperAdmin && !this.currentDepartmentId) { + return where; + } + + const ids = await this.getEffectiveScopeIds(); + if (ids.length === 0) return where; + + return { ...where, departmentId: In(ids) } as unknown as T; + } + + private async getEffectiveScopeIds(): Promise { + // Specific campus selected → campus + descendants + if (this.currentDepartmentId) { + return this.departmentsService.getDescendantIds(this.currentDepartmentId); + } + + // No campus selected → all user departments + descendants + if (!this.userId) return []; + + const userDeptIds = await this.departmentsService.getUserDepartments(this.userId); + const allIds = await Promise.all( + userDeptIds.map((id) => this.departmentsService.getDescendantIds(id)), + ); + return [...new Set(allIds.flat())]; + } +}