Первый коммит существующего состояния. Ядро кладётся под git ради того, чтобы половины разъезжались в продуктовые репозитории через git subtree, а не копированием: subtree делает настоящий merge, хранит историю и умеет отдавать правки обратно. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019LeBkZLiQrvdECf5nfgiMz
64 lines
3.4 KiB
TypeScript
64 lines
3.4 KiB
TypeScript
// Шаблон бэкенда панели — с этого начинается новый. Копируется вместе с
|
||
// ../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}`));
|