more updates

This commit is contained in:
Enki 2024-12-30 07:07:53 +00:00
parent dd2a1b4c83
commit b44860a0ab
6 changed files with 552 additions and 355 deletions

View File

@ -15,158 +15,223 @@ from ..utils.db.models import SubscriptionStatus
load_dotenv() load_dotenv()
# Enhanced logging configuration
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(),
logging.FileHandler('vpn_provisioner.log')
]
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Constants
BASE_DIR = Path(__file__).resolve().parent.parent.parent BASE_DIR = Path(__file__).resolve().parent.parent.parent
PLAYBOOK_PATH = BASE_DIR / 'ansible' / 'playbooks' / 'vpn_provision.yml' PLAYBOOK_PATH = BASE_DIR / 'ansible' / 'playbooks' / 'vpn_provision.yml'
CLEANUP_PLAYBOOK = BASE_DIR / 'ansible' / 'playbooks' / 'vpn_cleanup.yml' CLEANUP_PLAYBOOK = BASE_DIR / 'ansible' / 'playbooks' / 'vpn_cleanup.yml'
class WebhookError(Exception):
"""Custom exception for webhook handling errors"""
pass
def get_vault_values(): def get_vault_values():
"""Get decrypted values from Ansible vault""" """Get decrypted values from Ansible vault"""
try: try:
vault_pass = os.getenv('ANSIBLE_VAULT_PASSWORD', '') vault_pass = os.getenv('ANSIBLE_VAULT_PASSWORD')
if not vault_pass: if not vault_pass:
raise Exception("Vault password not found in environment variables") raise WebhookError("Vault password not found in environment variables")
with tempfile.NamedTemporaryFile(mode='w', delete=False) as vault_pass_file: with tempfile.NamedTemporaryFile(mode='w', delete=False) as vault_pass_file:
vault_pass_file.write(vault_pass) vault_pass_file.write(vault_pass)
vault_pass_file.flush() vault_pass_file.flush()
# Execute ansible-vault with error checking
result = subprocess.run( result = subprocess.run(
['ansible-vault', 'view', str(BASE_DIR / 'ansible/group_vars/vpn_servers/vault.yml')], ['ansible-vault', 'view', str(BASE_DIR / 'ansible/group_vars/vpn_servers/vault.yml')],
capture_output=True, capture_output=True,
text=True, text=True,
env={**os.environ, 'ANSIBLE_VAULT_PASSWORD_FILE': vault_pass_file.name} env={**os.environ, 'ANSIBLE_VAULT_PASSWORD_FILE': vault_pass_file.name},
check=True # This will raise CalledProcessError if command fails
) )
os.unlink(vault_pass_file.name) os.unlink(vault_pass_file.name)
if result.returncode != 0: try:
raise Exception(f"Failed to decrypt vault: {result.stderr}") vault_contents = yaml.safe_load(result.stdout)
if not vault_contents:
raise WebhookError("Empty vault contents")
vault_contents = yaml.safe_load(result.stdout) # Validate required vault values
vault_contents['webhook_full_url'] = ( required_keys = ['btcpay_base_url', 'btcpay_api_key', 'btcpay_store_id', 'webhook_secret']
f"{vault_contents['btcpay_base_url']}" missing_keys = [key for key in required_keys if key not in vault_contents]
f"{vault_contents['btcpay_webhook_path']}" if missing_keys:
) raise WebhookError(f"Missing required vault values: {', '.join(missing_keys)}")
return vault_contents vault_contents['webhook_full_url'] = (
f"{vault_contents['btcpay_base_url']}"
f"{vault_contents.get('btcpay_webhook_path', '/webhook/vpn')}"
)
return vault_contents
except yaml.YAMLError as e:
raise WebhookError(f"Failed to parse vault contents: {str(e)}")
except subprocess.CalledProcessError as e:
logger.error(f"Ansible vault command failed: {e.stderr}")
raise WebhookError("Failed to decrypt vault")
except Exception as e: except Exception as e:
logger.error(f"Error reading vault: {str(e)}") logger.error(f"Error reading vault: {traceback.format_exc()}")
raise raise WebhookError(f"Vault operation failed: {str(e)}")
def verify_signature(payload_body, signature_header): def verify_signature(payload_body: bytes, signature_header: str) -> bool:
"""Verify BTCPay webhook signature""" """
try: Verify BTCPay webhook signature with proper error handling
vault_values = get_vault_values()
secret = vault_values['webhook_secret'] Args:
payload_body: Raw request body bytes
signature_header: BTCPay-Sig header value
Returns:
bool: True if signature is valid
"""
try:
if not signature_header:
logger.error("Missing signature header")
return False
vault_values = get_vault_values()
webhook_secret = vault_values.get('webhook_secret')
if not webhook_secret:
logger.error("Webhook secret not found in vault")
return False
# Generate expected signature
expected_signature = hmac.new( expected_signature = hmac.new(
secret.encode('utf-8'), webhook_secret.encode('utf-8'),
payload_body, payload_body,
hashlib.sha256 hashlib.sha256
).hexdigest() ).hexdigest()
# Constant-time comparison
return hmac.compare_digest( return hmac.compare_digest(
signature_header.lower(), signature_header.lower(),
f"sha256={expected_signature}".lower() f"sha256={expected_signature}".lower()
) )
except Exception as e: except Exception as e:
logger.error(f"Signature verification failed: {str(e)}") logger.error(f"Signature verification failed: {traceback.format_exc()}")
return False return False
def run_ansible_playbook(invoice_id): def run_ansible_playbook(invoice_id: str, cleanup: bool = False) -> subprocess.CompletedProcess:
"""Run the VPN provisioning playbook""" """
vault_pass = os.getenv('ANSIBLE_VAULT_PASSWORD', '') Run the appropriate Ansible playbook with proper error handling
if not vault_pass:
raise Exception("Vault password not found in environment variables")
with tempfile.NamedTemporaryFile(mode='w', delete=False) as vault_pass_file: Args:
vault_pass_file.write(vault_pass) invoice_id: BTCPay invoice ID
vault_pass_file.flush() cleanup: Whether to run cleanup playbook instead of provision
cmd = [ Returns:
'ansible-playbook', subprocess.CompletedProcess: Playbook execution result
str(PLAYBOOK_PATH),
'-i', str(BASE_DIR / 'inventory.ini'),
'-e', f'invoice_id={invoice_id}',
'--vault-password-file', vault_pass_file.name,
'-vvv'
]
logger.info(f"Running ansible-playbook command: {' '.join(cmd)}") Raises:
WebhookError: If playbook execution fails
"""
try:
vault_pass = os.getenv('ANSIBLE_VAULT_PASSWORD')
if not vault_pass:
raise WebhookError("Vault password not found in environment variables")
result = subprocess.run( with tempfile.NamedTemporaryFile(mode='w', delete=False) as vault_pass_file:
cmd, vault_pass_file.write(vault_pass)
capture_output=True, vault_pass_file.flush()
text=True
) playbook = CLEANUP_PLAYBOOK if cleanup else PLAYBOOK_PATH
os.unlink(vault_pass_file.name) cmd = [
return result 'ansible-playbook',
str(playbook),
'-i', str(BASE_DIR / 'inventory.ini'),
'-e', f'invoice_id={invoice_id}',
'--vault-password-file', vault_pass_file.name,
'-vvv'
]
logger.info(f"Running ansible-playbook command: {' '.join(cmd)}")
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=True # This will raise CalledProcessError if playbook fails
)
if "fatal:" in result.stdout or "fatal:" in result.stderr:
raise WebhookError("Ansible playbook reported fatal error")
return result
except subprocess.CalledProcessError as e:
logger.error(f"Playbook execution failed: {e.stderr}")
raise WebhookError(f"Ansible playbook failed with return code {e.returncode}")
except Exception as e:
logger.error(f"Error running playbook: {traceback.format_exc()}")
raise WebhookError(f"Playbook execution failed: {str(e)}")
finally:
if 'vault_pass_file' in locals():
os.unlink(vault_pass_file.name)
def handle_subscription_status(data): def handle_subscription_status(data: dict) -> tuple:
"""Handle SubscriptionStatusUpdated webhook""" """Handle SubscriptionStatusUpdated webhook"""
sub_id = data['subscriptionId'] try:
status = data['status'] sub_id = data.get('subscriptionId')
status = data.get('status')
logger.info(f"Processing subscription status update: {sub_id} -> {status}")
subscription = DatabaseManager.get_subscription_by_invoice(sub_id)
if not subscription:
logger.error(f"Subscription {sub_id} not found")
return jsonify({"error": "Subscription not found"}), 404
if status == 'Active':
DatabaseManager.activate_subscription(sub_id)
else:
# Run cleanup for inactive subscriptions
result = subprocess.run([
'ansible-playbook',
str(CLEANUP_PLAYBOOK),
'-i', str(BASE_DIR / 'inventory.ini'),
'-e', f'subscription_id={sub_id}',
'-vvv'
], capture_output=True, text=True)
if result.returncode != 0: if not sub_id or not status:
logger.error(f"Failed to clean up subscription {sub_id}: {result.stderr}") return jsonify({"error": "Missing required fields"}), 400
DatabaseManager.expire_subscription(subscription.id) logger.info(f"Processing subscription status update: {sub_id} -> {status}")
logger.info(f"Subscription {sub_id} is no longer active")
return jsonify({ subscription = DatabaseManager.get_subscription_by_invoice(sub_id)
"status": "success", if not subscription:
"message": f"Subscription {sub_id} status updated to {status}" logger.error(f"Subscription {sub_id} not found")
}) return jsonify({"error": "Subscription not found"}), 404
def handle_subscription_renewal(data): if status == 'Active':
"""Handle SubscriptionRenewalRequested webhook""" DatabaseManager.activate_subscription(sub_id)
sub_id = data['subscriptionId'] else:
logger.info(f"Processing subscription renewal request: {sub_id}") # Run cleanup for inactive subscriptions
try:
subscription = DatabaseManager.get_subscription_by_invoice(sub_id) result = run_ansible_playbook(sub_id, cleanup=True)
if not subscription: logger.info(f"Cleanup playbook completed for {sub_id}: {result.stdout}")
logger.error(f"Subscription {sub_id} not found") except WebhookError as e:
return jsonify({"error": "Subscription not found"}), 404 logger.error(f"Failed to clean up subscription {sub_id}: {str(e)}")
# TODO: Send renewal notification to user DatabaseManager.expire_subscription(subscription.id)
return jsonify({ logger.info(f"Subscription {sub_id} is no longer active")
"status": "success",
"message": f"Subscription {sub_id} renewal requested" return jsonify({
}) "status": "success",
"message": f"Subscription {sub_id} status updated to {status}"
}), 200
except Exception as e:
logger.error(f"Error handling subscription status: {traceback.format_exc()}")
return jsonify({"error": str(e)}), 500
def handle_payment_webhook(request): def handle_payment_webhook(request) -> tuple:
"""Handle BTCPay Server webhook for VPN provisioning""" """
Handle BTCPay Server webhook for VPN provisioning
Returns:
tuple: (response, status_code)
"""
try: try:
vault_values = get_vault_values() vault_values = get_vault_values()
logger.info(f"Processing webhook on endpoint: {vault_values['webhook_full_url']}") logger.info(f"Processing webhook on endpoint: {vault_values['webhook_full_url']}")
# Verify signature
signature = request.headers.get('BTCPay-Sig') signature = request.headers.get('BTCPay-Sig')
if not signature: if not signature:
logger.error("Missing BTCPay-Sig header") logger.error("Missing BTCPay-Sig header")
@ -177,7 +242,16 @@ def handle_payment_webhook(request):
logger.error("Invalid signature") logger.error("Invalid signature")
return jsonify({"error": "Invalid signature"}), 401 return jsonify({"error": "Invalid signature"}), 401
data = request.json # Parse and validate payload
try:
data = request.json
except Exception as e:
logger.error(f"Invalid JSON payload: {str(e)}")
return jsonify({"error": "Invalid JSON"}), 400
if not data:
return jsonify({"error": "Empty payload"}), 400
logger.info(f"Received webhook data: {data}") logger.info(f"Received webhook data: {data}")
# Handle test webhooks # Handle test webhooks
@ -187,67 +261,61 @@ def handle_payment_webhook(request):
return jsonify({ return jsonify({
"status": "success", "status": "success",
"message": "Test webhook acknowledged" "message": "Test webhook acknowledged"
}) }), 200
webhook_type = data.get('type') webhook_type = data.get('type')
if not webhook_type:
return jsonify({"error": "Missing webhook type"}), 400
# Handle different webhook types
if webhook_type == 'SubscriptionStatusUpdated': if webhook_type == 'SubscriptionStatusUpdated':
return handle_subscription_status(data) return handle_subscription_status(data)
elif webhook_type == 'SubscriptionRenewalRequested': elif webhook_type == 'InvoiceSettled' or webhook_type == 'InvoicePaymentSettled':
return handle_subscription_renewal(data)
elif webhook_type in ['InvoiceSettled', 'InvoicePaymentSettled']:
invoice_id = data.get('invoiceId')
if not invoice_id: if not invoice_id:
logger.error("Missing invoiceId in webhook data") logger.error("Missing invoiceId in webhook data")
return jsonify({"error": "Missing invoiceId"}), 400 return jsonify({"error": "Missing invoiceId"}), 400
# Get subscription and run Ansible playbook try:
logger.info(f"Starting VPN provisioning for invoice {invoice_id}") # Run VPN provisioning
result = run_ansible_playbook(invoice_id) logger.info(f"Starting VPN provisioning for invoice {invoice_id}")
result = run_ansible_playbook(invoice_id)
if result.returncode != 0:
error_msg = f"Ansible playbook failed with return code {result.returncode}" # Update subscription status
logger.error(error_msg) subscription = DatabaseManager.get_subscription_by_invoice(invoice_id)
logger.error(f"Ansible stdout: {result.stdout}") if subscription:
logger.error(f"Ansible stderr: {result.stderr}") subscription = DatabaseManager.activate_subscription(invoice_id)
DatabaseManager.record_payment(
subscription.user_id,
subscription.id,
invoice_id,
data.get('amount', 0)
)
logger.info(f"VPN provisioning completed for invoice {invoice_id}")
return jsonify({
"status": "success",
"invoice_id": invoice_id,
"message": "VPN provisioning completed"
}), 200
except WebhookError as e:
logger.error(f"VPN provisioning failed: {str(e)}")
return jsonify({ return jsonify({
"error": "Provisioning failed", "error": "Provisioning failed",
"details": error_msg, "details": str(e)
"stdout": result.stdout,
"stderr": result.stderr
}), 500 }), 500
# Get subscription and activate it
subscription = DatabaseManager.get_subscription_by_invoice(invoice_id)
if subscription:
subscription = DatabaseManager.activate_subscription(invoice_id)
DatabaseManager.record_payment(
subscription.user_id,
subscription.id,
invoice_id,
data.get('amount', 0)
)
logger.info(f"VPN provisioning completed for invoice {invoice_id}")
return jsonify({
"status": "success",
"invoice_id": invoice_id,
"message": "VPN provisioning completed"
})
else: else:
logger.info(f"Received {webhook_type} webhook - no action required") logger.info(f"Received {webhook_type} webhook - no action required")
return jsonify({ return jsonify({
"status": "success", "status": "success",
"message": f"Webhook {webhook_type} acknowledged" "message": f"Webhook {webhook_type} acknowledged"
}) }), 200
except WebhookError as e:
logger.error(f"Webhook error: {str(e)}")
return jsonify({"error": str(e)}), 500
except Exception as e: except Exception as e:
logger.error(f"Error processing webhook: {str(e)}") logger.error(f"Unexpected error: {traceback.format_exc()}")
logger.error(traceback.format_exc()) return jsonify({"error": "Internal server error"}), 500
return jsonify({
"error": str(e),
"traceback": traceback.format_exc()
}), 500

View File

@ -1,137 +1,116 @@
// Base64 encoding/decoding utilities // Utility functions for duration formatting
const b64 = { function formatDuration(hours) {
encode: array => btoa(String.fromCharCode.apply(null, array)), if (hours < 24) {
decode: str => Uint8Array.from(atob(str), c => c.charCodeAt(0)) return `${hours} hour${hours === 1 ? '' : 's'}`;
}; }
if (hours < 168) {
return `${hours / 24} day${hours === 24 ? '' : 's'}`;
}
if (hours < 720) {
return `${Math.floor(hours / 168)} week${hours === 168 ? '' : 's'}`;
}
return `${Math.floor(hours / 720)} month${hours === 720 ? '' : 's'}`;
}
async function generateKeyPair() { // Price calculation with volume discounts
const keyPair = await window.crypto.subtle.generateKey( async function calculatePrice(hours) {
{ try {
name: 'X25519', const response = await fetch('/api/calculate-price', {
namedCurve: 'X25519', method: 'POST',
}, headers: { 'Content-Type': 'application/json' },
true, body: JSON.stringify({ hours: parseInt(hours) })
['deriveKey', 'deriveBits'] });
);
const privateKey = await window.crypto.subtle.exportKey('raw', keyPair.privateKey); if (!response.ok) {
const publicKey = await window.crypto.subtle.exportKey('raw', keyPair.publicKey); throw new Error('Failed to calculate price');
}
const data = await response.json();
return {
price: data.price,
formattedDuration: formatDuration(hours)
};
} catch (error) {
console.error('Price calculation failed:', error);
throw error;
}
}
// Form initialization and event handling
function initializeForm(config) {
const {
formId = 'subscription-form',
sliderId = 'duration-slider',
priceDisplayId = 'price-display',
durationDisplayId = 'duration-display',
presetButtonClass = 'duration-preset'
} = config;
const form = document.getElementById(formId);
const slider = document.getElementById(sliderId);
const priceDisplay = document.getElementById(priceDisplayId);
const durationDisplay = document.getElementById(durationDisplayId);
const presetButtons = document.querySelectorAll(`.${presetButtonClass}`);
if (!form || !slider || !priceDisplay || !durationDisplay) {
throw new Error('Required elements not found');
}
return { return {
privateKey: b64.encode(new Uint8Array(privateKey)), form,
publicKey: b64.encode(new Uint8Array(publicKey)) slider,
priceDisplay,
durationDisplay,
presetButtons
}; };
} }
document.addEventListener('DOMContentLoaded', async function() { // Main pricing interface
const form = document.getElementById('subscription-form'); export const Pricing = {
const slider = document.getElementById('duration-slider'); async init(config = {}) {
const durationDisplay = document.getElementById('duration-display');
const priceDisplay = document.getElementById('price-display');
const presetButtons = document.querySelectorAll('.duration-preset');
const userIdInput = document.getElementById('user-id');
const publicKeyInput = document.getElementById('public-key');
const regenerateButton = document.getElementById('regenerate-keys');
let currentKeyPair = null;
function formatDuration(hours) {
if (hours < 24) return `${hours} hour${hours === 1 ? '' : 's'}`;
if (hours < 168) return `${hours / 24} day${hours === 24 ? '' : 's'}`;
if (hours < 720) return `${Math.floor(hours / 168)} week${hours === 168 ? '' : 's'}`;
return `${Math.floor(hours / 720)} month${hours === 720 ? '' : 's'}`;
}
async function generateNewKeys() {
try { try {
currentKeyPair = await generateKeyPair(); const elements = initializeForm(config);
publicKeyInput.value = currentKeyPair.publicKey; const { form, slider, priceDisplay, durationDisplay, presetButtons } = elements;
// Save private key to localStorage // Update price when duration changes
const keyData = { const updateDisplay = async (hours) => {
privateKey: currentKeyPair.privateKey, try {
publicKey: currentKeyPair.publicKey, const { price, formattedDuration } = await calculatePrice(hours);
createdAt: new Date().toISOString() priceDisplay.textContent = price;
durationDisplay.textContent = formattedDuration;
} catch (error) {
console.error('Failed to update price display:', error);
priceDisplay.textContent = 'Error';
durationDisplay.textContent = 'Error calculating duration';
}
}; };
localStorage.setItem(`vpn_keys_${userIdInput.value}`, JSON.stringify(keyData));
} catch (error) {
console.error('Failed to generate keys:', error);
alert('Failed to generate WireGuard keys. Please try again.');
}
}
async function initializeForm() { // Set up event listeners
// Generate user ID slider.addEventListener('input', () => updateDisplay(slider.value));
userIdInput.value = crypto.randomUUID();
// Generate initial keys
await generateNewKeys();
}
async function updatePrice(hours) { presetButtons.forEach(button => {
try { button.addEventListener('click', (e) => {
const response = await fetch('/api/calculate-price', { const hours = e.target.dataset.hours;
method: 'POST', slider.value = hours;
headers: { 'Content-Type': 'application/json' }, updateDisplay(hours);
body: JSON.stringify({ hours: parseInt(hours) }) });
}); });
const data = await response.json();
priceDisplay.textContent = data.price; // Initial price calculation
durationDisplay.textContent = formatDuration(hours); await updateDisplay(slider.value);
return {
updatePrice: updateDisplay,
getCurrentDuration: () => parseInt(slider.value)
};
} catch (error) { } catch (error) {
console.error('Error calculating price:', error); console.error('Failed to initialize pricing:', error);
throw error;
} }
} },
// Event listeners formatDuration,
slider.addEventListener('input', () => updatePrice(slider.value)); calculatePrice
};
regenerateButton.addEventListener('click', generateNewKeys); export default Pricing;
presetButtons.forEach(button => {
button.addEventListener('click', (e) => {
const hours = e.target.dataset.hours;
slider.value = hours;
updatePrice(hours);
});
});
form.addEventListener('submit', async (e) => {
e.preventDefault();
if (!currentKeyPair) {
alert('No keys generated. Please refresh the page.');
return;
}
try {
const response = await fetch('/create-invoice', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
duration: parseInt(slider.value),
userId: userIdInput.value,
publicKey: currentKeyPair.publicKey
})
});
if (!response.ok) {
throw new Error('Failed to create invoice');
}
const data = await response.json();
window.location.href = data.checkout_url;
} catch (error) {
console.error('Error creating invoice:', error);
alert('Failed to create payment invoice. Please try again.');
}
});
// Initialize the form
await initializeForm();
// Initial price calculation
updatePrice(slider.value);
});

View File

@ -1,54 +1,134 @@
// Base64 encoding/decoding utilities // Base64 encoding/decoding utilities with error handling
const b64 = { const b64 = {
encode: array => btoa(String.fromCharCode.apply(null, array)), encode: (array) => {
decode: str => Uint8Array.from(atob(str), c => c.charCodeAt(0)) try {
}; return btoa(String.fromCharCode.apply(null, array));
} catch (error) {
async function generateKeyPair() { console.error('Base64 encoding failed:', error);
// Generate a random key pair using Web Crypto API throw new Error('Failed to encode key data');
const keyPair = await window.crypto.subtle.generateKey( }
{ },
name: 'X25519', decode: (str) => {
namedCurve: 'X25519', try {
}, return Uint8Array.from(atob(str), c => c.charCodeAt(0));
true, } catch (error) {
['deriveKey', 'deriveBits'] console.error('Base64 decoding failed:', error);
); throw new Error('Failed to decode key data');
}
// Export keys in raw format
const privateKey = await window.crypto.subtle.exportKey('raw', keyPair.privateKey);
const publicKey = await window.crypto.subtle.exportKey('raw', keyPair.publicKey);
// Convert to base64
return {
privateKey: b64.encode(new Uint8Array(privateKey)),
publicKey: b64.encode(new Uint8Array(publicKey))
};
} }
};
export async function generateWireGuardConfig(serverPublicKey, serverEndpoint, address) {
const keys = await generateKeyPair(); // Key storage management
const keyStorage = {
return { store: (userId, keyData) => {
keys, try {
config: `[Interface] const data = {
PrivateKey = ${keys.privateKey} privateKey: keyData.privateKey,
Address = ${address} publicKey: keyData.publicKey,
DNS = 1.1.1.1 createdAt: new Date().toISOString()
};
[Peer] localStorage.setItem(`vpn_keys_${userId}`, JSON.stringify(data));
PublicKey = ${serverPublicKey} } catch (error) {
Endpoint = ${serverEndpoint} console.error('Failed to store keys:', error);
AllowedIPs = 0.0.0.0/0 throw new Error('Failed to save key data');
PersistentKeepalive = 25` }
}; },
retrieve: (userId) => {
try {
const data = localStorage.getItem(`vpn_keys_${userId}`);
return data ? JSON.parse(data) : null;
} catch (error) {
console.error('Failed to retrieve keys:', error);
throw new Error('Failed to retrieve key data');
}
},
remove: (userId) => {
try {
localStorage.removeItem(`vpn_keys_${userId}`);
} catch (error) {
console.error('Failed to remove keys:', error);
}
} }
};
export async function generateKeys() {
try { // Main key generation function
async function generateKeyPair() {
try {
const keyPair = await window.crypto.subtle.generateKey(
{
name: 'X25519',
namedCurve: 'X25519',
},
true,
['deriveKey', 'deriveBits']
);
const privateKey = await window.crypto.subtle.exportKey('raw', keyPair.privateKey);
const publicKey = await window.crypto.subtle.exportKey('raw', keyPair.publicKey);
return {
privateKey: b64.encode(new Uint8Array(privateKey)),
publicKey: b64.encode(new Uint8Array(publicKey))
};
} catch (error) {
console.error('Key generation failed:', error);
throw new Error('Failed to generate WireGuard keys');
}
}
// Key validation function
function validateKey(key) {
try {
const decoded = b64.decode(key);
return decoded.length === 32;
} catch {
return false;
}
}
// WireGuard config generation
function generateConfig(keys, serverPublicKey, serverEndpoint, clientIp) {
if (!keys || !serverPublicKey || !serverEndpoint || !clientIp) {
throw new Error('Missing required configuration parameters');
}
return `[Interface]
PrivateKey = ${keys.privateKey}
Address = ${clientIp}/24
DNS = 1.1.1.1
[Peer]
PublicKey = ${serverPublicKey}
Endpoint = ${serverEndpoint}:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25`;
}
// Main interface for key management
export const WireGuard = {
generateKeys: async () => {
return await generateKeyPair(); return await generateKeyPair();
} catch (error) { },
console.error('Failed to generate WireGuard keys:', error);
throw error; saveKeys: (userId, keyPair) => {
} if (!validateKey(keyPair.publicKey) || !validateKey(keyPair.privateKey)) {
} throw new Error('Invalid key data');
}
keyStorage.store(userId, keyPair);
},
getKeys: (userId) => {
return keyStorage.retrieve(userId);
},
removeKeys: (userId) => {
keyStorage.remove(userId);
},
generateConfig,
validateKey
};
export default WireGuard;

View File

@ -17,7 +17,8 @@
} }
} }
</script> </script>
<script src="{{ url_for('static', filename='js/pricing.js') }}" defer></script> <!-- Import pricing.js from the correct path -->
<script type="module" src="/static/js/pricing.js" defer></script>
</head> </head>
<body class="bg-dark text-gray-100"> <body class="bg-dark text-gray-100">
{% block content %}{% endblock %} {% block content %}{% endblock %}

View File

@ -74,4 +74,36 @@
</form> </form>
</div> </div>
</div> </div>
<script type="module">
import { WireGuard } from '/static/js/utils/wireguard.js';
import { Pricing } from '/static/js/pricing.js';
document.addEventListener('DOMContentLoaded', async function() {
const userId = Date.now().toString(); // Generate a unique user ID without prefix
document.getElementById('user-id').value = userId;
// Generate keys and set public key
const keyPair = await WireGuard.generateKeys();
document.getElementById('public-key').value = keyPair.publicKey;
// Store keys in local storage
WireGuard.saveKeys(userId, keyPair);
// Initialize pricing
Pricing.init({
formId: 'subscription-form',
sliderId: 'duration-slider',
priceDisplayId: 'price-display',
durationDisplayId: 'duration-display',
presetButtonClass: 'duration-preset'
});
// Regenerate keys button
document.getElementById('regenerate-keys').addEventListener('click', async function() {
const newKeyPair = await WireGuard.generateKeys();
document.getElementById('public-key').value = newKeyPair.publicKey;
WireGuard.saveKeys(userId, newKeyPair);
});
});
</script>
{% endblock %} {% endblock %}

View File

@ -22,6 +22,12 @@
> >
Copy Configuration Copy Configuration
</button> </button>
<button
id="download-config"
class="mt-2 w-full bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded transition-colors"
>
Download Configuration
</button>
</div> </div>
<!-- Installation Instructions --> <!-- Installation Instructions -->
@ -58,53 +64,84 @@
</div> </div>
</div> </div>
<script> <script type="module">
document.addEventListener('DOMContentLoaded', function() { import { WireGuard } from '/static/js/utils/wireguard.js';
document.addEventListener('DOMContentLoaded', async function() {
const userId = new URLSearchParams(window.location.search).get('userId'); const userId = new URLSearchParams(window.location.search).get('userId');
if (userId) { if (!userId) {
// Retrieve keys from localStorage console.error('No user ID found in URL');
const keyData = localStorage.getItem(`vpn_keys_${userId}`); return;
if (keyData) { }
const keys = JSON.parse(keyData);
// Make config section visible
document.getElementById('config-section').classList.remove('hidden');
// Get server details from response
fetch(`/api/subscription/config?userId=${userId}`)
.then(response => response.json())
.then(data => {
const config = `[Interface]
PrivateKey = ${keys.privateKey}
Address = ${data.clientIp}/24
DNS = 1.1.1.1
[Peer] try {
PublicKey = ${data.serverPublicKey} // Retrieve keys from storage
Endpoint = ${data.serverEndpoint}:51820 const keyData = WireGuard.getKeys(userId);
AllowedIPs = 0.0.0.0/0 if (!keyData) {
PersistentKeepalive = 25`; console.error('No key data found');
return;
document.getElementById('wireguard-config').textContent = config;
})
.catch(error => {
console.error('Error fetching configuration:', error);
});
// Setup copy button
document.getElementById('copy-config').addEventListener('click', function() {
const config = document.getElementById('wireguard-config').textContent;
navigator.clipboard.writeText(config).then(() => {
this.textContent = 'Copied!';
setTimeout(() => {
this.textContent = 'Copy Configuration';
}, 2000);
});
});
// Clear keys from localStorage after showing config
localStorage.removeItem(`vpn_keys_${userId}`);
} }
// Hardcoded server details for demonstration purposes
const serverData = {
serverPublicKey: 'your-server-public-key',
serverEndpoint: 'your-server-endpoint',
clientIp: 'your-client-ip'
};
// Generate WireGuard config
const config = WireGuard.generateConfig(
keyData,
serverData.serverPublicKey,
serverData.serverEndpoint,
serverData.clientIp
);
// Show configuration section
const configSection = document.getElementById('config-section');
configSection.classList.remove('hidden');
// Display config
document.getElementById('wireguard-config').textContent = config;
// Setup copy button
document.getElementById('copy-config').addEventListener('click', async function() {
try {
await navigator.clipboard.writeText(config);
this.textContent = 'Copied!';
setTimeout(() => {
this.textContent = 'Copy Configuration';
}, 2000);
} catch (error) {
console.error('Failed to copy config:', error);
alert('Failed to copy configuration. Please try manually copying.');
}
});
// Setup download button
document.getElementById('download-config').addEventListener('click', function() {
try {
const blob = new Blob([config], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'wireguard.conf';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (error) {
console.error('Failed to download config:', error);
alert('Failed to download configuration. Please try copying manually.');
}
});
// Clear keys from storage after successful display
WireGuard.removeKeys(userId);
} catch (error) {
console.error('Error setting up configuration:', error);
alert('Failed to load VPN configuration. Please contact support.');
} }
}); });
</script> </script>