ma7engine: ядро, шаблон панели, скаффолдер и доктор
Первый коммит существующего состояния. Ядро кладётся под git ради того, чтобы половины разъезжались в продуктовые репозитории через git subtree, а не копированием: subtree делает настоящий merge, хранит историю и умеет отдавать правки обратно. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019LeBkZLiQrvdECf5nfgiMz
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
// Мидлвары авторизации: «пускать ли вообще» и «пускать ли к этому разделу».
|
||||
//
|
||||
// Проверка прав на фронте (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<string[]>;
|
||||
/** Тексты отказов — панели отдают ошибки как {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 };
|
||||
}
|
||||
Reference in New Issue
Block a user