// Мидлвары авторизации: «пускать ли вообще» и «пускать ли к этому разделу». // // Проверка прав на фронте (permission в metadata.json вкладки) — это только // UX: не показывать то, что всё равно отклонит сервер. Настоящая граница здесь, // и роут без requirePermission открыт всем залогиненным независимо от того, как // размечена вкладка. // // Откуда берутся права — развилка без единственно верного ответа, поэтому она // вынесена в конфиг (loadPermissions): // из клеймов токена — дёшево, но снятое право действует к истечению TTL; // из БД на запрос — дороже, зато отзыв мгновенный. import type { NextFunction, Request, Response } from 'express'; import type { SessionClaims, SessionStore } from './session'; export const OWNER_PERMISSION = '*'; // Ключ на объекте запроса. Symbol, а не req.session, чтобы не столкнуться с // express-session и другими мидлварами, занимающими это имя. const SESSION = Symbol.for('ma7engine.session'); export type Middleware = (req: Request, res: Response, next: NextFunction) => void; export interface AuthOptions { sessions: SessionStore; /** * Права на конкретный запрос. По умолчанию — из клеймов токена. * Верни свежие права из БД, если нужен мгновенный отзыв. */ loadPermissions?: (claims: SessionClaims, req: Request) => string[] | Promise; /** Тексты отказов — панели отдают ошибки как {message}. */ messages?: { unauthorized?: string; forbidden?: string }; } export interface Auth { /** Пускает только с живым токеном; кладёт клеймы в запрос. */ requireAuth: Middleware; /** Пускает только с указанным правом (или '*'). Ставится после requireAuth. */ requirePermission: (permission: string) => Middleware; /** Клеймы текущего запроса. Осмысленны только после requireAuth. */ getSession: (req: Request) => SessionClaims; /** Bearer-токен запроса — нужен, например, чтобы отозвать его на логауте. */ bearerToken: (req: Request) => string; } export function bearerToken(req: Request): string { const header = req.headers.authorization ?? ''; return header.startsWith('Bearer ') ? header.slice(7) : ''; } export function createAuth(options: AuthOptions): Auth { const { sessions, loadPermissions } = options; const unauthorized = options.messages?.unauthorized ?? 'Не авторизован'; const forbidden = options.messages?.forbidden ?? 'Недостаточно прав'; const getSession = (req: Request): SessionClaims => (req as Request & { [SESSION]?: SessionClaims })[SESSION] as SessionClaims; const requireAuth: Middleware = (req, res, next) => { const claims = sessions.verify(bearerToken(req)); if (!claims) { res.status(401).json({ message: unauthorized }); return; } (req as Request & { [SESSION]?: SessionClaims })[SESSION] = claims; next(); }; const requirePermission = (permission: string): Middleware => (req, res, next) => { const claims = getSession(req); if (!claims) { res.status(401).json({ message: unauthorized }); return; } Promise.resolve(loadPermissions ? loadPermissions(claims, req) : claims.permissions) .then((permissions) => { if (permissions.includes(OWNER_PERMISSION) || permissions.includes(permission)) { next(); return; } res.status(403).json({ message: forbidden }); }) .catch(next); }; return { requireAuth, requirePermission, getSession, bearerToken }; }