feat: per-user museum and channel access control

- PATCH /api/users/:id route to update user permissions
- Auth session stores and returns allowedMuseums/allowedChannels
- User type gains AllowedMuseums/AllowedChannels (JSON string fields)
- parseAllowed() with fail-closed semantics (empty string → null → no data)
- Dashboard/Comparison apply permission base filter before user filters
- Filter dropdowns (museums, channels, years, districts) derived from
  permission-filtered data — restricted users only see their allowed options
- Settings UserRow component with inline checkbox pickers for access config
- Access badges in users table showing current restriction summary

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
fahed
2026-04-08 18:03:19 +03:00
parent d4ce5b6478
commit e41cff831b
10 changed files with 259 additions and 51 deletions
+27
View File
@@ -3,6 +3,24 @@ export interface User {
Name: string;
PIN: string;
Role: string;
AllowedMuseums: string; // JSON-serialized string[], '[]' = unrestricted
AllowedChannels: string; // JSON-serialized string[]
}
// null = parse error → fail-closed (show nothing)
// [] = unrestricted (admin or no restriction set)
// string[] = restricted to this list
export function parseAllowed(raw: string | undefined | null): string[] | null {
if (raw == null) return []; // field not set → unrestricted
if (raw === '[]') return []; // explicit empty → unrestricted
if (raw === '') return null; // blank string → corrupted → fail-closed
try {
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) return null;
return parsed as string[];
} catch {
return null;
}
}
export async function fetchUsers(): Promise<User[]> {
@@ -25,6 +43,15 @@ export async function createUser(user: Omit<User, 'Id'>): Promise<User> {
return res.json();
}
export async function updateUser(id: number, fields: Partial<User>): Promise<void> {
const res = await fetch(`/api/users/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(fields),
});
if (!res.ok) throw new Error('Failed to update user');
}
export async function deleteUser(id: number): Promise<void> {
const res = await fetch(`/api/users/${id}`, { method: 'DELETE' });
if (!res.ok) throw new Error('Failed to delete user');