Логін: - core.users.username замість пошти як ідентифікатор: у мережевій інсталяції половина акаунтів технічні (noc, monitoring, oncall) і скриньки не мають узагалі. Пошта лишилась необов'язковим полем - наявним користувачам логін виведено з пошти, збіги розведено суфіксом: мовчки злити admin@a.com і admin@b.com в один логін — це втрата акаунта, а не міграція - сервер шукає за логіном і за поштою, тому звичка вводити email нікого не відхиляє Групи (модель Zabbix): - групи хостів і групи доступу; права read/write/deny на групу хостів - роль каже, що вільно робити; група — над якими хостами. Інженер над філією та інженер над усією мережею мають однакову роль і різний доступ - хто не входить у жодну групу, групами не обмежений — свідомо не по-заббіксівськи: там кожна нова інсталяція починається з питання «чому порожньо» - заборона перемагає дозвіл, інакше її обійти додаванням у сусідню групу - фільтр накладається в самому запиті, а не після вибірки Хости: додавання, редагування, м'яке видалення, прив'язка до груп, фільтр за групою в таблиці. Мапа: створення мапи з транслітерацією slug, інспектор вузла — підпис, значок, розмір, ширина, колір рамки, закріплення. Виправлено за скаргами: - перемикач вилазив на 14px за трек: у ручки не заданий left, а статичну позицію зсуває типове text-align: center у <button> - сторінка правил не оновлювала «активних»: після повернення правила алерти піднімаються наступним тіком движка, тобто ПІСЛЯ нашого перечитування — бракувало підписки на живі події - канал із секретом показувався як «секрету немає» Живий прогін: eng у групі з read на «Доступ» бачить 2 хости замість 3, обидва без запису, алертів 1 замість 3, редагування чужого хоста 403. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
78 lines
2.2 KiB
TypeScript
78 lines
2.2 KiB
TypeScript
import type { Permission } from '../types'
|
||
|
||
/**
|
||
* Сесія браузера.
|
||
*
|
||
* Access-токен живе **лише в пам'яті вкладки**, а не в localStorage:
|
||
* XSS-скрипт читає localStorage тривіально, а до змінної в замиканні
|
||
* модуля дістатись значно важче. Довгу частину тримає httpOnly-кукі з
|
||
* refresh-токеном, яку скрипт не бачить узагалі — тому після
|
||
* перезавантаження сторінки сесія відновлюється тихим refresh-ом.
|
||
*/
|
||
|
||
export interface Me {
|
||
userID?: string
|
||
username?: string
|
||
email?: string
|
||
fullName?: string
|
||
tokenName?: string
|
||
tenantID: string
|
||
tenantName?: string
|
||
roleName?: string
|
||
permissions: Permission[]
|
||
}
|
||
|
||
let accessToken = ''
|
||
let me: Me | null = null
|
||
const listeners = new Set<() => void>()
|
||
|
||
function notify() {
|
||
for (const l of listeners) l()
|
||
}
|
||
|
||
export const session = {
|
||
subscribe(fn: () => void) {
|
||
listeners.add(fn)
|
||
return () => listeners.delete(fn)
|
||
},
|
||
|
||
token: () => accessToken,
|
||
me: () => me,
|
||
|
||
set(token: string, who: Me) {
|
||
accessToken = token
|
||
me = who
|
||
notify()
|
||
},
|
||
|
||
clear() {
|
||
accessToken = ''
|
||
me = null
|
||
notify()
|
||
},
|
||
|
||
/** Право є, якщо воно явно надане або токен без обмежень (`*`). */
|
||
can(perm: Permission): boolean {
|
||
if (!me) return false
|
||
return me.permissions.includes(perm) || me.permissions.includes('*')
|
||
},
|
||
}
|
||
|
||
/** Розбір відповіді входу в єдину форму. */
|
||
export function meFromLogin(body: {
|
||
user?: { id: string; username?: string; email?: string; full_name?: string }
|
||
tenant?: { tenant_id: string; tenant_name?: string; role_name?: string } | null
|
||
permissions?: string[]
|
||
tenant_id?: string
|
||
}): Me {
|
||
return {
|
||
userID: body.user?.id,
|
||
username: body.user?.username,
|
||
email: body.user?.email,
|
||
fullName: body.user?.full_name,
|
||
tenantID: body.tenant?.tenant_id ?? body.tenant_id ?? '',
|
||
tenantName: body.tenant?.tenant_name,
|
||
roleName: body.tenant?.role_name,
|
||
permissions: (body.permissions ?? []) as Permission[],
|
||
}
|
||
}
|