feat(tags): moderators and admins can delete public tags
Add DELETE /api/tags/:name (gated by isModerator(), which also passes for admins per the admin-implies-moderator invariant in roles.ts). The endpoint normalises the name the same way creation does so the URL casing doesn't matter, then deletes the tag and detaches it from every activity_tags row in one transaction. UI: new "Etiketter" nav entry visible to moderators + admins, opens a ModerateTags.svelte view with search-as-you-type (reusing the /api/tags suggestion endpoint) and a Slett button per row. Private tags are unaffected — they're encrypted in the activity payload and never reach the server tag table. Tests: 3 new cases on top of the admin suite — moderator can delete, plain user gets 403 (anonymous gets 401), unknown tag gets 404.
This commit is contained in:
parent
dc349a9373
commit
54d8ed22f4
5 changed files with 231 additions and 2 deletions
|
|
@ -152,6 +152,66 @@ describe('admin user-list shape', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('tag deletion (moderator + admin)', () => {
|
||||
ttest('moderator can delete a public tag and it detaches from activities', async () => {
|
||||
const admin = await signupAndGetCookie(ctx, 'tag-admin@test.invalid');
|
||||
getDb().prepare('UPDATE users SET is_admin = 1 WHERE id = ?').run(admin.me.id);
|
||||
const mod = await signupAndGetCookie(ctx, 'tag-mod@test.invalid');
|
||||
getDb().prepare('UPDATE users SET is_moderator = 1 WHERE id = ?').run(mod.me.id);
|
||||
|
||||
// Two activities sharing a tag.
|
||||
await createActivity(ctx, admin.cookie, {
|
||||
visibility: 'public', title: 'with-tag-1', tags: ['Skismøring'],
|
||||
});
|
||||
await createActivity(ctx, admin.cookie, {
|
||||
visibility: 'semi', title: 'with-tag-2', tags: ['skismøring'],
|
||||
});
|
||||
|
||||
// Tag was normalised to lowercase.
|
||||
const before = await reqJson<{ name: string; usage_count: number }[]>(
|
||||
ctx, 'GET', '/api/tags?q=ski',
|
||||
);
|
||||
expect(before.some((t) => t.name === 'skismøring')).toBe(true);
|
||||
|
||||
const res = await req(ctx, 'DELETE', '/api/tags/skism%C3%B8ring', { cookie: mod.cookie });
|
||||
expect(res.status).toBe(204);
|
||||
|
||||
// Tag is gone from the suggestion list.
|
||||
const after = await reqJson<{ name: string }[]>(ctx, 'GET', '/api/tags?q=ski');
|
||||
expect(after.some((t) => t.name === 'skismøring')).toBe(false);
|
||||
|
||||
// activity_tags rows for the tag are also gone — verify the join surface.
|
||||
const db = getDb();
|
||||
const tagRow = db.prepare("SELECT id FROM tags WHERE name = 'skismøring'").get();
|
||||
expect(tagRow).toBeNull();
|
||||
});
|
||||
|
||||
ttest('plain user cannot delete a tag', async () => {
|
||||
const admin = await signupAndGetCookie(ctx, 'tag-admin2@test.invalid');
|
||||
getDb().prepare('UPDATE users SET is_admin = 1 WHERE id = ?').run(admin.me.id);
|
||||
const plain = await signupAndGetCookie(ctx, 'tag-plain@test.invalid');
|
||||
|
||||
await createActivity(ctx, admin.cookie, {
|
||||
visibility: 'public', title: 'plain-cannot', tags: ['victim-tag'],
|
||||
});
|
||||
|
||||
const res = await req(ctx, 'DELETE', '/api/tags/victim-tag', { cookie: plain.cookie });
|
||||
expect(res.status).toBe(403);
|
||||
|
||||
// Anonymous: 401 (requireAuth fires before the role check).
|
||||
const anon = await req(ctx, 'DELETE', '/api/tags/victim-tag');
|
||||
expect(anon.status).toBe(401);
|
||||
});
|
||||
|
||||
ttest('deleting an unknown tag returns 404', async () => {
|
||||
const admin = await signupAndGetCookie(ctx, 'tag-admin3@test.invalid');
|
||||
getDb().prepare('UPDATE users SET is_admin = 1 WHERE id = ?').run(admin.me.id);
|
||||
|
||||
const res = await req(ctx, 'DELETE', '/api/tags/does-not-exist', { cookie: admin.cookie });
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('role activity-delete crossover (admin/moderator can delete semi)', () => {
|
||||
ttest('admin can delete another user semi activity', async () => {
|
||||
const admin = await signupAndGetCookie(ctx, 'cross-admin@test.invalid');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue