fix: align permission navigation and page access
This commit is contained in:
@@ -32,6 +32,12 @@ export class DepositsController {
|
||||
@InjectRepository(Student) private studentRepo: Repository<Student>,
|
||||
) {}
|
||||
|
||||
@Get('student-lookups')
|
||||
@RequirePermission('deposit:create')
|
||||
getStudentLookups() {
|
||||
return this.service.getStudentLookups();
|
||||
}
|
||||
|
||||
@Get()
|
||||
@RequirePermission('deposit:view')
|
||||
findAll(@Query('studentId') studentId?: string, @Query('status') status?: string) {
|
||||
|
||||
15
apps/server/src/deposits/deposits.lookups.spec.ts
Normal file
15
apps/server/src/deposits/deposits.lookups.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { DepositsService } from './deposits.service';
|
||||
|
||||
describe('DepositsService permission-scoped lookups', () => {
|
||||
it('returns only minimal student fields needed by deposit forms', async () => {
|
||||
const studentRepo = {
|
||||
find: jest.fn().mockResolvedValue([{ id: 2, name: '张三', studentNo: 'S2' }]),
|
||||
};
|
||||
const service = new DepositsService({} as never, {} as never, studentRepo as never);
|
||||
|
||||
await expect(service.getStudentLookups()).resolves.toEqual([
|
||||
{ id: 2, name: '张三', studentNo: 'S2' },
|
||||
]);
|
||||
expect(studentRepo.find).toHaveBeenCalledWith(expect.objectContaining({ select: ['id', 'name', 'studentNo'] }));
|
||||
});
|
||||
});
|
||||
@@ -18,6 +18,14 @@ export class DepositsService {
|
||||
private studentRepo: Repository<Student>,
|
||||
) {}
|
||||
|
||||
async getStudentLookups() {
|
||||
return this.studentRepo.find({
|
||||
select: ['id', 'name', 'studentNo'],
|
||||
where: { status: 'active' },
|
||||
order: { name: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
async findAll(query?: { studentId?: number; status?: string }) {
|
||||
const qb = this.repo
|
||||
.createQueryBuilder('d')
|
||||
|
||||
@@ -72,6 +72,12 @@ export class ExpensesController {
|
||||
private logService: OperationLogsService,
|
||||
) {}
|
||||
|
||||
@Get('lookups')
|
||||
@RequirePermission('expense:create', 'expense:edit')
|
||||
getFormLookups() {
|
||||
return this.service.getFormLookups();
|
||||
}
|
||||
|
||||
@Post('room')
|
||||
@RequirePermission('expense:create')
|
||||
async createRoomExpense(@Body() dto: CreateRoomExpenseDto, @Request() req: any) {
|
||||
|
||||
25
apps/server/src/expenses/expenses.lookups.spec.ts
Normal file
25
apps/server/src/expenses/expenses.lookups.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ExpensesService } from './expenses.service';
|
||||
|
||||
describe('ExpensesService permission-scoped lookups', () => {
|
||||
it('returns only minimal room and student fields needed by expense forms', async () => {
|
||||
const roomRepo = {
|
||||
find: jest.fn().mockResolvedValue([{ id: 1, roomNumber: '101', building: 'A' }]),
|
||||
};
|
||||
const studentRepo = {
|
||||
find: jest.fn().mockResolvedValue([{ id: 2, name: '张三', studentNo: 'S2' }]),
|
||||
};
|
||||
const service = new ExpensesService(
|
||||
{} as never,
|
||||
{} as never,
|
||||
roomRepo as never,
|
||||
studentRepo as never,
|
||||
);
|
||||
|
||||
await expect(service.getFormLookups()).resolves.toEqual({
|
||||
rooms: [{ id: 1, roomNumber: '101', building: 'A' }],
|
||||
students: [{ id: 2, name: '张三', studentNo: 'S2' }],
|
||||
});
|
||||
expect(roomRepo.find).toHaveBeenCalledWith(expect.objectContaining({ select: ['id', 'roomNumber', 'building'] }));
|
||||
expect(studentRepo.find).toHaveBeenCalledWith(expect.objectContaining({ select: ['id', 'name', 'studentNo'] }));
|
||||
});
|
||||
});
|
||||
@@ -22,6 +22,21 @@ export class ExpensesService {
|
||||
@InjectRepository(Student) private studentRepo: Repository<Student>,
|
||||
) {}
|
||||
|
||||
async getFormLookups() {
|
||||
const [rooms, students] = await Promise.all([
|
||||
this.roomRepo.find({
|
||||
select: ['id', 'roomNumber', 'building'],
|
||||
order: { building: 'ASC', roomNumber: 'ASC' },
|
||||
}),
|
||||
this.studentRepo.find({
|
||||
select: ['id', 'name', 'studentNo'],
|
||||
where: { status: 'active' },
|
||||
order: { name: 'ASC' },
|
||||
}),
|
||||
]);
|
||||
return { rooms, students };
|
||||
}
|
||||
|
||||
// 宿舍费用
|
||||
async createRoomExpense(dto: CreateRoomExpenseDto, userId?: number) {
|
||||
const room = await this.roomRepo.findOne({ where: { id: dto.roomId } });
|
||||
|
||||
@@ -30,7 +30,7 @@ export class IntegrationConfigController {
|
||||
|
||||
/** 保存配置 */
|
||||
@Post()
|
||||
@RequirePermission('integration:read')
|
||||
@RequirePermission('integration:trigger')
|
||||
async saveConfig(@Body() body: SaveConfigRequest) {
|
||||
await this.service.saveConfig(body);
|
||||
return { success: true, message: '配置已保存' };
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { PRESET_ROLES } from './rbac.service';
|
||||
|
||||
function permissionsFor(roleCode: string): { groups: string[]; extras: string[] } {
|
||||
const role = PRESET_ROLES.find((item) => item.code === roleCode);
|
||||
if (!role) throw new Error(`missing role ${roleCode}`);
|
||||
return { groups: role.permissionGroups, extras: role.extraPermissions ?? [] };
|
||||
}
|
||||
|
||||
describe('preset role permissions', () => {
|
||||
it('gives teachers explicit workspace permissions without class/schedule delete privileges', () => {
|
||||
const teacher = PRESET_ROLES.find((role) => role.code === 'teacher');
|
||||
const teacher = permissionsFor('teacher');
|
||||
|
||||
expect(teacher).toBeDefined();
|
||||
expect(teacher?.permissionGroups).toEqual(['notification', 'profile']);
|
||||
expect(teacher?.extraPermissions).toEqual(
|
||||
expect(teacher.groups).toEqual(['notification', 'profile']);
|
||||
expect(teacher.extras).toEqual(
|
||||
expect.arrayContaining([
|
||||
'student:view',
|
||||
'class:view',
|
||||
@@ -16,8 +21,18 @@ describe('preset role permissions', () => {
|
||||
'attendance:export',
|
||||
]),
|
||||
);
|
||||
expect(teacher?.extraPermissions).not.toEqual(
|
||||
expect(teacher.extras).not.toEqual(
|
||||
expect.arrayContaining(['class:delete', 'schedule:delete']),
|
||||
);
|
||||
});
|
||||
|
||||
it('gives institution heads every read permission required by the classroom rental pages', () => {
|
||||
const role = permissionsFor('institution_head');
|
||||
expect(role.groups).toEqual(expect.arrayContaining(['classroom', 'rental', 'organization']));
|
||||
});
|
||||
|
||||
it('keeps roles without dashboard access off the dashboard', () => {
|
||||
expect(permissionsFor('teacher').groups).not.toContain('dashboard');
|
||||
expect(permissionsFor('institution_head').groups).not.toContain('dashboard');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -56,6 +56,16 @@ export class SchedulesController {
|
||||
);
|
||||
}
|
||||
|
||||
@Get('lookups')
|
||||
@RequirePermission('schedule:view')
|
||||
async getLookups(@Request() req: { user: RequestUser }) {
|
||||
const classIds = await this.service.getAccessibleClassIds(
|
||||
req.user.id,
|
||||
this.canManageAllSchedules(req),
|
||||
);
|
||||
return this.service.getLookups(classIds);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@RequirePermission('schedule:view')
|
||||
async findAll(@Query() query: QueryScheduleDto, @Request() req: { user: RequestUser }) {
|
||||
|
||||
31
apps/server/src/schedules/schedules.lookups.spec.ts
Normal file
31
apps/server/src/schedules/schedules.lookups.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { SchedulesService } from './schedules.service';
|
||||
|
||||
describe('SchedulesService permission-scoped lookups', () => {
|
||||
it('returns scoped classes and minimal classrooms for schedule viewers', async () => {
|
||||
const classRepo = {
|
||||
find: jest.fn().mockResolvedValue([{ id: 3, name: '三班', code: 'C3' }]),
|
||||
};
|
||||
const scheduleRepo = {
|
||||
createQueryBuilder: jest.fn().mockReturnValue({
|
||||
select: jest.fn().mockReturnThis(),
|
||||
addSelect: jest.fn().mockReturnThis(),
|
||||
innerJoin: jest.fn().mockReturnThis(),
|
||||
distinct: jest.fn().mockReturnThis(),
|
||||
orderBy: jest.fn().mockReturnThis(),
|
||||
addOrderBy: jest.fn().mockReturnThis(),
|
||||
getRawMany: jest.fn().mockResolvedValue([{ classroomId: 5, classroomName: '教室5', classroomBuilding: 'A' }]),
|
||||
}),
|
||||
};
|
||||
const service = new SchedulesService(
|
||||
scheduleRepo as never,
|
||||
classRepo as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
);
|
||||
|
||||
await expect(service.getLookups([3])).resolves.toEqual({
|
||||
classes: [{ id: 3, name: '三班', code: 'C3' }],
|
||||
classrooms: [{ id: 5, name: '教室5', building: 'A' }],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { ClassSchedule, Class, ClassroomRental, ClassTeacher } from '../entities';
|
||||
|
||||
import {
|
||||
@@ -33,6 +33,41 @@ export class SchedulesService {
|
||||
return [...new Set(assignments.map((assignment) => assignment.classId))];
|
||||
}
|
||||
|
||||
async getLookups(accessibleClassIds?: number[]) {
|
||||
const classes = accessibleClassIds
|
||||
? accessibleClassIds.length > 0
|
||||
? await this.classRepo.find({
|
||||
where: { id: In(accessibleClassIds) },
|
||||
select: ['id', 'name', 'code'],
|
||||
order: { name: 'ASC' },
|
||||
})
|
||||
: []
|
||||
: await this.classRepo.find({
|
||||
select: ['id', 'name', 'code'],
|
||||
order: { name: 'ASC' },
|
||||
});
|
||||
|
||||
const classroomRows = await this.scheduleRepo
|
||||
.createQueryBuilder('schedule')
|
||||
.select('classroom.id', 'classroomId')
|
||||
.addSelect('classroom.name', 'classroomName')
|
||||
.addSelect('classroom.building', 'classroomBuilding')
|
||||
.innerJoin('schedule.classroom', 'classroom')
|
||||
.distinct(true)
|
||||
.orderBy('classroom.building', 'ASC')
|
||||
.addOrderBy('classroom.name', 'ASC')
|
||||
.getRawMany();
|
||||
|
||||
return {
|
||||
classes,
|
||||
classrooms: classroomRows.map((row) => ({
|
||||
id: Number(row.classroomId),
|
||||
name: String(row.classroomName ?? ''),
|
||||
building: String(row.classroomBuilding ?? ''),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async findAll(query: QueryScheduleDto, accessibleClassIds?: number[]) {
|
||||
const qb = this.scheduleRepo.createQueryBuilder('cs');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user