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
+1629
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
{
"name": "ma7engine-example-backend",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "tsx server.ts",
"dev": "tsx watch server.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"express": "^4.19.0",
"jsonwebtoken": "^9.0.0",
"tsx": "^4.7.0"
},
"devDependencies": {
"@types/express": "^4.17.0",
"@types/jsonwebtoken": "^9.0.0",
"@types/node": "^26.3.0",
"typescript": "^5.4.0"
}
}
+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}`));
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020"],
"module": "CommonJS",
"moduleResolution": "node",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"esModuleInterop": true,
"resolveJsonModule": true
},
"include": ["server.ts"]
}