diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 756cd90..85133d0 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -4,60 +4,68 @@ ## Архитектура и основные компоненты - **Монорепозиторий**: две части — `backend` (Express, TypeScript, Prisma) и `frontend` (React, Vite, TypeScript). - **Backend**: - - Точка входа: `backend/src/index.ts` (Express, маршруты `/api/*`, CORS, логирование). - - Модули: `backend/src/modules/*` — домены (auth, ticket, check, os, server, tariff), каждый экспортирует маршруты и сервисы. - - Интеграция с Proxmox: через API, см. `backend/src/modules/server/proxmoxApi.ts` (создание/управление контейнерами, смена пароля root, статистика). + - Точка входа: `backend/src/index.ts` (Express, маршруты `/api/*`, CORS, логирование, WebSocket). + - Модули: `backend/src/modules/*` — домены (auth, admin, ticket, check, blog, notification, user, session, qr-auth, storage, payment, account, sitemap), каждый экспортирует маршруты и сервисы. + - Интеграция с MinIO: для S3-совместимого хранилища, параметры из `.env`. - ORM: Prisma, схема — `backend/prisma/schema.prisma`, миграции и seed-скрипты — в `backend/prisma/`. - Статические файлы чеков: `backend/uploads/checks` (доступны по `/uploads/checks`). - **Frontend**: - SPA на React + Vite, точка входа: `frontend/src/main.tsx`. - Страницы: `frontend/src/pages/*`, компоненты: `frontend/src/components/*`. - - Авторизация: `frontend/src/context/authcontext.tsx`, `useAuth.ts` (контекст, хуки). + - Авторизация: `frontend/src/context/authcontext.tsx` (контекст, хуки). - Дашборд: `frontend/src/pages/dashboard/mainpage.tsx` — реализует сайдбар, вкладки, загрузку данных пользователя, обработку токена, обновление данных через кастомное событие `userDataUpdate`. + - Локализация: поддержка ru/en через `useTranslation` и `LocaleProvider`. ## Ключевые паттерны и конвенции - **API**: все маршруты backend — с префиксом `/api/`. -- **Модули backend**: каждый домен — отдельная папка, экспортирует маршруты и сервисы (см. пример: `server/proxmoxApi.ts`). -- **Работа с Proxmox**: все операции через функции из `proxmoxApi.ts`, параметры берутся из `.env`. -- **Статусные поля**: для Server, Check, Ticket — строковые статусы (`creating`, `running`, `pending`, `open` и др.). -- **Пароли**: генерируются через `generateSecurePassword` (см. `proxmoxApi.ts`). -- **Описание тарифа**: парсится для выделения ресурсов (ядра, RAM, SSD) при создании контейнера. +- **Модули backend**: каждый домен — отдельная папка, экспортирует маршруты и сервисы. +- **Работа с MinIO**: все операции S3 через MinIO SDK, параметры берутся из `.env`. +- **Статусные поля**: для Check, Ticket, Post, StorageBucket — строковые статусы (`pending`, `approved`, `open`, `published`, `active` и др.). +- **Пароли**: генерируются через `generateSecurePassword` (для консольных учётных данных S3). +- **Описание тарифа**: для StoragePlan — цена за GB, трафик, операции. - **Frontend**: авторизация через контекст, проверка токена, автоматический logout при ошибке 401. -- **Дашборд**: вкладки и права оператора определяются по полю `operator` в userData, обновление данных через событие `userDataUpdate`. +- **Дашборд**: вкладки и права оператора/админа определяются по полям `operator` и `isAdmin` в userData, обновление данных через событие `userDataUpdate`. +- **Уведомления**: email и push через web-push, шаблоны в `notification/email.service.ts`. +- **QR-аутентификация**: временные коды с TTL 60 сек, статусы `pending`, `confirmed`, `expired`. ## Сборка, запуск и workflow - **Backend**: - `npm run dev` — запуск с hot-reload (ts-node-dev). - `npm run build` — компиляция TypeScript. - `npm start` — запуск собранного кода. + - PM2: `npm run pm2:start`, `pm2:restart`, etc. для production. - **Frontend**: - `npm run dev` — запуск Vite dev server. - - `npm run build` — сборка. + - `npm run build` — сборка TypeScript + Vite. - `npm run preview` — предпросмотр production-сборки. - `npm run lint` — проверка ESLint. ## Интеграции и взаимодействие -- **Frontend ↔ Backend**: через REST API (`/api/*`), авторизация через JWT-токен в localStorage. -- **Backend ↔ Proxmox**: через HTTP API, параметры из `.env`. +- **Frontend ↔ Backend**: через REST API (`/api/*`), авторизация через JWT-токен в localStorage, WebSocket для real-time обновлений. +- **Backend ↔ MinIO**: для S3 хранилища, параметры из `.env`. +- **OAuth**: поддержка Google, GitHub, VK, Yandex через Passport.js. +- **Push-уведомления**: через web-push API, подписки в PushSubscription. - **Prisma**: миграции и seed-скрипты — в `backend/prisma/`. ## Внешние зависимости -- **Backend**: express, prisma, axios, bcrypt, jsonwebtoken, multer, dotenv. -- **Frontend**: react, react-dom, react-router-dom, tailwindcss, axios. +- **Backend**: express, prisma, axios, bcrypt, jsonwebtoken, multer, dotenv, minio, web-push, passport, ws. +- **Frontend**: react, react-dom, react-router-dom, tailwindcss, axios, quill, recharts, xterm. ## Примеры ключевых файлов -- `backend/src/index.ts` — точка входа, маршрутизация. -- `backend/src/modules/server/proxmoxApi.ts` — интеграция с Proxmox. -- `backend/prisma/schema.prisma` — схема данных. +- `backend/src/index.ts` — точка входа, маршрутизация, WebSocket. +- `backend/src/modules/storage/storage.service.ts` — интеграция с MinIO. +- `backend/prisma/schema.prisma` — схема данных (User, StorageBucket, Ticket, etc.). - `frontend/src/pages/dashboard/mainpage.tsx` — дашборд, обработка токена, сайдбар, вкладки. -- `frontend/src/context/authcontext.tsx` — авторизация. +- `frontend/src/context/authcontext.tsx` — авторизация, JWT, logout. ## Особенности и conventions -- **CORS**: разрешены только локальные адреса для разработки. -- **Логирование**: каждый запрос логируется с датой и методом. +- **CORS**: разрешены origins из `.env` (PUBLIC_APP_ORIGIN, etc.). +- **Логирование**: каждый запрос логируется с датой и методом через `logger`. - **Статические файлы**: чеки доступны по `/uploads/checks`. -- **Пароли root**: генерируются и меняются через API Proxmox. -- **Frontend**: сайдбар и вкладки строятся динамически, права оператора определяются по userData. +- **Пароли для S3 консоли**: генерируются еженедельно, хэшируются. +- **Frontend**: сайдбар и вкладки строятся динамически, права по userData, локализация через `useTranslation`. +- **Блог**: Rich Text через Quill, статусы `draft`, `published`, `archived`. +- **Тикеты**: автоназначение операторам, внутренние комментарии. --- diff --git a/ospabhost/backend/node_modules.tar.gz b/ospabhost/backend/node_modules.tar.gz deleted file mode 100644 index 915f80f..0000000 Binary files a/ospabhost/backend/node_modules.tar.gz and /dev/null differ diff --git a/ospabhost/backend/package-lock.json b/ospabhost/backend/package-lock.json index a08ba2f..30966d7 100644 --- a/ospabhost/backend/package-lock.json +++ b/ospabhost/backend/package-lock.json @@ -15,9 +15,11 @@ "axios": "^1.12.2", "bcrypt": "^6.0.0", "bcryptjs": "^2.4.3", + "compression": "^1.8.1", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.2", + "express-rate-limit": "^8.2.1", "express-session": "^1.18.2", "helmet": "^8.1.0", "jsonwebtoken": "^9.0.2", @@ -39,6 +41,7 @@ "devDependencies": { "@types/bcrypt": "^6.0.0", "@types/bcryptjs": "^2.4.6", + "@types/compression": "^1.8.1", "@types/cors": "^2.8.17", "@types/express": "^4.17.23", "@types/express-session": "^1.18.2", @@ -1569,6 +1572,17 @@ "@types/node": "*" } }, + "node_modules/@types/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/node": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -1672,6 +1686,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2310,6 +2325,45 @@ "node": ">= 0.8" } }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2668,6 +2722,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -2709,6 +2764,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/express-session": { "version": "1.18.2", "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", @@ -3194,6 +3267,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", @@ -4046,6 +4128,7 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@prisma/config": "6.19.0", "@prisma/engines": "6.19.0" @@ -4847,6 +4930,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/ospabhost/backend/package.json b/ospabhost/backend/package.json index 960bb0b..bff6125 100644 --- a/ospabhost/backend/package.json +++ b/ospabhost/backend/package.json @@ -24,9 +24,11 @@ "axios": "^1.12.2", "bcrypt": "^6.0.0", "bcryptjs": "^2.4.3", + "compression": "^1.8.1", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.2", + "express-rate-limit": "^8.2.1", "express-session": "^1.18.2", "helmet": "^8.1.0", "jsonwebtoken": "^9.0.2", @@ -48,6 +50,7 @@ "devDependencies": { "@types/bcrypt": "^6.0.0", "@types/bcryptjs": "^2.4.6", + "@types/compression": "^1.8.1", "@types/cors": "^2.8.17", "@types/express": "^4.17.23", "@types/express-session": "^1.18.2", diff --git a/ospabhost/backend/src/index.ts b/ospabhost/backend/src/index.ts index 48c6208..76d8d9a 100644 --- a/ospabhost/backend/src/index.ts +++ b/ospabhost/backend/src/index.ts @@ -2,6 +2,9 @@ import express from 'express'; import cors from 'cors'; import dotenv from 'dotenv'; import http from 'http'; +import helmet from 'helmet'; +import rateLimit from 'express-rate-limit'; +import compression from 'compression'; import passport from './modules/auth/passport.config'; import authRoutes from './modules/auth/auth.routes'; import oauthRoutes from './modules/auth/oauth.routes'; @@ -66,6 +69,35 @@ app.use(cors({ allowedHeaders: ['Content-Type', 'Authorization'] })); +// Security headers +app.use(helmet({ + contentSecurityPolicy: false, // Отключаем CSP для совместимости с WebSocket + crossOriginEmbedderPolicy: false +})); + +// Response compression +app.use(compression()); + +// Global rate limiter - 1000 requests per 15 minutes +const globalLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 1000, + message: 'Too many requests, please try again later', + standardHeaders: true, + legacyHeaders: false, +}); + +// Strict rate limiter for auth endpoints - 10 requests per 15 minutes +const authLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 10, + message: 'Too many authentication attempts, please try again later', + standardHeaders: true, + legacyHeaders: false, +}); + +app.use(globalLimiter); + app.use(express.json({ limit: '100mb' })); app.use(express.urlencoded({ limit: '100mb', extended: true })); app.use(passport.initialize()); @@ -91,21 +123,8 @@ app.get('/health', (_req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); -app.get('/', async (req, res) => { - // Статистика WebSocket - const wsConnectedUsers = getConnectedUsersCount(); - const wsRoomsStats = getRoomsStats(); - - res.json({ - message: 'Сервер ospab.host запущен!', - timestamp: new Date().toISOString(), - port: PORT, - database: process.env.DATABASE_URL ? 'подключена' : 'НЕ НАСТРОЕНА', - websocket: { - connected_users: wsConnectedUsers, - rooms: wsRoomsStats - } - }); +app.get('/', (_req, res) => { + res.json({ status: 'active', message: 'ospab backend active' }); }); // ==================== SITEMAP ==================== @@ -306,8 +325,8 @@ app.use('/api/auth', (req, res, next) => { }); next(); }); -app.use('/api/auth', authRoutes); -app.use('/api/auth', oauthRoutes); +app.use('/api/auth', authLimiter, authRoutes); +app.use('/api/auth', authLimiter, oauthRoutes); app.use('/api/admin', adminRoutes); app.use('/api/ticket', ticketRoutes); app.use('/api/check', checkRoutes); @@ -318,7 +337,7 @@ app.use('/api/sessions', sessionRoutes); app.use('/api/qr-auth', qrAuthRoutes); app.use('/api/storage', storageRoutes); -const PORT = process.env.PORT || 5000; +const PORT = parseInt(process.env.PORT || '5000', 10); import { initWebSocketServer, getConnectedUsersCount, getRoomsStats } from './websocket/server'; import https from 'https'; @@ -381,7 +400,7 @@ app.use((err: any, _req: any, res: any, _next: any) => { } }); -server.listen(PORT, () => { +server.listen(PORT, '0.0.0.0', () => { logger.info(`${protocolLabel} сервер запущен на порту ${PORT}`); logger.info(`База данных: ${process.env.DATABASE_URL ? 'подключена' : 'НЕ НАСТРОЕНА'}`); logger.info(`API доступен: ${normalizedApiOrigin}`); diff --git a/ospabhost/backend/src/modules/account/account.controller.ts b/ospabhost/backend/src/modules/account/account.controller.ts index 4f4376a..2063f36 100644 --- a/ospabhost/backend/src/modules/account/account.controller.ts +++ b/ospabhost/backend/src/modules/account/account.controller.ts @@ -1,4 +1,5 @@ import { Request, Response } from 'express'; +import { logger } from '../../utils/logger'; import { requestPasswordChange, confirmPasswordChange, @@ -29,7 +30,7 @@ export const getAccountInfo = async (req: Request, res: Response) => { const userInfo = await getUserInfo(userId); res.json(userInfo); } catch (error) { - console.error('Ошибка получения информации об аккаунте:', error); + logger.error('Ошибка получения информации об аккаунте:', error); res.status(500).json({ error: 'Ошибка сервера' }); } }; @@ -76,7 +77,7 @@ export const requestPasswordChangeHandler = async (req: Request, res: Response) message: 'Код подтверждения отправлен на вашу почту' }); } catch (error: unknown) { - console.error('Ошибка запроса смены пароля:', error); + logger.error('Ошибка запроса смены пароля:', error); res.status(500).json({ error: getErrorMessage(error) || 'Ошибка сервера' }); } }; @@ -104,7 +105,7 @@ export const confirmPasswordChangeHandler = async (req: Request, res: Response) message: 'Пароль успешно изменён' }); } catch (error: unknown) { - console.error('Ошибка подтверждения смены пароля:', error); + logger.error('Ошибка подтверждения смены пароля:', error); res.status(400).json({ error: getErrorMessage(error) || 'Ошибка подтверждения' }); } }; @@ -144,7 +145,7 @@ export const requestUsernameChangeHandler = async (req: Request, res: Response) message: 'Код подтверждения отправлен на вашу почту' }); } catch (error: unknown) { - console.error('Ошибка запроса смены имени:', error); + logger.error('Ошибка запроса смены имени:', error); res.status(400).json({ error: getErrorMessage(error) || 'Ошибка сервера' }); } }; @@ -172,7 +173,7 @@ export const confirmUsernameChangeHandler = async (req: Request, res: Response) message: 'Имя пользователя успешно изменено' }); } catch (error: unknown) { - console.error('Ошибка подтверждения смены имени:', error); + logger.error('Ошибка подтверждения смены имени:', error); res.status(400).json({ error: getErrorMessage(error) || 'Ошибка подтверждения' }); } }; @@ -194,7 +195,7 @@ export const requestAccountDeletionHandler = async (req: Request, res: Response) message: 'Код подтверждения отправлен на вашу почту. После подтверждения аккаунт будет удалён безвозвратно.' }); } catch (error: unknown) { - console.error('Ошибка запроса удаления аккаунта:', error); + logger.error('Ошибка запроса удаления аккаунта:', error); res.status(500).json({ error: getErrorMessage(error) || 'Ошибка сервера' }); } }; @@ -222,7 +223,7 @@ export const confirmAccountDeletionHandler = async (req: Request, res: Response) message: 'Аккаунт успешно удалён' }); } catch (error: unknown) { - console.error('Ошибка подтверждения удаления аккаунта:', error); + logger.error('Ошибка подтверждения удаления аккаунта:', error); res.status(400).json({ error: getErrorMessage(error) || 'Ошибка подтверждения' }); } }; diff --git a/ospabhost/backend/src/modules/account/account.service.ts b/ospabhost/backend/src/modules/account/account.service.ts index 83c5a35..32ae187 100644 --- a/ospabhost/backend/src/modules/account/account.service.ts +++ b/ospabhost/backend/src/modules/account/account.service.ts @@ -1,4 +1,5 @@ import { prisma } from '../../prisma/client'; +import { Prisma } from '@prisma/client'; import bcrypt from 'bcrypt'; import crypto from 'crypto'; import nodemailer from 'nodemailer'; @@ -275,7 +276,7 @@ export async function confirmAccountDeletion(userId: number, code: string): Prom try { // Каскадное удаление всех связанных данных пользователя в правильном порядке - await prisma.$transaction(async (tx) => { + await prisma.$transaction(async (tx: Prisma.TransactionClient) => { // 1. Удаляем ответы в тикетах где пользователь является оператором const responses = await tx.response.deleteMany({ where: { operatorId: userId } diff --git a/ospabhost/backend/src/modules/admin/admin.controller.ts b/ospabhost/backend/src/modules/admin/admin.controller.ts index 0e33b93..78a38f0 100644 --- a/ospabhost/backend/src/modules/admin/admin.controller.ts +++ b/ospabhost/backend/src/modules/admin/admin.controller.ts @@ -1,7 +1,9 @@ import { Request, Response } from 'express'; import { prisma } from '../../prisma/client'; +import { Prisma } from '@prisma/client'; import { createNotification } from '../notification/notification.controller'; import { sendNotificationEmail } from '../notification/email.service'; +import { logger } from '../../utils/logger'; function toNumeric(value: unknown): number { if (typeof value === 'bigint') { @@ -35,7 +37,7 @@ export const requireAdmin = async (req: Request, res: Response, next: any) => { next(); } catch (error) { - console.error('Ошибка проверки прав админа:', error); + logger.error('Ошибка проверки прав админа:', error); res.status(500).json({ message: 'Ошибка сервера' }); } }; @@ -69,7 +71,7 @@ export class AdminController { res.json({ status: 'success', data: users }); } catch (error) { - console.error('Ошибка получения пользователей:', error); + logger.error('Ошибка получения пользователей:', error); res.status(500).json({ message: 'Ошибка получения пользователей' }); } } @@ -118,7 +120,7 @@ export class AdminController { res.json({ status: 'success', data: safeUser }); } catch (error) { - console.error('Ошибка получения данных пользователя:', error); + logger.error('Ошибка получения данных пользователя:', error); res.status(500).json({ message: 'Ошибка получения данных' }); } } @@ -177,7 +179,7 @@ export class AdminController { newBalance: balanceAfter }); } catch (error) { - console.error('Ошибка пополнения баланса:', error); + logger.error('Ошибка пополнения баланса:', error); res.status(500).json({ message: 'Ошибка пополнения баланса' }); } } @@ -240,7 +242,7 @@ export class AdminController { newBalance: balanceAfter }); } catch (error) { - console.error('Ошибка списания средств:', error); + logger.error('Ошибка списания средств:', error); res.status(500).json({ message: 'Ошибка списания средств' }); } } @@ -279,7 +281,7 @@ export class AdminController { message: `Бакет «${bucket.name}» удалён` }); } catch (error) { - console.error('Ошибка удаления бакета:', error); + logger.error('Ошибка удаления бакета:', error); res.status(500).json({ message: 'Ошибка удаления бакета' }); } } @@ -372,7 +374,7 @@ export class AdminController { } }); } catch (error) { - console.error('Ошибка получения статистики:', error); + logger.error('Ошибка получения статистики:', error); res.status(500).json({ message: 'Ошибка получения статистики' }); } } @@ -399,7 +401,7 @@ export class AdminController { message: 'Права пользователя обновлены' }); } catch (error) { - console.error('Ошибка обновления прав:', error); + logger.error('Ошибка обновления прав:', error); res.status(500).json({ message: 'Ошибка обновления прав' }); } } @@ -428,7 +430,7 @@ export class AdminController { return res.status(404).json({ message: 'Пользователь не найден' }); } - await prisma.$transaction(async (tx) => { + await prisma.$transaction(async (tx: Prisma.TransactionClient) => { await tx.ticket.updateMany({ where: { assignedTo: userId }, data: { assignedTo: null } @@ -460,7 +462,7 @@ export class AdminController { message: `Пользователь ${user.username} удалён.` }); } catch (error) { - console.error('Ошибка удаления пользователя администратором:', error); + logger.error('Ошибка удаления пользователя администратором:', error); res.status(500).json({ message: 'Не удалось удалить пользователя' }); } } @@ -479,7 +481,7 @@ export class AdminController { const now = new Date().toISOString(); const logMsg = `[Admin] PUSH-TEST | userId=${user.id} | username=${user.username} | email=${user.email} | time=${now}`; - console.log(logMsg); + logger.info(logMsg); // Здесь должна быть реальная отправка push (имитация) await new Promise(resolve => setTimeout(resolve, 500)); @@ -497,7 +499,7 @@ export class AdminController { } }); } catch (error) { - console.error('[Admin] Ошибка при тестировании push-уведомления:', error); + logger.error('[Admin] Ошибка при тестировании push-уведомления:', error); const message = error instanceof Error ? error.message : 'Неизвестная ошибка'; return res.status(500).json({ error: `Ошибка при тестировании: ${message}` }); } @@ -521,7 +523,7 @@ export class AdminController { const now = new Date().toISOString(); const logMsg = `[Admin] EMAIL-TEST | userId=${user.id} | username=${user.username} | email=${user.email} | time=${now}`; - console.log(logMsg); + logger.info(logMsg); // Отправляем реальное email уведомление const emailResult = await sendNotificationEmail({ @@ -562,7 +564,7 @@ export class AdminController { } }); } catch (error) { - console.error('[Admin] Ошибка при тестировании email-уведомления:', error); + logger.error('[Admin] Ошибка при тестировании email-уведомления:', error); const message = error instanceof Error ? error.message : 'Неизвестная ошибка'; return res.status(500).json({ error: `Ошибка при тестировании: ${message}` }); } diff --git a/ospabhost/backend/src/modules/auth/auth.middleware.ts b/ospabhost/backend/src/modules/auth/auth.middleware.ts index f016d1d..2b08702 100644 --- a/ospabhost/backend/src/modules/auth/auth.middleware.ts +++ b/ospabhost/backend/src/modules/auth/auth.middleware.ts @@ -1,6 +1,7 @@ import { Request, Response, NextFunction } from 'express'; import jwt from 'jsonwebtoken'; import { prisma } from '../../prisma/client'; +import { logger } from '../../utils/logger'; const JWT_SECRET = process.env.JWT_SECRET || 'your_super_secret_key'; export const authMiddleware = async (req: Request, res: Response, next: NextFunction) => { @@ -19,14 +20,14 @@ export const authMiddleware = async (req: Request, res: Response, next: NextFunc const user = await prisma.user.findUnique({ where: { id: decoded.id } }); if (!user) { - console.warn(`[Auth] Пользователь с ID ${decoded.id} не найден, токен отклонён`); + logger.warn(`[Auth] Пользователь с ID ${decoded.id} не найден, токен отклонён`); return res.status(401).json({ message: 'Сессия недействительна. Авторизуйтесь снова.' }); } req.user = user; return next(); } catch (error) { - console.error('Ошибка в мидлваре аутентификации:', error); + logger.error('Ошибка в мидлваре аутентификации:', error); if (error instanceof jwt.JsonWebTokenError) { return res.status(401).json({ message: 'Неверный или просроченный токен.' }); } @@ -65,18 +66,18 @@ export const optionalAuthMiddleware = async (req: Request, res: Response, next: const decoded = jwt.verify(token, JWT_SECRET) as { id: number }; const user = await prisma.user.findUnique({ where: { id: decoded.id } }); if (!user) { - console.warn(`[Auth][optional] Пользователь с ID ${decoded.id} не найден, токен отклонён`); + logger.warn(`[Auth][optional] Пользователь с ID ${decoded.id} не найден, токен отклонён`); return res.status(401).json({ message: 'Сессия недействительна. Авторизуйтесь снова.' }); } req.user = user; } catch (err) { - console.warn('[Auth][optional] Ошибка проверки токена:', err); + logger.warn('[Auth][optional] Ошибка проверки токена:', err); return res.status(401).json({ message: 'Неверный или просроченный токен.' }); } return next(); } catch (error) { - console.error('Ошибка в optionalAuthMiddleware:', error); + logger.error('Ошибка в optionalAuthMiddleware:', error); return res.status(503).json({ message: 'Авторизация временно недоступна. Попробуйте позже.' }); } }; \ No newline at end of file diff --git a/ospabhost/backend/src/modules/auth/turnstile.validator.ts b/ospabhost/backend/src/modules/auth/turnstile.validator.ts index 41787dc..e652144 100644 --- a/ospabhost/backend/src/modules/auth/turnstile.validator.ts +++ b/ospabhost/backend/src/modules/auth/turnstile.validator.ts @@ -1,4 +1,5 @@ import axios from 'axios'; +import { logger } from '../../utils/logger'; const TURNSTILE_SECRET_KEY = process.env.TURNSTILE_SECRET_KEY; const TURNSTILE_VERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'; @@ -20,7 +21,7 @@ export async function validateTurnstileToken( remoteip?: string ): Promise { if (!TURNSTILE_SECRET_KEY) { - console.error('TURNSTILE_SECRET_KEY не найден в переменных окружения'); + logger.error('TURNSTILE_SECRET_KEY не найден в переменных окружения'); return { success: false, message: 'Turnstile не настроен на сервере', @@ -60,7 +61,7 @@ export async function validateTurnstileToken( }; } } catch (error) { - console.error('Ошибка при валидации Turnstile:', error); + logger.error('Ошибка при валидации Turnstile:', error); return { success: false, message: 'Ошибка при проверке капчи', diff --git a/ospabhost/backend/src/modules/blog/blog.controller.ts b/ospabhost/backend/src/modules/blog/blog.controller.ts index cf8f551..af03018 100644 --- a/ospabhost/backend/src/modules/blog/blog.controller.ts +++ b/ospabhost/backend/src/modules/blog/blog.controller.ts @@ -1,5 +1,6 @@ import { Request, Response } from 'express'; import { prisma } from '../../prisma/client'; +import { logger } from '../../utils/logger'; // Получить все опубликованные посты (публичный доступ) export const getAllPosts = async (req: Request, res: Response) => { @@ -19,7 +20,7 @@ export const getAllPosts = async (req: Request, res: Response) => { res.json({ success: true, data: posts }); } catch (error) { - console.error('Ошибка получения постов:', error); + logger.error('Ошибка получения постов:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -59,7 +60,7 @@ export const getPostByUrl = async (req: Request, res: Response) => { res.json({ success: true, data: post }); } catch (error) { - console.error('Ошибка получения поста:', error); + logger.error('Ошибка получения поста:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -105,7 +106,7 @@ export const addComment = async (req: Request, res: Response) => { res.json({ success: true, data: comment, message: 'Комментарий отправлен на модерацию' }); } catch (error) { - console.error('Ошибка добавления комментария:', error); + logger.error('Ошибка добавления комментария:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -129,7 +130,7 @@ export const getAllPostsAdmin = async (req: Request, res: Response) => { res.json({ success: true, data: posts }); } catch (error) { - console.error('Ошибка получения постов:', error); + logger.error('Ошибка получения постов:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -157,7 +158,7 @@ export const getPostByIdAdmin = async (req: Request, res: Response) => { res.json({ success: true, data: post }); } catch (error) { - console.error('Ошибка получения поста:', error); + logger.error('Ошибка получения поста:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -201,7 +202,7 @@ export const createPost = async (req: Request, res: Response) => { res.json({ success: true, data: post }); } catch (error) { - console.error('Ошибка создания поста:', error); + logger.error('Ошибка создания поста:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -244,7 +245,7 @@ export const updatePost = async (req: Request, res: Response) => { res.json({ success: true, data: post }); } catch (error) { - console.error('Ошибка обновления поста:', error); + logger.error('Ошибка обновления поста:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -260,7 +261,7 @@ export const deletePost = async (req: Request, res: Response) => { res.json({ success: true, message: 'Пост удалён' }); } catch (error) { - console.error('Ошибка удаления поста:', error); + logger.error('Ошибка удаления поста:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -282,7 +283,7 @@ export const getAllComments = async (req: Request, res: Response) => { res.json({ success: true, data: comments }); } catch (error) { - console.error('Ошибка получения комментариев:', error); + logger.error('Ошибка получения комментариев:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -304,7 +305,7 @@ export const moderateComment = async (req: Request, res: Response) => { res.json({ success: true, data: comment }); } catch (error) { - console.error('Ошибка модерации комментария:', error); + logger.error('Ошибка модерации комментария:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -320,7 +321,7 @@ export const deleteComment = async (req: Request, res: Response) => { res.json({ success: true, message: 'Комментарий удалён' }); } catch (error) { - console.error('Ошибка удаления комментария:', error); + logger.error('Ошибка удаления комментария:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; diff --git a/ospabhost/backend/src/modules/blog/upload.controller.ts b/ospabhost/backend/src/modules/blog/upload.controller.ts index 2ff6013..d3d3dac 100644 --- a/ospabhost/backend/src/modules/blog/upload.controller.ts +++ b/ospabhost/backend/src/modules/blog/upload.controller.ts @@ -2,6 +2,7 @@ import { Request, Response } from 'express'; import path from 'path'; import fs from 'fs'; +import { logger } from '../../utils/logger'; export const uploadImage = async (req: Request, res: Response) => { try { @@ -23,7 +24,7 @@ export const uploadImage = async (req: Request, res: Response) => { } }); } catch (error) { - console.error('Ошибка загрузки изображения:', error); + logger.error('Ошибка загрузки изображения:', error); return res.status(500).json({ success: false, message: 'Ошибка загрузки изображения' @@ -58,7 +59,7 @@ export const deleteImage = async (req: Request, res: Response) => { }); } } catch (error) { - console.error('Ошибка удаления изображения:', error); + logger.error('Ошибка удаления изображения:', error); return res.status(500).json({ success: false, message: 'Ошибка удаления изображения' diff --git a/ospabhost/backend/src/modules/notification/notification.controller.ts b/ospabhost/backend/src/modules/notification/notification.controller.ts index eb748db..936482f 100644 --- a/ospabhost/backend/src/modules/notification/notification.controller.ts +++ b/ospabhost/backend/src/modules/notification/notification.controller.ts @@ -114,7 +114,7 @@ export const getNotifications = async (req: Request, res: Response) => { } }); } catch (error) { - console.error('Ошибка получения уведомлений:', error); + logger.error('Ошибка получения уведомлений:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -134,7 +134,7 @@ export const getUnreadCount = async (req: Request, res: Response) => { res.json({ success: true, count }); } catch (error) { - console.error('Ошибка подсчета непрочитанных:', error); + logger.error('Ошибка подсчета непрочитанных:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -174,7 +174,7 @@ export const markAsRead = async (req: Request, res: Response) => { res.json({ success: true, message: 'Отмечено как прочитанное' }); } catch (error) { - console.error('Ошибка отметки уведомления:', error); + logger.error('Ошибка отметки уведомления:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -195,7 +195,7 @@ export const markAllAsRead = async (req: Request, res: Response) => { res.json({ success: true, message: 'Все уведомления прочитаны' }); } catch (error) { - console.error('Ошибка отметки всех уведомлений:', error); + logger.error('Ошибка отметки всех уведомлений:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -234,7 +234,7 @@ export const deleteNotification = async (req: Request, res: Response) => { res.json({ success: true, message: 'Уведомление удалено' }); } catch (error) { - console.error('Ошибка удаления уведомления:', error); + logger.error('Ошибка удаления уведомления:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -254,7 +254,7 @@ export const deleteAllRead = async (req: Request, res: Response) => { res.json({ success: true, message: 'Прочитанные уведомления удалены' }); } catch (error) { - console.error('Ошибка удаления прочитанных:', error); + logger.error('Ошибка удаления прочитанных:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -322,7 +322,7 @@ export async function createNotification(params: CreateNotificationParams) { } }); } catch (pushError) { - console.error('Ошибка отправки Push:', pushError); + logger.error('Ошибка отправки Push:', pushError); // Не прерываем выполнение если Push не отправился } } else { @@ -346,7 +346,7 @@ export async function createNotification(params: CreateNotificationParams) { logger.warn(`[Email] Уведомление ${notification.id} пропущено: ${result.message}`); } } catch (emailError) { - console.error('Ошибка отправки email уведомления:', emailError); + logger.error('Ошибка отправки email уведомления:', emailError); } } else if (!email) { logger.debug(`Email уведомление для пользователя ${params.userId} пропущено: отсутствует адрес`); @@ -356,7 +356,7 @@ export async function createNotification(params: CreateNotificationParams) { return notification; } catch (error) { - console.error('Ошибка создания уведомления:', error); + logger.error('Ошибка создания уведомления:', error); throw error; } } @@ -367,7 +367,7 @@ export const getVapidKey = async (req: Request, res: Response) => { const publicKey = getVapidPublicKey(); res.json({ success: true, publicKey }); } catch (error) { - console.error('Ошибка получения VAPID ключа:', error); + logger.error('Ошибка получения VAPID ключа:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -388,7 +388,7 @@ export const subscribe = async (req: Request, res: Response) => { res.json({ success: true, message: 'Push-уведомления подключены' }); } catch (error) { - console.error('Ошибка подписки на Push:', error); + logger.error('Ошибка подписки на Push:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -408,7 +408,7 @@ export const unsubscribe = async (req: Request, res: Response) => { res.json({ success: true, message: 'Push-уведомления отключены' }); } catch (error) { - console.error('Ошибка отписки от Push:', error); + logger.error('Ошибка отписки от Push:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; diff --git a/ospabhost/backend/src/modules/notification/push.service.ts b/ospabhost/backend/src/modules/notification/push.service.ts index 0ba4ba8..6efb5ed 100644 --- a/ospabhost/backend/src/modules/notification/push.service.ts +++ b/ospabhost/backend/src/modules/notification/push.service.ts @@ -1,5 +1,6 @@ import webpush from 'web-push'; import { prisma } from '../../prisma/client'; +import { logger } from '../../utils/logger'; // VAPID ключи (нужно сгенерировать один раз и сохранить в .env) // Для генерации: npx web-push generate-vapid-keys @@ -55,7 +56,7 @@ export async function subscribePush(userId: number, subscription: { return pushSubscription; } catch (error) { - console.error('Ошибка сохранения Push-подписки:', error); + logger.error('Ошибка сохранения Push-подписки:', error); throw error; } } @@ -70,7 +71,7 @@ export async function unsubscribePush(userId: number, endpoint: string) { } }); } catch (error) { - console.error('Ошибка удаления Push-подписки:', error); + logger.error('Ошибка удаления Push-подписки:', error); throw error; } } @@ -130,14 +131,14 @@ export async function sendPushNotification( where: { id: sub.id } }); } else { - console.error(`Ошибка отправки Push на ${sub.endpoint}:`, error); + logger.error(`Ошибка отправки Push на ${sub.endpoint}:`, error); } } }); await Promise.allSettled(promises); } catch (error) { - console.error('Ошибка отправки Push-уведомлений:', error); + logger.error('Ошибка отправки Push-уведомлений:', error); throw error; } } diff --git a/ospabhost/backend/src/modules/qr-auth/qr-auth.controller.ts b/ospabhost/backend/src/modules/qr-auth/qr-auth.controller.ts index a20c2f0..69b683e 100644 --- a/ospabhost/backend/src/modules/qr-auth/qr-auth.controller.ts +++ b/ospabhost/backend/src/modules/qr-auth/qr-auth.controller.ts @@ -32,7 +32,7 @@ export async function createQRLoginRequest(req: Request, res: Response) { }); // Ensure QR creation is visible in production logs: write directly to stdout - console.log('[QR Create] Создан QR-запрос', JSON.stringify({ + logger.info('[QR Create] Создан QR-запрос', JSON.stringify({ code: qrRequest.code, ipAddress: qrRequest.ipAddress, userAgent: qrRequest.userAgent?.toString?.().slice(0, 200), diff --git a/ospabhost/backend/src/modules/session/session.controller.ts b/ospabhost/backend/src/modules/session/session.controller.ts index 3e88fd9..a1f3986 100644 --- a/ospabhost/backend/src/modules/session/session.controller.ts +++ b/ospabhost/backend/src/modules/session/session.controller.ts @@ -81,7 +81,7 @@ export async function getUserSessions(req: Request, res: Response) { res.json(sessionsWithCurrent); } catch (error) { - console.error('Ошибка получения сессий:', error); + logger.error('Ошибка получения сессий:', error); res.status(500).json({ error: 'Ошибка получения сессий' }); } } @@ -109,7 +109,7 @@ export async function deleteSession(req: Request, res: Response) { res.json({ message: 'Сессия удалена' }); } catch (error) { - console.error('Ошибка удаления сессии:', error); + logger.error('Ошибка удаления сессии:', error); res.status(500).json({ error: 'Ошибка удаления сессии' }); } } @@ -136,7 +136,7 @@ export async function deleteAllOtherSessions(req: Request, res: Response) { deletedCount: result.count }); } catch (error) { - console.error('Ошибка удаления сессий:', error); + logger.error('Ошибка удаления сессий:', error); res.status(500).json({ error: 'Ошибка удаления сессий' }); } } diff --git a/ospabhost/backend/src/modules/sitemap/sitemap.controller.ts b/ospabhost/backend/src/modules/sitemap/sitemap.controller.ts index 8b30580..16ad161 100644 --- a/ospabhost/backend/src/modules/sitemap/sitemap.controller.ts +++ b/ospabhost/backend/src/modules/sitemap/sitemap.controller.ts @@ -1,5 +1,6 @@ import { Request, Response } from 'express'; import { prisma } from '../../prisma/client'; +import { logger } from '../../utils/logger'; export async function generateSitemap(req: Request, res: Response) { try { @@ -51,7 +52,7 @@ export async function generateSitemap(req: Request, res: Response) { res.header('Content-Type', 'application/xml'); res.send(xml); } catch (error) { - console.error('Ошибка генерации sitemap:', error); + logger.error('Ошибка генерации sitemap:', error); res.status(500).json({ error: 'Ошибка генерации sitemap' }); } } \ No newline at end of file diff --git a/ospabhost/backend/src/modules/storage/fileScanner.ts b/ospabhost/backend/src/modules/storage/fileScanner.ts index acdc3f5..5094b83 100644 --- a/ospabhost/backend/src/modules/storage/fileScanner.ts +++ b/ospabhost/backend/src/modules/storage/fileScanner.ts @@ -1,4 +1,5 @@ import axios from 'axios'; +import { logger } from '../../utils/logger'; /** * Cloudflare API для проверки файлов @@ -32,7 +33,7 @@ export async function scanFileWithVirusTotal( fileName: string, ): Promise { if (!VIRUSTOTAL_API_KEY) { - console.warn('[FileScanner] VirusTotal API key не настроена'); + logger.warn('[FileScanner] VirusTotal API key не настроена'); return { isSafe: true, detections: 0, @@ -82,7 +83,7 @@ export async function scanFileWithVirusTotal( return uploadFileForAnalysis(fileBuffer, fileName); } - console.error('[FileScanner] Ошибка при проверке по хешу:', hashError); + logger.error('[FileScanner] Ошибка при проверке по хешу:', hashError); throw new Error('Не удалось проверить файл на вирусы'); } } @@ -117,7 +118,7 @@ async function uploadFileForAnalysis( return analysisResult; } catch (error) { - console.error('[FileScanner] Ошибка при загрузке файла на анализ:', error); + logger.error('[FileScanner] Ошибка при загрузке файла на анализ:', error); throw new Error('Не удалось загрузить файл на анализ'); } } @@ -167,7 +168,7 @@ async function waitForAnalysisCompletion( // Ждём перед следующей попыткой await new Promise((resolve) => setTimeout(resolve, 1000)); } catch (error) { - console.error(`[FileScanner] Ошибка при проверке статуса анализа (попытка ${attempt + 1}):`, error); + logger.error(`[FileScanner] Ошибка при проверке статуса анализа (попытка ${attempt + 1}):`, error); } } @@ -260,7 +261,7 @@ export async function validateFileForUpload( }; } catch (error) { // Если сканирование не удалось, позволяем загрузку, но логируем ошибку - console.error('[FileScanner] Ошибка сканирования:', error); + logger.error('[FileScanner] Ошибка сканирования:', error); return { isValid: true, // Не блокируем загрузку при ошибке сканирования }; diff --git a/ospabhost/backend/src/modules/storage/storage.routes.ts b/ospabhost/backend/src/modules/storage/storage.routes.ts index f6c43bc..24b2eeb 100644 --- a/ospabhost/backend/src/modules/storage/storage.routes.ts +++ b/ospabhost/backend/src/modules/storage/storage.routes.ts @@ -1,5 +1,6 @@ import { Router } from 'express'; import axios from 'axios'; +import { logger } from '../../utils/logger'; import { createBucket, listBuckets, @@ -62,7 +63,7 @@ router.put('/plans/:id', authMiddleware, async (req, res) => { return res.json({ success: true, plan: updated }); } catch (error) { - console.error('[Storage] Ошибка обновления тарифа:', error); + logger.error('[Storage] Ошибка обновления тарифа:', error); const message = error instanceof Error ? error.message : 'Не удалось обновить тариф'; return res.status(500).json({ error: message }); } @@ -73,7 +74,7 @@ router.get('/plans', async (_req, res) => { const plans = await listStoragePlans(); return res.json({ plans }); } catch (error) { - console.error('[Storage] Ошибка получения тарифов:', error); + logger.error('[Storage] Ошибка получения тарифов:', error); return res.status(500).json({ error: 'Не удалось загрузить тарифы' }); } }); @@ -83,7 +84,7 @@ router.get('/regions', async (_req, res) => { const regions = await listStorageRegions(); return res.json({ regions }); } catch (error) { - console.error('[Storage] Ошибка получения регионов:', error); + logger.error('[Storage] Ошибка получения регионов:', error); return res.status(500).json({ error: 'Не удалось загрузить список регионов' }); } }); @@ -93,7 +94,7 @@ router.get('/classes', async (_req, res) => { const classes = await listStorageClasses(); return res.json({ classes }); } catch (error) { - console.error('[Storage] Ошибка получения классов хранения:', error); + logger.error('[Storage] Ошибка получения классов хранения:', error); return res.status(500).json({ error: 'Не удалось загрузить список классов хранения' }); } }); @@ -103,7 +104,7 @@ router.get('/status', async (_req, res) => { const status = await getStorageStatus(); return res.json(status); } catch (error) { - console.error('[Storage] Ошибка получения статуса хранилища:', error); + logger.error('[Storage] Ошибка получения статуса хранилища:', error); return res.status(500).json({ error: 'Не удалось получить статус хранилища' }); } }); @@ -128,7 +129,7 @@ router.post('/checkout', optionalAuthMiddleware, async (req, res) => { return res.json(session); } catch (error) { const message = error instanceof Error ? error.message : 'Не удалось создать корзину'; - console.error('[Storage] Ошибка создания корзины:', error); + logger.error('[Storage] Ошибка создания корзины:', error); return res.status(400).json({ error: message }); } }); @@ -323,20 +324,20 @@ router.post('/buckets/:id/objects/download-from-uri', async (req, res) => { const id = Number(req.params.id); const { url } = req.body ?? {}; - console.log(`[Storage URI Download] Начало загрузки - userId: ${userId}, bucketId: ${id}, url: ${url}`); + logger.info(`[Storage URI Download] Начало загрузки - userId: ${userId}, bucketId: ${id}, url: ${url}`); if (!url) { - console.log('[Storage URI Download] Ошибка: URL не указан'); + logger.info('[Storage URI Download] Ошибка: URL не указан'); return res.status(400).json({ error: 'Не указан URL' }); } // Проверяем что пользователь имеет доступ к бакету - console.log('[Storage URI Download] Проверка доступа к бакету...'); + logger.info('[Storage URI Download] Проверка доступа к бакету...'); await getBucket(userId, id); // Проверка доступа - console.log('[Storage URI Download] Доступ к бакету подтверждён'); + logger.info('[Storage URI Download] Доступ к бакету подтверждён'); // Загружаем файл с URL с увеличенным timeout - console.log(`[Storage URI Download] Загрузка файла с ${url}...`); + logger.info(`[Storage URI Download] Загрузка файла с ${url}...`); const response = await axios.get(url, { responseType: 'arraybuffer', timeout: 120000, // 120 seconds (2 minutes) @@ -350,11 +351,11 @@ router.post('/buckets/:id/objects/download-from-uri', async (req, res) => { const buffer = response.data; const bufferSize = Buffer.isBuffer(buffer) ? buffer.length : (buffer as ArrayBuffer).byteLength; - console.log(`[Storage URI Download] Файл загружен успешно - размер: ${bufferSize} байт, mimeType: ${mimeType}`); + logger.info(`[Storage URI Download] Файл загружен успешно - размер: ${bufferSize} байт, mimeType: ${mimeType}`); // Конвертируем в base64 const base64Data = Buffer.isBuffer(buffer) ? buffer.toString('base64') : Buffer.from(buffer).toString('base64'); - console.log(`[Storage URI Download] Base64 длина: ${base64Data.length} символов`); + logger.info(`[Storage URI Download] Base64 длина: ${base64Data.length} символов`); return res.json({ blob: base64Data, @@ -362,10 +363,10 @@ router.post('/buckets/:id/objects/download-from-uri', async (req, res) => { }); } catch (e: unknown) { let message = 'Ошибка загрузки файла по URI'; - console.error('[Storage URI Download] Ошибка:', e); + logger.error('[Storage URI Download] Ошибка:', e); if (axios.isAxiosError(e)) { - console.error('[Storage URI Download] Axios ошибка:', { + logger.error('[Storage URI Download] Axios ошибка:', { status: e.response?.status, statusText: e.response?.statusText, headers: e.response?.headers, @@ -388,7 +389,7 @@ router.post('/buckets/:id/objects/download-from-uri', async (req, res) => { message = e.message; } - console.error('[Storage URI Download] Возвращаем ошибку клиенту:', message); + logger.error('[Storage URI Download] Возвращаем ошибку клиенту:', message); return res.status(400).json({ error: message }); } }); diff --git a/ospabhost/backend/src/modules/storage/storage.service.ts b/ospabhost/backend/src/modules/storage/storage.service.ts index 29f2423..651dbc0 100644 --- a/ospabhost/backend/src/modules/storage/storage.service.ts +++ b/ospabhost/backend/src/modules/storage/storage.service.ts @@ -4,6 +4,8 @@ import axios from 'axios'; import { exec } from 'child_process'; import { promisify } from 'util'; import type { StorageBucket } from '@prisma/client'; +import { Prisma } from '@prisma/client'; +import { logger } from '../../utils/logger'; import { prisma } from '../../prisma/client'; import { ensureBucketExists, buildPhysicalBucketName, minioClient } from './minioClient'; @@ -57,11 +59,11 @@ async function ensureMinioAlias(): Promise { try { // Quick check that alias exists and works await execAsync(`mc admin info ${MINIO_ALIAS}`, { timeout: 10000 }); - console.info(`[MinIO Admin] Using existing alias: ${MINIO_ALIAS}`); + logger.info(`[MinIO Admin] Using existing alias: ${MINIO_ALIAS}`); minioAliasConfigured = true; return; } catch (error) { - console.error(`[MinIO Admin] Existing alias "${MINIO_ALIAS}" not working:`, (error as Error).message); + logger.error(`[MinIO Admin] Existing alias "${MINIO_ALIAS}" not working:`, (error as Error).message); throw new Error(`mc alias "${MINIO_ALIAS}" не настроен или не работает. Настройте вручную: mc alias set ${MINIO_ALIAS} `); } } @@ -80,10 +82,10 @@ async function ensureMinioAlias(): Promise { const escapedSecretKey = escapeShellArg(MINIO_SECRET_KEY); const setupAliasCmd = `mc alias set ${MINIO_ALIAS} "${MINIO_ADMIN_URL}" "${escapedAccessKey}" "${escapedSecretKey}" --api S3v4`; const { stdout, stderr } = await execAsync(setupAliasCmd, { timeout: 10000 }); - console.info(`[MinIO Admin] Alias configured:`, stdout.trim() || stderr.trim() || 'OK'); + logger.info(`[MinIO Admin] Alias configured:`, stdout.trim() || stderr.trim() || 'OK'); minioAliasConfigured = true; } catch (error) { - console.error('[MinIO Admin] Failed to configure alias:', (error as Error).message); + logger.error('[MinIO Admin] Failed to configure alias:', (error as Error).message); throw new Error(`Не удалось настроить подключение к MinIO: ${(error as Error).message}`); } } @@ -205,7 +207,7 @@ function logConsoleWarning(error: unknown) { return; } consoleSupportLogged = true; - console.warn('[Storage] Таблица storage_console_credential недоступна. Продолжаем без данных консоли.', error); + logger.warn('[Storage] Таблица storage_console_credential недоступна. Продолжаем без данных консоли.', error); } async function ensureConsoleCredentialSupport(client: any = prisma): Promise { @@ -230,11 +232,11 @@ async function ensureConsoleCredentialSupport(client: any = prisma): Promise { if (!MINIO_MC_ENABLED) { - console.warn(`[MinIO Admin] mc CLI disabled, skipping user creation for ${username}`); + logger.warn(`[MinIO Admin] mc CLI disabled, skipping user creation for ${username}`); return; } @@ -393,21 +395,21 @@ async function createMinioUser(username: string, password: string): Promise)?.stderr || (error as Error)?.message || ''; // Check if error is because user already exists if (errorMsg.includes('already exists') || errorMsg.includes('exists')) { - console.warn(`[MinIO Admin] User ${username} already exists, updating password`); + logger.warn(`[MinIO Admin] User ${username} already exists, updating password`); // Try to update password try { const changePassCmd = `mc admin user chpass ${MINIO_ALIAS} "${username}" "${password}"`; const { stdout: chpassOut } = await execAsync(changePassCmd, { timeout: 10000 }); - console.info(`[MinIO Admin] Password updated for user ${username}:`, chpassOut.trim()); + logger.info(`[MinIO Admin] Password updated for user ${username}:`, chpassOut.trim()); } catch (changeError: unknown) { - console.error(`[MinIO Admin] Could not update password:`, (changeError as Error)?.message); + logger.error(`[MinIO Admin] Could not update password:`, (changeError as Error)?.message); // Don't throw, user exists anyway } } else { @@ -416,10 +418,10 @@ async function createMinioUser(username: string, password: string): Promise"`); + logger.warn(`[MinIO Admin] User creation failed but continuing. User will need to be created manually via mc: mc admin user add minio "${username}" ""`); } else { throw error; } @@ -432,7 +434,7 @@ async function createMinioUser(username: string, password: string): Promise { if (!MINIO_MC_ENABLED) { - console.warn(`[MinIO Admin] mc CLI disabled, skipping service account creation for ${accessKey}`); + logger.warn(`[MinIO Admin] mc CLI disabled, skipping service account creation for ${accessKey}`); return; } @@ -445,13 +447,13 @@ async function createMinioServiceAccount(accessKey: string, secretKey: string, b try { const { stdout, stderr } = await execAsync(createUserCmd, { timeout: 10000 }); - console.info(`[MinIO Admin] Service account ${accessKey} created:`, stdout.trim() || stderr.trim()); + logger.info(`[MinIO Admin] Service account ${accessKey} created:`, stdout.trim() || stderr.trim()); } catch (error: unknown) { const errorMsg = (error as Record)?.stderr || (error as Error)?.message || ''; if (!errorMsg.includes('already exists') && !errorMsg.includes('exists')) { throw error; } - console.warn(`[MinIO Admin] User ${accessKey} already exists`); + logger.warn(`[MinIO Admin] User ${accessKey} already exists`); } // Create bucket-specific policy JSON @@ -496,21 +498,21 @@ async function createMinioServiceAccount(accessKey: string, secretKey: string, b const addPolicyCmd = `mc admin policy create ${MINIO_ALIAS} "${policyName}" "${policyFile}"`; try { const { stdout } = await execAsync(addPolicyCmd, { timeout: 10000 }); - console.info(`[MinIO Admin] Policy ${policyName} created:`, stdout.trim()); + logger.info(`[MinIO Admin] Policy ${policyName} created:`, stdout.trim()); } catch (policyError: unknown) { const policyErrMsg = (policyError as Record)?.stderr || (policyError as Error)?.message || ''; // Policy might already exist, try to update it if (policyErrMsg.includes('already exists') || policyErrMsg.includes('exists')) { - console.info(`[MinIO Admin] Policy ${policyName} already exists, skipping...`); + logger.info(`[MinIO Admin] Policy ${policyName} already exists, skipping...`); } else { - console.warn(`[MinIO Admin] Policy creation warning:`, policyErrMsg); + logger.warn(`[MinIO Admin] Policy creation warning:`, policyErrMsg); } } // Attach policy to user const attachPolicyCmd = `mc admin policy attach ${MINIO_ALIAS} "${policyName}" --user "${accessKey}"`; const { stdout: attachOut } = await execAsync(attachPolicyCmd, { timeout: 10000 }); - console.info(`[MinIO Admin] Policy ${policyName} attached to user ${accessKey}:`, attachOut.trim()); + logger.info(`[MinIO Admin] Policy ${policyName} attached to user ${accessKey}:`, attachOut.trim()); } finally { // Cleanup temp file try { @@ -520,10 +522,10 @@ async function createMinioServiceAccount(accessKey: string, secretKey: string, b } } - console.info(`[MinIO Admin] Service account ${accessKey} created with access to bucket ${bucketName}`); + logger.info(`[MinIO Admin] Service account ${accessKey} created with access to bucket ${bucketName}`); } catch (error) { if (error instanceof Error) { - console.error('[MinIO Admin] Error creating service account:', error.message); + logger.error('[MinIO Admin] Error creating service account:', error.message); throw new Error(`Не удалось создать ключ доступа в MinIO: ${error.message}`); } throw error; @@ -866,7 +868,7 @@ export async function markCheckoutSessionConsumed(cartId: string) { const session = await checkoutSessionDelegate().findUnique({ where: { id: cartId } }) as CheckoutSessionRecord | null; if (!session) throw new Error('Корзина не найдена'); - await prisma.$transaction(async (tx) => { + await prisma.$transaction(async (tx: Prisma.TransactionClient) => { const checkoutDelegate = (tx as any).storageCheckoutSession; if (checkoutDelegate) { await checkoutDelegate.update({ where: { id: cartId }, data: { consumedAt: new Date() } }); @@ -877,7 +879,7 @@ export async function markCheckoutSessionConsumed(cartId: string) { } }); } catch (error) { - console.warn(`[Storage] Не удалось пометить корзину ${cartId} как использованную`, error); + logger.warn(`[Storage] Не удалось пометить корзину ${cartId} как использованную`, error); } } @@ -1016,7 +1018,7 @@ async function syncBucketUsage(bucket: BucketWithPlan): Promise }); return updated as BucketWithPlan; } catch (error) { - console.error(`[Storage] Не удалось синхронизировать usage для бакета ${bucket.id}`, error); + logger.error(`[Storage] Не удалось синхронизировать usage для бакета ${bucket.id}`, error); return bucket; } } @@ -1051,7 +1053,7 @@ async function applyPublicPolicy(physicalName: string, isPublic: boolean) { await minioClient.setBucketPolicy(physicalName, ''); } } catch (error) { - console.error(`[Storage] Не удалось применить политику для бакета ${physicalName}`, error); + logger.error(`[Storage] Не удалось применить политику для бакета ${physicalName}`, error); } } @@ -1061,7 +1063,7 @@ async function applyVersioning(physicalName: string, enabled: boolean) { Status: enabled ? 'Enabled' : 'Suspended' }); } catch (error) { - console.error(`[Storage] Не удалось обновить версионирование для ${physicalName}`, error); + logger.error(`[Storage] Не удалось обновить версионирование для ${physicalName}`, error); } } @@ -1133,7 +1135,7 @@ export async function createBucket(data: CreateBucketInput) { await ensureBucketExists(physicalName, regionCode); try { - const createdBucket = await prisma.$transaction(async (tx) => { + const createdBucket = await prisma.$transaction(async (tx: Prisma.TransactionClient) => { const reloadedUser = await tx.user.findUnique({ where: { id: data.userId } }); if (!reloadedUser) throw new Error('Пользователь не найден'); if (toPlainNumber(reloadedUser.balance) < planPrice) throw new Error('Недостаточно средств'); @@ -1236,11 +1238,11 @@ export async function createBucket(data: CreateBucketInput) { } catch (cleanupError) { // If cleanup fails due to auth or missing bucket, avoid spamming logs with stack traces if ((cleanupError as any)?.code === 'MINIO_AUTH_ERROR') { - console.error('[Storage] Cleanup skipped due to MinIO authentication error'); + logger.error('[Storage] Cleanup skipped due to MinIO authentication error'); } else if ((cleanupError as any)?.code === 'MINIO_BUCKET_NOT_FOUND') { - console.warn('[Storage] Cleanup skipped, bucket not found'); + logger.warn('[Storage] Cleanup skipped, bucket not found'); } else { - console.error('[Storage] Ошибка очистки бакета после неудачного создания', cleanupError); + logger.error('[Storage] Ошибка очистки бакета после неудачного создания', cleanupError); } } throw error; @@ -1306,11 +1308,11 @@ export async function generateConsoleCredentials(userId: number, id: number) { try { await createMinioUser(login, password); } catch (minioError) { - console.warn('[Storage] MinIO user creation failed, but continuing:', (minioError as Error).message); + logger.warn('[Storage] MinIO user creation failed, but continuing:', (minioError as Error).message); } try { - await prisma.$transaction(async (tx) => { + await prisma.$transaction(async (tx: Prisma.TransactionClient) => { if (!(await ensureConsoleCredentialSupport(tx))) { throw new Error('MinIO Console недоступна. Обратитесь в поддержку.'); } @@ -1343,7 +1345,7 @@ export async function generateConsoleCredentials(userId: number, id: number) { throw error; } - console.info(`[Storage] Пользователь ${userId} сгенерировал данные входа MinIO Console для бакета ${id}`); + logger.info(`[Storage] Пользователь ${userId} сгенерировал данные входа MinIO Console для бакета ${id}`); return { login, @@ -1361,11 +1363,11 @@ export async function deleteBucket(userId: number, id: number, force = false) { keys = await collectObjectKeys(physicalName); } catch (err: unknown) { if ((err as any)?.code === 'MINIO_AUTH_ERROR') { - console.error('[Storage] MinIO authentication error while deleting bucket — aborting deletion'); + logger.error('[Storage] MinIO authentication error while deleting bucket — aborting deletion'); throw new Error('MinIO authentication failed. Пожалуйста, проверьте настройки MINIO_ACCESS_KEY и MINIO_SECRET_KEY.'); } if ((err as any)?.code === 'MINIO_BUCKET_NOT_FOUND') { - console.warn('[Storage] Bucket not found in MinIO while attempting delete; continuing with DB cleanup'); + logger.warn('[Storage] Bucket not found in MinIO while attempting delete; continuing with DB cleanup'); keys = []; } else { throw err; @@ -1384,11 +1386,11 @@ export async function deleteBucket(userId: number, id: number, force = false) { await minioClient.removeObjects(physicalName, chunk); } catch (err: unknown) { if ((err as any)?.code === 'MINIO_AUTH_ERROR') { - console.error('[Storage] MinIO authentication error while deleting objects'); + logger.error('[Storage] MinIO authentication error while deleting objects'); throw new Error('MinIO authentication failed. Пожалуйста, проверьте настройки MINIO_ACCESS_KEY и MINIO_SECRET_KEY.'); } if ((err as any)?.code === 'MINIO_BUCKET_NOT_FOUND') { - console.warn('[Storage] Bucket not found while deleting objects; skipping'); + logger.warn('[Storage] Bucket not found while deleting objects; skipping'); break; } throw err; @@ -1400,11 +1402,11 @@ export async function deleteBucket(userId: number, id: number, force = false) { await minioClient.removeBucket(physicalName); } catch (err: unknown) { if ((err as any)?.code === 'MINIO_AUTH_ERROR') { - console.error('[Storage] MinIO authentication error while removing bucket'); + logger.error('[Storage] MinIO authentication error while removing bucket'); throw new Error('MinIO authentication failed. Пожалуйста, проверьте настройки MINIO_ACCESS_KEY и MINIO_SECRET_KEY.'); } if ((err as any)?.code === 'MINIO_BUCKET_NOT_FOUND') { - console.warn('[Storage] Bucket not found when attempting to remove; continuing with DB cleanup'); + logger.warn('[Storage] Bucket not found when attempting to remove; continuing with DB cleanup'); } else { throw err; } @@ -1663,7 +1665,7 @@ export async function revokeAccessKey(userId: number, id: number, keyId: number) */ async function deleteMinioServiceAccount(accessKey: string): Promise { if (!MINIO_MC_ENABLED) { - console.warn(`[MinIO Admin] mc CLI disabled, skipping service account deletion for ${accessKey}`); + logger.warn(`[MinIO Admin] mc CLI disabled, skipping service account deletion for ${accessKey}`); return; } @@ -1676,17 +1678,17 @@ async function deleteMinioServiceAccount(accessKey: string): Promise { try { const { stdout } = await execAsync(removeUserCmd, { timeout: 10000 }); - console.info(`[MinIO Admin] Service account ${accessKey} removed:`, stdout.trim()); + logger.info(`[MinIO Admin] Service account ${accessKey} removed:`, stdout.trim()); } catch (error: unknown) { const errorMsg = (error as Record)?.stderr || (error as Error)?.message || ''; // User might not exist, that's okay if (!errorMsg.includes('does not exist') && !errorMsg.includes('not found')) { - console.warn(`[MinIO Admin] Warning removing user ${accessKey}:`, errorMsg); + logger.warn(`[MinIO Admin] Warning removing user ${accessKey}:`, errorMsg); } } } catch (error) { // Non-critical - user will be orphaned in MinIO but key removed from DB - console.error('[MinIO Admin] Error deleting service account:', (error as Error).message); + logger.error('[MinIO Admin] Error deleting service account:', (error as Error).message); } } diff --git a/ospabhost/backend/src/modules/ticket/ticket.controller.ts b/ospabhost/backend/src/modules/ticket/ticket.controller.ts index ad40007..57d5108 100644 --- a/ospabhost/backend/src/modules/ticket/ticket.controller.ts +++ b/ospabhost/backend/src/modules/ticket/ticket.controller.ts @@ -3,6 +3,7 @@ import type { Request, Response } from 'express'; import multer from 'multer'; import path from 'path'; import fs from 'fs'; +import { logger } from '../../utils/logger'; interface SerializedUserSummary { id: number; @@ -198,7 +199,7 @@ export async function createTicket(req: Request, res: Response) { return res.json({ ticket: normalizedTicket }); } catch (err) { - console.error('Ошибка создания тикета:', err); + logger.error('Ошибка создания тикета:', err); return res.status(500).json({ error: 'Ошибка создания тикета' }); } } @@ -342,7 +343,7 @@ export async function getTickets(req: Request, res: Response) { stats, }); } catch (err) { - console.error('Ошибка получения тикетов:', err); + logger.error('Ошибка получения тикетов:', err); return res.status(500).json({ error: 'Ошибка получения тикетов' }); } } @@ -409,7 +410,7 @@ export async function getTicketById(req: Request, res: Response) { return res.json({ ticket: normalizedTicket }); } catch (err) { - console.error('Ошибка получения тикета:', err); + logger.error('Ошибка получения тикета:', err); return res.status(500).json({ error: 'Ошибка получения тикета' }); } } @@ -503,7 +504,7 @@ export async function respondTicket(req: Request, res: Response) { assignedTo: updateData.assignedTo ?? ticket.assignedTo ?? null, }); } catch (err) { - console.error('Ошибка ответа на тикет:', err); + logger.error('Ошибка ответа на тикет:', err); return res.status(500).json({ error: 'Ошибка ответа на тикет' }); } } @@ -573,7 +574,7 @@ export async function updateTicketStatus(req: Request, res: Response) { return res.json({ ticket: normalizedTicket }); } catch (err) { - console.error('Ошибка изменения статуса тикета:', err); + logger.error('Ошибка изменения статуса тикета:', err); return res.status(500).json({ error: 'Ошибка изменения статуса тикета' }); } } @@ -644,7 +645,7 @@ export async function assignTicket(req: Request, res: Response) { return res.json({ ticket: normalizedTicket }); } catch (err) { - console.error('Ошибка назначения тикета:', err); + logger.error('Ошибка назначения тикета:', err); return res.status(500).json({ error: 'Ошибка назначения тикета' }); } } @@ -689,7 +690,7 @@ export async function closeTicket(req: Request, res: Response) { return res.json({ success: true, message: 'Тикет закрыт' }); } catch (err) { - console.error('Ошибка закрытия тикета:', err); + logger.error('Ошибка закрытия тикета:', err); return res.status(500).json({ error: 'Ошибка закрытия тикета' }); } } diff --git a/ospabhost/backend/src/modules/user/user.controller.ts b/ospabhost/backend/src/modules/user/user.controller.ts index 064f604..9a26a80 100644 --- a/ospabhost/backend/src/modules/user/user.controller.ts +++ b/ospabhost/backend/src/modules/user/user.controller.ts @@ -2,6 +2,7 @@ import { Request, Response } from 'express'; import { prisma } from '../../prisma/client'; import bcrypt from 'bcrypt'; import crypto from 'crypto'; +import { logger } from '../../utils/logger'; // Получить профиль пользователя (расширенный) export const getProfile = async (req: Request, res: Response) => { @@ -34,7 +35,7 @@ export const getProfile = async (req: Request, res: Response) => { res.json({ success: true, data: userWithoutPassword }); } catch (error: unknown) { - console.error('Ошибка получения профиля:', error); + logger.error('Ошибка получения профиля:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -49,7 +50,8 @@ export const updateProfile = async (req: Request, res: Response) => { // Проверка email на уникальность if (email) { const existingUser = await prisma.user.findFirst({ - where: { email, id: { not: userId } } + where: { email, id: { not: userId } }, + select: { id: true } }); if (existingUser) { return res.status(400).json({ success: false, message: 'Email уже используется' }); @@ -87,7 +89,7 @@ export const updateProfile = async (req: Request, res: Response) => { data: { user: updatedUser, profile } }); } catch (error: unknown) { - console.error('Ошибка обновления профиля:', error); + logger.error('Ошибка обновления профиля:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -108,7 +110,10 @@ export const changePassword = async (req: Request, res: Response) => { } // Проверка текущего пароля - const user = await prisma.user.findUnique({ where: { id: userId } }); + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { id: true, password: true } + }); if (!user) { return res.status(404).json({ success: false, message: 'Пользователь не найден' }); } @@ -132,7 +137,7 @@ export const changePassword = async (req: Request, res: Response) => { res.json({ success: true, message: 'Пароль успешно изменён' }); } catch (error: unknown) { - console.error('Ошибка смены пароля:', error); + logger.error('Ошибка смены пароля:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -162,7 +167,7 @@ export const uploadAvatar = async (req: Request, res: Response) => { data: { avatarUrl } }); } catch (error: unknown) { - console.error('Ошибка загрузки аватара:', error); + logger.error('Ошибка загрузки аватара:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -180,7 +185,7 @@ export const deleteAvatar = async (req: Request, res: Response) => { res.json({ success: true, message: 'Аватар удалён' }); } catch (error: unknown) { - console.error('Ошибка удаления аватара:', error); + logger.error('Ошибка удаления аватара:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -201,7 +206,7 @@ export const getSessions = async (req: Request, res: Response) => { res.json({ success: true, data: sessions }); } catch (error: unknown) { - console.error('Ошибка получения сеансов:', error); + logger.error('Ошибка получения сеансов:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -229,7 +234,7 @@ export const terminateSession = async (req: Request, res: Response) => { res.json({ success: true, message: 'Сеанс завершён' }); } catch (error: unknown) { - console.error('Ошибка завершения сеанса:', error); + logger.error('Ошибка завершения сеанса:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -248,7 +253,7 @@ export const getLoginHistory = async (req: Request, res: Response) => { res.json({ success: true, data: history }); } catch (error: unknown) { - console.error('Ошибка получения истории:', error); + logger.error('Ошибка получения истории:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -277,7 +282,7 @@ export const getAPIKeys = async (req: Request, res: Response) => { res.json({ success: true, data: keys }); } catch (error: unknown) { - console.error('Ошибка получения API ключей:', error); + logger.error('Ошибка получения API ключей:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -314,7 +319,7 @@ export const createAPIKey = async (req: Request, res: Response) => { data: { ...apiKey, fullKey: key } }); } catch (error: unknown) { - console.error('Ошибка создания API ключа:', error); + logger.error('Ошибка создания API ключа:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -340,7 +345,7 @@ export const deleteAPIKey = async (req: Request, res: Response) => { res.json({ success: true, message: 'API ключ удалён' }); } catch (error: unknown) { - console.error('Ошибка удаления API ключа:', error); + logger.error('Ошибка удаления API ключа:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -363,7 +368,7 @@ export const getNotificationSettings = async (req: Request, res: Response) => { res.json({ success: true, data: settings }); } catch (error: unknown) { - console.error('Ошибка получения настроек уведомлений:', error); + logger.error('Ошибка получения настроек уведомлений:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -386,7 +391,7 @@ export const updateNotificationSettings = async (req: Request, res: Response) => data: updated }); } catch (error: unknown) { - console.error('Ошибка обновления настроек уведомлений:', error); + logger.error('Ошибка обновления настроек уведомлений:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; @@ -426,7 +431,7 @@ export const exportUserData = async (req: Request, res: Response) => { exportedAt: new Date().toISOString() }); } catch (error: unknown) { - console.error('Ошибка экспорта данных:', error); + logger.error('Ошибка экспорта данных:', error); res.status(500).json({ success: false, message: 'Ошибка сервера' }); } }; diff --git a/ospabhost/frontend/.env b/ospabhost/frontend/.env index 652454d..b3f533c 100644 --- a/ospabhost/frontend/.env +++ b/ospabhost/frontend/.env @@ -5,4 +5,4 @@ VITE_TURNSTILE_SITE_KEY=0x4AAAAAAB7306voAK0Pjx8O # API URLs (с портом 5000) VITE_API_URL=https://ospab.host:5000 -VITE_SOCKET_URL=wss://ospab.host:5000 \ No newline at end of file +VITE_SOCKET_URL=wss://ospab.host:5000 diff --git a/ospabhost/frontend/package-lock.json b/ospabhost/frontend/package-lock.json index cdc17f4..bbf301c 100644 --- a/ospabhost/frontend/package-lock.json +++ b/ospabhost/frontend/package-lock.json @@ -85,6 +85,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -1589,6 +1590,7 @@ "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1621,6 +1623,7 @@ "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -1688,6 +1691,7 @@ "integrity": "sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/types": "8.44.0", @@ -1940,6 +1944,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2167,6 +2172,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", @@ -2840,6 +2846,7 @@ "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4235,6 +4242,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -4478,6 +4486,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -4490,6 +4499,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -4531,7 +4541,8 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-qr-code": { "version": "2.0.18", @@ -4612,6 +4623,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -4734,7 +4746,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -5309,6 +5322,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -5368,6 +5382,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5492,6 +5507,7 @@ "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -5585,6 +5601,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/ospabhost/frontend/src/App.tsx b/ospabhost/frontend/src/App.tsx index 948cbe1..0d21a72 100644 --- a/ospabhost/frontend/src/App.tsx +++ b/ospabhost/frontend/src/App.tsx @@ -1,27 +1,29 @@ -import { useEffect } from 'react'; +import { useEffect, lazy, Suspense } from 'react'; import { BrowserRouter as Router, Route, Routes, useLocation } from 'react-router-dom'; import Pagetempl from './components/pagetempl'; import DashboardTempl from './components/dashboardtempl'; -import Homepage from './pages/index'; -import Dashboard from './pages/dashboard/mainpage'; -import Loginpage from './pages/login'; -import Registerpage from './pages/register'; -import QRLoginPage from './pages/qr-login'; -import Aboutpage from './pages/about'; -import S3PlansPage from './pages/s3plans'; -import Privacy from './pages/privacy'; -import Terms from './pages/terms'; -import Blog from './pages/blog'; -import BlogPost from './pages/blogpost'; -import NotFound from './pages/404'; -import Unauthorized from './pages/401'; -import Forbidden from './pages/403'; -import ServerError from './pages/500'; -import BadGateway from './pages/502'; -import ServiceUnavailable from './pages/503'; -import GatewayTimeout from './pages/504'; -import ErrorPage from './pages/errors'; -import NetworkError from './pages/errors/NetworkError'; + +// Lazy loading для оптимизации +const Homepage = lazy(() => import('./pages/index')); +const Dashboard = lazy(() => import('./pages/dashboard/mainpage')); +const Loginpage = lazy(() => import('./pages/login')); +const Registerpage = lazy(() => import('./pages/register')); +const QRLoginPage = lazy(() => import('./pages/qr-login')); +const Aboutpage = lazy(() => import('./pages/about')); +const S3PlansPage = lazy(() => import('./pages/s3plans')); +const Privacy = lazy(() => import('./pages/privacy')); +const Terms = lazy(() => import('./pages/terms')); +const Blog = lazy(() => import('./pages/blog')); +const BlogPost = lazy(() => import('./pages/blogpost')); +const NotFound = lazy(() => import('./pages/404')); +const Unauthorized = lazy(() => import('./pages/401')); +const Forbidden = lazy(() => import('./pages/403')); +const ServerError = lazy(() => import('./pages/500')); +const BadGateway = lazy(() => import('./pages/502')); +const ServiceUnavailable = lazy(() => import('./pages/503')); +const GatewayTimeout = lazy(() => import('./pages/504')); +const ErrorPage = lazy(() => import('./pages/errors')); +const NetworkError = lazy(() => import('./pages/errors/NetworkError')); import Privateroute from './components/privateroute'; import { AuthProvider } from './context/authcontext'; import { WebSocketProvider } from './context/WebSocketContext'; @@ -222,6 +224,102 @@ const SEO_CONFIG: Record { const pathname = location.pathname; + // Нормализуем путь, убирая префикс /en для поиска в SEO_CONFIG + let normalizedPath = pathname; + if (pathname.startsWith('/en/')) { + normalizedPath = pathname.slice(3); + } else if (pathname === '/en') { + normalizedPath = '/'; + } + // Получаем SEO данные для текущего маршрута, иначе используем дефолтные - const seoConfig = SEO_CONFIG[pathname]; + const seoConfig = SEO_CONFIG[normalizedPath]; const seoData = seoConfig ? seoConfig[locale] : { - title: locale === 'en' ? 'ospab.host - cloud hosting' : 'ospab.host - облачный хостинг', + title: locale === 'en' ? 'ospab.host - cloud storage' : 'ospab.host - облачное хранилище', description: locale === 'en' - ? 'ospab.host - reliable cloud hosting and virtual machines in Veliky Novgorod.' - : 'ospab.host - надёжный облачный хостинг и виртуальные машины в Великом Новгороде.', + ? 'ospab.host - reliable cloud S3-compatible storage in Veliky Novgorod. File storage, backups, media content.' + : 'ospab.host - надёжное облачное S3-совместимое хранилище в Великом Новгороде. Хранение файлов, резервные копии, медиа-контент.', keywords: locale === 'en' - ? 'hosting, cloud hosting, VPS, VDS' - : 'хостинг, облачный хостинг, VPS, VDS', + ? 'hosting, cloud storage, S3, file storage' + : 'хостинг, облачное хранилище, S3, хранение файлов', }; // Устанавливаем title @@ -277,7 +383,7 @@ function SEOUpdater() { canonicalTag.setAttribute('rel', 'canonical'); document.head.appendChild(canonicalTag); } - canonicalTag.setAttribute('href', `https://ospab.host${pathname}`); + canonicalTag.setAttribute('href', `https://ospab.host${normalizedPath}`); // Open Graph теги if (seoData.og) { @@ -312,9 +418,10 @@ function App() { - - {/* Русские маршруты (без префикса) */} - } /> + Loading...}> + + {/* Русские маршруты (без префикса) */} + } /> } /> } /> } /> @@ -379,6 +486,7 @@ function App() { } /> + diff --git a/ospabhost/frontend/src/config/api.ts b/ospabhost/frontend/src/config/api.ts index 63918d3..64f5bfd 100644 --- a/ospabhost/frontend/src/config/api.ts +++ b/ospabhost/frontend/src/config/api.ts @@ -5,14 +5,6 @@ const PRODUCTION_API_ORIGIN = 'https://api.ospab.host'; const resolveDefaultApiUrl = () => { - if (typeof window === 'undefined') { - return import.meta.env.DEV ? 'http://localhost:5000' : PRODUCTION_API_ORIGIN; - } - - if (import.meta.env.DEV) { - return 'http://localhost:5000'; - } - return PRODUCTION_API_ORIGIN; }; diff --git a/ospabhost/frontend/src/i18n/translations/en.ts b/ospabhost/frontend/src/i18n/translations/en.ts index cad78dd..ec5bbcf 100644 --- a/ospabhost/frontend/src/i18n/translations/en.ts +++ b/ospabhost/frontend/src/i18n/translations/en.ts @@ -61,9 +61,92 @@ export const en: TranslationKeys = { about: { title: 'About Us', subtitle: 'Ospab.host — modern cloud storage platform', + hero: { + title: 'The Story of ospab.host', + subtitle: 'The first data center in Veliky Novgorod.', + }, + founder: { + name: 'Georgy', + title: 'Founder & CEO', + age: '13 years old', + location: 'Veliky Novgorod', + github: 'Project source code', + bio: "At 13, I decided to create something that didn't exist in my city — a modern data center. Starting with learning technologies and working on my first hosting, I'm gradually turning a dream into reality. With the help of an investor friend, we're building the infrastructure of the future for Veliky Novgorod.", + alt: 'Georgy, founder of ospab.host', + }, story: { title: 'Our Story', text: 'We created ospab.host to provide reliable and affordable cloud storage for businesses and developers.', + sections: { + start: { + title: 'September 2025 — The Beginning', + text: "It all started with a simple idea: create a place where anyone can host their project, website, or server with maximum reliability and speed. Veliky Novgorod deserves its own data center, and I decided to take on this task.", + }, + support: { + title: 'Support and Development', + text: "My investor friend believed in the project and helps with infrastructure development. We're building not just a business, but a community where every client is like a friend, and support is always nearby.", + }, + future: { + title: 'Present and Future', + text: "Currently, we're actively working on hosting and preparing infrastructure for the future data center. ospab.host is the first step towards the digital future of Veliky Novgorod, and we're just getting started.", + }, + }, + }, + mission: { + title: 'Our Mission', + subtitle: "Make quality hosting accessible to everyone, and the data center — the city's pride", + features: { + technologies: { + title: 'Modern Technologies', + description: 'We use the latest equipment and software for maximum performance', + }, + security: { + title: 'Data Security', + description: 'Customer data protection is our priority. Regular backups and 24/7 monitoring', + }, + support: { + title: 'Personal Support', + description: 'Every customer receives personal attention and help from the founder', + }, + }, + }, + whyChoose: { + title: 'Why choose ospab.host?', + features: { + first: { + title: 'First data center in the city', + description: "We're making Veliky Novgorod history", + }, + pricing: { + title: 'Affordable pricing', + description: 'Quality hosting for everyone without overpaying', + }, + fastSupport: { + title: 'Fast support', + description: "We'll answer questions anytime", + }, + transparency: { + title: 'Transparency', + description: 'Honest about capabilities and limitations', + }, + infrastructure: { + title: 'Modern infrastructure', + description: 'Up-to-date software and equipment', + }, + dream: { + title: 'A dream becoming reality', + description: 'A story to be proud of', + }, + openSource: { + title: 'Source code on GitHub', + }, + }, + }, + cta: { + title: 'Become part of history', + subtitle: 'Join ospab.host and help create the digital future of Veliky Novgorod', + startFree: 'Start for free', + viewPlans: 'View plans', }, team: { title: 'Our Team', @@ -324,8 +407,13 @@ export const en: TranslationKeys = { title: 'S3 Storage Pricing', subtitle: 'Choose the right plan for your needs', popular: 'Popular', - features: 'Features', - storage: 'Storage', + features: 'Features', baseFeatures: [ + 'S3-compatible API and AWS SDK support', + 'Deployment in ru-central-1 region', + 'Versioning and presigned URLs', + 'Access management via Access Key/Secret Key', + 'Notifications and monitoring in client panel' + ], storage: 'Storage', traffic: 'Outbound Traffic', requests: 'Requests', support: 'Support', @@ -338,5 +426,45 @@ export const en: TranslationKeys = { contactUs: 'Contact Us', customPlan: 'Need a custom plan?', customPlanDescription: 'Contact us to discuss special requirements.', + error: { + loadFailed: 'Failed to load plans', + loadError: 'Error loading plans', + }, + page: { + title: 'Transparent pricing for any volume', + subtitle: 'Pay only for the resources you need. 12 ready-made plans for teams of any size, with included traffic, requests and priority support.', + network: 'network', + api: 'S3-compatible API', + loadReady: 'Load Ready', + loadReadyDesc: 'Infrastructure ready for peak loads up to 10 Gbit/s per server.', + security: 'Security', + securityDesc: 'AES-256 encryption, regular audits and compliance with standards.', + compatibility: 'Compatibility', + compatibilityDesc: 'Full compatibility with AWS S3 API and SDK.', + payAsYouGo: 'Pay as you go', + payAsYouGoDesc: 'Pay only for used resources without hidden fees.', + customPlanTitle: 'Custom Plan', + customPlanDesc: 'Calculate cost for your project', + gb: 'GB', + calculate: 'Calculate', + paymentError: 'Failed to start payment', + creatingCart: 'Creating cart...', + selectPlan: 'Select plan', + customTitle: 'Custom Plan', + customDesc: 'Specify the required amount of GB and get automatic cost calculation', + gbQuestion: 'How many GB do you need?', + useCases: { + backups: 'Backups & DR', + backupsDesc: 'Replication, Object Lock and object lifecycle allow storing backups and quick recovery.', + media: 'Media Platforms', + mediaDesc: 'CDN integration, presigned URLs and high bandwidth for video, images and audio.', + saas: 'SaaS & Data Lake', + saasDesc: 'IAM, API versions and audit logs ensure security and compliance with GDPR, 152-FZ and SOC 2.', + }, + cta: { + title: 'Ready to deploy S3 storage?', + subtitle: 'Create an account and get access to management console, API keys and detailed usage analytics.', + }, + }, }, }; diff --git a/ospabhost/frontend/src/i18n/translations/ru.ts b/ospabhost/frontend/src/i18n/translations/ru.ts index a52ee7f..4041039 100644 --- a/ospabhost/frontend/src/i18n/translations/ru.ts +++ b/ospabhost/frontend/src/i18n/translations/ru.ts @@ -59,9 +59,92 @@ export const ru = { about: { title: 'О компании', subtitle: 'Ospab.host — современная платформа облачного хранилища', + hero: { + title: 'История ospab.host', + subtitle: 'Первый дата-центр в Великом Новгороде.', + }, + founder: { + name: 'Георгий', + title: 'Основатель и CEO', + age: '13 лет', + location: 'Великий Новгород', + github: 'Исходный код проекта', + bio: 'В 13 лет я решил создать то, чего не было в моём городе — современный дата-центр. Начав с изучения технологий и работы над первым хостингом, я постепенно превращаю мечту в реальность. С помощью друга-инвестора мы строим инфраструктуру будущего для Великого Новгорода.', + alt: 'Георгий, основатель ospab.host', + }, story: { title: 'Наша история', text: 'Мы создали ospab.host чтобы предоставить надёжное и доступное облачное хранилище для бизнеса и разработчиков.', + sections: { + start: { + title: 'Сентябрь 2025 — Начало пути', + text: 'Всё началось с простой идеи: создать место, где любой сможет разместить свой проект, сайт или сервер с максимальной надёжностью и скоростью. Великий Новгород заслуживает свой дата-центр, и я решил взяться за эту задачу.', + }, + support: { + title: 'Поддержка и развитие', + text: 'Мой друг-инвестор поверил в проект и помогает с развитием инфраструктуры. Мы строим не просто бизнес, а сообщество, где каждый клиент — как друг, а поддержка всегда рядом.', + }, + future: { + title: 'Настоящее и будущее', + text: 'Сейчас мы активно работаем над хостингом и подготовкой инфраструктуры для будущего ЦОД. ospab.host — это первый шаг к цифровому будущему Великого Новгорода, и мы только начинаем.', + }, + }, + }, + mission: { + title: 'Наша миссия', + subtitle: 'Сделать качественный хостинг доступным для всех, а ЦОД — гордостью города', + features: { + technologies: { + title: 'Современные технологии', + description: 'Используем новейшее оборудование и программное обеспечение для максимальной производительности', + }, + security: { + title: 'Безопасность данных', + description: 'Защита информации клиентов — наш приоритет. Регулярные бэкапы и мониторинг 24/7', + }, + support: { + title: 'Личная поддержка', + description: 'Каждый клиент получает персональное внимание и помощь от основателя', + }, + }, + }, + whyChoose: { + title: 'Почему выбирают ospab.host?', + features: { + first: { + title: 'Первый ЦОД в городе', + description: 'Мы создаём историю Великого Новгорода', + }, + pricing: { + title: 'Доступные тарифы', + description: 'Качественный хостинг для всех без переплат', + }, + fastSupport: { + title: 'Быстрая поддержка', + description: 'Ответим на вопросы в любое время', + }, + transparency: { + title: 'Прозрачность', + description: 'Честно о возможностях и ограничениях', + }, + infrastructure: { + title: 'Современная инфраструктура', + description: 'Актуальное ПО и оборудование', + }, + dream: { + title: 'Мечта становится реальностью', + description: 'История, которой можно гордиться', + }, + openSource: { + title: 'Исходный код на GitHub', + }, + }, + }, + cta: { + title: 'Станьте частью истории', + subtitle: 'Присоединяйтесь к ospab.host и помогите создать цифровое будущее Великого Новгорода', + startFree: 'Начать бесплатно', + viewPlans: 'Посмотреть тарифы', }, team: { title: 'Наша команда', @@ -323,6 +406,13 @@ export const ru = { subtitle: 'Выберите подходящий план для ваших задач', popular: 'Популярный', features: 'Возможности', + baseFeatures: [ + 'S3-совместимый API и совместимость с AWS SDK', + 'Развёртывание в регионе ru-central-1', + 'Версионирование и presigned URL', + 'Управление доступом через Access Key/Secret Key', + 'Уведомления и мониторинг в панели клиента' + ], storage: 'Хранилище', traffic: 'Исходящий трафик', requests: 'Запросов', @@ -336,6 +426,46 @@ export const ru = { contactUs: 'Связаться с нами', customPlan: 'Нужен индивидуальный план?', customPlanDescription: 'Свяжитесь с нами для обсуждения особых условий.', + error: { + loadFailed: 'Не удалось загрузить тарифы', + loadError: 'Ошибка загрузки тарифов', + }, + page: { + title: 'Прозрачные тарифы для любого объёма', + subtitle: 'Оплачивайте только за необходимые ресурсы. 12 готовых тарифов для команд любого размера, с включённым трафиком, запросами и приоритетной поддержкой.', + network: 'сеть', + api: 'S3-совместимый API', + loadReady: 'Готовность к нагрузке', + loadReadyDesc: 'Инфраструктура готова к пиковым нагрузкам до 10 Гбит/с на сервер.', + security: 'Безопасность', + securityDesc: 'Шифрование AES-256, регулярные аудиты и соответствие стандартам.', + compatibility: 'Совместимость', + compatibilityDesc: 'Полная совместимость с AWS S3 API и SDK.', + payAsYouGo: 'Оплата по факту', + payAsYouGoDesc: 'Оплачивайте только за использованные ресурсы без скрытых платежей.', + customPlanTitle: 'Индивидуальный тариф', + customPlanDesc: 'Рассчитайте стоимость для вашего проекта', + gb: 'ГБ', + calculate: 'Рассчитать', + paymentError: 'Не удалось начать оплату', + creatingCart: 'Создание корзины...', + selectPlan: 'Выбрать план', + customTitle: 'Кастомный тариф', + customDesc: 'Укажите нужное количество GB и получите автоматический расчёт стоимости', + gbQuestion: 'Сколько GB вам нужно?', + useCases: { + backups: 'Бэкапы и DR', + backupsDesc: 'Репликация, Object Lock и цикл жизни объектов позволяют хранить резервные копии и быстро восстанавливаться.', + media: 'Медиа-платформы', + mediaDesc: 'CDN-интеграция, presigned URL и высокая пропускная способность для видео, изображений и аудио.', + saas: 'SaaS & Data Lake', + saasDesc: 'IAM, версии API и аудит логов обеспечивают безопасность и соответствие требованиям GDPR, 152-ФЗ и SOC 2.', + }, + cta: { + title: 'Готовы развернуть S3 хранилище?', + subtitle: 'Создайте аккаунт и получите доступ к консоли управления, API ключам и детальной аналитике использования.', + }, + }, }, }; diff --git a/ospabhost/frontend/src/i18n/useTranslation.ts b/ospabhost/frontend/src/i18n/useTranslation.ts index 2b9b7ad..b9130e3 100644 --- a/ospabhost/frontend/src/i18n/useTranslation.ts +++ b/ospabhost/frontend/src/i18n/useTranslation.ts @@ -24,7 +24,7 @@ type TranslationKey = NestedKeyOf; /** * Получить значение по вложенному ключу */ -function getNestedValue(obj: Record, path: string): string { +function getNestedValue(obj: Record, path: string): any { const keys = path.split('.'); let current: unknown = obj; @@ -36,7 +36,7 @@ function getNestedValue(obj: Record, path: string): string { } } - return typeof current === 'string' ? current : path; + return current; } /** @@ -46,18 +46,20 @@ export function useTranslation() { const { locale, setLocale } = useLocale(); const t = useCallback( - (key: TranslationKey, params?: Record): string => { + (key: TranslationKey, params?: Record): any => { const translation = getNestedValue( translations[locale] as unknown as Record, key ); - if (!params) return translation; + if (typeof translation === 'string' && params) { + // Замена параметров {{param}} + return translation.replace(/\{\{(\w+)\}\}/g, (_, paramKey) => { + return params[paramKey]?.toString() ?? `{{${paramKey}}}`; + }); + } - // Замена параметров {{param}} - return translation.replace(/\{\{(\w+)\}\}/g, (_, paramKey) => { - return params[paramKey]?.toString() ?? `{{${paramKey}}}`; - }); + return translation ?? key; }, [locale] ); diff --git a/ospabhost/frontend/src/pages/about.tsx b/ospabhost/frontend/src/pages/about.tsx index 9a9731c..5df1b51 100644 --- a/ospabhost/frontend/src/pages/about.tsx +++ b/ospabhost/frontend/src/pages/about.tsx @@ -3,9 +3,8 @@ import { useTranslation } from '../i18n'; import { useLocalePath } from '../middleware'; const AboutPage = () => { - const { locale } = useTranslation(); + const { t } = useTranslation(); const localePath = useLocalePath(); - const isEn = locale === 'en'; return (
@@ -19,10 +18,10 @@ const AboutPage = () => {

- {isEn ? 'The Story of ospab.host' : 'История ospab.host'} + {t('about.hero.title')}

- {isEn ? 'The first data center in Veliky Novgorod.' : 'Первый дата-центр в Великом Новгороде.'} + {t('about.hero.subtitle')}

@@ -36,7 +35,7 @@ const AboutPage = () => {
{isEn {
-

{isEn ? 'Georgy' : 'Георгий'}

-

{isEn ? 'Founder & CEO' : 'Основатель и CEO'}

+

{t('about.founder.name')}

+

{t('about.founder.title')}

- {isEn ? '13 years old' : '13 лет'} + {t('about.founder.age')} - {isEn ? 'Veliky Novgorod' : 'Великий Новгород'} + {t('about.founder.location')} GitHub @@ -70,9 +69,7 @@ const AboutPage = () => {

- {isEn - ? "At 13, I decided to create something that didn't exist in my city — a modern data center. Starting with learning technologies and working on my first hosting, I'm gradually turning a dream into reality. With the help of an investor friend, we're building the infrastructure of the future for Veliky Novgorod." - : 'В 13 лет я решил создать то, чего не было в моём городе — современный дата-центр. Начав с изучения технологий и работы над первым хостингом, я постепенно превращаю мечту в реальность. С помощью друга-инвестора мы строим инфраструктуру будущего для Великого Новгорода.'} + {t('about.founder.bio')}

@@ -84,43 +81,37 @@ const AboutPage = () => {

- {isEn ? 'Our Story' : 'Наша история'} + {t('about.story.title')}

- {isEn ? 'September 2025 — The Beginning' : 'Сентябрь 2025 — Начало пути'} + {t('about.story.sections.start.title')}

- {isEn - ? "It all started with a simple idea: create a place where anyone can host their project, website, or server with maximum reliability and speed. Veliky Novgorod deserves its own data center, and I decided to take on this task." - : 'Всё началось с простой идеи: создать место, где любой сможет разместить свой проект, сайт или сервер с максимальной надёжностью и скоростью. Великий Новгород заслуживает свой дата-центр, и я решил взяться за эту задачу.'} + {t('about.story.sections.start.text')}

- {isEn ? 'Support and Development' : 'Поддержка и развитие'} + {t('about.story.sections.support.title')}

- {isEn - ? "My investor friend believed in the project and helps with infrastructure development. We're building not just a business, but a community where every client is like a friend, and support is always nearby." - : 'Мой друг-инвестор поверил в проект и помогает с развитием инфраструктуры. Мы строим не просто бизнес, а сообщество, где каждый клиент — как друг, а поддержка всегда рядом.'} + {t('about.story.sections.support.text')}

- {isEn ? 'Present and Future' : 'Настоящее и будущее'} + {t('about.story.sections.future.title')}

- {isEn - ? "Currently, we're actively working on hosting and preparing infrastructure for the future data center. ospab.host is the first step towards the digital future of Veliky Novgorod, and we're just getting started." - : 'Сейчас мы активно работаем над хостингом и подготовкой инфраструктуры для будущего ЦОД. ospab.host — это первый шаг к цифровому будущему Великого Новгорода, и мы только начинаем.'} + {t('about.story.sections.future.text')}

@@ -132,12 +123,10 @@ const AboutPage = () => {

- {isEn ? 'Our Mission' : 'Наша миссия'} + {t('about.mission.title')}

- {isEn - ? "Make quality hosting accessible to everyone, and the data center — the city's pride" - : 'Сделать качественный хостинг доступным для всех, а ЦОД — гордостью города'} + {t('about.mission.subtitle')}

@@ -146,11 +135,9 @@ const AboutPage = () => {
-

{isEn ? 'Modern Technologies' : 'Современные технологии'}

+

{t('about.mission.features.technologies.title')}

- {isEn - ? 'We use the latest equipment and software for maximum performance' - : 'Используем новейшее оборудование и программное обеспечение для максимальной производительности'} + {t('about.mission.features.technologies.description')}

@@ -158,11 +145,9 @@ const AboutPage = () => {
-

{isEn ? 'Data Security' : 'Безопасность данных'}

+

{t('about.mission.features.security.title')}

- {isEn - ? 'Customer data protection is our priority. Regular backups and 24/7 monitoring' - : 'Защита информации клиентов — наш приоритет. Регулярные бэкапы и мониторинг 24/7'} + {t('about.mission.features.security.description')}

@@ -170,11 +155,9 @@ const AboutPage = () => {
-

{isEn ? 'Personal Support' : 'Личная поддержка'}

+

{t('about.mission.features.support.title')}

- {isEn - ? 'Every customer receives personal attention and help from the founder' - : 'Каждый клиент получает персональное внимание и помощь от основателя'} + {t('about.mission.features.support.description')}

@@ -186,7 +169,7 @@ const AboutPage = () => {

- {isEn ? 'Why choose ospab.host?' : 'Почему выбирают ospab.host?'} + {t('about.whyChoose.title')}

@@ -195,8 +178,8 @@ const AboutPage = () => {
-

{isEn ? 'First data center in the city' : 'Первый ЦОД в городе'}

-

{isEn ? "We're making Veliky Novgorod history" : 'Мы создаём историю Великого Новгорода'}

+

{t('about.whyChoose.features.first.title')}

+

{t('about.whyChoose.features.first.description')}

@@ -205,8 +188,8 @@ const AboutPage = () => {
-

{isEn ? 'Affordable pricing' : 'Доступные тарифы'}

-

{isEn ? 'Quality hosting for everyone without overpaying' : 'Качественный хостинг для всех без переплат'}

+

{t('about.whyChoose.features.pricing.title')}

+

{t('about.whyChoose.features.pricing.description')}

@@ -215,8 +198,8 @@ const AboutPage = () => {
-

{isEn ? 'Fast support' : 'Быстрая поддержка'}

-

{isEn ? "We'll answer questions anytime" : 'Ответим на вопросы в любое время'}

+

{t('about.whyChoose.features.fastSupport.title')}

+

{t('about.whyChoose.features.fastSupport.description')}

@@ -225,8 +208,8 @@ const AboutPage = () => {
-

{isEn ? 'Transparency' : 'Прозрачность'}

-

{isEn ? 'Honest about capabilities and limitations' : 'Честно о возможностях и ограничениях'}

+

{t('about.whyChoose.features.transparency.title')}

+

{t('about.whyChoose.features.transparency.description')}

@@ -235,8 +218,8 @@ const AboutPage = () => {
-

{isEn ? 'Modern infrastructure' : 'Современная инфраструктура'}

-

{isEn ? 'Up-to-date software and equipment' : 'Актуальное ПО и оборудование'}

+

{t('about.whyChoose.features.infrastructure.title')}

+

{t('about.whyChoose.features.infrastructure.description')}

@@ -245,8 +228,8 @@ const AboutPage = () => {
-

{isEn ? 'A dream becoming reality' : 'Мечта становится реальностью'}

-

{isEn ? 'A story to be proud of' : 'История, которой можно гордиться'}

+

{t('about.whyChoose.features.dream.title')}

+

{t('about.whyChoose.features.dream.description')}

@@ -263,7 +246,7 @@ const AboutPage = () => { rel="noopener noreferrer" className="hover:underline" > - {isEn ? 'Source code on GitHub' : 'Исходный код на GitHub'} + {t('about.whyChoose.features.openSource.title')}

@@ -277,25 +260,23 @@ const AboutPage = () => {

- {isEn ? 'Become part of history' : 'Станьте частью истории'} + {t('about.cta.title')}

- {isEn - ? 'Join ospab.host and help create the digital future of Veliky Novgorod' - : 'Присоединяйтесь к ospab.host и помогите создать цифровое будущее Великого Новгорода'} + {t('about.cta.subtitle')}

diff --git a/ospabhost/frontend/src/pages/s3plans.tsx b/ospabhost/frontend/src/pages/s3plans.tsx index 0d837e2..ceb0512 100644 --- a/ospabhost/frontend/src/pages/s3plans.tsx +++ b/ospabhost/frontend/src/pages/s3plans.tsx @@ -47,19 +47,7 @@ const S3PlansPage = () => { const [error, setError] = useState(null); const [selectingPlan, setSelectingPlan] = useState(null); - const BASE_FEATURES = locale === 'en' ? [ - 'S3-compatible API and AWS SDK support', - 'Deployment in ru-central-1 region', - 'Versioning and presigned URLs', - 'Access management via Access Key/Secret Key', - 'Notifications and monitoring in client panel' - ] : [ - 'S3-совместимый API и совместимость с AWS SDK', - 'Развёртывание в регионе ru-central-1', - 'Версионирование и presigned URL', - 'Управление доступом через Access Key/Secret Key', - 'Уведомления и мониторинг в панели клиента' - ]; + const BASE_FEATURES = t('tariffs.baseFeatures') as string[]; const formatMetric = (value: number, suffix: string) => `${value.toLocaleString(locale === 'en' ? 'en-US' : 'ru-RU')} ${suffix}`; @@ -73,14 +61,14 @@ const S3PlansPage = () => { setError(null); const response = await fetch(`${API_URL}/api/storage/plans`); if (!response.ok) { - throw new Error(locale === 'en' ? 'Failed to load plans' : 'Не удалось загрузить тарифы'); + throw new Error(t('tariffs.error.loadFailed')); } const data = await response.json(); if (!cancelled) { setPlans(Array.isArray(data?.plans) ? data.plans : []); } } catch (err) { - const message = err instanceof Error ? err.message : (locale === 'en' ? 'Error loading plans' : 'Ошибка загрузки тарифов'); + const message = err instanceof Error ? err.message : t('tariffs.error.loadError'); if (!cancelled) { setError(message); } @@ -164,7 +152,7 @@ const S3PlansPage = () => { } navigate(localePath(`/dashboard/checkout?cart=${encodeURIComponent(cartId)}`)); } catch (err) { - const message = err instanceof Error ? err.message : (locale === 'en' ? 'Failed to start payment' : 'Не удалось начать оплату'); + const message = err instanceof Error ? err.message : t('tariffs.page.paymentError'); setError(message); } finally { setSelectingPlan(null); @@ -180,22 +168,20 @@ const S3PlansPage = () => { S3 Object Storage

- {locale === 'en' ? 'Transparent pricing for any volume' : 'Прозрачные тарифы для любого объёма'} + {t('tariffs.page.title')}

- {locale === 'en' - ? 'Pay only for the resources you need. 12 ready-made plans for teams of any size, with included traffic, requests and priority support.' - : 'Оплачивайте только за необходимые ресурсы. 12 готовых тарифов для команд любого размера, с включённым трафиком, запросами и приоритетной поддержкой.'} + {t('tariffs.page.subtitle')}

- NVMe + 10Gb/s {locale === 'en' ? 'network' : 'сеть'} + NVMe + 10Gb/s {t('tariffs.page.network')} AES-256 at-rest - {locale === 'en' ? 'S3-compatible API' : 'S3-совместимый API'} + {t('tariffs.page.api')}
@@ -208,33 +194,27 @@ const S3PlansPage = () => {
-

{locale === 'en' ? 'Load Ready' : 'Готовность к нагрузке'}

+

{t('tariffs.page.loadReady')}

- {locale === 'en' - ? 'Unified NVMe platform with auto-scaling, CDN integration and cross-region replication.' - : 'Единая платформа на NVMe с автоматическим масштабированием, CDN-интеграцией и кросс-региональной репликацией.'} + {t('tariffs.page.loadReadyDesc')}

-

{locale === 'en' ? 'Security by Default' : 'Безопасность по умолчанию'}

+

{t('tariffs.page.security')}

- {locale === 'en' - ? '3 data copies, IAM roles, in-transit and at-rest encryption, audit logs, Object Lock and retention policies.' - : '3 копии данных, IAM роли, шифрование in-transit и at-rest, audit логи, Object Lock и политики хранения.'} + {t('tariffs.page.securityDesc')}

-

{locale === 'en' ? 'AWS SDK Compatibility' : 'Совместимость с AWS SDK'}

+

{t('tariffs.page.compatibility')}

- {locale === 'en' - ? 'Full S3 API, support for AWS CLI, Terraform, rclone, s3cmd and other tools without code changes.' - : 'Полный S3 API, поддержка AWS CLI, Terraform, rclone, s3cmd и других инструментов без изменений в коде.'} + {t('tariffs.page.compatibilityDesc')}

@@ -325,11 +305,11 @@ const S3PlansPage = () => { {selectingPlan === plan.code ? ( <> - {locale === 'en' ? 'Creating cart...' : 'Создание корзины...'} + {t('tariffs.page.creatingCart')} ) : ( <> - {locale === 'en' ? 'Select plan' : 'Выбрать план'} + {t('tariffs.page.selectPlan')} )} @@ -345,8 +325,8 @@ const S3PlansPage = () => { {customPlan && customPlanCalculated && (
-

{locale === 'en' ? 'Custom Plan' : 'Кастомный тариф'}

-

{locale === 'en' ? 'Specify the required amount of GB and get automatic cost calculation' : 'Укажите нужное количество GB и получите автоматический расчёт стоимости'}

+

{t('tariffs.page.customTitle')}

+

{t('tariffs.page.customDesc')}

@@ -354,7 +334,7 @@ const S3PlansPage = () => { {/* Input */}
{
-

{locale === 'en' ? 'Backups & DR' : 'Бэкапы и DR'}

+

{t('tariffs.page.useCases.backups')}

- {locale === 'en' - ? 'Replication, Object Lock and object lifecycle allow storing backups and quick recovery.' - : 'Репликация, Object Lock и цикл жизни объектов позволяют хранить резервные копии и быстро восстанавливаться.'} + {t('tariffs.page.useCases.backupsDesc')}

-

{locale === 'en' ? 'Media Platforms' : 'Медиа-платформы'}

+

{t('tariffs.page.useCases.media')}

- {locale === 'en' - ? 'CDN integration, presigned URLs and high bandwidth for video, images and audio.' - : 'CDN-интеграция, presigned URL и высокая пропускная способность для видео, изображений и аудио.'} + {t('tariffs.page.useCases.mediaDesc')}

SaaS & Data Lake

- {locale === 'en' - ? 'IAM, API versions and audit logs ensure security and compliance with GDPR, 152-FZ and SOC 2.' - : 'IAM, версии API и аудит логов обеспечивают безопасность и соответствие требованиям GDPR, 152-ФЗ и SOC 2.'} + {t('tariffs.page.useCases.saasDesc')}

@@ -495,11 +469,9 @@ const S3PlansPage = () => {
-

{locale === 'en' ? 'Ready to deploy S3 storage?' : 'Готовы развернуть S3 хранилище?'}

+

{t('tariffs.page.cta.title')}

- {locale === 'en' - ? 'Create an account and get access to management console, API keys and detailed usage analytics.' - : 'Создайте аккаунт и получите доступ к консоли управления, API ключам и детальной аналитике использования.'} + {t('tariffs.page.cta.subtitle')}