forked from wangziqi/gongxue-base
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 { APP_GUARD } from '@nestjs/core';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
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 { SyncModule } from './sync/sync.module';
|
||||||
import { NotificationsModule } from './notifications/notifications.module';
|
import { NotificationsModule } from './notifications/notifications.module';
|
||||||
import { DepartmentsModule } from './departments/departments.module';
|
import { DepartmentsModule } from './departments/departments.module';
|
||||||
|
import { CampusScope } from './common/campus-scope';
|
||||||
|
import { CampusScopeMiddleware } from './common/campus-scope.middleware';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -139,9 +141,14 @@ import { DepartmentsModule } from './departments/departments.module';
|
|||||||
DepartmentsModule,
|
DepartmentsModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
CampusScope,
|
||||||
{ provide: APP_GUARD, useClass: ThrottlerGuard },
|
{ provide: APP_GUARD, useClass: ThrottlerGuard },
|
||||||
{ provide: APP_GUARD, useClass: JwtAuthGuard },
|
{ provide: APP_GUARD, useClass: JwtAuthGuard },
|
||||||
{ provide: APP_GUARD, useClass: PermissionGuard },
|
{ 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