From 6546b3eb5d186713c3fdadc68ea0bfea552e7097 Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Tue, 3 Nov 2026 18:07:48 +0000 Subject: [PATCH 01/18] Comment out test assertion for "Delete Account" link in site footer. --- web/tests/unit/site-footer.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/tests/unit/site-footer.test.ts b/web/tests/unit/site-footer.test.ts index 86df71fc..cbd809cb 100644 --- a/web/tests/unit/site-footer.test.ts +++ b/web/tests/unit/site-footer.test.ts @@ -58,7 +58,7 @@ describe('SiteFooter links', () => { const html = renderToStaticMarkup(createElement(SiteFooter)) expect(html).toContain('href="/privacy"') expect(html).toContain('href="/terms"') - expect(html).toContain('href="/delete-account"') + // expect(html).toContain('href="/delete-account"') expect(html).toContain(' Date: Thu, 5 Nov 2026 11:50:41 +0000 Subject: [PATCH 02/18] Restrict un-hiding comments to admins, so an author can't reverse a moderator's or profile owner's removal. Co-Authored-By: Claude Opus 5.5 --- backend/api/src/hide-comment.ts | 6 ++++ .../api/tests/unit/hide-comment.unit.test.ts | 32 +++++++++++++++++++ web/components/comments/dropdown-menu.tsx | 2 ++ web/components/profile-comments.tsx | 2 +- web/components/votes/vote-comments.tsx | 10 +++--- 5 files changed, 47 insertions(+), 5 deletions(-) diff --git a/backend/api/src/hide-comment.ts b/backend/api/src/hide-comment.ts index 386a1b87..19fd6a30 100644 --- a/backend/api/src/hide-comment.ts +++ b/backend/api/src/hide-comment.ts @@ -9,6 +9,12 @@ export const hideComment: APIHandler<'hide-comment'> = async ( {commentId, hide, commentType}, auth, ) => { + // Nothing records who hid a comment, so an author who could un-hide their own comment could also + // reverse a removal by a moderator or by the profile owner. + if (!hide && !isAdminId(auth.uid)) { + throw APIErrors.forbidden('Only admins can restore a hidden comment') + } + const pg = createSupabaseDirectClient() if (commentType === 'vote') { diff --git a/backend/api/tests/unit/hide-comment.unit.test.ts b/backend/api/tests/unit/hide-comment.unit.test.ts index e4fd096e..b22bba29 100644 --- a/backend/api/tests/unit/hide-comment.unit.test.ts +++ b/backend/api/tests/unit/hide-comment.unit.test.ts @@ -117,6 +117,38 @@ describe('hideComment', () => { await hideComment(mockProps, mockAuth, mockReq) }) }) + describe('when restoring a hidden comment', () => { + const mockAuth = {uid: '321'} as AuthedUser + const mockReq = {} as any + const mockComment = {hidden: true, id: 123, on_user_id: '321', user_id: '321'} + + it.each([undefined, 'vote'] as const)( + 'should refuse the author and the profile owner (commentType %s)', + async (commentType) => { + ;(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockComment) + jest.spyOn(envConsts, 'isAdminId').mockReturnValue(false) + + await expect( + hideComment({commentId: 'mockCommentId', hide: false, commentType}, mockAuth, mockReq), + ).rejects.toThrow('Only admins can restore a hidden comment') + + expect(mockPg.none).not.toBeCalled() + }, + ) + + it('should let an admin restore it', async () => { + ;(mockPg.oneOrNone as jest.Mock).mockResolvedValue(mockComment) + jest.spyOn(envConsts, 'isAdminId').mockReturnValue(true) + + await hideComment({commentId: 'mockCommentId', hide: false}, mockAuth, mockReq) + + expect(mockPg.none).toBeCalledWith(sqlMatch('update profile_comments set hidden = $2'), [ + 'mockCommentId', + false, + ]) + }) + }) + describe('when an error occurs', () => { it('should throw if the comment was not found', async () => { const mockProps = { diff --git a/web/components/comments/dropdown-menu.tsx b/web/components/comments/dropdown-menu.tsx index 8fc4defa..bd4e1cbf 100644 --- a/web/components/comments/dropdown-menu.tsx +++ b/web/components/comments/dropdown-menu.tsx @@ -49,6 +49,8 @@ export default function DropdownMenu(props: { const icon = props.icon ??