🔐 API: Создание пользователя VPN с UUID и тарифами
This commit is contained in:
146
app/api/create-user/route.ts
Normal file
146
app/api/create-user/route.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { randomUUID } from 'crypto';
|
||||
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 {
|
||||
const { planType, telegramId, telegramUsername, firstName, lastName } = await request.json();
|
||||
|
||||
logger.debug('📥 Received data:', { planType, telegramId, telegramUsername, firstName, lastName });
|
||||
|
||||
// 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;
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user