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 ? 'Georgy' : 'Георгий'}
-
{isEn ? 'Founder & CEO' : 'Основатель и CEO'}
+
{t('about.founder.name')}
+
{t('about.founder.title')}
- {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' ? 'How many GB do you need?' : 'Сколько GB вам нужно?'}
+ {t('tariffs.page.gbQuestion')}
{
-
{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')}