Spaces:
Paused
Paused
File size: 5,888 Bytes
c562e21 88e2e79 c562e21 4ceda60 c562e21 e5037ae c562e21 e5037ae c562e21 e5037ae c562e21 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | // Ядро: сборка сервера. Ведёт Claude.
// Подхватывает core-роуты + автоматически все модули Кодекса из src/modules/*.js.
import Fastify from 'fastify';
import websocket from '@fastify/websocket';
import fastifyStatic from '@fastify/static';
import { readdirSync, existsSync } from 'node:fs';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { dirname, join } from 'node:path';
import './core/db.js'; // инициализация БД/миграций
import { requireAuth } from './core/auth.js';
import authRoutes from './core/routes-auth.js';
import spacesRoutes from './core/routes-spaces.js';
import { registerWs } from './core/ws.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
// Hugging Face Spaces: снимаем префикс пути через rewriteUrl
// HF пробрасывает полный путь /spaces/{user}/{repo}/... в контейнер.
const HF_PREFIX = process.env.HF_PREFIX || '';
const app = Fastify({
logger: true,
rewriteUrl: (rawReq) => {
const url = rawReq.url || '';
// /spaces/{user}/{repo}/... → /...
const m = url.match(/^\/spaces\/[^/]+\/[^/]+(\/.*)?$/);
if (m) return m[1] || '/';
if (HF_PREFIX && url.startsWith(HF_PREFIX)) return url.slice(HF_PREFIX.length) || '/';
return url;
},
});
await app.register(websocket);
// Пустое тело при Content-Type: application/json трактуем как {} —
// фронт шлёт так POST-ы без полезной нагрузки (например /spaces/:id/join).
// Контракт допускает `{ }`, иначе Fastify падает с FST_ERR_CTP_EMPTY_JSON_BODY (500).
app.addContentTypeParser('application/json', { parseAs: 'string' }, (req, body, done) => {
if (!body || !body.trim()) return done(null, {});
try {
done(null, JSON.parse(body));
} catch (e) {
e.statusCode = 400;
done(e, undefined);
}
});
// Единый обработчик ошибок в формате контракта { error: { code, message } }
app.setErrorHandler((err, request, reply) => {
if (err.status && err.code) {
return reply.code(err.status).send({ error: { code: err.code, message: err.message } });
}
request.log.error(err);
// Включаем detail для отладки (убрать в проде)
const detail = process.env.NODE_ENV === 'production' ? '' : ` (${err.message})`;
return reply.code(500).send({ error: { code: 'INTERNAL', message: 'Внутренняя ошибка' + detail } });
});
// CORS для локальной разработки (фронт на 5173)
app.addHook('onRequest', async (request, reply) => {
reply.header('Access-Control-Allow-Origin', '*');
reply.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
reply.header('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE, OPTIONS');
if (request.method === 'OPTIONS') reply.code(204).send();
});
// --- Публичные роуты (без токена) ---
await app.register(authRoutes);
// --- WebSocket ---
registerWs(app);
// --- Защищённые роуты (требуют JWT) ---
await app.register(async (secured) => {
secured.addHook('preHandler', requireAuth);
// ядро: пространства
await secured.register(spacesRoutes);
// модули Кодекса: автоподхват src/modules/*.js
const modulesDir = join(__dirname, 'modules');
if (existsSync(modulesDir)) {
for (const file of readdirSync(modulesDir)) {
if (file.endsWith('.js')) {
const mod = await import(pathToFileURL(join(modulesDir, file)).href);
if (typeof mod.default === 'function') {
await secured.register(mod.default);
app.log.info(`module loaded: ${file}`);
}
}
}
}
});
// --- Отдача собранного фронта (один origin для Telegram Mini App) ---
// Ищем dist: пробуем несколько путей (Docker vs локально)
const tryPaths = [
join(__dirname, '..', '..', 'web', 'dist'), // Docker: /app/src → /app/web/dist
join(__dirname, '..', 'web', 'dist'), // альтернатива
'/app/web/dist', // хардкод для Docker
];
let webDist = tryPaths.find(p => existsSync(join(p, 'index.html')));
if (!webDist) webDist = tryPaths.find(p => existsSync(p));
if (webDist) {
app.log.info(`serving static from: ${webDist}`);
await app.register(fastifyStatic, {
root: webDist,
wildcard: true,
setHeaders: (res) => res.setHeader('Cache-Control', 'no-store'),
});
// Явный маршрут для корня (на случай если wildcard не покрывает)
app.get('/', async (request, reply) => {
return reply.header('Cache-Control', 'no-store').sendFile('index.html');
});
// SPA-fallback: любой GET не-API путь отдаёт index.html
app.setNotFoundHandler((request, reply) => {
if (request.method === 'GET' && !request.url.startsWith('/auth') &&
!request.url.startsWith('/spaces') && !request.url.startsWith('/ws')) {
return reply.header('Cache-Control', 'no-store').sendFile('index.html');
}
return reply.code(404).send({ error: { code: 'NOT_FOUND', message: 'Не найдено' } });
});
} else {
app.log.warn('web/dist не найден по путям: ' + tryPaths.join(', '));
// Заглушка — чтобы не падать с Fastify default 404
app.get('/', async (request, reply) => {
return reply.type('text/html').send('<html><body><h1>МЫ</h1><p>Фронт не собран</p></body></html>');
});
}
const PORT = process.env.PORT || 3000;
app.listen({ port: PORT, host: '0.0.0.0' }).catch((e) => {
app.log.error(e);
process.exit(1);
});
|