diff --git a/server/src/index.js b/server/src/index.js index 4e8e07e..7cdac4e 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -1,104 +1,110 @@ -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 "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' +} 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(',') +const port = Number(process.env.PORT) || 3333; +const origin = (process.env.CORS_ORIGIN ?? "") + .split(",") .map((s) => s.trim()) - .filter(Boolean) + .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', -}) + secret: process.env.JWT_SECRET || "dev-jwt-secret-change-me", +}); await fastify.register(multipart, { limits: { files: 10, fileSize: getProductImageMaxFileBytes(), }, -}) +}); -registerUploadsResized(fastify) +registerUploadsResized(fastify); -const uploadsDir = path.join(process.cwd(), 'uploads') +const uploadsDir = path.join(process.cwd(), "uploads"); await fastify.register(fastifyStatic, { root: uploadsDir, - prefix: '/uploads/', + prefix: "/uploads/", setHeaders(res, filePath) { - if (filePath.includes('/.cache/')) { - res.setHeader('Cache-Control', 'public, max-age=31536000, immutable') + if (filePath.includes("/.cache/")) { + res.setHeader("Cache-Control", "public, max-age=31536000, immutable"); } else { - res.setHeader('Cache-Control', 'public, max-age=86400') + res.setHeader("Cache-Control", "public, max-age=86400"); } }, -}) +}); -fastify.decorate('authenticate', async function authenticate(request, reply) { +fastify.decorate("authenticate", async function authenticate(request, reply) { try { - await request.jwtVerify() + await request.jwtVerify(); } catch { - return reply.code(401).send({ error: 'Не авторизован' }) + return reply.code(401).send({ error: "Не авторизован" }); } -}) +}); -const eventBus = createEventBus() -const notificationQueue = createNotificationQueue() -fastify.decorate('eventBus', eventBus) -fastify.decorate('notificationQueue', notificationQueue) +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() +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() +await notificationQueue.flushPendingOnStartup(); +notificationQueue.start(); const { ORDER_CREATED, @@ -107,58 +113,62 @@ const { ORDER_MESSAGE_ADMIN_REPLY, PAYMENT_STATUS_CHANGED, AUTH_CODE_REQUESTED, -} = NOTIFICATION_EVENTS +} = NOTIFICATION_EVENTS; async function dispatchNotification(eventType, payload) { - const userTargets = await resolveUserNotificationTargets(eventType, payload) + 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', + status: "pending", payload: JSON.stringify(payload), }, - }) - notificationQueue.enqueue({ ...target, eventType, payload, logId: log.id }) + }); + notificationQueue.enqueue({ ...target, eventType, payload, logId: log.id }); } - const adminEventType = eventType === 'order:created:admin' ? ORDER_CREATED : eventType - const adminTargets = await resolveAdminNotificationTargets(adminEventType, payload) + 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', + status: "pending", payload: JSON.stringify(payload), }, - }) - notificationQueue.enqueue({ ...target, eventType, payload, logId: log.id }) + }); + notificationQueue.enqueue({ ...target, eventType, payload, logId: log.id }); } } -eventBus.on(ORDER_CREATED, dispatchNotification) -eventBus.on(ORDER_STATUS_CHANGED, dispatchNotification) -eventBus.on(ORDER_MESSAGE_SENT, dispatchNotification) -eventBus.on(ORDER_MESSAGE_ADMIN_REPLY, dispatchNotification) -eventBus.on(PAYMENT_STATUS_CHANGED, dispatchNotification) -eventBus.on(AUTH_CODE_REQUESTED, dispatchNotification) -eventBus.on('order:created:admin', dispatchNotification) -eventBus.on('review:created', dispatchNotification) +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)); async function shutdown() { - notificationQueue.stop() - await fastify.close() - process.exit(0) + notificationQueue.stop(); + await fastify.close(); + process.exit(0); } -process.on('SIGINT', shutdown) -process.on('SIGTERM', shutdown) +process.on("SIGINT", shutdown); +process.on("SIGTERM", shutdown); try { - await fastify.listen({ port, host: '0.0.0.0' }) + await fastify.listen({ port, host: "0.0.0.0" }); } catch (err) { - fastify.log.error(err) - process.exit(1) + fastify.log.error(err); + process.exit(1); } diff --git a/server/src/lib/notifications/preferences.js b/server/src/lib/notifications/preferences.js index 0ce256c..e67159f 100644 --- a/server/src/lib/notifications/preferences.js +++ b/server/src/lib/notifications/preferences.js @@ -1,5 +1,5 @@ -import { prisma } from '../prisma.js' -import { NOTIFICATION_EVENTS } from '../../../../shared/constants/notification-events.js' +import { prisma } from "../prisma.js"; +import { NOTIFICATION_EVENTS } from "../../../../shared/constants/notification-events.js"; const { ORDER_CREATED, @@ -8,93 +8,103 @@ const { ORDER_MESSAGE_ADMIN_REPLY, PAYMENT_STATUS_CHANGED, AUTH_CODE_REQUESTED, -} = NOTIFICATION_EVENTS +} = NOTIFICATION_EVENTS; const userEventFieldMap = { - [ORDER_CREATED]: 'orderCreated', - [ORDER_STATUS_CHANGED]: 'orderStatusChanged', - [ORDER_MESSAGE_ADMIN_REPLY]: 'orderMessageReceived', - [PAYMENT_STATUS_CHANGED]: 'paymentStatusChanged', -} + [ORDER_CREATED]: "orderCreated", + [ORDER_STATUS_CHANGED]: "orderStatusChanged", + [ORDER_MESSAGE_ADMIN_REPLY]: "orderMessageReceived", + [PAYMENT_STATUS_CHANGED]: "paymentStatusChanged", +}; const adminEventFieldMap = { - [ORDER_CREATED]: 'newOrder', - [ORDER_MESSAGE_SENT]: 'newOrderMessage', - 'review:created': 'newReview', -} + [ORDER_CREATED]: "newOrder", + [ORDER_MESSAGE_SENT]: "newOrderMessage", + "review:created": "newReview", +}; export async function resolveUserNotificationTargets(eventType, payload) { - const targets = [] + const targets = []; if (payload.userId) { const prefs = await prisma.notificationPreference.findUnique({ where: { userId: payload.userId }, - }) + }); if (prefs && prefs.globalEnabled) { - const field = userEventFieldMap[eventType] + const field = userEventFieldMap[eventType]; if (field && prefs[field]) { - const user = await prisma.user.findUnique({ where: { id: payload.userId }, select: { email: true } }) + const user = await prisma.user.findUnique({ + where: { id: payload.userId }, + select: { email: true }, + }); if (user) { - targets.push({ channel: 'email', recipient: user.email }) + targets.push({ channel: "email", recipient: user.email }); } } } } - return targets + return targets; } export async function resolveAdminNotificationTargets(eventType, payload) { - const targets = [] - const settings = await prisma.adminNotificationSettings.findFirst() - if (!settings) return targets + const targets = []; + const settings = await prisma.adminNotificationSettings.findFirst(); + if (!settings) return targets; - const field = adminEventFieldMap[eventType] - if (field === 'newReview') { - if (!settings.newReview) return targets + const field = adminEventFieldMap[eventType]; + if (field === "newReview") { + if (!settings.newReview) return targets; } else if (field && !settings[field]) { - return targets + return targets; } if (settings.emailEnabled) { const admin = await prisma.user.findFirst({ where: { email: process.env.ADMIN_EMAIL }, select: { email: true }, - }) + }); if (admin) { - targets.push({ channel: 'email', recipient: admin.email }) + targets.push({ channel: "email", recipient: admin.email }); } } if (settings.telegramEnabled && settings.telegramChatId) { - targets.push({ channel: 'telegram', recipient: settings.telegramChatId }) + targets.push({ channel: "telegram", recipient: settings.telegramChatId }); } - return targets + return targets; } export async function resolveAuthCodeTargets(eventType, payload) { - const targets = [] + const targets = []; if (payload.email) { - targets.push({ channel: 'email', recipient: payload.email }) + targets.push({ channel: "email", recipient: payload.email }); } if (payload.isAdmin) { - const settings = await prisma.adminNotificationSettings.findFirst() - if (settings && settings.telegramEnabled && settings.telegramChatId && settings.authCodeDuplicate) { - targets.push({ channel: 'telegram', recipient: settings.telegramChatId }) + const settings = await prisma.adminNotificationSettings.findFirst(); + if ( + settings && + settings.telegramEnabled && + settings.telegramChatId && + settings.authCodeDuplicate + ) { + targets.push({ channel: "telegram", recipient: settings.telegramChatId }); } } - return targets + return targets; } export async function ensureUserNotificationPreference(userId) { - const existing = await prisma.notificationPreference.findUnique({ where: { userId } }) - if (existing) return existing + const existing = await prisma.notificationPreference.findUnique({ + where: { userId }, + }); + if (existing) return existing; return prisma.notificationPreference.create({ data: { userId, globalEnabled: true }, - }) + }); } diff --git a/server/src/lib/notifications/templates/email-templates.js b/server/src/lib/notifications/templates/email-templates.js index 65c5790..7bcfa08 100644 --- a/server/src/lib/notifications/templates/email-templates.js +++ b/server/src/lib/notifications/templates/email-templates.js @@ -8,89 +8,113 @@ function baseLayout(title, body) { ${body}
Craftshop — магазин handmade изделий
+Любимый Креатив — магазин handmade изделий