Ребренд Мониторинг Орбита -> Железный Егорин; accent green->amber #F59E0B; LoginScreen правая половина = Lightning (WebGL, hue 35 тёплый) под стеклянной формой; левая панель — тёплый градиент. Доступ агентам: репо публичный, push german-токеном. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
90 lines
4.2 KiB
JavaScript
90 lines
4.2 KiB
JavaScript
// 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));
|
||
}
|