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()
# 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']}"
)
# 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)}")
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:
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"""
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:
vault_values = get_vault_values()
secret = vault_values['webhook_secret']
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
)
with tempfile.NamedTemporaryFile(mode='w', delete=False) as vault_pass_file:
vault_pass_file.write(vault_pass)
vault_pass_file.flush()
os.unlink(vault_pass_file.name)
return result
playbook = CLEANUP_PLAYBOOK if cleanup else PLAYBOOK_PATH
def handle_subscription_status(data):
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: dict) -> tuple:
"""Handle SubscriptionStatusUpdated webhook"""
sub_id = data['subscriptionId']
status = data['status']
try:
sub_id = data.get('subscriptionId')
status = data.get('status')
logger.info(f"Processing subscription status update: {sub_id} -> {status}")
if not sub_id or not status:
return jsonify({"error": "Missing required fields"}), 400
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
logger.info(f"Processing subscription status update: {sub_id} -> {status}")
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)
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 result.returncode != 0:
logger.error(f"Failed to clean up subscription {sub_id}: {result.stderr}")
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")
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}"
})
return jsonify({
"status": "success",
"message": f"Subscription {sub_id} status updated to {status}"
}), 200
def handle_subscription_renewal(data):
"""Handle SubscriptionRenewalRequested webhook"""
sub_id = data['subscriptionId']
logger.info(f"Processing subscription renewal request: {sub_id}")
except Exception as e:
logger.error(f"Error handling subscription status: {traceback.format_exc()}")
return jsonify({"error": str(e)}), 500
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_payment_webhook(request) -> tuple:
"""
Handle BTCPay Server webhook for VPN provisioning
# TODO: Send renewal notification to user
return jsonify({
"status": "success",
"message": f"Subscription {sub_id} renewal requested"
})
def handle_payment_webhook(request):
"""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)
try:
# Run VPN provisioning
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}")
# 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
logger.error(f"Unexpected error: {traceback.format_exc()}")
return jsonify({"error": "Internal server error"}), 500

View File

@ -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;
const elements = initializeForm(config);
const { form, slider, priceDisplay, durationDisplay, presetButtons } = elements;
// Save private key to localStorage
const keyData = {
privateKey: currentKeyPair.privateKey,
publicKey: currentKeyPair.publicKey,
createdAt: new Date().toISOString()
// 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();
// Set up event listeners
slider.addEventListener('input', () => updateDisplay(slider.value));
// Generate initial keys
await generateNewKeys();
}
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) })
});
const data = await response.json();
priceDisplay.textContent = data.price;
durationDisplay.textContent = formatDuration(hours);
} catch (error) {
console.error('Error calculating price:', error);
}
}
// Event listeners
slider.addEventListener('input', () => updatePrice(slider.value));
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
})
presetButtons.forEach(button => {
button.addEventListener('click', (e) => {
const hours = e.target.dataset.hours;
slider.value = hours;
updateDisplay(hours);
});
});
if (!response.ok) {
throw new Error('Failed to create invoice');
}
const data = await response.json();
window.location.href = data.checkout_url;
// Initial price calculation
await updateDisplay(slider.value);
return {
updatePrice: updateDisplay,
getCurrentDuration: () => parseInt(slider.value)
};
} catch (error) {
console.error('Error creating invoice:', error);
alert('Failed to create payment invoice. Please try again.');
console.error('Failed to initialize pricing:', error);
throw error;
}
});
},
// Initialize the form
await initializeForm();
// Initial price calculation
updatePrice(slider.value);
});
formatDuration,
calculatePrice
};
export default Pricing;

View File

@ -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))
};
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');
}
}
};
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']
);
// 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');
}
},
// 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);
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');
}
},
// Convert to base64
return {
privateKey: b64.encode(new Uint8Array(privateKey)),
publicKey: b64.encode(new Uint8Array(publicKey))
};
remove: (userId) => {
try {
localStorage.removeItem(`vpn_keys_${userId}`);
} catch (error) {
console.error('Failed to remove keys:', error);
}
}
};
// 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');
}
export async function generateWireGuardConfig(serverPublicKey, serverEndpoint, address) {
const keys = await generateKeyPair();
return `[Interface]
PrivateKey = ${keys.privateKey}
Address = ${clientIp}/24
DNS = 1.1.1.1
return {
keys,
config: `[Interface]
PrivateKey = ${keys.privateKey}
Address = ${address}
DNS = 1.1.1.1
[Peer]
PublicKey = ${serverPublicKey}
Endpoint = ${serverEndpoint}:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25`;
}
[Peer]
PublicKey = ${serverPublicKey}
Endpoint = ${serverEndpoint}
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25`
};
}
export async function generateKeys() {
try {
// 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;
}
}
},
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 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>
<body class="bg-dark text-gray-100">
{% block content %}{% endblock %}

View File

@ -74,4 +74,36 @@
</form>
</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 %}

View File

@ -22,6 +22,12 @@
>
Copy Configuration
</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>
<!-- Installation Instructions -->
@ -58,53 +64,84 @@
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
<script type="module">
import { WireGuard } from '/static/js/utils/wireguard.js';
document.addEventListener('DOMContentLoaded', async function() {
const userId = new URLSearchParams(window.location.search).get('userId');
if (userId) {
// Retrieve keys from localStorage
const keyData = localStorage.getItem(`vpn_keys_${userId}`);
if (keyData) {
const keys = JSON.parse(keyData);
if (!userId) {
console.error('No user ID found in URL');
return;
}
// 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]
PublicKey = ${data.serverPublicKey}
Endpoint = ${data.serverEndpoint}:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25`;
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}`);
try {
// Retrieve keys from storage
const keyData = WireGuard.getKeys(userId);
if (!keyData) {
console.error('No key data found');
return;
}
// 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>