Железный Егорин — форк Орбиты: тёплый стиль (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:
German
2026-07-07 23:57:16 +03:00
co-authored by Claude Opus 4.8
commit b1c52ab748
114 changed files with 14340 additions and 0 deletions
+89
View File
@@ -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));
}