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:
2026-08-26 20:39:37 +03:00
co-authored by Claude Opus 5
commit 18bd05bcbb
50 changed files with 8152 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
// Шаблон бэкенда панели — с этого начинается новый. Копируется вместе с
// ../frontend; ядро в свою панель приезжает через `npm run update`.
//
// Всё, что здесь есть: конфиг и свои маршруты. Логин, сессия, права,
// рейт-лимит, раздача SPA и порядок их монтирования — в createPanelServer.
//
// Запуск:
// ADMIN_PASS=... ADMIN_JWT_SECRET=... npm start
//
// Панель-прокси, у которой своих учёток нет (авторизация живёт в основном
// продукте), createPanelServer не использует и собирает express сама из кусков
// ядра — см. раздел про панель-прокси в README.
import path from 'node:path';
import type { Request, Response } from 'express';
// Ядро — вендоренная копия в ./core: её кладёт `npm run update`, запущенный во
// фронте (он же predev/prebuild). Поэтому express и jsonwebtoken резолвятся из
// backend/node_modules, как в любой панели.
import { createPanelServer } from './core';
const PORT = Number(process.env.PORT) || 8080;
const app = createPanelServer({
auth: {
user: process.env.ADMIN_USER ?? 'admin',
password: process.env.ADMIN_PASS,
passwordHash: process.env.ADMIN_PASS_HASH,
secret: process.env.ADMIN_JWT_SECRET ?? '',
ttl: '12h',
revocable: true,
// claims не задан — вошедший получает permissions: ['*'], то есть
// видит все вкладки. Появятся учётки с разным доступом — размечать
// здесь.
},
// Раскладка как в панелях: <панель>/backend рядом с <панель>/frontend.
staticDir: process.env.STATIC_DIR ?? path.join(__dirname, '..', 'frontend', 'dist'),
mountRoutes(app, { auth, forwardError }) {
const { requireAuth, requirePermission, getSession } = auth;
// Открыт любому залогиненному — у вкладки в metadata.json нет permission.
app.get('/api/demo/whoami', requireAuth, (req: Request, res: Response) => {
res.json(getSession(req));
});
// Закрыт правом. Гейт на фронте (permission во вкладке) — это только
// UX; настоящая граница здесь.
app.get('/api/demo/secrets', requireAuth, requirePermission('secrets.view'),
(_req: Request, res: Response) => {
res.json({ items: ['первый', 'второй'] });
});
// Поход в вышестоящий API: его статус и текст пробрасываются клиенту,
// а не превращаются в 500.
app.get('/api/demo/upstream', requireAuth, async (_req: Request, res: Response) => {
try {
throw { response: { status: 404, data: { message: 'Не найдено наверху' } } };
} catch (e) { forwardError(res, e); }
});
},
});
app.listen(PORT, () => console.log(`demo-panel: listening on :${PORT}`));