129 lines
4.3 KiB
TypeScript
129 lines
4.3 KiB
TypeScript
|
|
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 }
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|