import "dotenv/config"; import Fastify from "fastify"; import cors from "@fastify/cors"; import jwt from "@fastify/jwt"; import multipart from "@fastify/multipart"; import fastifyStatic from "@fastify/static"; import path from "node:path"; import { ensureAdminUser } from "./lib/bootstrap-admin.js"; import { getOrCreateUnspecifiedCategory } from "./lib/default-category.js"; import { getMaxUploadBodyBytes, getProductImageMaxFileBytes, } from "./lib/upload-limits.js"; import { createEventBus } from "./lib/notifications/event-bus.js"; import { createNotificationQueue } from "./lib/notifications/queue.js"; import { prisma } from "./lib/prisma.js"; import { resolveUserNotificationTargets, resolveAdminNotificationTargets, resolveAuthCodeTargets, } from "./lib/notifications/preferences.js"; import { NOTIFICATION_EVENTS, NOTIFICATION_CHANNELS, } from "../../shared/constants/notification-events.js"; import { registerAuth } from "./plugins/auth.js"; import { registerApiRoutes } from "./routes/api.js"; import { registerAuthRoutes } from "./routes/auth.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"; import { registerUserNotificationRoutes } from "./routes/user/notifications.js"; import { registerOAuthSocialRoutes } from "./routes/oauth-social.js"; import { registerUploadsResized } from "./routes/uploads-resized.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); }