english version update

This commit is contained in:
Georgiy Syralev
2025-12-31 19:59:43 +03:00
parent b799f278a4
commit a2809a705f
57 changed files with 4263 additions and 1333 deletions

View File

@@ -10,12 +10,18 @@ import {
} from './account.service';
import { prisma } from '../../prisma/client';
// Хелпер для извлечения сообщения из ошибки
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return getErrorMessage(error);
return String(error);
}
/**
* Получить информацию о текущем пользователе
*/
export const getAccountInfo = async (req: Request, res: Response) => {
try {
const userId = (req.user as any)?.id;
const userId = (req.user as { id?: number })?.id;
if (!userId) {
return res.status(401).json({ error: 'Не авторизован' });
}
@@ -33,7 +39,7 @@ export const getAccountInfo = async (req: Request, res: Response) => {
*/
export const requestPasswordChangeHandler = async (req: Request, res: Response) => {
try {
const userId = (req.user as any)?.id;
const userId = (req.user as { id?: number })?.id;
if (!userId) {
return res.status(401).json({ error: 'Не авторизован' });
}
@@ -71,7 +77,7 @@ export const requestPasswordChangeHandler = async (req: Request, res: Response)
});
} catch (error: unknown) {
console.error('Ошибка запроса смены пароля:', error);
res.status(500).json({ error: error.message || 'Ошибка сервера' });
res.status(500).json({ error: getErrorMessage(error) || 'Ошибка сервера' });
}
};
@@ -80,7 +86,7 @@ export const requestPasswordChangeHandler = async (req: Request, res: Response)
*/
export const confirmPasswordChangeHandler = async (req: Request, res: Response) => {
try {
const userId = (req.user as any)?.id;
const userId = (req.user as { id?: number })?.id;
if (!userId) {
return res.status(401).json({ error: 'Не авторизован' });
}
@@ -99,7 +105,7 @@ export const confirmPasswordChangeHandler = async (req: Request, res: Response)
});
} catch (error: unknown) {
console.error('Ошибка подтверждения смены пароля:', error);
res.status(400).json({ error: error.message || 'Ошибка подтверждения' });
res.status(400).json({ error: getErrorMessage(error) || 'Ошибка подтверждения' });
}
};
@@ -108,7 +114,7 @@ export const confirmPasswordChangeHandler = async (req: Request, res: Response)
*/
export const requestUsernameChangeHandler = async (req: Request, res: Response) => {
try {
const userId = (req.user as any)?.id;
const userId = (req.user as { id?: number })?.id;
if (!userId) {
return res.status(401).json({ error: 'Не авторизован' });
}
@@ -139,7 +145,7 @@ export const requestUsernameChangeHandler = async (req: Request, res: Response)
});
} catch (error: unknown) {
console.error('Ошибка запроса смены имени:', error);
res.status(400).json({ error: error.message || 'Ошибка сервера' });
res.status(400).json({ error: getErrorMessage(error) || 'Ошибка сервера' });
}
};
@@ -148,7 +154,7 @@ export const requestUsernameChangeHandler = async (req: Request, res: Response)
*/
export const confirmUsernameChangeHandler = async (req: Request, res: Response) => {
try {
const userId = (req.user as any)?.id;
const userId = (req.user as { id?: number })?.id;
if (!userId) {
return res.status(401).json({ error: 'Не авторизован' });
}
@@ -167,7 +173,7 @@ export const confirmUsernameChangeHandler = async (req: Request, res: Response)
});
} catch (error: unknown) {
console.error('Ошибка подтверждения смены имени:', error);
res.status(400).json({ error: error.message || 'Ошибка подтверждения' });
res.status(400).json({ error: getErrorMessage(error) || 'Ошибка подтверждения' });
}
};
@@ -176,7 +182,7 @@ export const confirmUsernameChangeHandler = async (req: Request, res: Response)
*/
export const requestAccountDeletionHandler = async (req: Request, res: Response) => {
try {
const userId = (req.user as any)?.id;
const userId = (req.user as { id?: number })?.id;
if (!userId) {
return res.status(401).json({ error: 'Не авторизован' });
}
@@ -189,7 +195,7 @@ export const requestAccountDeletionHandler = async (req: Request, res: Response)
});
} catch (error: unknown) {
console.error('Ошибка запроса удаления аккаунта:', error);
res.status(500).json({ error: error.message || 'Ошибка сервера' });
res.status(500).json({ error: getErrorMessage(error) || 'Ошибка сервера' });
}
};
@@ -198,7 +204,7 @@ export const requestAccountDeletionHandler = async (req: Request, res: Response)
*/
export const confirmAccountDeletionHandler = async (req: Request, res: Response) => {
try {
const userId = (req.user as any)?.id;
const userId = (req.user as { id?: number })?.id;
if (!userId) {
return res.status(401).json({ error: 'Не авторизован' });
}
@@ -217,7 +223,8 @@ export const confirmAccountDeletionHandler = async (req: Request, res: Response)
});
} catch (error: unknown) {
console.error('Ошибка подтверждения удаления аккаунта:', error);
res.status(400).json({ error: error.message || 'Ошибка подтверждения' });
res.status(400).json({ error: getErrorMessage(error) || 'Ошибка подтверждения' });
}
};

View File

@@ -1,6 +1,7 @@
import { Request, Response } from 'express';
import { prisma } from '../../prisma/client';
import { createNotification } from '../notification/notification.controller';
import { sendNotificationEmail } from '../notification/email.service';
function toNumeric(value: unknown): number {
if (typeof value === 'bigint') {
@@ -514,23 +515,50 @@ export class AdminController {
return res.status(404).json({ error: 'Пользователь не найден' });
}
if (!user.email) {
return res.status(400).json({ error: 'У пользователя не указан email' });
}
const now = new Date().toISOString();
const logMsg = `[Admin] EMAIL-TEST | userId=${user.id} | username=${user.username} | email=${user.email} | time=${now}`;
console.log(logMsg);
// Здесь должна быть реальная отправка email (имитация)
await new Promise(resolve => setTimeout(resolve, 800));
// Отправляем реальное email уведомление
const emailResult = await sendNotificationEmail({
to: user.email,
username: user.username,
title: 'Тестовое уведомление',
message: 'Это тестовое email-уведомление от ospab.host. Если вы получили это письмо, email-уведомления настроены корректно.',
actionUrl: '/dashboard/notifications',
type: 'test_email'
});
if (emailResult.status === 'error') {
return res.status(500).json({
success: false,
error: `Ошибка отправки email: ${emailResult.message}`,
details: { userId: user.id, email: user.email, time: now }
});
}
if (emailResult.status === 'skipped') {
return res.status(400).json({
success: false,
error: 'SMTP не настроен. Укажите SMTP_USER и SMTP_PASS в переменных окружения.',
details: { userId: user.id, email: user.email, time: now }
});
}
return res.json({
success: true,
message: 'Email-уведомление успешно отправлено (тест)',
message: 'Email-уведомление успешно отправлено',
details: {
userId: user.id,
username: user.username,
email: user.email,
type: 'email',
time: now,
status: 'sent (mock)'
messageId: 'messageId' in emailResult ? emailResult.messageId : undefined
}
});
} catch (error) {

View File

@@ -4,7 +4,7 @@ import crypto from 'crypto';
import { createSession } from '../session/session.controller';
import { logger } from '../../utils/logger';
const QR_EXPIRATION_SECONDS = 60; // QR-код живёт 60 секунд
const QR_EXPIRATION_SECONDS = 180; // QR-код живёт 180 секунд (3 минуты)
// Генерировать уникальный код для QR
function generateQRCode(): string {
@@ -14,7 +14,7 @@ function generateQRCode(): string {
// Создать новый QR-запрос для логина
export async function createQRLoginRequest(req: Request, res: Response) {
try {
const code = generateQRCode();
const code = generateQRCode();
const ipAddress = req.headers['x-forwarded-for'] as string || req.socket.remoteAddress || '';
const userAgent = req.headers['user-agent'] || '';
@@ -31,6 +31,16 @@ 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({
code: qrRequest.code,
ipAddress: qrRequest.ipAddress,
userAgent: qrRequest.userAgent?.toString?.().slice(0, 200),
host: req.headers.host,
origin: req.headers.origin,
referer: req.headers.referer
}));
res.json({
code: qrRequest.code,
expiresAt: qrRequest.expiresAt,
@@ -47,21 +57,38 @@ export async function checkQRStatus(req: Request, res: Response) {
try {
const { code } = req.params;
// Log incoming status checks for tracing
logger.debug('[QR Status] Проверка статуса QR', {
code,
ip: req.headers['x-forwarded-for'] || req.socket.remoteAddress,
ua: (req.headers['user-agent'] || '').toString().slice(0, 200)
});
const qrRequest = await prisma.qrLoginRequest.findUnique({
where: { code }
});
if (!qrRequest) {
// Log as error so it appears in production logs — include host/origin/referer and remote IP for tracing
logger.error('[QR Status] QR-код не найден', {
code,
host: req.headers.host,
origin: req.headers.origin,
referer: req.headers.referer,
ip: req.headers['x-forwarded-for'] || req.socket.remoteAddress
});
return res.status(404).json({ error: 'QR-код не найден' });
}
// Проверяем истёк ли QR-код
if (new Date() > qrRequest.expiresAt) {
const now = new Date();
const expiresIn = Math.max(0, Math.ceil((qrRequest.expiresAt.getTime() - now.getTime()) / 1000));
if (expiresIn <= 0) {
await prisma.qrLoginRequest.update({
where: { code },
data: { status: 'expired' }
});
return res.json({ status: 'expired' });
return res.json({ status: 'expired', expiresAt: qrRequest.expiresAt, expiresIn: 0 });
}
// Если подтверждён, создаём сессию и возвращаем токен
@@ -85,8 +112,12 @@ export async function checkQRStatus(req: Request, res: Response) {
// Создаём сессию для нового устройства
const { token } = await createSession(user.id, req);
// Удаляем использованный QR-запрос
await prisma.qrLoginRequest.delete({ where: { code } });
// Попытка безопасно удалить использованный QR-запрос (deleteMany не бросает если записи не найдено)
try {
await prisma.qrLoginRequest.deleteMany({ where: { code } });
} catch (err) {
logger.warn('[QR Status] Не удалось удалить QR-запрос (возможно уже удалён)', { code, error: err });
}
return res.json({
status: 'confirmed',
@@ -102,7 +133,7 @@ export async function checkQRStatus(req: Request, res: Response) {
});
}
res.json({ status: qrRequest.status });
return res.json({ status: qrRequest.status, expiresAt: qrRequest.expiresAt, expiresIn, ipAddress: qrRequest.ipAddress ?? undefined, userAgent: qrRequest.userAgent ? (qrRequest.userAgent as string).slice(0, 200) : undefined });
} catch (error) {
logger.error('Ошибка проверки статуса QR:', error);
res.status(500).json({ error: 'Ошибка проверки статуса' });
@@ -266,3 +297,61 @@ export async function cleanupExpiredQRRequests() {
logger.error('[QR Cleanup] Ошибка:', error);
}
}
// DEV-only: получить последние N QR-запросов (для отладки)
export async function listRecentQRRequests(req: Request, res: Response) {
try {
// In production allow only requests from localhost (for safe debugging)
if (process.env.NODE_ENV === 'production') {
const remote = req.socket.remoteAddress || '';
const isLocal = remote === '::1' || remote === '127.0.0.1' || remote.startsWith('::ffff:127.0.0.1');
if (!isLocal) {
return res.status(404).json({ error: 'Not found' });
}
}
const limit = Math.min(100, Number(req.query.limit) || 50);
const rows = await prisma.qrLoginRequest.findMany({
orderBy: { createdAt: 'desc' },
take: limit,
select: {
code: true,
status: true,
ipAddress: true,
userAgent: true,
createdAt: true,
expiresAt: true,
userId: true
}
});
res.json({ count: rows.length, rows });
} catch (error) {
logger.error('[QR Debug] Ошибка получения списка QR-запросов:', error);
res.status(500).json({ error: 'Ошибка получения списка' });
}
}
// DEV-only: получить QR-запрос по коду
export async function getQRRequestByCode(req: Request, res: Response) {
try {
// In production allow only requests from localhost (for safe debugging)
if (process.env.NODE_ENV === 'production') {
const remote = req.socket.remoteAddress || '';
const isLocal = remote === '::1' || remote === '127.0.0.1' || remote.startsWith('::ffff:127.0.0.1');
if (!isLocal) {
return res.status(404).json({ error: 'Not found' });
}
}
const { code } = req.params;
const row = await prisma.qrLoginRequest.findUnique({ where: { code } });
if (!row) {
return res.status(404).json({ error: 'QR-код не найден' });
}
res.json(row);
} catch (error) {
logger.error('[QR Debug] Ошибка получения QR по коду:', error);
res.status(500).json({ error: 'Ошибка получения' });
}
}

View File

@@ -4,7 +4,9 @@ import {
checkQRStatus,
confirmQRLogin,
rejectQRLogin,
markQRAsScanning
markQRAsScanning,
listRecentQRRequests,
getQRRequestByCode
} from './qr-auth.controller';
import { authMiddleware } from '../auth/auth.middleware';
@@ -16,6 +18,10 @@ router.post('/generate', createQRLoginRequest);
// Проверить статус QR-кода (polling, публичный endpoint)
router.get('/status/:code', checkQRStatus);
// DEV-only debug endpoints (возвращают информацию о последних QR-запросах)
router.get('/debug/list', listRecentQRRequests);
router.get('/debug/get/:code', getQRRequestByCode);
// Отметить что пользователь открыл страницу подтверждения (требует авторизации)
router.post('/scanning', authMiddleware, markQRAsScanning);

View File

@@ -160,7 +160,8 @@ router.post('/cart/:id/apply-promo', authMiddleware, async (req, res) => {
return res.json({ success: true, cart: result });
} catch (e: unknown) {
const message = e instanceof Error ? e.message : 'Не удалось применить промокод';
return res.status(400).json({ error: message });
const status = typeof message === 'string' && message.includes('PromoCode модель недоступна') ? 500 : 400;
return res.status(status).json({ error: message });
}
});

View File

@@ -661,9 +661,13 @@ function buildPlanFromSession(session: CheckoutSessionRecord, plan?: StoragePlan
isActive: true,
};
// Для custom тарифа всегда берём значения из сессии
return {
...base,
price: toPlainNumber(session.price),
quotaGb: session.quotaGb,
bandwidthGb: session.bandwidthGb,
requestLimit: session.requestLimit,
};
}
@@ -672,6 +676,8 @@ type CheckoutSessionPayload = {
plan: ReturnType<typeof serializePlan>;
price: number;
expiresAt: string;
originalPrice?: number | null;
promoDiscount?: number | null;
};
type CheckoutSessionResult = {
@@ -694,11 +700,14 @@ function ensureSessionActive(session: CheckoutSessionRecord, userId: number): Ch
}
function toCheckoutPayload(session: CheckoutSessionRecord, plan?: StoragePlanRecord | null): CheckoutSessionPayload {
const original = plan ? Number(plan.price) : toPlainNumber(session.price);
return {
cartId: session.id,
plan: buildPlanFromSession(session, plan),
price: toPlainNumber(session.price),
expiresAt: session.expiresAt.toISOString(),
originalPrice: original,
promoDiscount: session.promoDiscount ?? null,
};
}