import 'dotenv/config' import path from 'node:path' import cors from '@fastify/cors' import jwt from '@fastify/jwt' import multipart from '@fastify/multipart' import fastifyStatic from '@fastify/static' import Fastify from 'fastify' import { NOTIFICATION_EVENTS } from '../../shared/constants/notification-events.js' import { ensureAdminUser } from './lib/bootstrap-admin.js' import { getOrCreateUnspecifiedCategory } from './lib/default-category.js' import { createEventBus } from './lib/notifications/event-bus.js' import { resolveUserNotificationTargets, resolveAdminNotificationTargets, resolveAuthCodeTargets, } from './lib/notifications/preferences.js' import { createNotificationQueue } from './lib/notifications/queue.js' import { prisma } from './lib/prisma.js' import { getMaxUploadBodyBytes, getProductImageMaxFileBytes } from './lib/upload-limits.js' import { registerAuth } from './plugins/auth.js' import { registerApiRoutes } from './routes/api.js' import { registerAuthRoutes } from './routes/auth.js' import { registerOAuthSocialRoutes } from './routes/oauth-social.js' import { registerUploadsResized } from './routes/uploads-resized.js' import { registerUserNotificationRoutes } from './routes/user/notifications.js' import { registerUserAddressRoutes } from './routes/user-addresses.js' import { registerUserCartRoutes } from './routes/user-cart.js' import { registerUserMessageRoutes } from './routes/user-messages.js' import { registerUserOrderRoutes } from './routes/user-orders.js' import { registerUserPaymentRoutes } from './routes/user-payments.js' const port = Number(process.env.PORT) || 3333 const origin = (process.env.CORS_ORIGIN ?? '') .split(',') .map((s) => s.trim()) .filter(Boolean) const fastify = Fastify({ logger: true, bodyLimit: getMaxUploadBodyBytes(), }) await fastify.register(cors, { origin: origin.length ? origin : true, credentials: true, }) await fastify.register(jwt, { secret: process.env.JWT_SECRET || 'dev-jwt-secret-change-me', }) await fastify.register(multipart, { limits: { files: 10, fileSize: getProductImageMaxFileBytes(), }, }) registerUploadsResized(fastify) const uploadsDir = path.join(process.cwd(), 'uploads') await fastify.register(fastifyStatic, { root: uploadsDir, prefix: '/uploads/', setHeaders(res, filePath) { if (filePath.includes('/.cache/')) { res.setHeader('Cache-Control', 'public, max-age=31536000, immutable') } else { res.setHeader('Cache-Control', 'public, max-age=86400') } }, }) fastify.decorate('authenticate', async function authenticate(request, reply) { try { await request.jwtVerify() } catch { return reply.code(401).send({ error: 'Не авторизован' }) } }) const eventBus = createEventBus() const notificationQueue = createNotificationQueue() fastify.decorate('eventBus', eventBus) fastify.decorate('notificationQueue', notificationQueue) registerAuth(fastify) await registerAuthRoutes(fastify) await registerUserAddressRoutes(fastify) await registerUserCartRoutes(fastify) await registerUserMessageRoutes(fastify) await registerUserOrderRoutes(fastify) await registerUserPaymentRoutes(fastify) await registerUserNotificationRoutes(fastify) await registerOAuthSocialRoutes(fastify) await registerApiRoutes(fastify) await ensureAdminUser() await getOrCreateUnspecifiedCategory() await notificationQueue.flushPendingOnStartup() notificationQueue.start() const { ORDER_CREATED, ORDER_STATUS_CHANGED, ORDER_MESSAGE_SENT, ORDER_MESSAGE_ADMIN_REPLY, PAYMENT_STATUS_CHANGED, AUTH_CODE_REQUESTED, DELIVERY_FEE_ADJUSTED, } = NOTIFICATION_EVENTS async function dispatchNotification(eventType, payload) { if (eventType === AUTH_CODE_REQUESTED) { const targets = await resolveAuthCodeTargets(eventType, payload) for (const target of targets.filter((t) => t.channel === 'telegram')) { const log = await prisma.notificationLog.create({ data: { eventType, channel: target.channel, status: 'pending', payload: JSON.stringify(payload), }, }) notificationQueue.enqueue({ ...target, eventType, payload, logId: log.id }) } return } const userTargets = await resolveUserNotificationTargets(eventType, payload) for (const target of userTargets) { const log = await prisma.notificationLog.create({ data: { userId: payload.userId, eventType, channel: target.channel, status: 'pending', payload: JSON.stringify(payload), }, }) notificationQueue.enqueue({ ...target, eventType, payload, logId: log.id }) } const adminEventType = eventType === 'order:created:admin' ? ORDER_CREATED : eventType const adminTargets = await resolveAdminNotificationTargets(adminEventType, payload) for (const target of adminTargets) { const log = await prisma.notificationLog.create({ data: { eventType, channel: target.channel, status: 'pending', payload: JSON.stringify(payload), }, }) notificationQueue.enqueue({ ...target, eventType, payload, logId: log.id }) } } eventBus.on(ORDER_CREATED, (payload) => dispatchNotification(ORDER_CREATED, payload)) eventBus.on(ORDER_STATUS_CHANGED, (payload) => dispatchNotification(ORDER_STATUS_CHANGED, payload)) eventBus.on(ORDER_MESSAGE_SENT, (payload) => dispatchNotification(ORDER_MESSAGE_SENT, payload)) eventBus.on(ORDER_MESSAGE_ADMIN_REPLY, (payload) => dispatchNotification(ORDER_MESSAGE_ADMIN_REPLY, payload)) eventBus.on(PAYMENT_STATUS_CHANGED, (payload) => dispatchNotification(PAYMENT_STATUS_CHANGED, payload)) eventBus.on(AUTH_CODE_REQUESTED, (payload) => dispatchNotification(AUTH_CODE_REQUESTED, payload)) eventBus.on('order:created:admin', (payload) => dispatchNotification('order:created:admin', payload)) eventBus.on('review:created', (payload) => dispatchNotification('review:created', payload)) eventBus.on(DELIVERY_FEE_ADJUSTED, (payload) => dispatchNotification(DELIVERY_FEE_ADJUSTED, payload)) async function shutdown() { notificationQueue.stop() await fastify.close() process.exit(0) } process.on('SIGINT', shutdown) process.on('SIGTERM', shutdown) try { await fastify.listen({ port, host: '0.0.0.0' }) } catch (err) { fastify.log.error(err) process.exit(1) }