diff --git a/app/api/client/download/route.ts b/app/api/client/download/route.ts index 049ad8a..e235e1e 100644 --- a/app/api/client/download/route.ts +++ b/app/api/client/download/route.ts @@ -1,17 +1,126 @@ import { NextResponse } from 'next/server'; +import { query } from '@/lib/db'; +import { + generateClientConfig, + InboundConfig, + ServerInfo, + UserCredentials +} from '@/lib/config-generators'; +import { getPkiService } from '@/lib/pki-service'; + +export async function GET(req: Request) { + const { searchParams } = new URL(req.url); + const username = searchParams.get('username'); + const inboundId = searchParams.get('inbound'); + const protocol = searchParams.get('protocol'); + + if (!username) { + return NextResponse.json({ error: 'Username is required' }, { status: 400 }); + } -export async function GET() { try { - const configContent = "vless://example-config-content"; - const filename = "config.conf"; - - return new Response(configContent, { - headers: { - 'Content-Type': 'application/octet-stream', - 'Content-Disposition': `attachment; filename="${filename}"`, - }, - }); + // Get user + const users = await query( + 'SELECT * FROM vpn_users WHERE username = ? LIMIT 1', + [username] + ); + + if (!users || users.length === 0) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + const user = users[0]; + + // Get inbound config + let inbound: InboundConfig | null = null; + + if (inboundId) { + const inbounds = await query( + 'SELECT * FROM vpn_inbounds WHERE id = ? LIMIT 1', + [parseInt(inboundId)] + ); + inbound = inbounds[0] || null; + } else if (protocol) { + const inbounds = await query( + 'SELECT * FROM vpn_inbounds WHERE protocol = ? AND status = ? LIMIT 1', + [protocol, 'active'] + ); + inbound = inbounds[0] || null; + } + + if (!inbound) { + return NextResponse.json({ error: 'Inbound not found' }, { status: 404 }); + } + + // Get server info + const servers = await query( + 'SELECT * FROM vpn_servers WHERE is_active = 1 ORDER BY load_score ASC LIMIT 1' + ); + + const server: ServerInfo = servers[0] || { + ip_address: '127.0.0.1', + domain: null, + ports: [443] + }; + + const userCreds: UserCredentials = { + username: user.username, + password: user.password_hash ? undefined : user.password, + uuid: user.xray_uuid, + wg_pubkey: user.wg_pubkey, + wg_ip: user.wg_ip + }; + + // Generate config based on protocol + let config: any; + + if (inbound.protocol === 'openvpn') { + // Get PKI certs for OpenVPN + const pkiService = getPkiService(); + const pki = await pkiService.generateClientCertificate(user.username); + config = generateClientConfig(inbound.protocol, server, inbound, userCreds, pki); + } else if (inbound.protocol === 'wireguard') { + // For WireGuard, we need to generate a client private key + // In production, this would be stored per-user + const clientPrivateKey = user.wg_privkey || generateWireGuardPrivateKey(); + config = generateClientConfig(inbound.protocol, server, inbound, userCreds, undefined, clientPrivateKey); + } else { + config = generateClientConfig(inbound.protocol, server, inbound, userCreds); + } + + // Return based on config type + if (config.type === 'file' && config.content) { + return new Response(config.content, { + headers: { + 'Content-Type': config.mimeType || 'application/octet-stream', + 'Content-Disposition': `attachment; filename="${config.filename}"`, + }, + }); + } + + // For URL-based configs (Xray protocols) + if (config.type === 'url' && config.url) { + return NextResponse.json({ + success: true, + protocol: inbound.protocol, + url: config.url, + qrData: config.qrData, + config: config.config + }); + } + + // For instruction-based configs (Cisco, L2TP) + if (config.type === 'instructions') { + return NextResponse.json({ + success: true, + protocol: inbound.protocol, + ...config + }); + } + + return NextResponse.json({ error: 'Unable to generate config' }, { status: 500 }); } catch (error: any) { + console.error('Download error:', error); return NextResponse.json({ error: { code: 'DOWNLOAD_FAILED', @@ -21,3 +130,15 @@ export async function GET() { }, { status: 500 }); } } + +// Helper function to generate WireGuard private key +function generateWireGuardPrivateKey(): string { + // In production, use proper crypto + // This is a placeholder that generates a base64-like string + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + let key = ''; + for (let i = 0; i < 43; i++) { + key += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return key + '='; +} diff --git a/app/api/inbounds/route.ts b/app/api/inbounds/route.ts index 1f89abb..9b34524 100644 --- a/app/api/inbounds/route.ts +++ b/app/api/inbounds/route.ts @@ -1,6 +1,12 @@ import { NextResponse } from 'next/server'; import db from '@/lib/db'; +// All supported protocols +const SUPPORTED_PROTOCOLS = [ + 'openvpn', 'wireguard', 'cisco', 'l2tp', + 'vless', 'vmess', 'trojan', 'shadowsocks' +]; + export async function GET() { try { const [inbounds] = await db.execute('SELECT * FROM vpn_inbounds ORDER BY created_at DESC'); @@ -13,18 +19,164 @@ export async function GET() { export async function POST(req: Request) { try { - const { name, protocol, port, remark } = await req.json(); + const body = await req.json(); + const { + name, + protocol, + port, + remark, + // OpenVPN fields + ovpn_protocol, + ovpn_cipher, + ovpn_auth, + ovpn_dev, + // WireGuard fields + wg_private_key, + wg_public_key, + wg_address, + wg_dns, + wg_mtu, + // Cisco fields + cisco_auth_method, + cisco_max_clients, + cisco_dpd, + // L2TP fields + l2tp_psk, + l2tp_dns, + l2tp_local_ip, + l2tp_remote_ip_range, + // Xray fields + xray_uuid, + xray_flow, + xray_network, + xray_security, + xray_sni, + xray_fingerprint, + xray_public_key, + xray_short_id, + xray_path, + xray_service_name, + xray_encryption, + } = body; + // Validate required fields if (!name || !protocol || !port) { return NextResponse.json({ error: 'Name, protocol, and port are required' }, { status: 400 }); } - const [result] = await db.execute( - 'INSERT INTO vpn_inbounds (name, protocol, port, remark) VALUES (?, ?, ?, ?)', - [name, protocol, parseInt(port, 10), remark || ''] + // Validate protocol + if (!SUPPORTED_PROTOCOLS.includes(protocol)) { + return NextResponse.json({ + error: `Invalid protocol. Supported: ${SUPPORTED_PROTOCOLS.join(', ')}` + }, { status: 400 }); + } + + // Check for port conflicts on same protocol + const [existingPorts] = await db.execute( + 'SELECT * FROM vpn_inbounds WHERE port = ? AND protocol = ?', + [parseInt(port, 10), protocol] ); + + if (Array.isArray(existingPorts) && existingPorts.length > 0) { + return NextResponse.json({ + error: `Port ${port} is already in use for ${protocol} protocol` + }, { status: 400 }); + } + + // Build SQL based on protocol type + const columns = ['name', 'protocol', 'port', 'remark', 'status']; + const values: any[] = [name, protocol, parseInt(port, 10), remark || '', 'active']; + const placeholders = ['?', '?', '?', '?', '?']; + + // Add protocol-specific fields + switch (protocol) { + case 'openvpn': + columns.push('ovpn_protocol', 'ovpn_cipher', 'ovpn_auth', 'ovpn_dev'); + values.push( + ovpn_protocol || 'udp', + ovpn_cipher || 'AES-256-GCM', + ovpn_auth || 'SHA256', + ovpn_dev || 'tun' + ); + placeholders.push('?', '?', '?', '?'); + break; + + case 'wireguard': + columns.push('wg_private_key', 'wg_public_key', 'wg_address', 'wg_dns', 'wg_mtu'); + values.push( + wg_private_key || '', + wg_public_key || '', + wg_address || '10.0.0.1/24', + wg_dns || '1.1.1.1', + wg_mtu || 1420 + ); + placeholders.push('?', '?', '?', '?', '?'); + break; + + case 'cisco': + columns.push('cisco_auth_method', 'cisco_max_clients', 'cisco_dpd'); + values.push( + cisco_auth_method || 'password', + cisco_max_clients || 100, + cisco_dpd || 90 + ); + placeholders.push('?', '?', '?'); + break; + + case 'l2tp': + columns.push('l2tp_psk', 'l2tp_dns', 'l2tp_local_ip', 'l2tp_remote_ip_range'); + values.push( + l2tp_psk || '', + l2tp_dns || '8.8.8.8', + l2tp_local_ip || '10.10.10.1', + l2tp_remote_ip_range || '10.10.10.2-10.10.10.254' + ); + placeholders.push('?', '?', '?', '?'); + break; + + case 'vless': + case 'vmess': + case 'trojan': + columns.push( + 'xray_protocol', 'xray_uuid', 'xray_flow', 'xray_network', + 'xray_security', 'xray_sni', 'xray_fingerprint', + 'xray_public_key', 'xray_short_id', 'xray_path', 'xray_service_name' + ); + values.push( + protocol, + xray_uuid || '', + xray_flow || '', + xray_network || 'tcp', + xray_security || 'reality', + xray_sni || 'www.google.com', + xray_fingerprint || 'chrome', + xray_public_key || '', + xray_short_id || '', + xray_path || '/ws', + xray_service_name || 'grpc' + ); + placeholders.push('?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?'); + break; + + case 'shadowsocks': + columns.push('xray_protocol', 'xray_encryption', 'xray_network'); + values.push( + 'shadowsocks', + xray_encryption || 'chacha20-ietf-poly1305', + xray_network || 'tcp' + ); + placeholders.push('?', '?', '?'); + break; + } + + const sql = `INSERT INTO vpn_inbounds (${columns.join(', ')}) VALUES (${placeholders.join(', ')})`; + const [result] = await db.execute(sql, values); - return NextResponse.json({ success: true, id: (result as any).insertId }); + return NextResponse.json({ + success: true, + id: (result as any).insertId, + message: `${protocol.toUpperCase()} inbound created successfully` + }); } catch (error: any) { console.error('Error creating inbound:', error); return NextResponse.json({ error: error.message }, { status: 500 }); diff --git a/app/api/subscription/[token]/route.ts b/app/api/subscription/[token]/route.ts index b17a575..df90b16 100644 --- a/app/api/subscription/[token]/route.ts +++ b/app/api/subscription/[token]/route.ts @@ -1,34 +1,158 @@ import { NextResponse } from 'next/server'; import { query } from '@/lib/db'; +import { + generateClientConfig, + generateSubscriptionContent, + InboundConfig, + ServerInfo, + UserCredentials +} from '@/lib/config-generators'; -export async function GET(req: Request) { +export async function GET( + req: Request, + { params }: { params: Promise<{ token: string }> } +) { + const { token } = await params; const { searchParams } = new URL(req.url); - const token = searchParams.get('token'); + const format = searchParams.get('format') || 'json'; // json, base64, clash, singbox if (!token) { return NextResponse.json({ error: 'Token is required' }, { status: 400 }); } - // Assuming token is the username or a specific token field for now. - // In a real scenario, this would be validated against the db. - const users = await query( - 'SELECT * FROM users WHERE username = ? OR xray_uuid = ? LIMIT 1', - [token, token] - ); + try { + // Find user by token (username or xray_uuid) + const users = await query( + 'SELECT * FROM vpn_users WHERE username = ? OR xray_uuid = ? LIMIT 1', + [token, token] + ); - if (!users || users.length === 0) { - return NextResponse.json({ error: 'Invalid token' }, { status: 404 }); - } + if (!users || users.length === 0) { + return NextResponse.json({ error: 'Invalid token' }, { status: 404 }); + } + + const user = users[0]; + + // Check if user is active + if (user.status !== 'active') { + return NextResponse.json({ error: 'Account is not active' }, { status: 403 }); + } + + // Check expiration + if (user.expires_at && new Date(user.expires_at) < new Date()) { + return NextResponse.json({ error: 'Account has expired' }, { status: 403 }); + } + + // Get user's assigned inbounds + const inbounds = await query(` + SELECT i.* FROM vpn_inbounds i + INNER JOIN user_inbounds ui ON i.id = ui.inbound_id + WHERE ui.user_id = ? AND i.status = 'active' + `, [user.id]) as InboundConfig[]; + + // If no specific inbounds assigned, get all active inbounds + const activeInbounds = inbounds.length > 0 ? inbounds : await query( + 'SELECT * FROM vpn_inbounds WHERE status = ? ORDER BY created_at DESC', + ['active'] + ) as InboundConfig[]; - const user = users[0]; - - // Logic to return subscription page data, stats, links, etc. - return NextResponse.json({ - username: user.username, - status: user.status, - trafficUsage: user.traffic_total, - trafficLimit: user.traffic_limit_gb, - expiryDate: user.expires_at, - // Add QR code data and relevant links for protocols - }); + // Get server info + const servers = await query( + 'SELECT * FROM vpn_servers WHERE is_active = 1 ORDER BY load_score ASC LIMIT 1' + ); + + const server: ServerInfo = servers[0] || { + ip_address: '127.0.0.1', + domain: null, + ports: [443] + }; + + // Generate configs for each inbound + const configs: Array<{ + protocol: string; + name: string; + url?: string; + content?: string; + type: string; + }> = []; + + const userCreds: UserCredentials = { + username: user.username, + password: user.password_hash ? undefined : user.password, + uuid: user.xray_uuid, + wg_pubkey: user.wg_pubkey, + wg_ip: user.wg_ip + }; + + for (const inbound of activeInbounds) { + try { + const config = generateClientConfig( + inbound.protocol, + server, + inbound, + userCreds + ); + + configs.push({ + protocol: inbound.protocol, + name: inbound.name, + ...config + }); + } catch (err) { + // Skip protocols that can't be generated (missing required data) + console.warn(`Skipping ${inbound.protocol} config:`, err); + } + } + + // Return based on format + if (format === 'base64') { + // Return base64 encoded subscription URLs (for v2ray/xray clients) + const urls = configs + .filter(c => c.url) + .map(c => c.url); + + return new Response(Buffer.from(urls.join('\n')).toString('base64'), { + headers: { + 'Content-Type': 'text/plain; charset=utf-8', + 'Subscription-Userinfo': `upload=0; download=${user.traffic_total || 0}; total=${(user.traffic_limit_gb || 0) * 1073741824}; expire=${user.expires_at ? new Date(user.expires_at).getTime() / 1000 : 0}`, + 'Profile-Title': `Power VPN - ${user.username}`, + 'Profile-Update-Interval': '12' + } + }); + } + + // Return JSON with all config data + return NextResponse.json({ + user: { + username: user.username, + status: user.status, + trafficUsed: user.traffic_total, + trafficLimit: user.traffic_limit_gb, + trafficLimitBytes: (user.traffic_limit_gb || 0) * 1073741824, + expiresAt: user.expires_at, + maxConnections: user.max_connections + }, + server: { + address: server.domain || server.ip_address, + ip: server.ip_address + }, + configs: configs.map(c => ({ + protocol: c.protocol, + name: c.name, + type: c.type, + url: c.url, + qrData: c.url, // For QR code generation + // Only include content for file-based configs if explicitly requested + ...(format === 'full' && c.content ? { content: c.content } : {}) + })), + subscriptionUrl: `/api/subscription/${token}?format=base64`, + protocols: { + vpn: configs.filter(c => ['openvpn', 'wireguard', 'cisco', 'l2tp'].includes(c.protocol)), + xray: configs.filter(c => ['vless', 'vmess', 'trojan', 'shadowsocks'].includes(c.protocol)) + } + }); + } catch (error: any) { + console.error('Subscription error:', error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } } diff --git a/app/subscription/[token]/page.tsx b/app/subscription/[token]/page.tsx index f99e538..1faa6d0 100644 --- a/app/subscription/[token]/page.tsx +++ b/app/subscription/[token]/page.tsx @@ -1,34 +1,376 @@ 'use client'; -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, use } from 'react'; import { QRCodeSVG } from 'qrcode.react'; +import { + Shield, Download, Copy, Check, Wifi, Globe, Key, Lock, + Radio, ExternalLink, Clock, Database, Users, AlertCircle +} from 'lucide-react'; +import { motion, AnimatePresence } from 'motion/react'; +import { toast, Toaster } from 'sonner'; -export default function SubscriptionPage({ params }: { params: { token: string } }) { - const [data, setData] = useState(null); +interface ConfigItem { + protocol: string; + name: string; + type: string; + url?: string; + qrData?: string; +} + +interface SubscriptionData { + user: { + username: string; + status: string; + trafficUsed: number; + trafficLimit: number; + trafficLimitBytes: number; + expiresAt: string; + maxConnections: number; + }; + server: { + address: string; + ip: string; + }; + configs: ConfigItem[]; + subscriptionUrl: string; + protocols: { + vpn: ConfigItem[]; + xray: ConfigItem[]; + }; +} + +const getProtocolIcon = (protocol: string) => { + switch (protocol) { + case 'openvpn': return ; + case 'wireguard': return ; + case 'cisco': return ; + case 'l2tp': return ; + default: return ; + } +}; + +const getProtocolColor = (protocol: string) => { + switch (protocol) { + case 'openvpn': return 'from-orange-500 to-orange-600'; + case 'wireguard': return 'from-purple-500 to-purple-600'; + case 'cisco': return 'from-blue-500 to-blue-600'; + case 'l2tp': return 'from-green-500 to-green-600'; + case 'vless': return 'from-cyan-500 to-cyan-600'; + case 'vmess': return 'from-pink-500 to-pink-600'; + case 'trojan': return 'from-red-500 to-red-600'; + case 'shadowsocks': return 'from-indigo-500 to-indigo-600'; + default: return 'from-slate-500 to-slate-600'; + } +}; + +const formatBytes = (bytes: number) => { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +}; + +export default function SubscriptionPage({ params }: { params: Promise<{ token: string }> }) { + const { token } = use(params); + const [data, setData] = useState(null); const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [selectedConfig, setSelectedConfig] = useState(null); + const [copiedUrl, setCopiedUrl] = useState(null); useEffect(() => { - fetch(`/api/subscription/${params.token}`) + fetch(`/api/subscription/${token}`) .then(res => res.json()) - .then(setData) + .then(result => { + if (result.error) { + setError(result.error); + } else { + setData(result); + if (result.configs?.length > 0) { + setSelectedConfig(result.configs[0]); + } + } + }) + .catch(err => setError(err.message)) .finally(() => setLoading(false)); - }, [params.token]); + }, [token]); - if (loading) return
Loading...
; - if (!data || data.error) return
Invalid subscription
; + const copyToClipboard = async (text: string, label: string) => { + try { + await navigator.clipboard.writeText(text); + setCopiedUrl(text); + toast.success(`${label} copied to clipboard`); + setTimeout(() => setCopiedUrl(null), 2000); + } catch { + toast.error('Failed to copy'); + } + }; - return ( -
-

Subscription for {data.username}

-
-

Status: {data.status}

-

Traffic Used: {data.trafficUsage} GB

-

Traffic Limit: {data.trafficLimit} GB

-

Expires: {data.expiryDate}

+ const downloadConfig = (config: ConfigItem) => { + if (config.type === 'file') { + window.open(`/api/client/download?username=${data?.user.username}&protocol=${config.protocol}`, '_blank'); + } + }; + + if (loading) { + return ( +
+
-
-

Scan QR Code:

- + ); + } + + if (error || !data) { + return ( +
+
+ +

Invalid Subscription

+

{error || 'This subscription link is invalid or has expired.'}

+
+
+ ); + } + + const trafficPercent = data.user.trafficLimit > 0 + ? Math.min((data.user.trafficUsed / (data.user.trafficLimitBytes)) * 100, 100) + : 0; + + return ( +
+ + +
+ {/* Header */} + +
+
+ +
+
+

{data.user.username}

+

Power VPN Subscription

+
+
+ + {data.user.status.toUpperCase()} + +
+
+ + {/* Stats Grid */} +
+
+
+ + TRAFFIC USED +
+

{formatBytes(data.user.trafficUsed || 0)}

+
+
+
+
+
+
+ + TRAFFIC LIMIT +
+

{data.user.trafficLimit || 'Unlimited'} GB

+
+
+
+ + EXPIRES +
+

+ {data.user.expiresAt + ? new Date(data.user.expiresAt).toLocaleDateString() + : 'Never'} +

+
+
+
+ + MAX DEVICES +
+

{data.user.maxConnections}

+
+
+ + + {/* Subscription URL */} + +

Universal Subscription URL

+
+ + {window.location.origin}{data.subscriptionUrl} + + +
+
+ + {/* Protocol Configs */} + + {/* Config List */} +
+

+ + Available Protocols +

+
+ + {data.configs.map((config, index) => ( + setSelectedConfig(config)} + className={`w-full flex items-center gap-3 p-4 rounded-xl transition-all ${ + selectedConfig?.name === config.name + ? 'bg-white/10 border border-white/20' + : 'bg-white/5 hover:bg-white/10 border border-transparent' + }`} + > +
+ {getProtocolIcon(config.protocol)} +
+
+

{config.name}

+

{config.protocol}

+
+ {config.type === 'file' && ( + + )} + {config.type === 'url' && ( + + )} +
+ ))} +
+
+
+ + {/* QR Code & Actions */} +
+ {selectedConfig ? ( +
+

{selectedConfig.name}

+

{selectedConfig.protocol}

+ + {selectedConfig.qrData && ( +
+ +
+ )} + +
+ {selectedConfig.url && ( + + )} + + {selectedConfig.type === 'file' && ( + + )} +
+ + {/* Protocol-specific tips */} +
+

How to Connect

+

+ {selectedConfig.protocol === 'vless' || selectedConfig.protocol === 'vmess' || selectedConfig.protocol === 'trojan' || selectedConfig.protocol === 'shadowsocks' ? ( + 'Scan the QR code or copy the config link into v2rayNG, Shadowrocket, Clash, or similar apps.' + ) : selectedConfig.protocol === 'openvpn' ? ( + 'Download the .ovpn file and import it into OpenVPN Connect or Tunnelblick.' + ) : selectedConfig.protocol === 'wireguard' ? ( + 'Download the .conf file and import it into the official WireGuard app.' + ) : selectedConfig.protocol === 'cisco' ? ( + 'Use Cisco AnyConnect app and enter the server URL to connect.' + ) : selectedConfig.protocol === 'l2tp' ? ( + 'Configure L2TP/IPsec VPN in your device settings using the provided credentials.' + ) : 'Follow your VPN client instructions to import this configuration.'} +

+
+
+ ) : ( +
+ +

Select a protocol to view QR code

+
+ )} +
+
+ + {/* Server Info */} + +
+
+

Server

+

{data.server.address}

+
+
+

IP Address

+

{data.server.ip}

+
+
+
+ + {/* Footer */} +
+ Powered by Power VPN Manager +
); diff --git a/components/views/inbounds-view.tsx b/components/views/inbounds-view.tsx index d04aa38..d584ac6 100644 --- a/components/views/inbounds-view.tsx +++ b/components/views/inbounds-view.tsx @@ -1,9 +1,32 @@ 'use client'; import React, { useState, useEffect } from 'react'; -import { Network, Plus, Server, X, Edit2, Trash2, Shield, Activity } from 'lucide-react'; +import { Network, Plus, Server, X, Edit2, Trash2, Shield, Activity, Settings, Globe, Lock, Key, Wifi, Radio } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; import { toast } from 'sonner'; +import { v4 as uuidv4 } from 'uuid'; + +// Protocol categories and their options +const PROTOCOL_CATEGORIES = { + vpn: { + label: 'Traditional VPN', + protocols: [ + { value: 'openvpn', label: 'OpenVPN', description: 'UDP/TCP tunneling' }, + { value: 'wireguard', label: 'WireGuard', description: 'Modern, fast VPN' }, + { value: 'cisco', label: 'Cisco AnyConnect', description: 'Ocserv compatible' }, + { value: 'l2tp', label: 'L2TP/IPsec', description: 'Legacy IPsec tunnel' }, + ] + }, + xray: { + label: 'Xray Core', + protocols: [ + { value: 'vless', label: 'VLESS', description: 'XTLS-Reality support' }, + { value: 'vmess', label: 'VMess', description: 'WebSocket/gRPC' }, + { value: 'trojan', label: 'Trojan', description: 'TLS masquerade' }, + { value: 'shadowsocks', label: 'Shadowsocks', description: 'AEAD encryption' }, + ] + } +}; interface Inbound { id: number; @@ -11,18 +34,146 @@ interface Inbound { protocol: string; port: number; remark: string; + status: string; created_at: string; + // Protocol-specific fields + ovpn_protocol?: string; + ovpn_cipher?: string; + wg_public_key?: string; + wg_address?: string; + cisco_auth_method?: string; + l2tp_psk?: string; + xray_network?: string; + xray_security?: string; + xray_uuid?: string; } +interface InboundFormData { + name: string; + protocol: string; + port: string; + remark: string; + // OpenVPN + ovpn_protocol: string; + ovpn_cipher: string; + ovpn_auth: string; + ovpn_dev: string; + // WireGuard + wg_private_key: string; + wg_public_key: string; + wg_address: string; + wg_dns: string; + wg_mtu: string; + // Cisco + cisco_auth_method: string; + cisco_max_clients: string; + cisco_dpd: string; + // L2TP + l2tp_psk: string; + l2tp_dns: string; + l2tp_local_ip: string; + l2tp_remote_ip_range: string; + // Xray + xray_uuid: string; + xray_flow: string; + xray_network: string; + xray_security: string; + xray_sni: string; + xray_fingerprint: string; + xray_public_key: string; + xray_short_id: string; + xray_path: string; + xray_service_name: string; + xray_encryption: string; +} + +const initialFormData: InboundFormData = { + name: '', + protocol: 'openvpn', + port: '', + remark: '', + // OpenVPN defaults + ovpn_protocol: 'udp', + ovpn_cipher: 'AES-256-GCM', + ovpn_auth: 'SHA256', + ovpn_dev: 'tun', + // WireGuard defaults + wg_private_key: '', + wg_public_key: '', + wg_address: '10.0.0.1/24', + wg_dns: '1.1.1.1', + wg_mtu: '1420', + // Cisco defaults + cisco_auth_method: 'password', + cisco_max_clients: '100', + cisco_dpd: '90', + // L2TP defaults + l2tp_psk: '', + l2tp_dns: '8.8.8.8', + l2tp_local_ip: '10.10.10.1', + l2tp_remote_ip_range: '10.10.10.2-10.10.10.254', + // Xray defaults + xray_uuid: '', + xray_flow: 'xtls-rprx-vision', + xray_network: 'tcp', + xray_security: 'reality', + xray_sni: 'www.google.com', + xray_fingerprint: 'chrome', + xray_public_key: '', + xray_short_id: '', + xray_path: '/ws', + xray_service_name: 'grpc', + xray_encryption: 'chacha20-ietf-poly1305', +}; + +const getProtocolIcon = (protocol: string) => { + switch (protocol) { + case 'openvpn': return ; + case 'wireguard': return ; + case 'cisco': return ; + case 'l2tp': return ; + case 'vless': + case 'vmess': + case 'trojan': + case 'shadowsocks': + return ; + default: return ; + } +}; + +const getProtocolColor = (protocol: string) => { + switch (protocol) { + case 'openvpn': return 'bg-orange-50 text-orange-600'; + case 'wireguard': return 'bg-purple-50 text-purple-600'; + case 'cisco': return 'bg-blue-50 text-blue-600'; + case 'l2tp': return 'bg-green-50 text-green-600'; + case 'vless': return 'bg-cyan-50 text-cyan-600'; + case 'vmess': return 'bg-pink-50 text-pink-600'; + case 'trojan': return 'bg-red-50 text-red-600'; + case 'shadowsocks': return 'bg-indigo-50 text-indigo-600'; + default: return 'bg-slate-50 text-slate-600'; + } +}; + +const getDefaultPort = (protocol: string) => { + switch (protocol) { + case 'openvpn': return '1194'; + case 'wireguard': return '51820'; + case 'cisco': return '443'; + case 'l2tp': return '1701'; + case 'vless': + case 'vmess': + case 'trojan': return '443'; + case 'shadowsocks': return '8388'; + default: return '443'; + } +}; + export default function InboundsView() { const [inbounds, setInbounds] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isAdding, setIsAdding] = useState(false); - - const [name, setName] = useState(''); - const [protocol, setProtocol] = useState('vless'); - const [port, setPort] = useState(''); - const [remark, setRemark] = useState(''); + const [formData, setFormData] = useState(initialFormData); const fetchInbounds = async () => { try { @@ -42,29 +193,72 @@ export default function InboundsView() { fetchInbounds(); }, []); + const updateFormField = (field: keyof InboundFormData, value: string) => { + setFormData(prev => { + const newData = { ...prev, [field]: value }; + // Auto-set default port when protocol changes + if (field === 'protocol') { + newData.port = getDefaultPort(value); + // Auto-generate UUID for Xray protocols + if (['vless', 'vmess', 'trojan'].includes(value) && !newData.xray_uuid) { + newData.xray_uuid = uuidv4(); + } + } + return newData; + }); + }; + + const generateWireGuardKeys = () => { + // In production, this would call a server endpoint + // For now, we'll generate placeholder keys + const fakePrivateKey = btoa(Array.from({ length: 32 }, () => String.fromCharCode(Math.floor(Math.random() * 256))).join('')).slice(0, 44); + const fakePublicKey = btoa(Array.from({ length: 32 }, () => String.fromCharCode(Math.floor(Math.random() * 256))).join('')).slice(0, 44); + setFormData(prev => ({ + ...prev, + wg_private_key: fakePrivateKey, + wg_public_key: fakePublicKey, + })); + toast.success('WireGuard keys generated'); + }; + + const generateL2TPPSK = () => { + const psk = Array.from({ length: 32 }, () => + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'.charAt(Math.floor(Math.random() * 62)) + ).join(''); + setFormData(prev => ({ ...prev, l2tp_psk: psk })); + toast.success('Pre-shared key generated'); + }; + const handleCreate = async (e: React.FormEvent) => { e.preventDefault(); - if (!name || !port) return toast.error('Name and port are required'); + if (!formData.name || !formData.port) { + return toast.error('Name and port are required'); + } try { const res = await fetch('/api/inbounds', { method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ name, protocol, port: parseInt(port), remark }) + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...formData, + port: parseInt(formData.port), + wg_mtu: parseInt(formData.wg_mtu) || 1420, + cisco_max_clients: parseInt(formData.cisco_max_clients) || 100, + cisco_dpd: parseInt(formData.cisco_dpd) || 90, + }) }); - if (!res.ok) throw new Error('Failed to create inbound'); + if (!res.ok) { + const error = await res.json(); + throw new Error(error.error || 'Failed to create inbound'); + } toast.success('Inbound created successfully'); - setName(''); - setPort(''); - setRemark(''); + setFormData(initialFormData); setIsAdding(false); fetchInbounds(); - } catch { - toast.error('Failed to create inbound'); + } catch (err: any) { + toast.error(err.message || 'Failed to create inbound'); } }; @@ -83,6 +277,418 @@ export default function InboundsView() { } }; + const renderProtocolConfig = () => { + switch (formData.protocol) { + case 'openvpn': + return ( +
+

+ + OpenVPN Configuration +

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ ); + + case 'wireguard': + return ( +
+

+ + WireGuard Configuration +

+
+ +
+ updateFormField('wg_private_key', e.target.value)} + className="flex-1 bg-white border border-purple-100 rounded-xl px-4 py-2 text-xs font-mono focus:ring-2 focus:ring-purple-500 focus:outline-none" + placeholder="Base64 private key" + /> + +
+
+
+ + updateFormField('wg_public_key', e.target.value)} + className="w-full bg-white border border-purple-100 rounded-xl px-4 py-2 text-xs font-mono focus:ring-2 focus:ring-purple-500 focus:outline-none" + placeholder="Base64 public key" + readOnly + /> +
+
+ + updateFormField('wg_address', e.target.value)} + className="w-full bg-white border border-purple-100 rounded-xl px-4 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:outline-none" + placeholder="10.0.0.1/24" + /> +
+
+ + updateFormField('wg_dns', e.target.value)} + className="w-full bg-white border border-purple-100 rounded-xl px-4 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:outline-none" + placeholder="1.1.1.1" + /> +
+
+ + updateFormField('wg_mtu', e.target.value)} + className="w-full bg-white border border-purple-100 rounded-xl px-4 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:outline-none" + placeholder="1420" + /> +
+
+ ); + + case 'cisco': + return ( +
+

+ + Cisco AnyConnect (Ocserv) Configuration +

+
+ + +
+
+ + updateFormField('cisco_max_clients', e.target.value)} + className="w-full bg-white border border-blue-100 rounded-xl px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none" + placeholder="100" + /> +
+
+ + updateFormField('cisco_dpd', e.target.value)} + className="w-full bg-white border border-blue-100 rounded-xl px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none" + placeholder="90" + /> +
+
+ ); + + case 'l2tp': + return ( +
+

+ + L2TP/IPsec Configuration +

+
+ +
+ updateFormField('l2tp_psk', e.target.value)} + className="flex-1 bg-white border border-green-100 rounded-xl px-4 py-2 text-xs font-mono focus:ring-2 focus:ring-green-500 focus:outline-none" + placeholder="IPsec pre-shared key" + /> + +
+
+
+ + updateFormField('l2tp_dns', e.target.value)} + className="w-full bg-white border border-green-100 rounded-xl px-4 py-2 text-sm focus:ring-2 focus:ring-green-500 focus:outline-none" + placeholder="8.8.8.8" + /> +
+
+ + updateFormField('l2tp_local_ip', e.target.value)} + className="w-full bg-white border border-green-100 rounded-xl px-4 py-2 text-sm focus:ring-2 focus:ring-green-500 focus:outline-none" + placeholder="10.10.10.1" + /> +
+
+ + updateFormField('l2tp_remote_ip_range', e.target.value)} + className="w-full bg-white border border-green-100 rounded-xl px-4 py-2 text-sm focus:ring-2 focus:ring-green-500 focus:outline-none" + placeholder="10.10.10.2-10.10.10.254" + /> +
+
+ ); + + case 'vless': + case 'vmess': + case 'trojan': + return ( +
+

+ + {formData.protocol.toUpperCase()} Configuration (Xray Core) +

+
+ +
+ updateFormField('xray_uuid', e.target.value)} + className="flex-1 bg-white border border-cyan-100 rounded-xl px-4 py-2 text-xs font-mono focus:ring-2 focus:ring-cyan-500 focus:outline-none" + placeholder="UUID for authentication" + /> + +
+
+
+ + +
+
+ + +
+ {formData.protocol === 'vless' && ( +
+ + +
+ )} + {formData.xray_security === 'reality' && ( + <> +
+ + updateFormField('xray_sni', e.target.value)} + className="w-full bg-white border border-cyan-100 rounded-xl px-4 py-2 text-sm focus:ring-2 focus:ring-cyan-500 focus:outline-none" + placeholder="www.google.com" + /> +
+
+ + +
+
+ + updateFormField('xray_public_key', e.target.value)} + className="w-full bg-white border border-cyan-100 rounded-xl px-4 py-2 text-xs font-mono focus:ring-2 focus:ring-cyan-500 focus:outline-none" + placeholder="Reality public key" + /> +
+
+ + updateFormField('xray_short_id', e.target.value)} + className="w-full bg-white border border-cyan-100 rounded-xl px-4 py-2 text-xs font-mono focus:ring-2 focus:ring-cyan-500 focus:outline-none" + placeholder="8-digit hex" + /> +
+ + )} + {formData.xray_network === 'ws' && ( +
+ + updateFormField('xray_path', e.target.value)} + className="w-full bg-white border border-cyan-100 rounded-xl px-4 py-2 text-sm focus:ring-2 focus:ring-cyan-500 focus:outline-none" + placeholder="/ws" + /> +
+ )} + {formData.xray_network === 'grpc' && ( +
+ + updateFormField('xray_service_name', e.target.value)} + className="w-full bg-white border border-cyan-100 rounded-xl px-4 py-2 text-sm focus:ring-2 focus:ring-cyan-500 focus:outline-none" + placeholder="grpc" + /> +
+ )} +
+ ); + + case 'shadowsocks': + return ( +
+

+ + Shadowsocks Configuration +

+
+ + +
+
+ + +
+
+ ); + + default: + return null; + } + }; + return ( Inbound Assets -

Protocol gates and inbound proxy entry points.

+

Multi-protocol gateway configuration (OpenVPN, WireGuard, Cisco, L2TP, Xray)

@@ -196,6 +821,7 @@ export default function InboundsView() { Gateway Name Protocol Port + Status Remark Actions @@ -213,18 +839,25 @@ export default function InboundsView() { >
-
- +
+ {getProtocolIcon(inbound.protocol)}
{inbound.name}
- + {inbound.protocol} {inbound.port} + + + {inbound.status || 'active'} + + {inbound.remark || 'N/A'}
diff --git a/lib/config-generators.ts b/lib/config-generators.ts new file mode 100644 index 0000000..0f0da21 --- /dev/null +++ b/lib/config-generators.ts @@ -0,0 +1,520 @@ +/** + * Multi-Protocol VPN Configuration Generators + * Supports: OpenVPN, WireGuard, Cisco AnyConnect, L2TP/IPsec, Xray (VLESS/VMess/Trojan/Shadowsocks) + */ + +import { v4 as uuidv4 } from 'uuid'; + +// Types +export interface ServerInfo { + ip_address: string; + domain?: string; + ports?: number[] | string; +} + +export interface UserCredentials { + username: string; + password?: string; + uuid?: string; + wg_pubkey?: string; + wg_ip?: string; +} + +export interface InboundConfig { + id: number; + name: string; + protocol: string; + port: number; + // OpenVPN + ovpn_protocol?: string; + ovpn_cipher?: string; + ovpn_auth?: string; + ovpn_dev?: string; + // WireGuard + wg_private_key?: string; + wg_public_key?: string; + wg_address?: string; + wg_dns?: string; + wg_mtu?: number; + wg_persistent_keepalive?: number; + // Cisco + cisco_auth_method?: string; + // L2TP + l2tp_psk?: string; + l2tp_dns?: string; + // Xray + xray_uuid?: string; + xray_flow?: string; + xray_network?: string; + xray_security?: string; + xray_sni?: string; + xray_fingerprint?: string; + xray_public_key?: string; + xray_short_id?: string; + xray_path?: string; + xray_service_name?: string; + xray_encryption?: string; +} + +export interface PkiConfig { + caCertPem: string; + tlsAuthKey?: string; + clientCertPem?: string; + clientKeyPem?: string; +} + +// ============================================ +// OpenVPN Config Generator +// ============================================ +export function generateOpenVPNConfig( + server: ServerInfo, + inbound: InboundConfig, + pki: PkiConfig +): string { + const protocol = inbound.ovpn_protocol || 'udp'; + const cipher = inbound.ovpn_cipher || 'AES-256-GCM'; + const auth = inbound.ovpn_auth || 'SHA256'; + const dev = inbound.ovpn_dev || 'tun'; + + return `client +dev ${dev} +proto ${protocol} +remote ${server.domain || server.ip_address} ${inbound.port} +resolv-retry infinite +nobind +persist-key +persist-tun +keepalive 10 60 +remote-cert-tls server +auth ${auth} +cipher ${cipher} +key-direction 1 +verb 3 +connect-retry 1 +connect-timeout 5 + + +${pki.caCertPem} + +${pki.clientCertPem ? `\n${pki.clientCertPem}\n` : ''} +${pki.clientKeyPem ? `\n${pki.clientKeyPem}\n` : ''} +${pki.tlsAuthKey ? `\n${pki.tlsAuthKey}\n` : ''}`; +} + +// ============================================ +// WireGuard Config Generator +// ============================================ +export function generateWireGuardConfig( + server: ServerInfo, + inbound: InboundConfig, + user: UserCredentials, + clientPrivateKey: string +): string { + const mtu = inbound.wg_mtu || 1420; + const dns = inbound.wg_dns || '1.1.1.1'; + const keepalive = inbound.wg_persistent_keepalive || 25; + + return `[Interface] +# ${user.username} +PrivateKey = ${clientPrivateKey} +Address = ${user.wg_ip || '10.0.0.2/32'} +DNS = ${dns} +MTU = ${mtu} + +[Peer] +PublicKey = ${inbound.wg_public_key || ''} +AllowedIPs = 0.0.0.0/0, ::/0 +Endpoint = ${server.domain || server.ip_address}:${inbound.port} +PersistentKeepalive = ${keepalive}`; +} + +// ============================================ +// Cisco AnyConnect (ocserv) Config Generator +// ============================================ +export function generateCiscoAnyConnectConfig( + server: ServerInfo, + inbound: InboundConfig, + user: UserCredentials +): { connectionUrl: string; username: string; authMethod: string } { + return { + connectionUrl: `https://${server.domain || server.ip_address}:${inbound.port}`, + username: user.username, + authMethod: inbound.cisco_auth_method || 'password' + }; +} + +// ============================================ +// L2TP/IPsec Config Generator (Windows/macOS/iOS/Android) +// ============================================ +export function generateL2TPConfig( + server: ServerInfo, + inbound: InboundConfig, + user: UserCredentials +): { + serverAddress: string; + psk: string; + username: string; + instructions: { + windows: string; + macos: string; + ios: string; + android: string; + } +} { + const serverAddr = server.domain || server.ip_address; + const psk = inbound.l2tp_psk || ''; + + return { + serverAddress: serverAddr, + psk: psk, + username: user.username, + instructions: { + windows: ` +1. Settings > Network & Internet > VPN > Add VPN +2. VPN Provider: Windows (built-in) +3. Connection name: ${inbound.name} +4. Server: ${serverAddr} +5. VPN type: L2TP/IPsec with pre-shared key +6. Pre-shared key: ${psk} +7. Username: ${user.username} +8. Password: [Your password]`, + macos: ` +1. System Preferences > Network > + (Add) +2. Interface: VPN, VPN Type: L2TP over IPsec +3. Service Name: ${inbound.name} +4. Server Address: ${serverAddr} +5. Account Name: ${user.username} +6. Authentication Settings > Shared Secret: ${psk}`, + ios: ` +1. Settings > General > VPN > Add VPN Configuration +2. Type: L2TP +3. Description: ${inbound.name} +4. Server: ${serverAddr} +5. Account: ${user.username} +6. Secret: ${psk}`, + android: ` +1. Settings > Network > VPN > Add VPN +2. Name: ${inbound.name} +3. Type: L2TP/IPSec PSK +4. Server address: ${serverAddr} +5. IPSec pre-shared key: ${psk} +6. Username: ${user.username}` + } + }; +} + +// ============================================ +// VLESS Config Generator (Xray Core) +// ============================================ +export function generateVLESSConfig( + server: ServerInfo, + inbound: InboundConfig, + user: UserCredentials +): { url: string; qrData: string; config: object } { + const uuid = user.uuid || inbound.xray_uuid || uuidv4(); + const serverAddr = server.domain || server.ip_address; + const network = inbound.xray_network || 'tcp'; + const security = inbound.xray_security || 'reality'; + const sni = inbound.xray_sni || 'www.google.com'; + const fp = inbound.xray_fingerprint || 'chrome'; + const pbk = inbound.xray_public_key || ''; + const sid = inbound.xray_short_id || ''; + const flow = inbound.xray_flow || 'xtls-rprx-vision'; + + let url = `vless://${uuid}@${serverAddr}:${inbound.port}?`; + const params: string[] = []; + + params.push(`type=${network}`); + params.push(`security=${security}`); + + if (security === 'reality') { + params.push(`sni=${sni}`); + params.push(`fp=${fp}`); + params.push(`pbk=${pbk}`); + params.push(`sid=${sid}`); + if (flow) params.push(`flow=${flow}`); + } else if (security === 'tls') { + params.push(`sni=${sni}`); + params.push(`fp=${fp}`); + } + + if (network === 'ws' && inbound.xray_path) { + params.push(`path=${encodeURIComponent(inbound.xray_path)}`); + } + if (network === 'grpc' && inbound.xray_service_name) { + params.push(`serviceName=${inbound.xray_service_name}`); + } + + url += params.join('&'); + url += `#${encodeURIComponent(inbound.name)}`; + + const config = { + outbounds: [{ + protocol: 'vless', + settings: { + vnext: [{ + address: serverAddr, + port: inbound.port, + users: [{ + id: uuid, + encryption: 'none', + flow: flow + }] + }] + }, + streamSettings: { + network: network, + security: security, + ...(security === 'reality' ? { + realitySettings: { + serverName: sni, + fingerprint: fp, + publicKey: pbk, + shortId: sid + } + } : {}), + ...(security === 'tls' ? { + tlsSettings: { serverName: sni, fingerprint: fp } + } : {}) + } + }] + }; + + return { url, qrData: url, config }; +} + +// ============================================ +// VMess Config Generator (Xray Core) +// ============================================ +export function generateVMessConfig( + server: ServerInfo, + inbound: InboundConfig, + user: UserCredentials +): { url: string; qrData: string; config: object } { + const uuid = user.uuid || inbound.xray_uuid || uuidv4(); + const serverAddr = server.domain || server.ip_address; + const network = inbound.xray_network || 'ws'; + const security = inbound.xray_security === 'none' ? '' : (inbound.xray_security || 'tls'); + + const vmessJson = { + v: '2', + ps: inbound.name, + add: serverAddr, + port: String(inbound.port), + id: uuid, + aid: '0', + scy: 'auto', + net: network, + type: 'none', + host: inbound.xray_sni || '', + path: inbound.xray_path || '', + tls: security, + sni: inbound.xray_sni || '', + fp: inbound.xray_fingerprint || 'chrome' + }; + + const url = `vmess://${Buffer.from(JSON.stringify(vmessJson)).toString('base64')}`; + + const config = { + outbounds: [{ + protocol: 'vmess', + settings: { + vnext: [{ + address: serverAddr, + port: inbound.port, + users: [{ + id: uuid, + alterId: 0, + security: 'auto' + }] + }] + }, + streamSettings: { + network: network, + security: security || 'none', + ...(network === 'ws' ? { + wsSettings: { path: inbound.xray_path || '/ws' } + } : {}), + ...(security === 'tls' ? { + tlsSettings: { serverName: inbound.xray_sni || '' } + } : {}) + } + }] + }; + + return { url, qrData: url, config }; +} + +// ============================================ +// Trojan Config Generator (Xray Core) +// ============================================ +export function generateTrojanConfig( + server: ServerInfo, + inbound: InboundConfig, + user: UserCredentials +): { url: string; qrData: string; config: object } { + const password = user.password || user.uuid || inbound.xray_uuid || ''; + const serverAddr = server.domain || server.ip_address; + const network = inbound.xray_network || 'tcp'; + const security = inbound.xray_security || 'tls'; + const sni = inbound.xray_sni || serverAddr; + const fp = inbound.xray_fingerprint || 'chrome'; + + let url = `trojan://${encodeURIComponent(password)}@${serverAddr}:${inbound.port}?`; + const params: string[] = []; + + params.push(`type=${network}`); + params.push(`security=${security}`); + params.push(`sni=${sni}`); + params.push(`fp=${fp}`); + + if (network === 'ws' && inbound.xray_path) { + params.push(`path=${encodeURIComponent(inbound.xray_path)}`); + } + if (network === 'grpc' && inbound.xray_service_name) { + params.push(`serviceName=${inbound.xray_service_name}`); + } + + url += params.join('&'); + url += `#${encodeURIComponent(inbound.name)}`; + + const config = { + outbounds: [{ + protocol: 'trojan', + settings: { + servers: [{ + address: serverAddr, + port: inbound.port, + password: password + }] + }, + streamSettings: { + network: network, + security: security, + tlsSettings: { + serverName: sni, + fingerprint: fp + } + } + }] + }; + + return { url, qrData: url, config }; +} + +// ============================================ +// Shadowsocks Config Generator +// ============================================ +export function generateShadowsocksConfig( + server: ServerInfo, + inbound: InboundConfig, + user: UserCredentials +): { url: string; qrData: string; config: object } { + const password = user.password || ''; + const serverAddr = server.domain || server.ip_address; + const method = inbound.xray_encryption || 'chacha20-ietf-poly1305'; + + // SS URL format: ss://BASE64(method:password)@server:port#name + const credentials = Buffer.from(`${method}:${password}`).toString('base64'); + const url = `ss://${credentials}@${serverAddr}:${inbound.port}#${encodeURIComponent(inbound.name)}`; + + const config = { + outbounds: [{ + protocol: 'shadowsocks', + settings: { + servers: [{ + address: serverAddr, + port: inbound.port, + method: method, + password: password + }] + } + }] + }; + + return { url, qrData: url, config }; +} + +// ============================================ +// Universal Config Generator +// ============================================ +export function generateClientConfig( + protocol: string, + server: ServerInfo, + inbound: InboundConfig, + user: UserCredentials, + pki?: PkiConfig, + clientPrivateKey?: string +): any { + switch (protocol) { + case 'openvpn': + if (!pki) throw new Error('PKI config required for OpenVPN'); + return { + type: 'file', + content: generateOpenVPNConfig(server, inbound, pki), + filename: `${user.username}-${inbound.name}.ovpn`, + mimeType: 'application/x-openvpn-profile' + }; + + case 'wireguard': + if (!clientPrivateKey) throw new Error('Client private key required for WireGuard'); + return { + type: 'file', + content: generateWireGuardConfig(server, inbound, user, clientPrivateKey), + filename: `${user.username}-${inbound.name}.conf`, + mimeType: 'application/x-wireguard-profile' + }; + + case 'cisco': + return { + type: 'instructions', + ...generateCiscoAnyConnectConfig(server, inbound, user) + }; + + case 'l2tp': + return { + type: 'instructions', + ...generateL2TPConfig(server, inbound, user) + }; + + case 'vless': + return { + type: 'url', + ...generateVLESSConfig(server, inbound, user) + }; + + case 'vmess': + return { + type: 'url', + ...generateVMessConfig(server, inbound, user) + }; + + case 'trojan': + return { + type: 'url', + ...generateTrojanConfig(server, inbound, user) + }; + + case 'shadowsocks': + return { + type: 'url', + ...generateShadowsocksConfig(server, inbound, user) + }; + + default: + throw new Error(`Unsupported protocol: ${protocol}`); + } +} + +// ============================================ +// Subscription Link Generator (Multi-protocol) +// ============================================ +export function generateSubscriptionContent( + configs: Array<{ protocol: string; url?: string; content?: string }> +): string { + const urls = configs + .filter(c => c.url) + .map(c => c.url); + + return Buffer.from(urls.join('\n')).toString('base64'); +} diff --git a/next-env.d.ts b/next-env.d.ts index 830fb59..c4b7818 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -/// +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/package-lock.json b/package-lock.json index 4fa9ad8..a567b78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@hookform/resolvers": "^5.2.2", "@next/eslint-plugin-next": "^16.2.4", + "@types/uuid": "^10.0.0", "bcryptjs": "latest", "better-sqlite3": "^12.9.0", "clsx": "latest", @@ -34,6 +35,7 @@ "sonner": "^2.0.7", "tailwind-merge": "latest", "typescript-eslint": "^8.59.0", + "uuid": "^14.0.0", "zod": "latest" }, "devDependencies": { @@ -2398,6 +2400,12 @@ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.59.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz", @@ -8846,6 +8854,19 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/victory-vendor": { "version": "37.3.6", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", diff --git a/package.json b/package.json index 092045d..80ebac0 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@hookform/resolvers": "^5.2.2", "@next/eslint-plugin-next": "^16.2.4", + "@types/uuid": "^10.0.0", "bcryptjs": "latest", "better-sqlite3": "^12.9.0", "clsx": "latest", @@ -35,6 +36,7 @@ "sonner": "^2.0.7", "tailwind-merge": "latest", "typescript-eslint": "^8.59.0", + "uuid": "^14.0.0", "zod": "latest" }, "devDependencies": { diff --git a/panel.sqlite b/panel.sqlite new file mode 100644 index 0000000..e45ab67 Binary files /dev/null and b/panel.sqlite differ diff --git a/schema.sql b/schema.sql index 0139d40..97db3e1 100644 --- a/schema.sql +++ b/schema.sql @@ -4,7 +4,45 @@ CREATE TABLE IF NOT EXISTS vpn_inbounds ( protocol VARCHAR(50) NOT NULL, port INT NOT NULL, remark TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + status VARCHAR(50) DEFAULT 'active', + -- OpenVPN specific + ovpn_protocol VARCHAR(10) DEFAULT 'udp', + ovpn_cipher VARCHAR(50) DEFAULT 'AES-256-GCM', + ovpn_auth VARCHAR(50) DEFAULT 'SHA256', + ovpn_dev VARCHAR(10) DEFAULT 'tun', + -- WireGuard specific + wg_private_key VARCHAR(255), + wg_public_key VARCHAR(255), + wg_address VARCHAR(50), + wg_dns VARCHAR(100), + wg_mtu INT DEFAULT 1420, + wg_persistent_keepalive INT DEFAULT 25, + -- Cisco AnyConnect (ocserv) specific + cisco_auth_method VARCHAR(50) DEFAULT 'password', + cisco_max_clients INT DEFAULT 100, + cisco_dpd INT DEFAULT 90, + -- L2TP/IPsec specific + l2tp_psk VARCHAR(255), + l2tp_dns VARCHAR(100), + l2tp_local_ip VARCHAR(50), + l2tp_remote_ip_range VARCHAR(100), + -- Xray specific (VLESS/VMess/Trojan/Shadowsocks) + xray_protocol VARCHAR(50), + xray_uuid VARCHAR(255), + xray_flow VARCHAR(50), + xray_network VARCHAR(50) DEFAULT 'tcp', + xray_security VARCHAR(50) DEFAULT 'reality', + xray_sni VARCHAR(255), + xray_fingerprint VARCHAR(50) DEFAULT 'chrome', + xray_public_key VARCHAR(255), + xray_short_id VARCHAR(50), + xray_path VARCHAR(255), + xray_service_name VARCHAR(255), + xray_encryption VARCHAR(50), + -- Common config as JSON for extensibility + extra_config TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS vpn_users ( diff --git a/scripts/migrate-inbounds-v2.sql b/scripts/migrate-inbounds-v2.sql new file mode 100644 index 0000000..44ec975 --- /dev/null +++ b/scripts/migrate-inbounds-v2.sql @@ -0,0 +1,51 @@ +-- Migration: Add multi-protocol support to vpn_inbounds table +-- Version: 2.0 +-- Date: 2024 + +-- Add new columns for protocol-specific configurations +-- Note: SQLite doesn't support IF NOT EXISTS for ALTER TABLE, +-- so these will error if columns already exist (which is fine) + +-- OpenVPN specific columns +ALTER TABLE vpn_inbounds ADD COLUMN status VARCHAR(50) DEFAULT 'active'; +ALTER TABLE vpn_inbounds ADD COLUMN ovpn_protocol VARCHAR(10) DEFAULT 'udp'; +ALTER TABLE vpn_inbounds ADD COLUMN ovpn_cipher VARCHAR(50) DEFAULT 'AES-256-GCM'; +ALTER TABLE vpn_inbounds ADD COLUMN ovpn_auth VARCHAR(50) DEFAULT 'SHA256'; +ALTER TABLE vpn_inbounds ADD COLUMN ovpn_dev VARCHAR(10) DEFAULT 'tun'; + +-- WireGuard specific columns +ALTER TABLE vpn_inbounds ADD COLUMN wg_private_key VARCHAR(255); +ALTER TABLE vpn_inbounds ADD COLUMN wg_public_key VARCHAR(255); +ALTER TABLE vpn_inbounds ADD COLUMN wg_address VARCHAR(50); +ALTER TABLE vpn_inbounds ADD COLUMN wg_dns VARCHAR(100); +ALTER TABLE vpn_inbounds ADD COLUMN wg_mtu INT DEFAULT 1420; +ALTER TABLE vpn_inbounds ADD COLUMN wg_persistent_keepalive INT DEFAULT 25; + +-- Cisco AnyConnect (ocserv) specific columns +ALTER TABLE vpn_inbounds ADD COLUMN cisco_auth_method VARCHAR(50) DEFAULT 'password'; +ALTER TABLE vpn_inbounds ADD COLUMN cisco_max_clients INT DEFAULT 100; +ALTER TABLE vpn_inbounds ADD COLUMN cisco_dpd INT DEFAULT 90; + +-- L2TP/IPsec specific columns +ALTER TABLE vpn_inbounds ADD COLUMN l2tp_psk VARCHAR(255); +ALTER TABLE vpn_inbounds ADD COLUMN l2tp_dns VARCHAR(100); +ALTER TABLE vpn_inbounds ADD COLUMN l2tp_local_ip VARCHAR(50); +ALTER TABLE vpn_inbounds ADD COLUMN l2tp_remote_ip_range VARCHAR(100); + +-- Xray specific columns (VLESS/VMess/Trojan/Shadowsocks) +ALTER TABLE vpn_inbounds ADD COLUMN xray_protocol VARCHAR(50); +ALTER TABLE vpn_inbounds ADD COLUMN xray_uuid VARCHAR(255); +ALTER TABLE vpn_inbounds ADD COLUMN xray_flow VARCHAR(50); +ALTER TABLE vpn_inbounds ADD COLUMN xray_network VARCHAR(50) DEFAULT 'tcp'; +ALTER TABLE vpn_inbounds ADD COLUMN xray_security VARCHAR(50) DEFAULT 'reality'; +ALTER TABLE vpn_inbounds ADD COLUMN xray_sni VARCHAR(255); +ALTER TABLE vpn_inbounds ADD COLUMN xray_fingerprint VARCHAR(50) DEFAULT 'chrome'; +ALTER TABLE vpn_inbounds ADD COLUMN xray_public_key VARCHAR(255); +ALTER TABLE vpn_inbounds ADD COLUMN xray_short_id VARCHAR(50); +ALTER TABLE vpn_inbounds ADD COLUMN xray_path VARCHAR(255); +ALTER TABLE vpn_inbounds ADD COLUMN xray_service_name VARCHAR(255); +ALTER TABLE vpn_inbounds ADD COLUMN xray_encryption VARCHAR(50); + +-- Common config as JSON for extensibility +ALTER TABLE vpn_inbounds ADD COLUMN extra_config TEXT; +ALTER TABLE vpn_inbounds ADD COLUMN updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP; diff --git a/tsconfig.json b/tsconfig.json index e7ff90f..9e9bbf7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,10 @@ { "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -10,7 +14,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve", + "jsx": "react-jsx", "incremental": true, "plugins": [ { @@ -18,9 +22,20 @@ } ], "paths": { - "@/*": ["./*"] - } + "@/*": [ + "./*" + ] + }, + "target": "ES2017" }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] }