feat(20-01): add PATCH /members/:id member-profile update with last-admin guard

- Add isAdmin field to GET /members select and mapped member object (D-02)
- Add updateMemberSchema (displayName optional string, isAdmin optional boolean)
- Register adminRouter.patch('/members/:id') with noEchoHook and requireAdmin (inherited)
- Handler: parsePositiveIntParam id validation (400), existence check (404),
  D-03 last-admin guard via COUNT(*) query (409), partial set() update (200)
- Fix Test D: switch to adminId2 for GET after self-demotion (adminId1 no longer admin)
- All 44 tests green including 7 new PATCH/isAdmin tests
This commit is contained in:
Lucas Berger
2026-06-18 17:19:43 -04:00
parent a0a82ac9b6
commit bc48632756
2 changed files with 81 additions and 7 deletions
+4 -1
View File
@@ -1151,6 +1151,7 @@ describe('PATCH /api/admin/members/:id', () => {
it('Test D (self-demotion allowed): with two admins, PATCH { isAdmin: false } returns 200; one admin remains', async () => {
const adminId1 = await seedUser('admin-demote-1', true);
const adminId2 = await seedUser('admin-demote-2', true);
// Log in as adminId1 to perform the self-demotion
currentDevUserId = adminId1;
const app = await getApp();
@@ -1159,7 +1160,9 @@ describe('PATCH /api/admin/members/:id', () => {
);
expect(res.status).toBe(200);
// Only adminId2 should remain as admin
// Switch to adminId2 to verify the outcome — adminId1 is now non-admin
// and can no longer call GET /members (would 403).
currentDevUserId = adminId2;
const getRes = await app.fetch(jsonRequest('GET', '/api/admin/members'));
expect(getRes.status).toBe(200);
const getBody = (await getRes.json()) as {