Files
shop-server/server/src/index.js
T

193 lines
6.3 KiB
JavaScript

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);
}