fix: close permission review gaps
fix: harden permission-gated UI — minimum-org endpoint, modal/Popconfirm fail-closed on revocation
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
import 'reflect-metadata';
|
||||
import { PERMISSION_KEY } from '../auth/decorators/permission.decorator';
|
||||
import { OrganizationsController } from './organizations.controller';
|
||||
|
||||
describe('OrganizationsController permissions', () => {
|
||||
it('allows student editors to use the options endpoint without full entity exposure', () => {
|
||||
expect(
|
||||
Reflect.getMetadata(PERMISSION_KEY, OrganizationsController.prototype.findOptions),
|
||||
).toEqual(['organization:view', 'student:create', 'student:edit']);
|
||||
});
|
||||
|
||||
it('keeps the full entity list restricted to organization viewers only', () => {
|
||||
expect(Reflect.getMetadata(PERMISSION_KEY, OrganizationsController.prototype.findAll)).toEqual([
|
||||
'organization:view',
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps organization detail restricted to organization viewers', () => {
|
||||
expect(Reflect.getMetadata(PERMISSION_KEY, OrganizationsController.prototype.findOne)).toEqual([
|
||||
'organization:view',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,12 @@ export class OrganizationsController {
|
||||
private logService: OperationLogsService,
|
||||
) {}
|
||||
|
||||
@Get('options')
|
||||
@RequirePermission('organization:view', 'student:create', 'student:edit')
|
||||
findOptions() {
|
||||
return this.service.findOptions();
|
||||
}
|
||||
|
||||
@Get()
|
||||
@RequirePermission('organization:view')
|
||||
findAll(
|
||||
|
||||
@@ -27,4 +27,21 @@ describe('OrganizationsService — host organization rules', () => {
|
||||
await expect(service.remove(1)).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(repo.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('findOptions returns only id, name, isHost for active organizations', async () => {
|
||||
const orgs = [
|
||||
{ id: 1, name: '本机构', isHost: true },
|
||||
{ id: 2, name: '分校', isHost: false },
|
||||
];
|
||||
repo.find.mockResolvedValue(orgs as Organization[]);
|
||||
|
||||
const result = await service.findOptions();
|
||||
|
||||
expect(repo.find).toHaveBeenCalledWith({
|
||||
select: ['id', 'name', 'isHost'],
|
||||
where: { status: 'active' },
|
||||
order: { isHost: 'DESC', name: 'ASC' },
|
||||
});
|
||||
expect(result).toEqual(orgs);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,6 +30,14 @@ export class OrganizationsService {
|
||||
return this.repo.find({ where, order: { isHost: 'DESC', name: 'ASC' } });
|
||||
}
|
||||
|
||||
async findOptions() {
|
||||
return this.repo.find({
|
||||
select: ['id', 'name', 'isHost'] as const,
|
||||
where: { status: 'active' },
|
||||
order: { isHost: 'DESC' as const, name: 'ASC' as const },
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: number) {
|
||||
const organization = await this.repo.findOne({ where: { id } });
|
||||
if (!organization) throw new NotFoundException('机构不存在');
|
||||
|
||||
Reference in New Issue
Block a user