fix: harden permission-gated UI — minimum-org endpoint, modal/Popconfirm fail-closed on revocation

This commit is contained in:
2026-07-23 14:15:52 +08:00
parent f39136d9ce
commit 3ac99b808e
11 changed files with 282 additions and 142 deletions

View File

@@ -3,11 +3,15 @@ import { PERMISSION_KEY } from '../auth/decorators/permission.decorator';
import { OrganizationsController } from './organizations.controller';
describe('OrganizationsController permissions', () => {
it('allows student editors to list organization options', () => {
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',
'student:create',
'student:edit',
]);
});

View File

@@ -25,8 +25,14 @@ export class OrganizationsController {
private logService: OperationLogsService,
) {}
@Get()
@Get('options')
@RequirePermission('organization:view', 'student:create', 'student:edit')
findOptions() {
return this.service.findOptions();
}
@Get()
@RequirePermission('organization:view')
findAll(
@Query('includeArchived') includeArchived?: string,
@Query('scope') scope?: 'all' | 'host' | 'external',

View File

@@ -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);
});
});

View File

@@ -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('机构不存在');