// RBAC-зеркало фронта + простой HMAC-токен (без внешних зависимостей). import crypto from 'node:crypto'; import { config } from './config.js'; export const USERS = [ { id: 'admin', name: 'Администратор', role: 'Полный доступ', admin: true, access: { energy: { caps: ['view', 'edit', 'reports'], objects: 'all' }, smart: { caps: ['view', 'edit', 'reports'], objects: 'all' }, gps: { caps: ['view', 'edit', 'reports'], objects: 'all' }, netinfra: { caps: ['view', 'edit', 'reports'], objects: 'all' }, security: { caps: ['view', 'edit', 'reports'], objects: 'all' }, }, }, { id: 'pcn-operator', name: 'Дежурный ПЦН', role: 'Оператор пульта', access: { security: { caps: ['view', 'edit', 'reports'], objects: 'all' } } }, { id: 'energetik-ro', name: 'Сидоров А.', role: 'Энергетик · просмотр', access: { energy: { caps: ['view'], objects: ['cab-07'] } } }, { id: 'energetik-rw', name: 'Петров В.', role: 'Энергетик · правка', access: { energy: { caps: ['view', 'edit', 'reports'], objects: 'all' } } }, { id: 'logist', name: 'Кузнецова М.', role: 'Логист транспорта', access: { gps: { caps: ['view', 'edit', 'reports'], objects: 'all' } } }, { id: 'netadmin', name: 'Орлов Д.', role: 'Сетевой инженер', access: { netinfra: { caps: ['view', 'edit', 'reports'], objects: 'all' } } }, { id: 'demo', name: 'Демо-доступ', role: 'Только просмотр', access: { energy: { caps: ['view'], objects: 'all' }, smart: { caps: ['view'], objects: 'all' }, gps: { caps: ['view'], objects: 'all' }, netinfra: { caps: ['view'], objects: 'all' }, security: { caps: ['view'], objects: 'all' }, }, }, ]; export const TYPE_SECTION = { transport: 'gps', energy: 'energy', realty: 'smart', network: 'netinfra' }; const TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000; // срок жизни сессии — 30 дней const STATE_TTL_MS = 10 * 60 * 1000; // OAuth state (CSRF) — 10 минут export function sign(user) { const body = Buffer.from(JSON.stringify({ id: user.id, t: Date.now() })).toString('base64url'); const sig = crypto.createHmac('sha256', config.jwtSecret).update(body).digest('base64url'); return `${body}.${sig}`; } // Возвращает id пользователя из токена (сам пользователь резолвится в tenants). null если подпись/срок невалидны. export function verifyId(token) { if (!token) return null; const [body, sig] = token.split('.'); if (!body || !sig) return null; const exp = crypto.createHmac('sha256', config.jwtSecret).update(body).digest('base64url'); if (exp !== sig) return null; try { const p = JSON.parse(Buffer.from(body, 'base64url').toString()); if (!p.t || Date.now() - p.t > TOKEN_TTL_MS) return null; // сессия просрочена return p.id || null; } catch { return null; } } // CSRF-state для OAuth: подписанный короткоживущий токен (stateless, без хранилища сессий) export function signState() { const body = Buffer.from(JSON.stringify({ n: crypto.randomBytes(9).toString('hex'), t: Date.now() })).toString('base64url'); const sig = crypto.createHmac('sha256', config.jwtSecret).update('state:' + body).digest('base64url'); return `${body}.${sig}`; } export function verifyState(state) { if (!state) return false; const [body, sig] = String(state).split('.'); if (!body || !sig) return false; const exp = crypto.createHmac('sha256', config.jwtSecret).update('state:' + body).digest('base64url'); if (exp !== sig) return false; try { const p = JSON.parse(Buffer.from(body, 'base64url').toString()); return !!p.t && Date.now() - p.t <= STATE_TTL_MS; } catch { return false; } } export const can = (user, section, cap) => !!user?.access?.[section]?.caps?.includes(cap); export function isObjectAllowed(user, section, id) { const o = user?.access?.[section]?.objects; return o === 'all' || (Array.isArray(o) && o.includes(id)); }