Spaces:
Paused
Paused
Download apps/api/src/server.js from nenbuj/we-superapp: direct link, hf CLI and curl.
- Browser
- Download file 5.89 kB
-
https://huggingface.co/spaces/nenbuj/we-superapp/resolve/main/apps/api/src/server.js
- Command line
-
hf download hf://spaces/nenbuj/we-superapp/apps/api/src/server.js
-
curl -L -o server.js https://huggingface.co/spaces/nenbuj/we-superapp/resolve/main/apps/api/src/server.js
5.89 kB
| // Ядро: сборка сервера. Ведёт 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); | |
| }); | |