initial commit

This commit is contained in:
root
2026-05-13 14:20:41 +00:00
commit 6e178d2012
6022 changed files with 399872 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
import { env } from '../config/env.js';
import { verifyAccessToken } from '../services/token.service.js';
export async function authRequired(req, res, next) {
try {
let token = req.cookies?.[env.COOKIE_ACCESS_NAME];
// Если нет cookie — пробуем Authorization: Bearer <token>
if (!token) {
const auth = req.headers['authorization'];
if (auth && auth.startsWith('Bearer ')) {
token = auth.slice(7);
}
}
if (!token) {
return res.status(401).json({ error: 'UNAUTHORIZED', message: 'Access token is missing' });
}
const payload = await verifyAccessToken(token);
req.user = {
id: payload.sub,
role: payload.role,
email: payload.email,
sessionId: payload.sid,
};
next();
} catch (err) {
return res.status(401).json({ error: 'UNAUTHORIZED', message: 'Invalid access token' });
}
}
+28
View File
@@ -0,0 +1,28 @@
import { env } from '../config/env.js';
export function csrfRequired(req, res, next) {
const cookieToken = req.cookies?.[env.COOKIE_CSRF_NAME];
const headerToken = req.get('x-csrf-token');
// Для SameSite cookie защита уже встроена в браузер
// Если cookie есть и SameSite установлен - это уже защита от CSRF
// Дополнительная проверка заголовка для обратной совместимости
if (cookieToken) {
// Cookie с SameSite=Lax/Strict уже защищает от CSRF
// Если заголовок есть - проверяем совпадение (double submit pattern)
if (headerToken && cookieToken !== headerToken) {
return res.status(403).json({
error: 'CSRF_INVALID',
message: 'CSRF token mismatch',
});
}
// Если заголовка нет - разрешаем (SameSite cookie защищает)
return next();
}
// Cookie нет - это ошибка
return res.status(403).json({
error: 'CSRF_INVALID',
message: 'CSRF token is missing',
});
}
+24
View File
@@ -0,0 +1,24 @@
export function errorHandler(err, req, res, next) {
const statusCode = err.statusCode || 500;
const code = err.code || 'INTERNAL_ERROR';
console.error('[BFF ERROR]', {
code,
statusCode,
message: err.message,
stack: err.stack,
});
const response = {
ok: false,
error: code,
message: err.message || 'Internal server error',
};
// Добавляем детали валидации если есть
if (err.details && Array.isArray(err.details)) {
response.details = err.details;
}
res.status(statusCode).json(response);
}
+51
View File
@@ -0,0 +1,51 @@
const ALLOWED_MIME_TYPES = new Set([
'image/jpeg',
'image/jpg',
'image/png',
'image/gif',
'image/webp',
]);
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
export function validateImage(req, res, next) {
const file = req.file;
if (!file) {
return res.status(400).json({
ok: false,
error: 'BAD_REQUEST',
message: 'Файл не предоставлен',
});
}
if (!ALLOWED_MIME_TYPES.has(file.mimetype)) {
return res.status(400).json({
ok: false,
error: 'INVALID_FILE_TYPE',
message: `Недопустимый тип файла. Разрешены: JPEG, PNG, GIF, WebP`,
});
}
if (file.size > MAX_FILE_SIZE) {
return res.status(400).json({
ok: false,
error: 'FILE_TOO_LARGE',
message: `Файл слишком большой. Максимум: ${MAX_FILE_SIZE / 1024 / 1024}MB`,
});
}
const filename = file.originalname || file.filename;
const ext = filename.split('.').pop().toLowerCase();
const validExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (!validExtensions.includes(ext)) {
return res.status(400).json({
ok: false,
error: 'INVALID_EXTENSION',
message: 'Недопустимое расширение файла',
});
}
next();
}