Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/api/backup/export/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ export const dynamic = 'force-dynamic';

export async function GET() {
try {
const users: any[] = await query('SELECT id, username, role, parent_id, status, traffic_limit_gb, traffic_total, traffic_up, traffic_down, max_connections, port, main_protocol, expires_at, created_at, last_connected FROM vpn_users');
const servers: any[] = await query('SELECT id, name, ip_address, domain, protocol, status, is_active, load_score, connected_clients, bandwidth_ingress, bandwidth_egress, latency_ms, last_check, ports, supports_openvpn, supports_cisco, supports_l2tp, supports_wireguard, supports_xray FROM vpn_servers');
const users: any[] = await query('SELECT id, username, role, parent_id, status, traffic_limit_gb, traffic_total, max_connections, port, main_protocol, expires_at, created_at, last_connected FROM vpn_users');
const servers: any[] = await query('SELECT id, name, ip_address, domain, protocol, status, is_active, load_score, bandwidth_ingress, bandwidth_egress, latency_ms, ports, supports_openvpn, supports_cisco, supports_l2tp, supports_wireguard, supports_xray FROM vpn_servers');
const settings: any[] = await query('SELECT `key`, `value` FROM settings');
const resellers: any[] = await query('SELECT id, reseller_id, max_users, allocated_traffic_gb FROM reseller_limits');

Expand Down
114 changes: 71 additions & 43 deletions app/api/backup/import/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,76 +17,104 @@ export async function POST(req: Request) {
try {
const body = await req.json();
const validated = BackupSchema.safeParse(body);

if (!validated.success) {
return NextResponse.json({ error: 'Invalid backup format', details: validated.error.format() }, { status: 400 });
}

const backup = validated.data;
const connection = await pool.getConnection();

try {
await connection.beginTransaction();

if (backup.settings && backup.settings.length > 0) {
for (const row of backup.settings) {
await connection.query('INSERT INTO settings (`key`, `value`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)', [row.key, row.value]);
await connection.query(
'INSERT INTO settings (`key`, `value`) VALUES (?, ?) ON CONFLICT(`key`) DO UPDATE SET `value` = excluded.`value`',
[row.key, row.value]
);
}
}

if (backup.users && backup.users.length > 0) {
for (const user of backup.users) {
// Update only specific safe fields to avoid breaking sensitive data if it exists
await connection.query(`
INSERT INTO vpn_users
(id, username, role, parent_id, status, traffic_limit_gb, traffic_total, traffic_up, traffic_down, max_connections, port, main_protocol, expires_at, created_at, last_connected)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
username = VALUES(username), role = VALUES(role), parent_id = VALUES(parent_id), status = VALUES(status),
traffic_limit_gb = VALUES(traffic_limit_gb), traffic_total = VALUES(traffic_total), traffic_up = VALUES(traffic_up),
traffic_down = VALUES(traffic_down), max_connections = VALUES(max_connections), port = VALUES(port),
main_protocol = VALUES(main_protocol), expires_at = VALUES(expires_at)
`, [
user.id, user.username, user.role, user.parent_id,
user.status, user.traffic_limit_gb, user.traffic_total, user.traffic_up, user.traffic_down, user.max_connections,
user.port, user.main_protocol, user.expires_at, user.created_at, user.last_connected
]);
await connection.query(
`INSERT INTO vpn_users
(id, username, role, parent_id, status, traffic_limit_gb, traffic_total, max_connections, port, main_protocol, expires_at, created_at, last_connected)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
username = excluded.username,
role = excluded.role,
parent_id = excluded.parent_id,
status = excluded.status,
traffic_limit_gb = excluded.traffic_limit_gb,
traffic_total = excluded.traffic_total,
max_connections = excluded.max_connections,
port = excluded.port,
main_protocol = excluded.main_protocol,
expires_at = excluded.expires_at,
last_connected = excluded.last_connected`,
[
user.id, user.username, user.role, user.parent_id ?? null,
user.status, user.traffic_limit_gb ?? 0, user.traffic_total ?? 0,
user.max_connections ?? 1, user.port ?? null, user.main_protocol ?? null,
user.expires_at ?? null, user.created_at ?? null, user.last_connected ?? null
]
);
}
}

if (backup.servers && backup.servers.length > 0) {
for (const server of backup.servers) {
await connection.query(`
INSERT INTO vpn_servers
(id, name, ip_address, domain, protocol, status, is_active, load_score, connected_clients, bandwidth_ingress, bandwidth_egress, latency_ms, last_check, ports, supports_openvpn, supports_cisco, supports_l2tp, supports_wireguard, supports_xray)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
name = VALUES(name), ip_address = VALUES(ip_address), domain = VALUES(domain), protocol = VALUES(protocol),
status = VALUES(status), is_active = VALUES(is_active), load_score = VALUES(load_score),
connected_clients = VALUES(connected_clients), bandwidth_ingress = VALUES(bandwidth_ingress),
bandwidth_egress = VALUES(bandwidth_egress), latency_ms = VALUES(latency_ms), last_check = VALUES(last_check),
ports = VALUES(ports), supports_openvpn = VALUES(supports_openvpn), supports_cisco = VALUES(supports_cisco),
supports_l2tp = VALUES(supports_l2tp), supports_wireguard = VALUES(supports_wireguard), supports_xray = VALUES(supports_xray)
`, [
server.id, server.name, server.ip_address, server.domain || null, server.protocol, server.status, server.is_active,
server.load_score, server.connected_clients, server.bandwidth_ingress, server.bandwidth_egress, server.latency_ms, server.last_check, JSON.stringify(server.ports || []),
server.supports_openvpn, server.supports_cisco, server.supports_l2tp, server.supports_wireguard, server.supports_xray
]);
await connection.query(
`INSERT INTO vpn_servers
(id, name, ip_address, domain, protocol, status, is_active, load_score, bandwidth_ingress, bandwidth_egress, latency_ms, ports, supports_openvpn, supports_cisco, supports_l2tp, supports_wireguard, supports_xray)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
name = excluded.name,
ip_address = excluded.ip_address,
domain = excluded.domain,
protocol = excluded.protocol,
status = excluded.status,
is_active = excluded.is_active,
load_score = excluded.load_score,
bandwidth_ingress = excluded.bandwidth_ingress,
bandwidth_egress = excluded.bandwidth_egress,
latency_ms = excluded.latency_ms,
ports = excluded.ports,
supports_openvpn = excluded.supports_openvpn,
supports_cisco = excluded.supports_cisco,
supports_l2tp = excluded.supports_l2tp,
supports_wireguard = excluded.supports_wireguard,
supports_xray = excluded.supports_xray`,
[
server.id, server.name, server.ip_address, server.domain || null,
server.protocol, server.status, server.is_active ? 1 : 0, server.load_score ?? 0,
server.bandwidth_ingress ?? 0, server.bandwidth_egress ?? 0, server.latency_ms ?? 0,
JSON.stringify(server.ports || []),
server.supports_openvpn ? 1 : 0, server.supports_cisco ? 1 : 0,
server.supports_l2tp ? 1 : 0, server.supports_wireguard ? 1 : 0,
server.supports_xray ? 1 : 0
]
);
}
}

if (backup.resellers && backup.resellers.length > 0) {
for (const res of backup.resellers) {
await connection.query(`
INSERT INTO reseller_limits
(id, reseller_id, max_users, allocated_traffic_gb)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
max_users = VALUES(max_users), allocated_traffic_gb = VALUES(allocated_traffic_gb)
`, [res.id, res.reseller_id, res.max_users, res.allocated_traffic_gb]);
await connection.query(
`INSERT INTO reseller_limits
(id, reseller_id, max_users, allocated_traffic_gb)
VALUES (?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
max_users = excluded.max_users,
allocated_traffic_gb = excluded.allocated_traffic_gb`,
[res.id, res.reseller_id, res.max_users, res.allocated_traffic_gb]
);
}
}

await connection.commit();
logger.info('Backup imported successfully');
} catch (err) {
Expand Down
42 changes: 22 additions & 20 deletions app/api/client/download/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { NextResponse } from 'next/server';
import { query } from '@/lib/db';
import {
import {
generateClientConfig,
InboundConfig,
ServerInfo,
UserCredentials
UserCredentials
} from '@/lib/config-generators';
import { getPkiService } from '@/lib/pki-service';
import crypto from 'crypto';

export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
Expand Down Expand Up @@ -37,7 +38,7 @@ export async function GET(req: Request) {
if (inboundId) {
const inbounds = await query(
'SELECT * FROM vpn_inbounds WHERE id = ? LIMIT 1',
[parseInt(inboundId)]
[parseInt(inboundId, 10)]
);
inbound = inbounds[0] || null;
} else if (protocol) {
Expand All @@ -56,7 +57,7 @@ export async function GET(req: Request) {
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,
Expand All @@ -73,16 +74,22 @@ export async function GET(req: Request) {

// 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();
// Persist a per-user WireGuard private key on first use so subsequent
// downloads return a stable config instead of rotating the key.
let clientPrivateKey: string | undefined = user.wg_privkey;
if (!clientPrivateKey) {
clientPrivateKey = generateWireGuardPrivateKey();
await query(
'UPDATE vpn_users SET wg_privkey = ? WHERE id = ?',
[clientPrivateKey, user.id]
);
}
config = generateClientConfig(inbound.protocol, server, inbound, userCreds, undefined, clientPrivateKey);
} else {
config = generateClientConfig(inbound.protocol, server, inbound, userCreds);
Expand All @@ -98,7 +105,6 @@ export async function GET(req: Request) {
});
}

// For URL-based configs (Xray protocols)
if (config.type === 'url' && config.url) {
return NextResponse.json({
success: true,
Expand All @@ -109,7 +115,6 @@ export async function GET(req: Request) {
});
}

// For instruction-based configs (Cisco, L2TP)
if (config.type === 'instructions') {
return NextResponse.json({
success: true,
Expand All @@ -131,14 +136,11 @@ export async function GET(req: Request) {
}
}

// Helper function to generate WireGuard private key
// WireGuard private keys are 32 random bytes encoded as base64.
// Note: this is not a Curve25519-clamped key — for production use, derive the
// key with a proper WireGuard library and persist the matching public key on
// the server. For Power VPN's single-tenant flow we only need a deterministic,
// cryptographically-random base64 string of the right shape.
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 + '=';
return crypto.randomBytes(32).toString('base64');
}
42 changes: 8 additions & 34 deletions app/api/migrate/route.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,15 @@
import { NextResponse } from 'next/server';
import pool from '@/lib/db';
import { validateConnection } from '@/lib/db';

/**
* Schema migrations for the SQLite-backed control plane are now applied
* automatically inside `validateConnection()` (see `lib/db.ts`). Hitting this
* endpoint simply triggers the connection bootstrap so any pending column
* additions land before the next request.
*/
export async function GET() {
try {
const migrations = [
// 28: Add user_id to logs and foreign key
`ALTER TABLE logs
ADD COLUMN IF NOT EXISTS user_id INT,
ADD FOREIGN KEY IF NOT EXISTS (user_id) REFERENCES vpn_users(id) ON DELETE SET NULL;`,

// 29: Unify traffic naming
`ALTER TABLE vpn_users
CHANGE COLUMN IF EXISTS traffic_up traffic_uploaded_bytes BIGINT DEFAULT 0,
CHANGE COLUMN IF EXISTS traffic_down traffic_downloaded_bytes BIGINT DEFAULT 0;`,

// 30: JSON indexes (using generated columns for portability)
`ALTER TABLE vpn_users
ADD COLUMN IF NOT EXISTS config_server_name VARCHAR(255) GENERATED ALWAYS AS (custom_config->>'$.server_name') VIRTUAL,
ADD INDEX IF NOT EXISTS idx_config_server_name (config_server_name);`,

// 31: Settings table improvements
`ALTER TABLE settings
ADD COLUMN IF NOT EXISTS value_type ENUM('string', 'number', 'boolean', 'json') DEFAULT 'string',
ADD COLUMN IF NOT EXISTS is_encrypted BOOLEAN DEFAULT FALSE;`,

// 32: Parent ID index
`ALTER TABLE vpn_users ADD INDEX IF NOT EXISTS idx_parent_id (parent_id);`
];

for (const sql of migrations) {
try {
await pool.execute(sql);
} catch (err: any) {
console.warn('Migration step failed (might already exist):', err.message);
}
}

await validateConnection();
return NextResponse.json({ message: 'Migrations completed' });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
Expand Down
5 changes: 2 additions & 3 deletions app/api/subscription/[token]/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { NextResponse } from 'next/server';
import { query } from '@/lib/db';
import {
generateClientConfig,
generateSubscriptionContent,
import {
generateClientConfig,
InboundConfig,
ServerInfo,
UserCredentials
Expand Down
2 changes: 1 addition & 1 deletion app/api/users/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { auditLog } from '@/lib/audit-logger';

const UpdateUserSchema = z.object({
role: z.enum(['admin', 'user', 'reseller']).optional().nullable(),
status: z.enum(['active', 'disabled', 'suspended', 'inactive']).optional().nullable(),
status: z.enum(['active', 'inactive', 'disabled', 'suspended', 'revoked']).optional().nullable(),
traffic_limit_gb: z.number().optional().nullable(),
max_connections: z.number().optional().nullable(),
expires_at: z.string().optional().nullable(),
Expand Down
19 changes: 14 additions & 5 deletions app/api/users/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const CreateUserSchema = z.object({
username: z.string().min(3),
password: z.string().min(6).optional().nullable(),
role: z.enum(['admin', 'user', 'reseller']).default('user'),
status: z.enum(['active', 'inactive', 'suspended']).default('active'),
status: z.enum(['active', 'inactive', 'disabled', 'suspended', 'revoked']).default('active'),
traffic_limit_gb: z.number().min(0).default(10),
max_connections: z.number().min(1).default(1),
expires_at: z.string().optional().nullable(),
Expand Down Expand Up @@ -106,9 +106,9 @@ export async function POST(request: Request) {

// In a real app, hash password here
const [result]: any = await pool.execute(
`INSERT INTO vpn_users
(username, password_hash, role, status, traffic_limit_gb, max_connections, expires_at, created_at, cisco_password, l2tp_password, wg_pubkey, xray_uuid, port, main_protocol)
VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), ?, ?, ?, ?, ?, ?)`,
`INSERT INTO vpn_users
(username, password_hash, role, status, traffic_limit_gb, max_connections, expires_at, cisco_password, l2tp_password, wg_pubkey, xray_uuid, port, main_protocol)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[username, password || null, role, status, traffic_limit_gb, max_connections, finalExpiresAt, cisco_password || null, l2tp_password || null, wg_pubkey || null, xray_uuid || null, port || null, main_protocol || null]
);

Expand All @@ -133,7 +133,16 @@ export async function POST(request: Request) {
}
}, { status: 201 });
} catch (error: any) {
if (error.code === 'ER_DUP_ENTRY') {
// SQLite reports unique-constraint violations via SQLITE_CONSTRAINT_UNIQUE.
// Keep the legacy MySQL ER_DUP_ENTRY check so the same code paths work
// if the deployment is later swapped back to MySQL.
const isDuplicate =
error.code === 'SQLITE_CONSTRAINT_UNIQUE' ||
error.code === 'SQLITE_CONSTRAINT' ||
error.code === 'ER_DUP_ENTRY' ||
/UNIQUE constraint failed/i.test(error.message || '');

if (isDuplicate) {
return NextResponse.json({
error: {
code: 'DUPLICATE_USER',
Expand Down
Loading
Loading