🔒 Аудит: безопасность, 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:
@@ -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 });
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
189
app/api/telegram-webhook/route.ts
Normal file
189
app/api/telegram-webhook/route.ts
Normal 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'
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user