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:
@@ -10,12 +10,16 @@ interface UserRecord {
|
||||
Name: string;
|
||||
PIN: string;
|
||||
Role: string;
|
||||
AllowedMuseums?: string;
|
||||
AllowedChannels?: string;
|
||||
}
|
||||
|
||||
interface Session {
|
||||
name: string;
|
||||
role: string;
|
||||
createdAt: number;
|
||||
allowedMuseums: string;
|
||||
allowedChannels: string;
|
||||
}
|
||||
|
||||
const sessions = new Map<string, Session>();
|
||||
@@ -46,9 +50,9 @@ router.post('/login', async (req: Request, res: Response) => {
|
||||
// Check super admin PIN from env first
|
||||
if (auth.adminPin && pin === auth.adminPin) {
|
||||
const sessionId = generateSessionId();
|
||||
sessions.set(sessionId, { name: 'Admin', role: 'admin', createdAt: Date.now() });
|
||||
sessions.set(sessionId, { name: 'Admin', role: 'admin', createdAt: Date.now(), allowedMuseums: '[]', allowedChannels: '[]' });
|
||||
res.cookie('hihala_session', sessionId, { httpOnly: true, sameSite: 'lax', maxAge: SESSION_MAX_AGE, path: '/' });
|
||||
res.json({ ok: true, name: 'Admin', role: 'admin' });
|
||||
res.json({ ok: true, name: 'Admin', role: 'admin', allowedMuseums: '[]', allowedChannels: '[]' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -60,9 +64,21 @@ router.post('/login', async (req: Request, res: Response) => {
|
||||
const user = users.find(u => u.PIN === pin);
|
||||
if (user) {
|
||||
const sessionId = generateSessionId();
|
||||
sessions.set(sessionId, { name: user.Name, role: user.Role || 'viewer', createdAt: Date.now() });
|
||||
sessions.set(sessionId, {
|
||||
name: user.Name,
|
||||
role: user.Role || 'viewer',
|
||||
createdAt: Date.now(),
|
||||
allowedMuseums: user.AllowedMuseums || '[]',
|
||||
allowedChannels: user.AllowedChannels || '[]',
|
||||
});
|
||||
res.cookie('hihala_session', sessionId, { httpOnly: true, sameSite: 'lax', maxAge: SESSION_MAX_AGE, path: '/' });
|
||||
res.json({ ok: true, name: user.Name, role: user.Role || 'viewer' });
|
||||
res.json({
|
||||
ok: true,
|
||||
name: user.Name,
|
||||
role: user.Role || 'viewer',
|
||||
allowedMuseums: user.AllowedMuseums || '[]',
|
||||
allowedChannels: user.AllowedChannels || '[]',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -81,6 +97,8 @@ router.get('/check', (req: Request, res: Response) => {
|
||||
authenticated: !!session,
|
||||
name: session?.name || null,
|
||||
role: session?.role || null,
|
||||
allowedMuseums: session?.allowedMuseums ?? '[]',
|
||||
allowedChannels: session?.allowedChannels ?? '[]',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { discoverTableIds, fetchAllRecords, createRecord, deleteRecord } from '../services/nocodbClient';
|
||||
import { discoverTableIds, fetchAllRecords, createRecord, updateRecord, deleteRecord } from '../services/nocodbClient';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -32,6 +32,17 @@ router.post('/', async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
// PATCH /api/users/:id
|
||||
router.patch('/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const tableId = await getUsersTableId();
|
||||
await updateRecord(tableId, parseInt(req.params.id), req.body);
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: (err as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/users/:id
|
||||
router.delete('/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user