fix(clients): archiving is edit-level, not delete (unblocks Sales/Director)
Every client-archive route gated on clients:delete, which only super_admin holds, so Sales Agent/Manager and Director users (clients.delete=false) got 403 on archive — the button was shown but never worked. Archiving is reversible, so gate it on clients:edit instead; permanent deletion stays the separate admin:permanently_delete_clients permission. - New single source of truth CLIENT_ARCHIVE_ACTION='edit' (permissions.ts). - All 5 archive routes use it: [id] DELETE, archive, archive-dossier, bulk-archive-preflight, bulk POST. - UI affordances gate on clients:edit (detail-header archive/restore, bulk archive, per-row list + mobile-card archive) so view-only users don't see a button that 403s. - permission-matrix regression test locks the policy (Sales/Director/Agent can archive; Viewer cannot). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L2qc3xZTfif7N4Wq3QDa8X
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
import { withPermission, deepMerge, type AuthContext } from '@/lib/api/helpers';
|
||||
import { CLIENT_ARCHIVE_ACTION } from '@/lib/auth/permissions';
|
||||
import {
|
||||
makeViewerPermissions,
|
||||
makeSalesAgentPermissions,
|
||||
@@ -64,6 +65,43 @@ async function checkPermission(
|
||||
return response.status;
|
||||
}
|
||||
|
||||
// ─── client archive policy ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Archiving a client is REVERSIBLE (sets archivedAt; restorable), so it is an
|
||||
* edit-level action — NOT the destructive `clients:delete` (which only
|
||||
* super_admin holds) and NOT `admin:permanently_delete_clients` (hard delete).
|
||||
* Regression guard: prod showed Sales Managers/Directors getting 403 on archive
|
||||
* because every archive route gated on `delete`. All archive routes now gate on
|
||||
* CLIENT_ARCHIVE_ACTION; if anyone flips it back to a permission the sales roles
|
||||
* lack, these fail.
|
||||
*/
|
||||
describe('Permission Matrix - client archive policy', () => {
|
||||
it('archiving is an edit-level action, not delete', () => {
|
||||
expect(CLIENT_ARCHIVE_ACTION).toBe('edit');
|
||||
});
|
||||
|
||||
it('a Sales Manager can archive a client', async () => {
|
||||
const ctx = makeCtx({ permissions: makeSalesManagerPermissions() });
|
||||
expect(await checkPermission(ctx, 'clients', CLIENT_ARCHIVE_ACTION)).toBe(200);
|
||||
});
|
||||
|
||||
it('a Director can archive a client', async () => {
|
||||
const ctx = makeCtx({ permissions: makeDirectorPermissions() });
|
||||
expect(await checkPermission(ctx, 'clients', CLIENT_ARCHIVE_ACTION)).toBe(200);
|
||||
});
|
||||
|
||||
it('a Sales Agent can archive a client', async () => {
|
||||
const ctx = makeCtx({ permissions: makeSalesAgentPermissions() });
|
||||
expect(await checkPermission(ctx, 'clients', CLIENT_ARCHIVE_ACTION)).toBe(200);
|
||||
});
|
||||
|
||||
it('a Viewer cannot archive a client', async () => {
|
||||
const ctx = makeCtx({ permissions: makeViewerPermissions() });
|
||||
expect(await checkPermission(ctx, 'clients', CLIENT_ARCHIVE_ACTION)).toBe(403);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── super_admin ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Permission Matrix - super_admin', () => {
|
||||
|
||||
Reference in New Issue
Block a user