feat: add CASL authorization and AI configuration
This commit is contained in:
@@ -35,6 +35,8 @@ import { OperationLogsService } from '../operation-logs/operation-logs.service';
|
||||
import { extractRequestInfo } from '../common/request-utils';
|
||||
import { RequirePermission } from '../auth/decorators/permission.decorator';
|
||||
import * as ExcelJS from 'exceljs';
|
||||
import { AuthorizationService, CaslAction, SubjectName } from '../authorization';
|
||||
import type { AuthenticatedUser } from '../authorization';
|
||||
|
||||
/** SSE event shape for @Sse() decorator */
|
||||
interface SseEvent {
|
||||
@@ -47,8 +49,9 @@ interface SseEvent {
|
||||
interface RequestUser {
|
||||
id: number;
|
||||
username: string;
|
||||
permissions?: string[];
|
||||
isSuperAdmin?: boolean;
|
||||
permissions: string[];
|
||||
isSuperAdmin: boolean;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@@ -58,6 +61,7 @@ export class AttendanceController {
|
||||
private readonly service: AttendanceService,
|
||||
private readonly importService: AttendanceImportService,
|
||||
private readonly logService: OperationLogsService,
|
||||
private readonly authz: AuthorizationService,
|
||||
) {}
|
||||
|
||||
private getTodayDateOnly(): string {
|
||||
@@ -68,16 +72,20 @@ export class AttendanceController {
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
private canManageAllAttendance(user: RequestUser): boolean {
|
||||
return user.isSuperAdmin === true || user.permissions?.includes('class:edit') === true;
|
||||
private canManageAllAttendance(req: { user: RequestUser }): boolean {
|
||||
return (
|
||||
this.authz.can(req, CaslAction.Manage, SubjectName.Attendance) ||
|
||||
// Legacy: class:edit grants broad attendance access for teacher scoping
|
||||
this.authz.can(req, CaslAction.Update, SubjectName.Class)
|
||||
);
|
||||
}
|
||||
|
||||
private getAccessibleClassIds(user: RequestUser) {
|
||||
return this.service.getAccessibleClassIds(user.id, this.canManageAllAttendance(user));
|
||||
private getAccessibleClassIds(req: { user: RequestUser }) {
|
||||
return this.service.getAccessibleClassIds(req.user.id, this.canManageAllAttendance(req));
|
||||
}
|
||||
|
||||
private assertClassAccess(user: RequestUser, classId: number) {
|
||||
return this.service.assertClassAccess(user.id, classId, this.canManageAllAttendance(user));
|
||||
private assertClassAccess(req: { user: RequestUser }, classId: number) {
|
||||
return this.service.assertClassAccess(req.user.id, classId, this.canManageAllAttendance(req));
|
||||
}
|
||||
|
||||
// ── Batch create attendance records ──
|
||||
@@ -124,8 +132,8 @@ export class AttendanceController {
|
||||
@Res() res: Response,
|
||||
@Request() req: { user: RequestUser },
|
||||
) {
|
||||
if (query.classId) await this.assertClassAccess(req.user, query.classId);
|
||||
const classIds = await this.getAccessibleClassIds(req.user);
|
||||
if (query.classId) await this.assertClassAccess(req, query.classId);
|
||||
const classIds = await this.getAccessibleClassIds(req);
|
||||
const records = await this.service.findAllForExport(query, classIds);
|
||||
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
@@ -176,8 +184,8 @@ export class AttendanceController {
|
||||
@Get('attendance-records')
|
||||
@RequirePermission('attendance:view')
|
||||
async findAll(@Query() query: QueryAttendanceRecordsDto, @Request() req: { user: RequestUser }) {
|
||||
if (query.classId) await this.assertClassAccess(req.user, query.classId);
|
||||
return this.service.findAll(query, await this.getAccessibleClassIds(req.user));
|
||||
if (query.classId) await this.assertClassAccess(req, query.classId);
|
||||
return this.service.findAll(query, await this.getAccessibleClassIds(req));
|
||||
}
|
||||
|
||||
// ── Update a single attendance record ──
|
||||
@@ -228,7 +236,7 @@ export class AttendanceController {
|
||||
@Get('attendance-records/classes')
|
||||
@RequirePermission('attendance:view')
|
||||
async getClasses(@Request() req: { user: RequestUser }) {
|
||||
return this.service.getClasses(await this.getAccessibleClassIds(req.user));
|
||||
return this.service.getClasses(await this.getAccessibleClassIds(req));
|
||||
}
|
||||
|
||||
// ── Attendance summary ──
|
||||
@@ -238,8 +246,8 @@ export class AttendanceController {
|
||||
@Query() query: AttendanceSummaryQueryDto,
|
||||
@Request() req: { user: RequestUser },
|
||||
) {
|
||||
if (query.classId) await this.assertClassAccess(req.user, query.classId);
|
||||
return this.service.getSummary(query, await this.getAccessibleClassIds(req.user));
|
||||
if (query.classId) await this.assertClassAccess(req, query.classId);
|
||||
return this.service.getSummary(query, await this.getAccessibleClassIds(req));
|
||||
}
|
||||
|
||||
// ── Attendance calendar ──
|
||||
@@ -249,7 +257,7 @@ export class AttendanceController {
|
||||
@Query() query: AttendanceCalendarQueryDto,
|
||||
@Request() req: { user: RequestUser },
|
||||
) {
|
||||
await this.assertClassAccess(req.user, query.classId);
|
||||
await this.assertClassAccess(req, query.classId);
|
||||
return this.service.getCalendar(query);
|
||||
}
|
||||
|
||||
@@ -257,8 +265,8 @@ export class AttendanceController {
|
||||
@Get('ding-attendance-raw')
|
||||
@RequirePermission('attendance:view')
|
||||
async getDingRaw(@Query() query: QueryDingRawDto, @Request() req: { user: RequestUser }) {
|
||||
if (query.classId) await this.assertClassAccess(req.user, query.classId);
|
||||
return this.service.getDingRaw(query, await this.getAccessibleClassIds(req.user));
|
||||
if (query.classId) await this.assertClassAccess(req, query.classId);
|
||||
return this.service.getDingRaw(query, await this.getAccessibleClassIds(req));
|
||||
}
|
||||
|
||||
// ── Match a dingtalk record to a student ──
|
||||
@@ -293,11 +301,8 @@ export class AttendanceController {
|
||||
@Res() res: Response,
|
||||
@Request() req: any,
|
||||
) {
|
||||
if (query.classId) await this.assertClassAccess(req.user, query.classId);
|
||||
const reportData = await this.service.getReport(
|
||||
query,
|
||||
await this.getAccessibleClassIds(req.user),
|
||||
);
|
||||
if (query.classId) await this.assertClassAccess(req, query.classId);
|
||||
const reportData = await this.service.getReport(query, await this.getAccessibleClassIds(req));
|
||||
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const ws = workbook.addWorksheet('考勤统计报表');
|
||||
@@ -363,7 +368,7 @@ export class AttendanceController {
|
||||
return this.service.getAlerts(
|
||||
days ? +days : 14,
|
||||
threshold ? +threshold : 3,
|
||||
await this.getAccessibleClassIds(req.user),
|
||||
await this.getAccessibleClassIds(req),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -380,7 +385,7 @@ export class AttendanceController {
|
||||
@Get('attendance-records/import/dingtalk/classes')
|
||||
@RequirePermission('attendance:create')
|
||||
getDingTalkImportClasses(@Request() req: { user: RequestUser }) {
|
||||
return this.service.getImportableClasses(req.user.id, this.canManageAllAttendance(req.user));
|
||||
return this.service.getImportableClasses(req.user.id, this.canManageAllAttendance(req));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -392,7 +397,7 @@ export class AttendanceController {
|
||||
@RequirePermission('attendance:create')
|
||||
async importFromDingTalk(@Body() dto: DingTalkImportDto, @Request() req: { user: RequestUser }) {
|
||||
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||
const canManageAll = this.canManageAllAttendance(req.user);
|
||||
const canManageAll = this.canManageAllAttendance(req);
|
||||
let userIds: string[];
|
||||
|
||||
if (dto.users) {
|
||||
|
||||
Reference in New Issue
Block a user