|
|
|
@@ -46,6 +46,8 @@ interface IPersistedClient {
|
|
|
|
|
assignedIp?: string;
|
|
|
|
|
noisePublicKey: string;
|
|
|
|
|
wgPublicKey: string;
|
|
|
|
|
/** WireGuard private key — stored so exports and QR codes produce valid configs */
|
|
|
|
|
wgPrivateKey?: string;
|
|
|
|
|
createdAt: number;
|
|
|
|
|
updatedAt: number;
|
|
|
|
|
expiresAt?: string;
|
|
|
|
@@ -188,7 +190,7 @@ export class VpnManager {
|
|
|
|
|
description: opts.description,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Persist client entry (without private keys)
|
|
|
|
|
// Persist client entry (including WG private key for export/QR)
|
|
|
|
|
const persisted: IPersistedClient = {
|
|
|
|
|
clientId: bundle.entry.clientId,
|
|
|
|
|
enabled: bundle.entry.enabled ?? true,
|
|
|
|
@@ -197,6 +199,7 @@ export class VpnManager {
|
|
|
|
|
assignedIp: bundle.entry.assignedIp,
|
|
|
|
|
noisePublicKey: bundle.entry.publicKey,
|
|
|
|
|
wgPublicKey: bundle.entry.wgPublicKey || '',
|
|
|
|
|
wgPrivateKey: bundle.secrets?.wgPrivateKey,
|
|
|
|
|
createdAt: Date.now(),
|
|
|
|
|
updatedAt: Date.now(),
|
|
|
|
|
expiresAt: bundle.entry.expiresAt,
|
|
|
|
@@ -265,11 +268,12 @@ export class VpnManager {
|
|
|
|
|
if (!this.vpnServer) throw new Error('VPN server not running');
|
|
|
|
|
const bundle = await this.vpnServer.rotateClientKey(clientId);
|
|
|
|
|
|
|
|
|
|
// Update persisted entry with new public keys
|
|
|
|
|
// Update persisted entry with new keys (including private key for export/QR)
|
|
|
|
|
const client = this.clients.get(clientId);
|
|
|
|
|
if (client) {
|
|
|
|
|
client.noisePublicKey = bundle.entry.publicKey;
|
|
|
|
|
client.wgPublicKey = bundle.entry.wgPublicKey || '';
|
|
|
|
|
client.wgPrivateKey = bundle.secrets?.wgPrivateKey;
|
|
|
|
|
client.updatedAt = Date.now();
|
|
|
|
|
await this.persistClient(client);
|
|
|
|
|
}
|
|
|
|
@@ -278,11 +282,24 @@ export class VpnManager {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Export a client config (without secrets).
|
|
|
|
|
* Export a client config. Injects the stored WG private key for complete configs.
|
|
|
|
|
*/
|
|
|
|
|
public async exportClientConfig(clientId: string, format: 'smartvpn' | 'wireguard'): Promise<string> {
|
|
|
|
|
if (!this.vpnServer) throw new Error('VPN server not running');
|
|
|
|
|
return this.vpnServer.exportClientConfig(clientId, format);
|
|
|
|
|
let config = await this.vpnServer.exportClientConfig(clientId, format);
|
|
|
|
|
|
|
|
|
|
// Inject stored WG private key so exports produce valid, scannable configs
|
|
|
|
|
if (format === 'wireguard') {
|
|
|
|
|
const persisted = this.clients.get(clientId);
|
|
|
|
|
if (persisted?.wgPrivateKey) {
|
|
|
|
|
config = config.replace(
|
|
|
|
|
'[Interface]\n',
|
|
|
|
|
`[Interface]\nPrivateKey = ${persisted.wgPrivateKey}\n`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return config;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Tag-based access control ───────────────────────────────────────────
|
|
|
|
|