81 lines
2.5 KiB
TypeScript
81 lines
2.5 KiB
TypeScript
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) {
|
|
// Non-super-admin with no scoping → match nothing, never leak unfiltered data
|
|
if (!this.isSuperAdmin) {
|
|
return { ...where, departmentId: In([]) };
|
|
}
|
|
return where;
|
|
}
|
|
|
|
return { ...where, departmentId: In(ids) };
|
|
}
|
|
|
|
/** Returns department IDs for QueryBuilder .andWhere() usage. null = no filtering needed. */
|
|
async getScopeDepartmentIds(): Promise<number[] | null> {
|
|
if (this.isSuperAdmin && !this.currentDepartmentId) return null;
|
|
const ids = await this.getEffectiveScopeIds();
|
|
return ids.length > 0 ? ids : null;
|
|
}
|
|
|
|
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())];
|
|
}
|
|
}
|