2026-02-04 05:25:10 +03:00
|
|
|
// API endpoint для создания нового пользователя VPN
|
|
|
|
|
// POST /api/create-user
|
|
|
|
|
// Принимает: { planType, telegramId, telegramUsername, firstName, lastName }
|
|
|
|
|
// Возвращает: { success: true, token, username, subscriptionUrl }
|
|
|
|
|
|
2026-02-04 05:03:56 +03:00
|
|
|
import { NextRequest, NextResponse } from 'next/server';
|
2026-02-04 05:25:10 +03:00
|
|
|
import { randomUUID } from 'crypto'; // Криптографически безопасная генерация UUID
|
2026-02-04 05:03:56 +03:00
|
|
|
import { MARZBAN_PANEL_URL, getSubscriptionUrl } from '@/lib/constants';
|
|
|
|
|
import { logger } from '@/lib/logger';
|
|
|
|
|
import type { PlanType, CreateUserRequest, CreateUserResponse } from '@/types/marzban';
|
|
|
|
|
|
|
|
|
|
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 POST(request: NextRequest) {
|
|
|
|
|
try {
|
2026-02-06 20:51:40 +03:00
|
|
|
const { planType, telegramId, telegramUsername, firstName, lastName, referrerId } = await request.json();
|
2026-02-04 05:03:56 +03:00
|
|
|
|
2026-02-06 20:51:40 +03:00
|
|
|
logger.debug('📥 Received data:', { planType, telegramId, telegramUsername, firstName, lastName, referrerId });
|
2026-02-04 05:03:56 +03:00
|
|
|
|
|
|
|
|
// 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}`,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!tokenResponse.ok) {
|
|
|
|
|
throw new Error('Failed to get admin token');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { access_token } = await tokenResponse.json();
|
|
|
|
|
|
|
|
|
|
// 2. Определяем параметры тарифа
|
|
|
|
|
const planConfig = getPlanConfig(planType);
|
|
|
|
|
|
|
|
|
|
// 3. Генерируем уникальное имя пользователя (приоритет: @username > firstName_ID > userID > random)
|
|
|
|
|
// ВАЖНО: Marzban принимает только a-z, 0-9 и подчеркивания, БЕЗ @
|
|
|
|
|
let username: string;
|
|
|
|
|
if (telegramUsername) {
|
|
|
|
|
// Есть @username в Telegram - используем БЕЗ @
|
|
|
|
|
username = telegramUsername.toLowerCase().replace(/[^a-z0-9_]/g, '_');
|
|
|
|
|
} else if (firstName && telegramId) {
|
|
|
|
|
// Нет username, используем имя + ID
|
|
|
|
|
const cleanName = firstName.toLowerCase().replace(/[^a-z0-9]/g, '_');
|
|
|
|
|
username = `${cleanName}_${telegramId}`;
|
|
|
|
|
} else if (telegramId) {
|
|
|
|
|
// Только ID
|
|
|
|
|
username = `user_${telegramId}`;
|
|
|
|
|
} else {
|
|
|
|
|
// Для тестирования вне Telegram
|
|
|
|
|
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 });
|
|
|
|
|
// 4. Создаем пользователя в Marzban по ПРАВИЛЬНОЙ схеме
|
|
|
|
|
const createUserResponse = await fetch(`${MARZBAN_API}/api/user`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Authorization': `Bearer ${access_token}`,
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
username: username,
|
|
|
|
|
status: 'active',
|
|
|
|
|
expire: planConfig.expireTimestamp,
|
|
|
|
|
data_limit: planConfig.dataLimitBytes,
|
|
|
|
|
data_limit_reset_strategy: 'no_reset',
|
|
|
|
|
proxies: {
|
|
|
|
|
vmess: {
|
|
|
|
|
id: randomUUID() // Криптографически безопасный UUID
|
|
|
|
|
},
|
|
|
|
|
vless: {},
|
|
|
|
|
trojan: {}
|
|
|
|
|
},
|
|
|
|
|
inbounds: {
|
|
|
|
|
vless: ['VLESS TCP', 'VLESS Reality'],
|
|
|
|
|
vmess: ['VMess WS'],
|
|
|
|
|
trojan: ['Trojan TCP']
|
|
|
|
|
},
|
|
|
|
|
note: `Тариф: ${planConfig.name}, TG: ${telegramId || 'N/A'}, Создан: ${new Date().toISOString()}`,
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!createUserResponse.ok) {
|
|
|
|
|
const errorText = await createUserResponse.text();
|
|
|
|
|
throw new Error(`Failed to create user: ${errorText}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const userData = await createUserResponse.json();
|
|
|
|
|
|
|
|
|
|
// 5. Получаем subscription token
|
|
|
|
|
const subscriptionToken = userData.subscription_url?.split('/sub/')[1]?.replace(/\/$/, '') || username;
|
|
|
|
|
|
2026-02-06 20:51:40 +03:00
|
|
|
// 6. Track referral if referrerId provided
|
|
|
|
|
if (referrerId) {
|
|
|
|
|
try {
|
|
|
|
|
await fetch(`${request.nextUrl.origin}/api/referral/track`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
username: username,
|
|
|
|
|
referrer_username: referrerId,
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
logger.debug('✅ Referral tracked:', { username, referrer: referrerId });
|
|
|
|
|
} catch (refError) {
|
|
|
|
|
logger.error('❌ Referral tracking failed:', refError);
|
|
|
|
|
// Don't fail user creation if referral tracking fails
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 05:03:56 +03:00
|
|
|
return NextResponse.json({
|
|
|
|
|
success: true,
|
|
|
|
|
token: subscriptionToken,
|
|
|
|
|
username: username,
|
|
|
|
|
expiresAt: new Date(planConfig.expireTimestamp * 1000).toISOString(),
|
|
|
|
|
subscriptionUrl: getSubscriptionUrl(subscriptionToken),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('Create user error:', error);
|
|
|
|
|
return NextResponse.json(
|
|
|
|
|
{
|
|
|
|
|
success: false,
|
|
|
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
|
|
|
},
|
|
|
|
|
{ status: 500 }
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getPlanConfig(planType: PlanType) {
|
|
|
|
|
const now = Math.floor(Date.now() / 1000);
|
|
|
|
|
|
|
|
|
|
const configs: Record<string, { name: string; expireTimestamp: number; dataLimitBytes: number }> = {
|
|
|
|
|
trial: {
|
|
|
|
|
name: '7 дней бесплатно',
|
|
|
|
|
expireTimestamp: now + (7 * 24 * 60 * 60), // 7 дней
|
|
|
|
|
dataLimitBytes: 0, // безлимит
|
|
|
|
|
},
|
|
|
|
|
start: {
|
|
|
|
|
name: 'Базовый',
|
|
|
|
|
expireTimestamp: now + (30 * 24 * 60 * 60), // 30 дней
|
|
|
|
|
dataLimitBytes: 50 * 1024 * 1024 * 1024, // 50 ГБ
|
|
|
|
|
},
|
|
|
|
|
plus: {
|
|
|
|
|
name: 'Расширенный',
|
|
|
|
|
expireTimestamp: now + (30 * 24 * 60 * 60), // 30 дней
|
|
|
|
|
dataLimitBytes: 299 * 1024 * 1024 * 1024, // 299 ГБ
|
|
|
|
|
},
|
|
|
|
|
max: {
|
|
|
|
|
name: 'Премиум',
|
|
|
|
|
expireTimestamp: now + (30 * 24 * 60 * 60), // 30 дней
|
|
|
|
|
dataLimitBytes: 0, // безлимит
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return configs[planType] || configs.trial;
|
|
|
|
|
}
|