From e36aadca616e02acabe186ea345ba9d623b5e621 Mon Sep 17 00:00:00 2001 From: Umbrix Dev Date: Wed, 4 Feb 2026 05:03:56 +0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=90=20API:=20=D0=A1=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8F=20VPN=20=D1=81=20UUID=20?= =?UTF-8?q?=D0=B8=20=D1=82=D0=B0=D1=80=D0=B8=D1=84=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/create-user/route.ts | 146 +++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 app/api/create-user/route.ts diff --git a/app/api/create-user/route.ts b/app/api/create-user/route.ts new file mode 100644 index 0000000..629e950 --- /dev/null +++ b/app/api/create-user/route.ts @@ -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 = { + 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; +}