diff --git a/app/api/user-subscription/route.ts b/app/api/user-subscription/route.ts new file mode 100644 index 0000000..0f45b04 --- /dev/null +++ b/app/api/user-subscription/route.ts @@ -0,0 +1,128 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { MARZBAN_PANEL_URL, getSubscriptionUrl } from '@/lib/constants'; +import { logger } from '@/lib/logger'; +import type { MarzbanUser } from '@/types/marzban'; + +// Next.js 13+ caching: no-store для реального времени данных подписки +export const dynamic = 'force-dynamic'; +export const revalidate = 0; + +const MARZBAN_API = MARZBAN_PANEL_URL; +const ADMIN_USERNAME = process.env.MARZBAN_ADMIN_USERNAME; +const ADMIN_PASSWORD = process.env.MARZBAN_ADMIN_PASSWORD; + +if (!ADMIN_USERNAME || !ADMIN_PASSWORD) { + throw new Error('MARZBAN_ADMIN_USERNAME and MARZBAN_ADMIN_PASSWORD must be set in .env'); +} + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const telegramId = searchParams.get('telegramId'); + const telegramUsername = searchParams.get('telegramUsername'); + + if (!telegramId && !telegramUsername) { + return NextResponse.json( + { success: false, error: 'telegramId or telegramUsername required' }, + { status: 400 } + ); + } + + // 1. Получаем токен админа + const tokenResponse = await fetch(`${MARZBAN_API}/api/admin/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: `grant_type=password&username=${ADMIN_USERNAME}&password=${ADMIN_PASSWORD}`, + // Кешируем токен на 30 минут (стандартное время жизни JWT) + cache: 'force-cache', + next: { revalidate: 1800 } + }); + + if (!tokenResponse.ok) { + throw new Error('Failed to get admin token'); + } + + const { access_token } = await tokenResponse.json(); + + // 2. Получаем ВСЕХ пользователей (Marzban API не поддерживает фильтр по note) + const usersResponse = await fetch(`${MARZBAN_API}/api/users?offset=0&limit=1000`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${access_token}`, + }, + // Данные пользователей НЕ кешируем - нужны в реальном времени + cache: 'no-store', + next: { revalidate: 0 } + }); + + if (!usersResponse.ok) { + throw new Error('Failed to get users'); + } + + const usersData = await usersResponse.json(); + + logger.debug(`🔍 Searching for telegramId: ${telegramId}, username: ${telegramUsername}`); + logger.debug(`📊 Total users in Marzban: ${usersData.users?.length || 0}`); + + // 3. Ищем пользователя по Telegram ID в note или по username + const user: MarzbanUser | undefined = usersData.users?.find((u: any) => { + // Поиск по note: "TG: 1270320642" + if (telegramId && u.note?.includes(`TG: ${telegramId}`)) { + return true; + } + + // Поиск по username patterns + if (telegramUsername) { + const cleanUsername = telegramUsername.toLowerCase().replace(/[^a-z0-9_]/g, '_'); + if (u.username === cleanUsername) { + return true; + } + } + + // Поиск по username с ID: "user_1270320642" или "имя_1270320642" + if (telegramId && u.username?.includes(`_${telegramId}`)) { + return true; + } + + return false; + }); + + if (!user) { + logger.debug('❌ User not found'); + return NextResponse.json( + { success: false, hasSubscription: false }, + { status: 200 } + ); + } + + logger.debug(`✅ Found user: ${user.username}`); + + // 4. Получаем subscription token из subscription_url + const subscriptionToken = user.subscription_url?.split('/sub/')[1]?.replace(/\/$/, '') || user.username; + + return NextResponse.json({ + success: true, + hasSubscription: true, + token: subscriptionToken, + username: user.username, + status: user.status, + expire: user.expire, + dataLimit: user.data_limit, + dataLimitResetStrategy: user.data_limit_reset_strategy, + usedTraffic: user.used_traffic, + subscriptionUrl: getSubscriptionUrl(subscriptionToken), + }); + + } catch (error) { + logger.error('Get subscription error:', error); + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error' + }, + { status: 500 } + ); + } +}