new api endpoint and api rate limit

This commit is contained in:
Georgiy Syralev
2026-01-01 16:55:17 +03:00
parent 4690bdf23e
commit bdb333958a
32 changed files with 884 additions and 377 deletions

View File

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

View File

@@ -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) || 'Ошибка подтверждения' });
}
};

View File

@@ -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 }

View File

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

View File

@@ -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: 'Авторизация временно недоступна. Попробуйте позже.' });
}
};

View File

@@ -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<TurnstileValidationResult> {
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: 'Ошибка при проверке капчи',

View File

@@ -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: 'Ошибка сервера' });
}
};

View File

@@ -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: 'Ошибка удаления изображения'

View File

@@ -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: 'Ошибка сервера' });
}
};

View File

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

View File

@@ -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),

View File

@@ -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: 'Ошибка удаления сессий' });
}
}

View File

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

View File

@@ -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<FileScanResult> {
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, // Не блокируем загрузку при ошибке сканирования
};

View File

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

View File

@@ -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<void> {
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} <url> <access> <secret>`);
}
}
@@ -80,10 +82,10 @@ async function ensureMinioAlias(): Promise<void> {
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<boolean> {
@@ -230,11 +232,11 @@ async function ensureConsoleCredentialSupport(client: any = prisma): Promise<boo
} catch (error) {
// If the error is a MinIO authentication error or bucket not found, surface a clear message and skip cleanup
if ((error as any)?.code === 'MINIO_AUTH_ERROR') {
console.error('[Storage] MinIO authentication error while creating bucket — check MINIO_ACCESS_KEY/MINIO_SECRET_KEY');
logger.error('[Storage] MinIO authentication error while creating bucket — check MINIO_ACCESS_KEY/MINIO_SECRET_KEY');
throw new Error('MinIO authentication failed. Пожалуйста, проверьте настройки MINIO_ACCESS_KEY и MINIO_SECRET_KEY.');
}
if ((error as any)?.code === 'MINIO_BUCKET_NOT_FOUND') {
console.warn('[Storage] MinIO reports bucket inaccessible or not found during create; skipping cleanup');
logger.warn('[Storage] MinIO reports bucket inaccessible or not found during create; skipping cleanup');
throw new Error('MinIO bucket not found or inaccessible. Проверьте доступность MinIO и права доступа.');
}
if (isConsoleCredentialError(error)) {
@@ -380,7 +382,7 @@ function generateConsolePassword(): string {
*/
async function createMinioUser(username: string, password: string): Promise<void> {
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<void
try {
const { stdout, stderr } = await execAsync(createUserCmd, { timeout: 10000 });
console.info(`[MinIO Admin] User ${username} created/updated:`, stdout.trim() || stderr.trim());
logger.info(`[MinIO Admin] User ${username} created/updated:`, stdout.trim() || stderr.trim());
} catch (error: unknown) {
const errorMsg = (error as Record<string, any>)?.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<void
}
} catch (error) {
if (error instanceof Error) {
console.error('[MinIO Admin] Error creating user:', error.message);
logger.error('[MinIO Admin] Error creating user:', error.message);
// Don't throw - this is a non-critical operation
// The credential will still be saved in DB
console.warn(`[MinIO Admin] User creation failed but continuing. User will need to be created manually via mc: mc admin user add minio "${username}" "<password>"`);
logger.warn(`[MinIO Admin] User creation failed but continuing. User will need to be created manually via mc: mc admin user add minio "${username}" "<password>"`);
} else {
throw error;
}
@@ -432,7 +434,7 @@ async function createMinioUser(username: string, password: string): Promise<void
*/
async function createMinioServiceAccount(accessKey: string, secretKey: string, bucketName: string): Promise<void> {
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<string, any>)?.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<string, any>)?.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<BucketWithPlan>
});
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<BucketWithPlan>(async (tx) => {
const createdBucket = await prisma.$transaction<BucketWithPlan>(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<void> {
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<void> {
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<string, any>)?.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);
}
}

View File

@@ -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: 'Ошибка закрытия тикета' });
}
}

View File

@@ -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: 'Ошибка сервера' });
}
};