diff --git a/src/routes/activeSession.ts b/src/routes/activeSession.ts index 8ff8aa7..7b7c598 100644 --- a/src/routes/activeSession.ts +++ b/src/routes/activeSession.ts @@ -1,5 +1,6 @@ import { Router } from "express"; import ActiveSession from "../models/ActiveSession"; +import { createError, ErrorCode } from "../utils/i18n"; // import { isValidObjectId } from "mongoose"; const router = Router(); @@ -8,18 +9,23 @@ router.get("/:id", async (req, res) => { const activeSessionId = req.params.id; // if (!isValidObjectId(activeSessionId)) { - // return res.json({ status: "error", message: "Invalid ObjectId" }); + // return res.json(createError(ErrorCode.INVALID_OBJECT_ID, req)); // } try { const activeSession = await ActiveSession.findById(activeSessionId); + + if (!activeSession) { + return res.json(createError(ErrorCode.SESSION_NOT_FOUND, req)); + } + await ActiveSession.findByIdAndUpdate(activeSessionId, { updatedAt: new Date(), }); res.json(activeSession); } catch (error) { - res.json({ error: (error as Error).message }); + res.json(createError(ErrorCode.SESSION_FETCH_ERROR, req)); } }); diff --git a/src/routes/getCountryCode.ts b/src/routes/getCountryCode.ts index 9158f8a..80ee161 100644 --- a/src/routes/getCountryCode.ts +++ b/src/routes/getCountryCode.ts @@ -1,15 +1,15 @@ import { Router } from "express"; import got from "got-cjs"; +import { createError, ErrorCode } from "../utils/i18n"; const router = Router(); router.get("/", async (req, res) => { const ip = req.headers["x-real-ip"]; - if (!ip) - return res.json({ - error: "An error occurred while obtaining an IP address", - }); + if (!ip) { + return res.json(createError(ErrorCode.IP_ADDRESS_ERROR, req)); + } try { const { countryCode }: { countryCode: string } = await got @@ -18,7 +18,7 @@ router.get("/", async (req, res) => { res.json({ countryCode }); } catch (error) { - res.json({ error: (error as Error).message }); + res.json(createError(ErrorCode.COUNTRY_CODE_FETCH_ERROR, req)); } }); diff --git a/src/routes/sendInvite.ts b/src/routes/sendInvite.ts index 8a178df..351b5be 100644 --- a/src/routes/sendInvite.ts +++ b/src/routes/sendInvite.ts @@ -1,5 +1,6 @@ import { Router } from "express"; import nodemailer from "nodemailer"; +import { createError, ErrorCode, getLanguageFromRequest } from "../utils/i18n"; const router = Router(); @@ -9,6 +10,33 @@ router.post("/", async (req, res) => { console.log("email", email); console.log("link", link); + // Валидация входных данных + if (!email) { + return res.json(createError(ErrorCode.EMAIL_REQUIRED, req)); + } + + if (!link) { + return res.json(createError(ErrorCode.LINK_REQUIRED, req)); + } + + const lang = getLanguageFromRequest(req); + + // Локализованные тексты письма + const emailContent = { + EN: { + subject: "Invitation to demonstration - stream.graff.tech", + body: `
+ Link to connect to the demonstration: ${link} +
`, + }, + RU: { + subject: "Приглашение на демонстрацию - stream.graff.tech", + body: `
+ Ссылка для подключения к демонстрации: ${link} +
`, + }, + }; + // create reusable transporter object using the default SMTP transport let transporter = nodemailer.createTransport({ host: "mail.netangels.ru", @@ -25,16 +53,15 @@ router.post("/", async (req, res) => { await transporter.sendMail({ from: "stream@graff.tech", // sender address to: email, // list of receivers - subject: "Приглашение на демонстрацию - stream.graff.tech", // Subject line - html: `
- Ссылка для подключения к демонстрации: ${link} -
`, + subject: emailContent[lang].subject, // Subject line + html: emailContent[lang].body, }); + + res.json({ ok: 1 }); } catch (error) { console.log("error", (error as Error).message); + res.json(createError(ErrorCode.EMAIL_SEND_ERROR, req)); } - - res.json({ ok: 1 }); }); const sendInviteRouter = router; diff --git a/src/types/errorCodes.ts b/src/types/errorCodes.ts new file mode 100644 index 0000000..1db9367 --- /dev/null +++ b/src/types/errorCodes.ts @@ -0,0 +1,38 @@ +/** + * Коды ошибок API + * Этот файл можно скопировать на клиент для типизации ошибок + */ + +export enum ErrorCode { + // General errors + INTERNAL_ERROR = "INTERNAL_ERROR", + + // Active Session errors + INVALID_OBJECT_ID = "INVALID_OBJECT_ID", + SESSION_NOT_FOUND = "SESSION_NOT_FOUND", + SESSION_FETCH_ERROR = "SESSION_FETCH_ERROR", + + // Country Code errors + IP_ADDRESS_ERROR = "IP_ADDRESS_ERROR", + COUNTRY_CODE_FETCH_ERROR = "COUNTRY_CODE_FETCH_ERROR", + + // Email/Invite errors + EMAIL_REQUIRED = "EMAIL_REQUIRED", + LINK_REQUIRED = "LINK_REQUIRED", + EMAIL_SEND_ERROR = "EMAIL_SEND_ERROR", +} + +/** + * Интерфейс ответа с ошибкой + */ +export interface ErrorResponse { + error: string; + errorCode: ErrorCode; +} + +/** + * Type guard для проверки, является ли ответ ошибкой + */ +export function isErrorResponse(response: any): response is ErrorResponse { + return response && typeof response.error === "string" && typeof response.errorCode === "string"; +} diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts new file mode 100644 index 0000000..31db81b --- /dev/null +++ b/src/utils/i18n.ts @@ -0,0 +1,126 @@ +import { Request } from "express"; +import { ErrorCode, ErrorResponse } from "../types/errorCodes"; + +type Language = "RU" | "EN"; + +interface Translations { + [key: string]: { + EN: string; + RU: string; + }; +} + +const translations: Translations = { + // General errors + INTERNAL_ERROR: { + EN: "Internal server error", + RU: "Внутренняя ошибка сервера", + }, + + // Active Session errors + INVALID_OBJECT_ID: { + EN: "Invalid ObjectId", + RU: "Неверный идентификатор объекта", + }, + SESSION_NOT_FOUND: { + EN: "Session not found", + RU: "Сессия не найдена", + }, + SESSION_FETCH_ERROR: { + EN: "Error fetching session", + RU: "Ошибка получения сессии", + }, + + // Country Code errors + IP_ADDRESS_ERROR: { + EN: "An error occurred while obtaining an IP address", + RU: "Произошла ошибка при получении IP-адреса", + }, + COUNTRY_CODE_FETCH_ERROR: { + EN: "Error fetching country code", + RU: "Ошибка получения кода страны", + }, + + // Email/Invite errors + EMAIL_REQUIRED: { + EN: "Email is required", + RU: "Требуется адрес электронной почты", + }, + LINK_REQUIRED: { + EN: "Link is required", + RU: "Требуется ссылка", + }, + EMAIL_SEND_ERROR: { + EN: "Error sending email", + RU: "Ошибка отправки письма", + }, + EMAIL_SENT_SUCCESS: { + EN: "Invitation sent successfully", + RU: "Приглашение успешно отправлено", + }, +}; + +/** + * Получает язык из заголовка X-User-Region + * @param req - Express Request объект + * @returns Код языка (EN или RU) + */ +export function getLanguageFromRequest(req: Request): Language { + const region = req.headers["x-user-region"] as string; + + // Поддерживаемые русскоязычные регионы + const russianRegions = ["RU", "BY", "KZ", "UA"]; + + if (region && russianRegions.includes(region.toUpperCase())) { + return "RU"; + } + + return "EN"; +} + +/** + * Получает переведенное сообщение + * @param key - Ключ сообщения из translations + * @param req - Express Request объект + * @returns Переведенное сообщение + */ +export function t(key: string, req: Request): string { + const lang = getLanguageFromRequest(req); + + if (translations[key]) { + return translations[key][lang]; + } + + // Возвращаем ключ, если перевод не найден + return key; +} + +/** + * Получает переведенное сообщение для конкретного языка + * @param key - Ключ сообщения из translations + * @param lang - Код языка + * @returns Переведенное сообщение + */ +export function tLang(key: string, lang: Language = "EN"): string { + if (translations[key]) { + return translations[key][lang]; + } + + return key; +} + +/** + * Создает объект ошибки с кодом и переведенным сообщением + * @param errorCode - Код ошибки из ErrorCode enum + * @param req - Express Request объект + * @returns Объект с ошибкой и кодом ошибки + */ +export function createError(errorCode: ErrorCode, req: Request): ErrorResponse { + return { + error: t(errorCode, req), + errorCode: errorCode, + }; +} + +export { ErrorCode } from "../types/errorCodes"; +export default { t, getLanguageFromRequest, tLang, createError };