🔒 Аудит: безопасность, TypeScript, UI, BottomNav

Безопасность:
- proxy: белый список путей (только /sub/*), POST заблокирован
- console.log заменён на logger (утечки URL/данных)
- OnboardingFlow: убраны --tg-theme-* (не существуют в проекте)

TypeScript (0 ошибок):
- tsconfig target es5→es2017 (regex /u flag fix)
- layout.tsx: viewport перенесён в metadata (Next.js 13.5)
- telegram-webhook: fix text possibly undefined
- hooks/useTelegramWebApp: fix Object possibly undefined
- types/telegram: убрана дублирующая Window декларация

UI:
- BottomNav: новый компонент (Назад/Главная/Помощь)
- safe-area-bottom CSS класс добавлен в globals.css
- dashboard: spacer h-20, toast поднят над BottomNav
- OnboardingFlow: цены 149/249/350₽ (были 200/350/500₽)

Очистка:
- page_NEW.tsx удалён локально (не был в git)
This commit is contained in:
Umbrix Dev
2026-02-08 18:59:02 +03:00
parent 3dffb35eaa
commit 33b20b979f
21 changed files with 1446 additions and 266 deletions

View File

@@ -23,8 +23,8 @@ export async function POST(request: NextRequest) {
try {
const {
planType,
period = '1month', // NEW: период оплаты
locationIds = [], // NEW: выбранные локации (ID нод)
period = '1month',
locationIds = [],
telegramId,
telegramUsername,
firstName,
@@ -79,7 +79,6 @@ export async function POST(request: NextRequest) {
username = `user_${Date.now()}_${Math.random().toString(36).substring(7)}`;
}
console.log('✅ Generated username:', username);
logger.debug('✅ Generated username:', username);
logger.debug('📊 Telegram data:', { telegramId, telegramUsername, firstName, lastName });

View File

@@ -50,7 +50,10 @@ function extractCountryCode(nodeName: string): string {
// Конвертируем emoji флаг в код страны
const flag = flagMatch[0];
const codePoints = [...flag].map(char => char.codePointAt(0)! - 0x1F1E6 + 65);
const codePoints = Array.from(flag).map(char => {
const codePoint = char.codePointAt(0);
return codePoint ? codePoint - 0x1F1E6 + 65 : 65;
});
return String.fromCharCode(...codePoints);
}

View File

@@ -1,5 +1,20 @@
// app/api/proxy/[...path]/route.ts
// API Proxy для обращения к Marzban серверу
// API Proxy для обращения к Marzban серверу подписок
// ТОЛЬКО разрешённые пути: /sub/{token}/*
import { logger } from '@/lib/logger';
// Белый список разрешённых path-паттернов (regex)
const ALLOWED_PATHS = [
/^sub\/[a-zA-Z0-9_-]+\/?$/, // /sub/{token}/
/^sub\/[a-zA-Z0-9_-]+\/info$/, // /sub/{token}/info
/^sub\/[a-zA-Z0-9_-]+\/usage$/, // /sub/{token}/usage
/^sub\/[a-zA-Z0-9_-]+\/(sing-box|clash-meta|clash|outline|v2ray|v2ray-json)$/, // configs
];
function isPathAllowed(path: string): boolean {
return ALLOWED_PATHS.some((pattern) => pattern.test(path));
}
export async function GET(
request: Request,
@@ -7,9 +22,19 @@ export async function GET(
) {
const path = params.path.join('/');
const baseUrl = 'https://umbrix2.3to3.sbs';
// Проверяем что путь в белом списке
if (!isPathAllowed(path)) {
logger.warn(`[Proxy] Blocked path: ${path}`);
return Response.json(
{ error: 'Forbidden: path not allowed' },
{ status: 403 }
);
}
const url = `${baseUrl}/${path}`;
console.log('[Proxy] Fetching:', url);
logger.debug('[Proxy] Fetching:', url);
try {
const response = await fetch(url, {
@@ -17,27 +42,22 @@ export async function GET(
'Accept': 'application/json',
'User-Agent': 'Umbrix-TelegramBot/1.0',
},
// Отключаем кэш для актуальных данных
cache: 'no-store',
});
if (!response.ok) {
console.error('[Proxy] HTTP error:', response.status, response.statusText);
logger.error('[Proxy] HTTP error:', response.status);
throw new Error(`HTTP error! status: ${response.status}`);
}
const contentType = response.headers.get('content-type');
// Если это JSON - парсим
if (contentType?.includes('application/json')) {
const data = await response.json();
console.log('[Proxy] Success (JSON):', Object.keys(data));
return Response.json(data);
}
// Если это текст/HTML - возвращаем как есть
const text = await response.text();
console.log('[Proxy] Success (Text):', text.substring(0, 100));
return new Response(text, {
headers: {
'Content-Type': contentType || 'text/plain',
@@ -45,46 +65,18 @@ export async function GET(
});
} catch (error) {
console.error('[Proxy] Error:', error);
logger.error('[Proxy] Error:', error);
return Response.json(
{
error: 'Failed to fetch data from Marzban server',
details: error instanceof Error ? error.message : 'Unknown error',
url: url,
},
{ error: 'Failed to fetch data from subscription server' },
{ status: 500 }
);
}
}
// Также поддерживаем POST для будущих API calls
export async function POST(
request: Request,
{ params }: { params: { path: string[] } }
) {
const path = params.path.join('/');
const baseUrl = 'https://umbrix2.3to3.sbs';
const url = `${baseUrl}/${path}`;
try {
const body = await request.json();
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(body),
});
const data = await response.json();
return Response.json(data);
} catch (error) {
return Response.json(
{ error: 'Failed to post data' },
{ status: 500 }
);
}
// POST запрещён — нет причин для POST к серверу подписок
export async function POST() {
return Response.json(
{ error: 'Method not allowed' },
{ status: 405 }
);
}

View File

@@ -3,6 +3,7 @@
import { NextRequest, NextResponse } from 'next/server';
import mysql from 'mysql2/promise';
import { logger } from '@/lib/logger';
// Database connection config from ENV
const dbConfig = {
@@ -31,7 +32,7 @@ export async function GET(request: NextRequest) {
// Return mock data if DB not configured (dev mode)
if (!isDbConfigured) {
console.log('⚠️ Referral stats skipped: DB not configured');
logger.info('⚠️ Referral stats skipped: DB not configured');
return NextResponse.json({
success: true,
hasReferrals: false,
@@ -98,7 +99,7 @@ export async function GET(request: NextRequest) {
await connection.end();
}
} catch (error) {
console.error('Referral stats error:', error);
logger.error('Referral stats error:', error);
return NextResponse.json(
{
success: false,

View File

@@ -3,6 +3,7 @@
import { NextRequest, NextResponse } from 'next/server';
import mysql from 'mysql2/promise';
import { logger } from '@/lib/logger';
// Database connection config from ENV
const dbConfig = {
@@ -31,7 +32,7 @@ export async function POST(request: NextRequest) {
// Return mock success if DB not configured (dev mode)
if (!isDbConfigured) {
console.log('⚠️ Referral tracking skipped: DB not configured');
logger.info('⚠️ Referral tracking skipped: DB not configured');
return NextResponse.json({
success: true,
message: 'Referral tracking disabled (dev mode)',
@@ -138,7 +139,7 @@ export async function POST(request: NextRequest) {
await connection.end();
}
} catch (error) {
console.error('Referral track error:', error);
logger.error('Referral track error:', error);
return NextResponse.json(
{
success: false,

View File

@@ -0,0 +1,189 @@
// Telegram Bot Webhook Handler
// POST /api/telegram-webhook
// Принимает вебхуки от Telegram и обрабатывает команды
import { NextRequest, NextResponse } from 'next/server';
import { logger } from '@/lib/logger';
const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || '';
const WEBAPP_URL = process.env.NEXT_PUBLIC_APP_URL || 'https://app.umbrix.net';
interface TelegramUpdate {
message?: {
message_id: number;
from: {
id: number;
first_name: string;
last_name?: string;
username?: string;
};
chat: {
id: number;
};
text?: string;
};
}
interface InlineKeyboardButton {
text: string;
web_app?: { url: string };
url?: string;
}
async function sendMessage(
chatId: number,
text: string,
replyMarkup?: { inline_keyboard: InlineKeyboardButton[][] }
) {
const url = `https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`;
const body = {
chat_id: chatId,
text: text,
parse_mode: 'HTML',
reply_markup: replyMarkup,
};
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!response.ok) {
logger.error('Telegram API error:', await response.text());
}
} catch (error) {
logger.error('Failed to send Telegram message:', error);
}
}
export async function POST(request: NextRequest) {
try {
const update: TelegramUpdate = await request.json();
logger.debug('📨 Telegram webhook received:', update);
if (!update.message || !update.message.text) {
return NextResponse.json({ ok: true });
}
const { message } = update;
const { chat, from } = message;
const text = message.text!;
// Обработка команды /start
if (text.startsWith('/start')) {
const parts = text.split(' ');
const startParam = parts[1]; // Например: "ref_john_doe"
let webAppUrl = WEBAPP_URL;
let welcomeText = `👋 Привет, ${from.first_name}!\n\n`;
// Если есть реферальный параметр
if (startParam && startParam.startsWith('ref_')) {
const referrerId = startParam.replace('ref_', '');
webAppUrl = `${WEBAPP_URL}?ref=${encodeURIComponent(referrerId)}`;
welcomeText += `🎁 Вы перешли по реферальной ссылке от <b>${referrerId}</b>!\n\n`;
welcomeText += `При регистрации вы получите:\n`;
welcomeText += `✅ <b>7 дней бесплатно</b>\n`;
welcomeText += `А ваш друг получит <b>+7 дней</b> к подписке!\n\n`;
logger.info(`🎁 Referral link opened: ${from.id} -> ${referrerId}`);
} else {
welcomeText += `🚀 <b>Umbrix VPN</b> - быстрый и безопасный VPN!\n\n`;
welcomeText += `✅ Надежная защита данных\n`;
welcomeText += `✅ Высокая скорость соединения\n`;
welcomeText += `✅ Простая настройка\n\n`;
}
welcomeText += `Нажмите кнопку ниже, чтобы начать:`;
const keyboard = {
inline_keyboard: [
[
{
text: '🚀 Открыть Umbrix',
web_app: { url: webAppUrl },
},
],
],
};
await sendMessage(chat.id, welcomeText, keyboard);
return NextResponse.json({ ok: true });
}
// Обработка других команд
if (text === '/help') {
const helpText = `
<b>Доступные команды:</b>
/start - Открыть приложение
/help - Показать эту справку
/referral - Получить свою реферальную ссылку
<b>Как получить реферальную ссылку?</b>
1. Откройте приложение
2. Перейдите в раздел "Реферальная программа"
3. Скопируйте ссылку и отправьте друзьям!
<b>Условия реферальной программы:</b>
За каждого друга: <b>+7 дней</b> бесплатно
За 5 друзей: <b>1 месяц в подарок</b>
• При оплате: <b>10% скидка</b>
`;
await sendMessage(chat.id, helpText.trim());
return NextResponse.json({ ok: true });
}
if (text === '/referral') {
const referralText = `
🎁 <b>Ваша реферальная ссылка:</b>
Чтобы получить ссылку, откройте приложение и перейдите в раздел "Пригласить друга".
Там вы найдете свою уникальную ссылку и сможете поделиться ей!
`;
const keyboard = {
inline_keyboard: [
[
{
text: '🚀 Открыть приложение',
web_app: { url: WEBAPP_URL },
},
],
],
};
await sendMessage(chat.id, referralText.trim(), keyboard);
return NextResponse.json({ ok: true });
}
// Неизвестная команда
await sendMessage(
chat.id,
'Неизвестная команда. Используйте /help для списка команд.'
);
return NextResponse.json({ ok: true });
} catch (error) {
logger.error('❌ Telegram webhook error:', error);
return NextResponse.json(
{ ok: false, error: 'Internal server error' },
{ status: 500 }
);
}
}
// Для проверки что webhook работает
export async function GET() {
return NextResponse.json({
status: 'ok',
message: 'Telegram webhook endpoint is running'
});
}