feat: add CampusScope request-level provider for data isolation
This commit is contained in:
@@ -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('*');
|
||||
}
|
||||
}
|
||||
|
||||
13
apps/server/src/common/campus-scope.middleware.ts
Normal file
13
apps/server/src/common/campus-scope.middleware.ts
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
67
apps/server/src/common/campus-scope.ts
Normal file
67
apps/server/src/common/campus-scope.ts
Normal file
@@ -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<T extends Record<string, unknown>>(where: T): Promise<T> {
|
||||
// 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<number[]> {
|
||||
// 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())];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user