e41cff831b
- 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>
114 lines
3.3 KiB
TypeScript
114 lines
3.3 KiB
TypeScript
import { Router, Request, Response } from 'express';
|
|
import crypto from 'crypto';
|
|
import { auth } from '../config';
|
|
import { discoverTableIds, fetchAllRecords } from '../services/nocodbClient';
|
|
|
|
const router = Router();
|
|
|
|
interface UserRecord {
|
|
Id: number;
|
|
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>();
|
|
const SESSION_MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
|
|
function generateSessionId(): string {
|
|
return crypto.randomBytes(32).toString('hex');
|
|
}
|
|
|
|
function getSession(sessionId: string): Session | null {
|
|
const session = sessions.get(sessionId);
|
|
if (!session) return null;
|
|
if (Date.now() - session.createdAt > SESSION_MAX_AGE) {
|
|
sessions.delete(sessionId);
|
|
return null;
|
|
}
|
|
return session;
|
|
}
|
|
|
|
// POST /auth/login
|
|
router.post('/login', async (req: Request, res: Response) => {
|
|
const { pin } = req.body;
|
|
if (!pin) {
|
|
res.status(400).json({ error: 'PIN required' });
|
|
return;
|
|
}
|
|
|
|
// 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(), allowedMuseums: '[]', allowedChannels: '[]' });
|
|
res.cookie('hihala_session', sessionId, { httpOnly: true, sameSite: 'lax', maxAge: SESSION_MAX_AGE, path: '/' });
|
|
res.json({ ok: true, name: 'Admin', role: 'admin', allowedMuseums: '[]', allowedChannels: '[]' });
|
|
return;
|
|
}
|
|
|
|
// Check NocoDB Users table
|
|
try {
|
|
const tables = await discoverTableIds();
|
|
if (tables['Users']) {
|
|
const users = await fetchAllRecords<UserRecord>(tables['Users']);
|
|
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(),
|
|
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',
|
|
allowedMuseums: user.AllowedMuseums || '[]',
|
|
allowedChannels: user.AllowedChannels || '[]',
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.warn('Failed to check Users table:', (err as Error).message);
|
|
}
|
|
|
|
res.status(401).json({ error: 'Invalid PIN' });
|
|
});
|
|
|
|
// GET /auth/check
|
|
router.get('/check', (req: Request, res: Response) => {
|
|
const sessionId = req.cookies?.hihala_session;
|
|
const session = sessionId ? getSession(sessionId) : null;
|
|
res.json({
|
|
authenticated: !!session,
|
|
name: session?.name || null,
|
|
role: session?.role || null,
|
|
allowedMuseums: session?.allowedMuseums ?? '[]',
|
|
allowedChannels: session?.allowedChannels ?? '[]',
|
|
});
|
|
});
|
|
|
|
// POST /auth/logout
|
|
router.post('/logout', (req: Request, res: Response) => {
|
|
const sessionId = req.cookies?.hihala_session;
|
|
if (sessionId) sessions.delete(sessionId);
|
|
res.clearCookie('hihala_session', { path: '/' });
|
|
res.json({ ok: true });
|
|
});
|
|
|
|
export default router;
|