From 25b7c6238ea9d3ac82a29bb9fab81ea04325ec60 Mon Sep 17 00:00:00 2001 From: jjbrunton Date: Sun, 30 Aug 2026 19:04:54 +0100 Subject: [PATCH] feat(admin-api): expose notification_preferences as read-only Resolve the earlier open question by mapping the profile-settings domain's notification toggles to the existing notification_preferences table (primary key user_id, no sensitive columns). Read-only, gated on view_users, with a test covering read access and rejected writes. --- apps/api/src/domains/admin/repository.ts | 1 + apps/api/test/admin.integration.test.ts | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/apps/api/src/domains/admin/repository.ts b/apps/api/src/domains/admin/repository.ts index 7693ad0..4fda17f 100644 --- a/apps/api/src/domains/admin/repository.ts +++ b/apps/api/src/domains/admin/repository.ts @@ -40,6 +40,7 @@ const resources: Record = { quiz_answers: { read: ['view_users', 'view_activity'] }, sent_dares: { read: ['view_users', 'view_activity', 'view_chats'] }, dare_messages: { read: ['view_chats', 'view_activity'] }, + notification_preferences: { read: 'view_users', idColumn: 'user_id' }, }; function identifier(value: string): string { diff --git a/apps/api/test/admin.integration.test.ts b/apps/api/test/admin.integration.test.ts index bd4855d..be67b6c 100644 --- a/apps/api/test/admin.integration.test.ts +++ b/apps/api/test/admin.integration.test.ts @@ -196,6 +196,21 @@ describe.skipIf(!databaseUrl || !isLocal)('admin repository authorization + audi }])).rejects.toMatchObject({ code: 'forbidden' }); }); + it('reads notification preferences for user support visibility without granting write access', async () => { + await pool.query( + "insert into notification_preferences(user_id,matches_enabled,dares_enabled) values($1,true,false)", + [creatorUser], + ); + const principal = await repository.principal(superUser); + const result = await repository.query(principal, 'notification_preferences', { + filters: [{ column: 'user_id', op: 'eq', value: creatorUser }], + }); + expect(result).toMatchObject({ count: 1, rows: [expect.objectContaining({ matches_enabled: true, dares_enabled: false })] }); + await expect(repository.insert(principal, 'notification_preferences', [{ + user_id: moderatorUser, + }])).rejects.toMatchObject({ code: 'forbidden' }); + }); + it('creates all legacy admin and dare cutover tables with preserved foreign keys', async () => { const tables = await pool.query<{ table_name: string }>( `select table_name from information_schema.tables where table_schema=current_schema()