Железный Егорин — форк Орбиты: тёплый стиль (amber), логин с odyssey WebGL-молнией справа, энергомониторинг+панель
Ребренд Мониторинг Орбита -> Железный Егорин; accent green->amber #F59E0B; LoginScreen правая половина = Lightning (WebGL, hue 35 тёплый) под стеклянной формой; левая панель — тёплый градиент. Доступ агентам: репо публичный, push german-токеном. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
// 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));
|
||||
}
|
||||
Reference in New Issue
Block a user