english version update
This commit is contained in:
@@ -111,15 +111,72 @@ app.get('/', async (req, res) => {
|
||||
// ==================== SITEMAP ====================
|
||||
app.get('/sitemap.xml', (req, res) => {
|
||||
const baseUrl = 'https://ospab.host';
|
||||
|
||||
const staticPages = [
|
||||
{ loc: '/', priority: '1.0', changefreq: 'weekly', description: 'Ospab Host - Облачное S3 хранилище и хостинг сайтов' },
|
||||
{ loc: '/about', priority: '0.9', changefreq: 'monthly', description: 'О компании - Современная платформа хранения данных' },
|
||||
{ loc: '/login', priority: '0.7', changefreq: 'monthly', description: 'Вход в панель управления с QR-аутентификацией' },
|
||||
{ loc: '/register', priority: '0.8', changefreq: 'monthly', description: 'Регистрация аккаунта - Начните за 2 минуты' },
|
||||
{ loc: '/blog', priority: '0.85', changefreq: 'daily', description: 'Блог о S3 хранилище и хостинге' },
|
||||
{ loc: '/terms', priority: '0.5', changefreq: 'yearly', description: 'Условия использования сервиса' },
|
||||
{ loc: '/privacy', priority: '0.5', changefreq: 'yearly', description: 'Политика конфиденциальности и защита данных' },
|
||||
|
||||
const pages = [
|
||||
// Главная страница
|
||||
{
|
||||
loc: '/',
|
||||
priority: '1.0',
|
||||
changefreq: 'weekly',
|
||||
ru: { title: 'Ospab Host - Облачное S3 хранилище и хостинг сайтов', description: 'Надёжное облачное S3-совместимое хранилище в Великом Новгороде' },
|
||||
en: { title: 'Ospab Host - Cloud S3 Storage and Website Hosting', description: 'Reliable S3-compatible cloud storage in Veliky Novgorod' }
|
||||
},
|
||||
// О компании
|
||||
{
|
||||
loc: '/about',
|
||||
priority: '0.9',
|
||||
changefreq: 'monthly',
|
||||
ru: { title: 'О компании - Современная платформа хранения данных', description: 'Узнайте о ospab.host - платформе облачного хранилища в Великом Новгороде' },
|
||||
en: { title: 'About Us - Modern Data Storage Platform', description: 'Learn about ospab.host - cloud storage platform in Veliky Novgorod' }
|
||||
},
|
||||
// Вход
|
||||
{
|
||||
loc: '/login',
|
||||
priority: '0.7',
|
||||
changefreq: 'monthly',
|
||||
ru: { title: 'Вход в панель управления с QR-аутентификацией', description: 'Войдите в ваш личный кабинет ospab.host' },
|
||||
en: { title: 'Login to Control Panel with QR Authentication', description: 'Sign in to your ospab.host account' }
|
||||
},
|
||||
// Регистрация
|
||||
{
|
||||
loc: '/register',
|
||||
priority: '0.8',
|
||||
changefreq: 'monthly',
|
||||
ru: { title: 'Регистрация аккаунта - Начните за 2 минуты', description: 'Зарегистрируйтесь в ospab.host и начните пользоваться облачным хранилищем' },
|
||||
en: { title: 'Account Registration - Start in 2 Minutes', description: 'Register with ospab.host and start using cloud storage' }
|
||||
},
|
||||
// Блог
|
||||
{
|
||||
loc: '/blog',
|
||||
priority: '0.85',
|
||||
changefreq: 'daily',
|
||||
ru: { title: 'Блог о S3 хранилище и хостинге', description: 'Статьи о хостинге, S3 хранилище и облачных технологиях' },
|
||||
en: { title: 'Blog about S3 Storage and Hosting', description: 'Articles about hosting, S3 storage and cloud technologies' }
|
||||
},
|
||||
// Тарифы
|
||||
{
|
||||
loc: '/tariffs',
|
||||
priority: '0.9',
|
||||
changefreq: 'weekly',
|
||||
ru: { title: 'Тарифы на облачное S3 хранилище', description: 'Выберите подходящий тариф для вашего проекта' },
|
||||
en: { title: 'Cloud S3 Storage Plans', description: 'Choose the right plan for your project' }
|
||||
},
|
||||
// Условия использования
|
||||
{
|
||||
loc: '/terms',
|
||||
priority: '0.5',
|
||||
changefreq: 'yearly',
|
||||
ru: { title: 'Условия использования сервиса', description: 'Правила и условия использования ospab.host' },
|
||||
en: { title: 'Terms of Service', description: 'Rules and conditions for using ospab.host' }
|
||||
},
|
||||
// Политика конфиденциальности
|
||||
{
|
||||
loc: '/privacy',
|
||||
priority: '0.5',
|
||||
changefreq: 'yearly',
|
||||
ru: { title: 'Политика конфиденциальности и защита данных', description: 'Как мы защищаем ваши данные и обеспечиваем конфиденциальность' },
|
||||
en: { title: 'Privacy Policy and Data Protection', description: 'How we protect your data and ensure privacy' }
|
||||
}
|
||||
];
|
||||
|
||||
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
||||
@@ -127,12 +184,25 @@ app.get('/sitemap.xml', (req, res) => {
|
||||
|
||||
const lastmod = new Date().toISOString().split('T')[0];
|
||||
|
||||
for (const page of staticPages) {
|
||||
for (const page of pages) {
|
||||
// Русская версия (без префикса)
|
||||
xml += ' <url>\n';
|
||||
xml += ` <loc>${baseUrl}${page.loc}</loc>\n`;
|
||||
xml += ` <lastmod>${lastmod}</lastmod>\n`;
|
||||
xml += ` <priority>${page.priority}</priority>\n`;
|
||||
xml += ` <changefreq>${page.changefreq}</changefreq>\n`;
|
||||
xml += ' <xhtml:link rel="alternate" hreflang="ru" href="' + baseUrl + page.loc + '"/>\n';
|
||||
xml += ' <xhtml:link rel="alternate" hreflang="en" href="' + baseUrl + '/en' + page.loc + '"/>\n';
|
||||
xml += ' </url>\n';
|
||||
|
||||
// Английская версия (с префиксом /en)
|
||||
xml += ' <url>\n';
|
||||
xml += ` <loc>${baseUrl}/en${page.loc}</loc>\n`;
|
||||
xml += ` <lastmod>${lastmod}</lastmod>\n`;
|
||||
xml += ` <priority>${page.priority}</priority>\n`;
|
||||
xml += ` <changefreq>${page.changefreq}</changefreq>\n`;
|
||||
xml += ' <xhtml:link rel="alternate" hreflang="ru" href="' + baseUrl + page.loc + '"/>\n';
|
||||
xml += ' <xhtml:link rel="alternate" hreflang="en" href="' + baseUrl + '/en' + page.loc + '"/>\n';
|
||||
xml += ' </url>\n';
|
||||
}
|
||||
|
||||
@@ -145,20 +215,29 @@ app.get('/sitemap.xml', (req, res) => {
|
||||
// ==================== ROBOTS.TXT ====================
|
||||
app.get('/robots.txt', (req, res) => {
|
||||
const robots = `# ospab Host - Облачное S3 хранилище и хостинг
|
||||
# Хранение данных, техподдержка 24/7
|
||||
# Cloud S3 Storage and Website Hosting
|
||||
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Allow: /about
|
||||
Allow: /en/about
|
||||
Allow: /login
|
||||
Allow: /en/login
|
||||
Allow: /register
|
||||
Allow: /en/register
|
||||
Allow: /blog
|
||||
Allow: /en/blog
|
||||
Allow: /blog/*
|
||||
Allow: /en/blog/*
|
||||
Allow: /tariffs
|
||||
Allow: /en/tariffs
|
||||
Allow: /terms
|
||||
Allow: /en/terms
|
||||
Allow: /privacy
|
||||
Allow: /en/privacy
|
||||
Allow: /uploads/blog
|
||||
|
||||
# Запрет индексации приватных разделов
|
||||
# Disallow private sections / Запрет индексации приватных разделов
|
||||
Disallow: /dashboard
|
||||
Disallow: /dashboard/*
|
||||
Disallow: /api/
|
||||
@@ -171,7 +250,7 @@ Disallow: /uploads/checks
|
||||
|
||||
Sitemap: https://ospab.host/sitemap.xml
|
||||
|
||||
# Поисковые роботы
|
||||
# Search engine robots / Поисковые роботы
|
||||
User-agent: Googlebot
|
||||
Allow: /
|
||||
Crawl-delay: 0
|
||||
|
||||
@@ -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) || 'Ошибка подтверждения' });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: 'Ошибка получения' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user