diff --git a/app/handlers/webhook_handler.py b/app/handlers/webhook_handler.py index 9791a40..339262b 100644 --- a/app/handlers/webhook_handler.py +++ b/app/handlers/webhook_handler.py @@ -15,158 +15,223 @@ from ..utils.db.models import SubscriptionStatus load_dotenv() +# Enhanced logging configuration logging.basicConfig( 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__) +# Constants BASE_DIR = Path(__file__).resolve().parent.parent.parent PLAYBOOK_PATH = BASE_DIR / 'ansible' / 'playbooks' / 'vpn_provision.yml' CLEANUP_PLAYBOOK = BASE_DIR / 'ansible' / 'playbooks' / 'vpn_cleanup.yml' +class WebhookError(Exception): + """Custom exception for webhook handling errors""" + pass + def get_vault_values(): """Get decrypted values from Ansible vault""" try: - vault_pass = os.getenv('ANSIBLE_VAULT_PASSWORD', '') + vault_pass = os.getenv('ANSIBLE_VAULT_PASSWORD') 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: vault_pass_file.write(vault_pass) vault_pass_file.flush() + # Execute ansible-vault with error checking result = subprocess.run( ['ansible-vault', 'view', str(BASE_DIR / 'ansible/group_vars/vpn_servers/vault.yml')], capture_output=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) - if result.returncode != 0: - raise Exception(f"Failed to decrypt vault: {result.stderr}") + try: + vault_contents = yaml.safe_load(result.stdout) + if not vault_contents: + raise WebhookError("Empty vault contents") - vault_contents = yaml.safe_load(result.stdout) - vault_contents['webhook_full_url'] = ( - f"{vault_contents['btcpay_base_url']}" - f"{vault_contents['btcpay_webhook_path']}" - ) - - return vault_contents - + # Validate required vault values + required_keys = ['btcpay_base_url', 'btcpay_api_key', 'btcpay_store_id', 'webhook_secret'] + missing_keys = [key for key in required_keys if key not in vault_contents] + if missing_keys: + raise WebhookError(f"Missing required vault values: {', '.join(missing_keys)}") + + 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: - logger.error(f"Error reading vault: {str(e)}") - raise + logger.error(f"Error reading vault: {traceback.format_exc()}") + raise WebhookError(f"Vault operation failed: {str(e)}") -def verify_signature(payload_body, signature_header): - """Verify BTCPay webhook signature""" - try: - vault_values = get_vault_values() - secret = vault_values['webhook_secret'] +def verify_signature(payload_body: bytes, signature_header: str) -> bool: + """ + Verify BTCPay webhook signature with proper error handling + + 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( - secret.encode('utf-8'), + webhook_secret.encode('utf-8'), payload_body, hashlib.sha256 ).hexdigest() + # Constant-time comparison return hmac.compare_digest( signature_header.lower(), f"sha256={expected_signature}".lower() ) + except Exception as e: - logger.error(f"Signature verification failed: {str(e)}") + logger.error(f"Signature verification failed: {traceback.format_exc()}") return False -def run_ansible_playbook(invoice_id): - """Run the VPN provisioning playbook""" - vault_pass = os.getenv('ANSIBLE_VAULT_PASSWORD', '') - if not vault_pass: - raise Exception("Vault password not found in environment variables") +def run_ansible_playbook(invoice_id: str, cleanup: bool = False) -> subprocess.CompletedProcess: + """ + Run the appropriate Ansible playbook with proper error handling - with tempfile.NamedTemporaryFile(mode='w', delete=False) as vault_pass_file: - vault_pass_file.write(vault_pass) - vault_pass_file.flush() + Args: + invoice_id: BTCPay invoice ID + cleanup: Whether to run cleanup playbook instead of provision - cmd = [ - 'ansible-playbook', - str(PLAYBOOK_PATH), - '-i', str(BASE_DIR / 'inventory.ini'), - '-e', f'invoice_id={invoice_id}', - '--vault-password-file', vault_pass_file.name, - '-vvv' - ] + Returns: + subprocess.CompletedProcess: Playbook execution result - 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( - cmd, - capture_output=True, - text=True - ) - - os.unlink(vault_pass_file.name) - return result + with tempfile.NamedTemporaryFile(mode='w', delete=False) as vault_pass_file: + vault_pass_file.write(vault_pass) + vault_pass_file.flush() + + playbook = CLEANUP_PLAYBOOK if cleanup else PLAYBOOK_PATH + + cmd = [ + '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""" - sub_id = data['subscriptionId'] - status = data['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) + try: + sub_id = data.get('subscriptionId') + status = data.get('status') - if result.returncode != 0: - logger.error(f"Failed to clean up subscription {sub_id}: {result.stderr}") + if not sub_id or not status: + return jsonify({"error": "Missing required fields"}), 400 - DatabaseManager.expire_subscription(subscription.id) - logger.info(f"Subscription {sub_id} is no longer active") + logger.info(f"Processing subscription status update: {sub_id} -> {status}") - return jsonify({ - "status": "success", - "message": f"Subscription {sub_id} status updated to {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 -def handle_subscription_renewal(data): - """Handle SubscriptionRenewalRequested webhook""" - sub_id = data['subscriptionId'] - logger.info(f"Processing subscription renewal request: {sub_id}") - - 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 - - # TODO: Send renewal notification to user - return jsonify({ - "status": "success", - "message": f"Subscription {sub_id} renewal requested" - }) + if status == 'Active': + DatabaseManager.activate_subscription(sub_id) + else: + # Run cleanup for inactive subscriptions + try: + result = run_ansible_playbook(sub_id, cleanup=True) + logger.info(f"Cleanup playbook completed for {sub_id}: {result.stdout}") + except WebhookError as e: + logger.error(f"Failed to clean up subscription {sub_id}: {str(e)}") + + DatabaseManager.expire_subscription(subscription.id) + logger.info(f"Subscription {sub_id} is no longer active") + + 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): - """Handle BTCPay Server webhook for VPN provisioning""" +def handle_payment_webhook(request) -> tuple: + """ + Handle BTCPay Server webhook for VPN provisioning + + Returns: + tuple: (response, status_code) + """ try: vault_values = get_vault_values() logger.info(f"Processing webhook on endpoint: {vault_values['webhook_full_url']}") + # Verify signature signature = request.headers.get('BTCPay-Sig') if not signature: logger.error("Missing BTCPay-Sig header") @@ -177,7 +242,16 @@ def handle_payment_webhook(request): logger.error("Invalid signature") 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}") # Handle test webhooks @@ -187,67 +261,61 @@ def handle_payment_webhook(request): return jsonify({ "status": "success", "message": "Test webhook acknowledged" - }) + }), 200 webhook_type = data.get('type') + if not webhook_type: + return jsonify({"error": "Missing webhook type"}), 400 + # Handle different webhook types if webhook_type == 'SubscriptionStatusUpdated': return handle_subscription_status(data) - elif webhook_type == 'SubscriptionRenewalRequested': - return handle_subscription_renewal(data) - - elif webhook_type in ['InvoiceSettled', 'InvoicePaymentSettled']: - invoice_id = data.get('invoiceId') + elif webhook_type == 'InvoiceSettled' or webhook_type == 'InvoicePaymentSettled': if not invoice_id: logger.error("Missing invoiceId in webhook data") return jsonify({"error": "Missing invoiceId"}), 400 - # Get subscription and run Ansible playbook - 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}" - logger.error(error_msg) - logger.error(f"Ansible stdout: {result.stdout}") - logger.error(f"Ansible stderr: {result.stderr}") + try: + # Run VPN provisioning + logger.info(f"Starting VPN provisioning for invoice {invoice_id}") + result = run_ansible_playbook(invoice_id) + + # Update subscription status + 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" + }), 200 + + except WebhookError as e: + logger.error(f"VPN provisioning failed: {str(e)}") return jsonify({ "error": "Provisioning failed", - "details": error_msg, - "stdout": result.stdout, - "stderr": result.stderr + "details": str(e) }), 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: logger.info(f"Received {webhook_type} webhook - no action required") return jsonify({ "status": "success", "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: - logger.error(f"Error processing webhook: {str(e)}") - logger.error(traceback.format_exc()) - return jsonify({ - "error": str(e), - "traceback": traceback.format_exc() - }), 500 \ No newline at end of file + logger.error(f"Unexpected error: {traceback.format_exc()}") + return jsonify({"error": "Internal server error"}), 500 \ No newline at end of file diff --git a/app/static/js/pricing.js b/app/static/js/pricing.js index 698d9a7..8cadf86 100644 --- a/app/static/js/pricing.js +++ b/app/static/js/pricing.js @@ -1,137 +1,116 @@ -// Base64 encoding/decoding utilities -const b64 = { - encode: array => btoa(String.fromCharCode.apply(null, array)), - decode: str => Uint8Array.from(atob(str), c => c.charCodeAt(0)) -}; +// Utility functions for duration formatting +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 generateKeyPair() { - const keyPair = await window.crypto.subtle.generateKey( - { - name: 'X25519', - namedCurve: 'X25519', - }, - true, - ['deriveKey', 'deriveBits'] - ); +// Price calculation with volume discounts +async function calculatePrice(hours) { + try { + const response = await fetch('/api/calculate-price', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ hours: parseInt(hours) }) + }); - const privateKey = await window.crypto.subtle.exportKey('raw', keyPair.privateKey); - const publicKey = await window.crypto.subtle.exportKey('raw', keyPair.publicKey); + if (!response.ok) { + 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 { - privateKey: b64.encode(new Uint8Array(privateKey)), - publicKey: b64.encode(new Uint8Array(publicKey)) + form, + slider, + priceDisplay, + durationDisplay, + presetButtons }; } -document.addEventListener('DOMContentLoaded', async function() { - const form = document.getElementById('subscription-form'); - const slider = document.getElementById('duration-slider'); - 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() { +// Main pricing interface +export const Pricing = { + async init(config = {}) { try { - currentKeyPair = await generateKeyPair(); - publicKeyInput.value = currentKeyPair.publicKey; - - // Save private key to localStorage - const keyData = { - privateKey: currentKeyPair.privateKey, - publicKey: currentKeyPair.publicKey, - createdAt: new Date().toISOString() + const elements = initializeForm(config); + const { form, slider, priceDisplay, durationDisplay, presetButtons } = elements; + + // Update price when duration changes + const updateDisplay = async (hours) => { + try { + const { price, formattedDuration } = await calculatePrice(hours); + 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() { - // Generate user ID - userIdInput.value = crypto.randomUUID(); - - // Generate initial keys - await generateNewKeys(); - } + // Set up event listeners + slider.addEventListener('input', () => updateDisplay(slider.value)); - async function updatePrice(hours) { - try { - const response = await fetch('/api/calculate-price', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ hours: parseInt(hours) }) + presetButtons.forEach(button => { + button.addEventListener('click', (e) => { + const hours = e.target.dataset.hours; + slider.value = hours; + updateDisplay(hours); + }); }); - const data = await response.json(); - priceDisplay.textContent = data.price; - durationDisplay.textContent = formatDuration(hours); + + // Initial price calculation + await updateDisplay(slider.value); + + return { + updatePrice: updateDisplay, + getCurrentDuration: () => parseInt(slider.value) + }; } catch (error) { - console.error('Error calculating price:', error); + console.error('Failed to initialize pricing:', error); + throw error; } - } + }, - // Event listeners - slider.addEventListener('input', () => updatePrice(slider.value)); + formatDuration, + calculatePrice +}; - regenerateButton.addEventListener('click', generateNewKeys); - - 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); -}); \ No newline at end of file +export default Pricing; \ No newline at end of file diff --git a/app/static/js/utils/wireguard.js b/app/static/js/utils/wireguard.js index 21985ba..eb6d189 100644 --- a/app/static/js/utils/wireguard.js +++ b/app/static/js/utils/wireguard.js @@ -1,54 +1,134 @@ -// Base64 encoding/decoding utilities +// Base64 encoding/decoding utilities with error handling const b64 = { - encode: array => btoa(String.fromCharCode.apply(null, array)), - decode: str => Uint8Array.from(atob(str), c => c.charCodeAt(0)) - }; - - async function generateKeyPair() { - // Generate a random key pair using Web Crypto API - const keyPair = await window.crypto.subtle.generateKey( - { - name: 'X25519', - namedCurve: 'X25519', - }, - true, - ['deriveKey', 'deriveBits'] - ); - - // 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)) - }; + encode: (array) => { + try { + return btoa(String.fromCharCode.apply(null, array)); + } catch (error) { + console.error('Base64 encoding failed:', error); + throw new Error('Failed to encode key data'); + } + }, + decode: (str) => { + try { + return Uint8Array.from(atob(str), c => c.charCodeAt(0)); + } catch (error) { + console.error('Base64 decoding failed:', error); + throw new Error('Failed to decode key data'); + } } - - export async function generateWireGuardConfig(serverPublicKey, serverEndpoint, address) { - const keys = await generateKeyPair(); - - return { - keys, - config: `[Interface] - PrivateKey = ${keys.privateKey} - Address = ${address} - DNS = 1.1.1.1 - - [Peer] - PublicKey = ${serverPublicKey} - Endpoint = ${serverEndpoint} - AllowedIPs = 0.0.0.0/0 - PersistentKeepalive = 25` - }; +}; + +// Key storage management +const keyStorage = { + store: (userId, keyData) => { + try { + const data = { + privateKey: keyData.privateKey, + publicKey: keyData.publicKey, + createdAt: new Date().toISOString() + }; + localStorage.setItem(`vpn_keys_${userId}`, JSON.stringify(data)); + } catch (error) { + console.error('Failed to store keys:', error); + throw new Error('Failed to save key data'); + } + }, + + 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(); - } catch (error) { - console.error('Failed to generate WireGuard keys:', error); - throw error; - } - } \ No newline at end of file + }, + + 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; \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html index 8f3b5b6..86eb7e9 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -17,7 +17,8 @@ } } - + + {% block content %}{% endblock %} diff --git a/app/templates/index.html b/app/templates/index.html index 284d890..24c1115 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -74,4 +74,36 @@ + {% endblock %} \ No newline at end of file diff --git a/app/templates/payment_success.html b/app/templates/payment_success.html index d309da0..c335c9f 100644 --- a/app/templates/payment_success.html +++ b/app/templates/payment_success.html @@ -22,6 +22,12 @@ > Copy Configuration + @@ -58,53 +64,84 @@ -