flask update
This commit is contained in:
parent
274f2413cb
commit
1347ca8ea5
11
.env.example
11
.env.example
@ -1,16 +1,7 @@
|
||||
# Ansible Configuration
|
||||
ANSIBLE_VAULT_PASSWORD=your_vault_password_here
|
||||
|
||||
# BTCPay Server Configuration
|
||||
BTCPAY_BASE_URL=https://your-btcpay-server.com
|
||||
BTCPAY_STORE_ID=your_store_id
|
||||
BTCPAY_WEBHOOK_SECRET=your_webhook_secret
|
||||
|
||||
# Flask Configuration
|
||||
FLASK_ENV=development
|
||||
FLASK_APP=app/handlers/webhook_handler.py
|
||||
FLASK_DEBUG=1
|
||||
|
||||
# Server Configuration
|
||||
VPN_SERVER_IP=your_server_ip
|
||||
WIREGUARD_PORT=51820
|
||||
FLASK_DEBUG=1
|
30
ansible/playbooks/vpn_cleanup.yml
Normal file
30
ansible/playbooks/vpn_cleanup.yml
Normal file
@ -0,0 +1,30 @@
|
||||
---
|
||||
- name: Cleanup expired VPN configuration
|
||||
hosts: vpn_servers
|
||||
become: yes
|
||||
vars:
|
||||
client_dir: /etc/wireguard/clients
|
||||
wg_interface: wg0
|
||||
|
||||
tasks:
|
||||
- name: Debug subscription ID
|
||||
debug:
|
||||
msg: "Cleaning up subscription ID: {{ subscription_id }}"
|
||||
|
||||
- name: Remove client configuration directory
|
||||
file:
|
||||
path: "{{ client_dir }}/{{ subscription_id }}"
|
||||
state: absent
|
||||
|
||||
- name: Remove client from server config
|
||||
blockinfile:
|
||||
path: "/etc/wireguard/{{ wg_interface }}.conf"
|
||||
marker: "# {mark} ANSIBLE MANAGED BLOCK FOR {{ subscription_id }}"
|
||||
state: absent
|
||||
notify: restart wireguard
|
||||
|
||||
handlers:
|
||||
- name: restart wireguard
|
||||
service:
|
||||
name: "wg-quick@{{ wg_interface }}"
|
||||
state: restarted
|
@ -1,6 +1,9 @@
|
||||
from flask import Flask, request, jsonify
|
||||
# app/__init__.py
|
||||
|
||||
from flask import Flask, request, jsonify, render_template
|
||||
import logging
|
||||
from .handlers.webhook_handler import handle_payment_webhook
|
||||
from .handlers.payment_handler import BTCPayHandler
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(
|
||||
@ -10,10 +13,92 @@ logging.basicConfig(
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = Flask(__name__)
|
||||
btcpay_handler = BTCPayHandler()
|
||||
|
||||
# Existing webhook route
|
||||
@app.route('/webhook/vpn', methods=['POST'])
|
||||
def handle_payment():
|
||||
return handle_payment_webhook(request)
|
||||
|
||||
# Frontend routes
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
@app.route('/api/calculate-price', methods=['POST'])
|
||||
def calculate_price():
|
||||
hours = request.json.get('hours', 0)
|
||||
# Basic price calculation - adjust formula as needed
|
||||
base_price = hours * 100 # 100 sats per hour
|
||||
|
||||
# Apply volume discounts
|
||||
if hours >= 720: # 1 month
|
||||
base_price = base_price * 0.85 # 15% discount
|
||||
elif hours >= 168: # 1 week
|
||||
base_price = base_price * 0.90 # 10% discount
|
||||
elif hours >= 24: # 1 day
|
||||
base_price = base_price * 0.95 # 5% discount
|
||||
|
||||
return jsonify({
|
||||
'price': int(base_price),
|
||||
'duration': hours
|
||||
})
|
||||
|
||||
@app.route('/create-invoice', methods=['POST'])
|
||||
def create_invoice():
|
||||
try:
|
||||
logger.info("Received invoice creation request")
|
||||
data = request.json
|
||||
logger.debug(f"Request data: {data}")
|
||||
|
||||
# Validate input data
|
||||
duration_hours = data.get('duration')
|
||||
email = data.get('email')
|
||||
|
||||
if not email:
|
||||
logger.error("Email address missing from request")
|
||||
return jsonify({'error': 'Email is required'}), 400
|
||||
|
||||
if not duration_hours:
|
||||
logger.error("Duration missing from request")
|
||||
return jsonify({'error': 'Duration is required'}), 400
|
||||
|
||||
try:
|
||||
duration_hours = int(duration_hours)
|
||||
except ValueError:
|
||||
logger.error(f"Invalid duration value: {duration_hours}")
|
||||
return jsonify({'error': 'Invalid duration value'}), 400
|
||||
|
||||
# Calculate price using same logic as calculate-price endpoint
|
||||
base_price = duration_hours * 100 # 100 sats per hour
|
||||
|
||||
if duration_hours >= 720: # 1 month
|
||||
base_price = base_price * 0.85 # 15% discount
|
||||
elif duration_hours >= 168: # 1 week
|
||||
base_price = base_price * 0.90 # 10% discount
|
||||
elif duration_hours >= 24: # 1 day
|
||||
base_price = base_price * 0.95 # 5% discount
|
||||
|
||||
amount_sats = int(base_price)
|
||||
logger.info(f"Calculated price: {amount_sats} sats for {duration_hours} hours")
|
||||
|
||||
# Create BTCPay invoice
|
||||
invoice_data = btcpay_handler.create_invoice(amount_sats, duration_hours, email)
|
||||
|
||||
if not invoice_data:
|
||||
logger.error("Failed to create invoice - no data returned from BTCPayHandler")
|
||||
return jsonify({'error': 'Failed to create invoice'}), 500
|
||||
|
||||
logger.info(f"Successfully created invoice with ID: {invoice_data.get('invoice_id')}")
|
||||
return jsonify(invoice_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in create_invoice endpoint: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/payment/success')
|
||||
def payment_success():
|
||||
return render_template('payment_success.html')
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5000)
|
144
app/handlers/payment_handler.py
Normal file
144
app/handlers/payment_handler.py
Normal file
@ -0,0 +1,144 @@
|
||||
# app/handlers/payment_handler.py
|
||||
|
||||
import logging
|
||||
import requests
|
||||
import os
|
||||
from flask import jsonify
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from pathlib import Path
|
||||
import traceback
|
||||
from .webhook_handler import get_vault_values
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class BTCPayHandler:
|
||||
def __init__(self):
|
||||
logger.info("Initializing BTCPayHandler")
|
||||
try:
|
||||
# Get configuration from Ansible vault
|
||||
logger.debug("Retrieving vault values")
|
||||
vault_values = get_vault_values()
|
||||
|
||||
self.base_url = vault_values['btcpay_base_url']
|
||||
self.api_key = vault_values['btcpay_api_key']
|
||||
self.store_id = vault_values['btcpay_store_id']
|
||||
|
||||
# Email configuration
|
||||
self.smtp_config = vault_values.get('smtp_config', {})
|
||||
|
||||
logger.info(f"BTCPayHandler initialized with base URL: {self.base_url}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize BTCPayHandler: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
raise
|
||||
|
||||
def create_invoice(self, amount_sats, duration_hours, email):
|
||||
"""Create BTCPay invoice for VPN subscription"""
|
||||
try:
|
||||
logger.info(f"Creating invoice: {amount_sats} sats, {duration_hours}h for {email}")
|
||||
|
||||
headers = {
|
||||
'Authorization': f'token {self.api_key}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
# Get app URL from environment or use a default
|
||||
app_url = os.getenv('APP_BASE_URL', 'http://localhost:5000').rstrip('/')
|
||||
logger.debug(f"Using app URL for redirect: {app_url}")
|
||||
|
||||
payload = {
|
||||
'amount': amount_sats,
|
||||
'currency': 'SATS',
|
||||
'metadata': {
|
||||
'duration_hours': duration_hours,
|
||||
'email': email,
|
||||
'orderId': f'vpn_sub_{duration_hours}h',
|
||||
},
|
||||
'checkout': {
|
||||
'redirectURL': f'{app_url}/payment/success',
|
||||
'redirectAutomatically': True
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(f"Sending request to BTCPay Server: {self.base_url}/api/v1/stores/{self.store_id}/invoices")
|
||||
logger.debug(f"Request payload: {payload}")
|
||||
|
||||
response = requests.post(
|
||||
f'{self.base_url}/api/v1/stores/{self.store_id}/invoices',
|
||||
headers=headers,
|
||||
json=payload
|
||||
)
|
||||
|
||||
logger.debug(f"BTCPay response status: {response.status_code}")
|
||||
logger.debug(f"BTCPay response content: {response.text}")
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"BTCPay invoice creation failed: {response.text}")
|
||||
return None
|
||||
|
||||
invoice_data = response.json()
|
||||
logger.info(f"Successfully created invoice {invoice_data.get('id')}")
|
||||
|
||||
return {
|
||||
'invoice_id': invoice_data['id'],
|
||||
'checkout_url': invoice_data['checkoutLink']
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error creating BTCPay invoice:")
|
||||
logger.error(traceback.format_exc())
|
||||
return None
|
||||
|
||||
def send_confirmation_email(self, email, config_data):
|
||||
"""Send VPN configuration details via email"""
|
||||
try:
|
||||
logger.info(f"Sending confirmation email to {email}")
|
||||
|
||||
if not self.smtp_config:
|
||||
logger.warning("SMTP configuration not found in vault")
|
||||
return False
|
||||
|
||||
msg = MIMEMultipart()
|
||||
msg['From'] = self.smtp_config['sender_email']
|
||||
msg['To'] = email
|
||||
msg['Subject'] = "Your VPN Configuration"
|
||||
|
||||
body = f"""
|
||||
Thank you for subscribing to our VPN service!
|
||||
|
||||
Please find your WireGuard configuration below:
|
||||
|
||||
{config_data}
|
||||
|
||||
Installation instructions:
|
||||
1. Install WireGuard client for your platform from https://www.wireguard.com/install/
|
||||
2. Save the above configuration to a file named 'wg0.conf'
|
||||
3. Import the configuration file into your WireGuard client
|
||||
|
||||
Need help? Reply to this email for support.
|
||||
"""
|
||||
|
||||
msg.attach(MIMEText(body, 'plain'))
|
||||
|
||||
logger.debug("Connecting to SMTP server")
|
||||
with smtplib.SMTP(
|
||||
self.smtp_config['server'],
|
||||
self.smtp_config.get('port', 587)
|
||||
) as server:
|
||||
server.starttls()
|
||||
server.login(
|
||||
self.smtp_config['username'],
|
||||
self.smtp_config['password']
|
||||
)
|
||||
server.send_message(msg)
|
||||
|
||||
logger.info("Confirmation email sent successfully")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error sending confirmation email:")
|
||||
logger.error(traceback.format_exc())
|
||||
return False
|
@ -6,6 +6,8 @@ import logging
|
||||
import hmac
|
||||
import hashlib
|
||||
import yaml
|
||||
import json
|
||||
import datetime
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
@ -20,6 +22,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
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'
|
||||
SUBSCRIPTION_DB = BASE_DIR / 'data' / 'subscriptions.json'
|
||||
|
||||
def get_vault_values():
|
||||
"""Get decrypted values from Ansible vault"""
|
||||
@ -76,6 +80,107 @@ def verify_signature(payload_body, signature_header):
|
||||
logger.error(f"Signature verification failed: {str(e)}")
|
||||
return False
|
||||
|
||||
def load_subscriptions():
|
||||
"""Load subscription data from JSON file"""
|
||||
if not SUBSCRIPTION_DB.parent.exists():
|
||||
SUBSCRIPTION_DB.parent.mkdir(parents=True)
|
||||
|
||||
if not SUBSCRIPTION_DB.exists():
|
||||
return {}
|
||||
|
||||
with open(SUBSCRIPTION_DB, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
def save_subscription(subscription_data):
|
||||
"""Save subscription data to JSON file"""
|
||||
subscriptions = load_subscriptions()
|
||||
sub_id = subscription_data['subscriptionId']
|
||||
subscriptions[sub_id] = subscription_data
|
||||
|
||||
with open(SUBSCRIPTION_DB, 'w') as f:
|
||||
json.dump(subscriptions, f, indent=2)
|
||||
|
||||
def calculate_expiry(duration_hours):
|
||||
"""Calculate expiry date based on subscription duration"""
|
||||
return (datetime.datetime.now() +
|
||||
datetime.timedelta(hours=duration_hours)).isoformat()
|
||||
|
||||
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")
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', delete=False) as vault_pass_file:
|
||||
vault_pass_file.write(vault_pass)
|
||||
vault_pass_file.flush()
|
||||
|
||||
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'
|
||||
]
|
||||
|
||||
logger.info(f"Running ansible-playbook command: {' '.join(cmd)}")
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
os.unlink(vault_pass_file.name)
|
||||
return result
|
||||
|
||||
def handle_subscription_status(data):
|
||||
"""Handle SubscriptionStatusUpdated webhook"""
|
||||
sub_id = data['subscriptionId']
|
||||
status = data['status']
|
||||
|
||||
logger.info(f"Processing subscription status update: {sub_id} -> {status}")
|
||||
|
||||
# Store subscription data
|
||||
data['last_updated'] = datetime.datetime.now().isoformat()
|
||||
save_subscription(data)
|
||||
|
||||
if status != 'Active':
|
||||
# Run cleanup playbook 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:
|
||||
logger.error(f"Failed to clean up subscription {sub_id}: {result.stderr}")
|
||||
|
||||
logger.info(f"Subscription {sub_id} is no longer active")
|
||||
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"message": f"Subscription {sub_id} status updated to {status}"
|
||||
})
|
||||
|
||||
def handle_subscription_renewal(data):
|
||||
"""Handle SubscriptionRenewalRequested webhook"""
|
||||
sub_id = data['subscriptionId']
|
||||
logger.info(f"Processing subscription renewal request: {sub_id}")
|
||||
|
||||
# Update subscription data
|
||||
data['renewal_requested'] = datetime.datetime.now().isoformat()
|
||||
save_subscription(data)
|
||||
|
||||
# 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"""
|
||||
try:
|
||||
@ -94,66 +199,100 @@ def handle_payment_webhook(request):
|
||||
|
||||
data = request.json
|
||||
logger.info(f"Received webhook data: {data}")
|
||||
webhook_type = data.get('type')
|
||||
|
||||
invoice_id = data.get('invoiceId')
|
||||
if not invoice_id:
|
||||
logger.error("Missing invoiceId in webhook data")
|
||||
return jsonify({"error": "Missing invoiceId"}), 400
|
||||
if webhook_type == 'SubscriptionStatusUpdated':
|
||||
return handle_subscription_status(data)
|
||||
|
||||
if invoice_id.startswith('__test__') and invoice_id.endswith('__test__'):
|
||||
invoice_id = invoice_id[8:-8]
|
||||
logger.info(f"Stripped test markers from invoice ID: {invoice_id}")
|
||||
|
||||
vault_pass = os.getenv('ANSIBLE_VAULT_PASSWORD', '')
|
||||
if not vault_pass:
|
||||
raise Exception("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()
|
||||
elif webhook_type == 'SubscriptionRenewalRequested':
|
||||
return handle_subscription_renewal(data)
|
||||
|
||||
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'
|
||||
]
|
||||
elif webhook_type == 'InvoiceSettled':
|
||||
# Handle regular invoice payment
|
||||
invoice_id = data.get('invoiceId')
|
||||
metadata = data.get('metadata', {})
|
||||
|
||||
logger.info(f"Running ansible-playbook command: {' '.join(cmd)}")
|
||||
if not invoice_id:
|
||||
logger.error("Missing invoiceId in webhook data")
|
||||
return jsonify({"error": "Missing invoiceId"}), 400
|
||||
|
||||
if invoice_id.startswith('__test__') and invoice_id.endswith('__test__'):
|
||||
invoice_id = invoice_id[8:-8]
|
||||
logger.info(f"Stripped test markers from invoice ID: {invoice_id}")
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
os.unlink(vault_pass_file.name)
|
||||
|
||||
logger.info(f"Ansible playbook stdout: {result.stdout}")
|
||||
if result.stderr:
|
||||
logger.error(f"Ansible playbook stderr: {result.stderr}")
|
||||
|
||||
if result.returncode != 0:
|
||||
error_msg = f"Ansible playbook failed with return code {result.returncode}"
|
||||
logger.error(error_msg)
|
||||
# Run Ansible playbook with enhanced logging
|
||||
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}")
|
||||
return jsonify({
|
||||
"error": "Provisioning failed",
|
||||
"details": error_msg,
|
||||
"stdout": result.stdout,
|
||||
"stderr": result.stderr
|
||||
}), 500
|
||||
|
||||
logger.info(f"VPN provisioning completed for invoice {invoice_id}")
|
||||
|
||||
# Update subscription database
|
||||
try:
|
||||
duration_hours = metadata.get('duration_hours', 24)
|
||||
subscriptions = load_subscriptions()
|
||||
subscriptions[invoice_id] = {
|
||||
'email': metadata.get('email'),
|
||||
'duration_hours': duration_hours,
|
||||
'start_time': datetime.datetime.now().isoformat(),
|
||||
'expiry': calculate_expiry(duration_hours),
|
||||
'status': 'Active'
|
||||
}
|
||||
with open(SUBSCRIPTION_DB, 'w') as f:
|
||||
json.dump(subscriptions, f, indent=2)
|
||||
logger.info(f"Updated subscription database for invoice {invoice_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating subscription database: {str(e)}")
|
||||
|
||||
# Send email confirmation if email is provided
|
||||
try:
|
||||
email = metadata.get('email')
|
||||
if email:
|
||||
config_path = f"/etc/wireguard/clients/{invoice_id}/wg0.conf"
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, 'r') as f:
|
||||
config_data = f.read()
|
||||
|
||||
btcpay_handler = BTCPayHandler()
|
||||
email_sent = btcpay_handler.send_confirmation_email(email, config_data)
|
||||
if email_sent:
|
||||
logger.info(f"Sent configuration email to {email}")
|
||||
else:
|
||||
logger.warning(f"Failed to send configuration email to {email}")
|
||||
else:
|
||||
logger.warning(f"Config file not found at {config_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending confirmation email: {str(e)}")
|
||||
|
||||
logger.info(f"Successfully processed invoice {invoice_id}")
|
||||
return jsonify({
|
||||
"error": "Provisioning failed",
|
||||
"details": error_msg,
|
||||
"stdout": result.stdout,
|
||||
"stderr": result.stderr
|
||||
}), 500
|
||||
"status": "success",
|
||||
"invoice_id": invoice_id,
|
||||
"message": "VPN provisioning completed"
|
||||
})
|
||||
|
||||
else:
|
||||
# Log other webhook types as info instead of warning
|
||||
logger.info(f"Received {webhook_type} webhook - no action required")
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"message": f"Webhook {webhook_type} acknowledged"
|
||||
})
|
||||
|
||||
logger.info(f"Successfully processed invoice {invoice_id}")
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"invoice_id": invoice_id,
|
||||
"message": "VPN provisioning initiated"
|
||||
})
|
||||
|
||||
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()
|
||||
|
84
app/static/js/pricing.js
Normal file
84
app/static/js/pricing.js
Normal file
@ -0,0 +1,84 @@
|
||||
// app/static/js/pricing.js
|
||||
document.addEventListener('DOMContentLoaded', 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 emailInput = document.getElementById('email');
|
||||
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
|
||||
async function createInvoice(duration, email) {
|
||||
try {
|
||||
const response = await fetch('/create-invoice', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
duration: parseInt(duration),
|
||||
email: email
|
||||
})
|
||||
});
|
||||
|
||||
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.');
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
slider.addEventListener('input', () => updatePrice(slider.value));
|
||||
|
||||
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();
|
||||
|
||||
const email = emailInput.value.trim();
|
||||
if (!email) {
|
||||
alert('Please enter your email address');
|
||||
return;
|
||||
}
|
||||
|
||||
const duration = slider.value;
|
||||
await createInvoice(duration, email);
|
||||
});
|
||||
|
||||
// Initial price calculation
|
||||
updatePrice(slider.value);
|
||||
});
|
25
app/templates/base.html
Normal file
25
app/templates/base.html
Normal file
@ -0,0 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>VPN Service</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'dark': '#121212',
|
||||
'dark-lighter': '#1e1e1e'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='js/pricing.js') }}" defer></script>
|
||||
</head>
|
||||
<body class="bg-dark text-gray-100">
|
||||
{% block content %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
56
app/templates/index.html
Normal file
56
app/templates/index.html
Normal file
@ -0,0 +1,56 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-screen bg-dark py-8 px-4">
|
||||
<div class="max-w-xl mx-auto bg-dark-lighter rounded-lg shadow-lg p-6">
|
||||
<h1 class="text-2xl font-bold mb-6 text-center">Subscribe to VPN Service</h1>
|
||||
|
||||
<form id="subscription-form" class="space-y-6">
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium mb-2">Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
required
|
||||
class="w-full px-3 py-2 bg-dark border border-gray-600 rounded-md text-white focus:outline-none focus:border-blue-500"
|
||||
placeholder="your@email.com"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Duration</label>
|
||||
<div class="space-y-4">
|
||||
<input
|
||||
type="range"
|
||||
id="duration-slider"
|
||||
min="1"
|
||||
max="720"
|
||||
value="24"
|
||||
class="w-full"
|
||||
>
|
||||
<div class="flex justify-between text-sm text-gray-400">
|
||||
<button type="button" data-hours="1" class="duration-preset hover:text-blue-400">1 Hour</button>
|
||||
<button type="button" data-hours="24" class="duration-preset hover:text-blue-400">1 Day</button>
|
||||
<button type="button" data-hours="168" class="duration-preset hover:text-blue-400">1 Week</button>
|
||||
<button type="button" data-hours="720" class="duration-preset hover:text-blue-400">1 Month</button>
|
||||
</div>
|
||||
</div>
|
||||
<p id="duration-display" class="mt-2 text-center text-gray-400"></p>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<p class="text-2xl font-bold text-blue-400">
|
||||
<span id="price-display">-</span> sats
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition-colors"
|
||||
>
|
||||
Pay with Bitcoin
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
18
app/templates/payment_success.html
Normal file
18
app/templates/payment_success.html
Normal file
@ -0,0 +1,18 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-screen bg-dark py-8 px-4">
|
||||
<div class="max-w-xl mx-auto bg-dark-lighter rounded-lg shadow-lg p-6 text-center">
|
||||
<h1 class="text-2xl font-bold mb-4">Payment Successful!</h1>
|
||||
<p class="text-gray-300 mb-6">
|
||||
Thank you for your payment. Your VPN configuration will be sent to your email shortly.
|
||||
</p>
|
||||
<p class="text-gray-400 mb-4">
|
||||
Please check your email for further instructions on setting up your VPN connection.
|
||||
</p>
|
||||
<a href="/" class="inline-block bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition-colors">
|
||||
Return to Home
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
92
data/subscriptions.json
Normal file
92
data/subscriptions.json
Normal file
@ -0,0 +1,92 @@
|
||||
{
|
||||
"__test__ee2b820c-d1a0-4c4d-8b7c-0e5550fbb42e__test__": {
|
||||
"deliveryId": "P4su7aNvuaa7mGEJLFJ5mk",
|
||||
"webhookId": "CoGqJKKuE3838AWZQkncSJ",
|
||||
"originalDeliveryId": "__test__73469547-03db-4961-9d2f-da9b1ed7519f__test__",
|
||||
"isRedelivery": false,
|
||||
"type": "SubscriptionRenewalRequested",
|
||||
"timestamp": 1733961630,
|
||||
"storeId": "DcnEUCckb8eo5WBFBABb7EXGRP49a8UjYQRKkvz7AcJY",
|
||||
"appId": "__test__dda0ab87-3962-435e-9cf6-2c9749dd21dc__test__",
|
||||
"subscriptionId": "__test__ee2b820c-d1a0-4c4d-8b7c-0e5550fbb42e__test__",
|
||||
"status": "Active",
|
||||
"paymentRequestId": null,
|
||||
"email": null,
|
||||
"renewal_requested": "2024-12-12T00:00:31.600171"
|
||||
},
|
||||
"__test__f3e4e7cf-304b-4a97-a341-9e78e0f0bf7f__test__": {
|
||||
"deliveryId": "7BPjwRNtFd2k3kzQLH8RoJ",
|
||||
"webhookId": "CoGqJKKuE3838AWZQkncSJ",
|
||||
"originalDeliveryId": "__test__50e7d2ea-18d3-46b0-bad3-fc5475a9994f__test__",
|
||||
"isRedelivery": false,
|
||||
"type": "SubscriptionRenewalRequested",
|
||||
"timestamp": 1733972813,
|
||||
"storeId": "DcnEUCckb8eo5WBFBABb7EXGRP49a8UjYQRKkvz7AcJY",
|
||||
"appId": "__test__aa25dc16-84ac-4687-a2e9-41a97c5cc112__test__",
|
||||
"subscriptionId": "__test__f3e4e7cf-304b-4a97-a341-9e78e0f0bf7f__test__",
|
||||
"status": "Active",
|
||||
"paymentRequestId": null,
|
||||
"email": null,
|
||||
"renewal_requested": "2024-12-12T03:06:54.340514"
|
||||
},
|
||||
"__test__0caaab4d-026d-4df6-b09b-164fa0edde79__test__": {
|
||||
"deliveryId": "PhRMLk717pAWj2L1Z3LZtr",
|
||||
"webhookId": "CoGqJKKuE3838AWZQkncSJ",
|
||||
"originalDeliveryId": "__test__36b2350d-45df-4fe0-a4fa-c701684c9209__test__",
|
||||
"isRedelivery": false,
|
||||
"type": "SubscriptionStatusUpdated",
|
||||
"timestamp": 1733972824,
|
||||
"storeId": "DcnEUCckb8eo5WBFBABb7EXGRP49a8UjYQRKkvz7AcJY",
|
||||
"appId": "__test__7dc7f4d8-020a-41b6-b728-84ca1ca0ab4c__test__",
|
||||
"subscriptionId": "__test__0caaab4d-026d-4df6-b09b-164fa0edde79__test__",
|
||||
"status": "Active",
|
||||
"paymentRequestId": null,
|
||||
"email": null,
|
||||
"last_updated": "2024-12-12T03:07:05.439848"
|
||||
},
|
||||
"__test__9aa786d3-053c-4b70-8b01-6df0f0207b79__test__": {
|
||||
"deliveryId": "U7stmUDcB4qseDh29mhke7",
|
||||
"webhookId": "CoGqJKKuE3838AWZQkncSJ",
|
||||
"originalDeliveryId": "__test__c5eba94d-ea9e-46e9-b127-74234fcd7002__test__",
|
||||
"isRedelivery": false,
|
||||
"type": "SubscriptionStatusUpdated",
|
||||
"timestamp": 1733973182,
|
||||
"storeId": "DcnEUCckb8eo5WBFBABb7EXGRP49a8UjYQRKkvz7AcJY",
|
||||
"appId": "__test__c3149b37-ad19-428a-aa95-251adc96e5ae__test__",
|
||||
"subscriptionId": "__test__9aa786d3-053c-4b70-8b01-6df0f0207b79__test__",
|
||||
"status": "Active",
|
||||
"paymentRequestId": null,
|
||||
"email": null,
|
||||
"last_updated": "2024-12-12T03:13:03.056437"
|
||||
},
|
||||
"__test__d57b8102-87f7-4143-b175-32353b6eaec7__test__": {
|
||||
"deliveryId": "834dSuc5bdeXF2zRxovj2x",
|
||||
"webhookId": "CoGqJKKuE3838AWZQkncSJ",
|
||||
"originalDeliveryId": "__test__16c072e3-5702-4f57-bc32-7181f585bef2__test__",
|
||||
"isRedelivery": false,
|
||||
"type": "SubscriptionStatusUpdated",
|
||||
"timestamp": 1733973198,
|
||||
"storeId": "DcnEUCckb8eo5WBFBABb7EXGRP49a8UjYQRKkvz7AcJY",
|
||||
"appId": "__test__59579b1c-c393-4fa4-b4be-6691b7a6db27__test__",
|
||||
"subscriptionId": "__test__d57b8102-87f7-4143-b175-32353b6eaec7__test__",
|
||||
"status": "Active",
|
||||
"paymentRequestId": null,
|
||||
"email": null,
|
||||
"last_updated": "2024-12-12T03:13:19.677073"
|
||||
},
|
||||
"__test__5f28edc1-1c9e-4f1a-9fe9-591bc4ae7988__test__": {
|
||||
"deliveryId": "9LUJAQmU89k8ofSKc7M7Kt",
|
||||
"webhookId": "CoGqJKKuE3838AWZQkncSJ",
|
||||
"originalDeliveryId": "__test__c274b638-568b-459f-b4f4-fd10029f5dce__test__",
|
||||
"isRedelivery": false,
|
||||
"type": "SubscriptionRenewalRequested",
|
||||
"timestamp": 1733973203,
|
||||
"storeId": "DcnEUCckb8eo5WBFBABb7EXGRP49a8UjYQRKkvz7AcJY",
|
||||
"appId": "__test__6896db98-91f3-458e-b8c8-6c6280a5698f__test__",
|
||||
"subscriptionId": "__test__5f28edc1-1c9e-4f1a-9fe9-591bc4ae7988__test__",
|
||||
"status": "Active",
|
||||
"paymentRequestId": null,
|
||||
"email": null,
|
||||
"renewal_requested": "2024-12-12T03:13:24.040298"
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
flask==3.0.0
|
||||
pyyaml==6.0.1
|
||||
python-dotenv==1.0.0
|
||||
cryptography==41.0.7 # For ansible-vault operations
|
||||
cryptography==41.0.7
|
||||
ansible==9.1.0
|
||||
requests==2.31.0
|
253
scripts/subscription_checker.py
Normal file
253
scripts/subscription_checker.py
Normal file
@ -0,0 +1,253 @@
|
||||
# scripts/subscription_checker.py
|
||||
|
||||
import json
|
||||
import datetime
|
||||
import logging
|
||||
import subprocess
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from dateutil.parser import parse
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Path setup
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
PROJECT_ROOT = SCRIPT_DIR.parent
|
||||
SUBSCRIPTION_DB = PROJECT_ROOT / 'data' / 'subscriptions.json'
|
||||
CLEANUP_PLAYBOOK = PROJECT_ROOT / 'ansible' / 'playbooks' / 'vpn_cleanup.yml'
|
||||
INVENTORY_FILE = PROJECT_ROOT / 'inventory.ini'
|
||||
|
||||
# Notification thresholds configuration
|
||||
NOTIFICATION_THRESHOLDS = {
|
||||
'minimum_duration': datetime.timedelta(hours=1), # Minimum subscription duration
|
||||
'short_term': {
|
||||
'max_duration': datetime.timedelta(days=1),
|
||||
'warning_fraction': 0.5, # Warn when 50% of time remains
|
||||
'grace_fraction': 0.1 # Grace period of 10% of subscription length
|
||||
},
|
||||
'medium_term': {
|
||||
'max_duration': datetime.timedelta(days=7),
|
||||
'warning_fraction': 0.25, # Warn when 25% of time remains
|
||||
'grace_hours': 12 # Fixed 12-hour grace period
|
||||
},
|
||||
'long_term': {
|
||||
'warning_days': 7, # 7 days warning for longer subscriptions
|
||||
'grace_days': 2 # 2 days grace period
|
||||
}
|
||||
}
|
||||
|
||||
def load_subscriptions():
|
||||
"""Load subscription data from JSON file"""
|
||||
if not SUBSCRIPTION_DB.parent.exists():
|
||||
SUBSCRIPTION_DB.parent.mkdir(parents=True)
|
||||
|
||||
if not SUBSCRIPTION_DB.exists():
|
||||
return {}
|
||||
|
||||
with open(SUBSCRIPTION_DB, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
def save_subscriptions(subscriptions):
|
||||
"""Save subscriptions to JSON file"""
|
||||
with open(SUBSCRIPTION_DB, 'w') as f:
|
||||
json.dump(subscriptions, f, indent=2)
|
||||
|
||||
def run_cleanup_playbook(sub_id):
|
||||
"""Run the VPN cleanup playbook"""
|
||||
logger.info(f"Running cleanup playbook for subscription {sub_id}")
|
||||
|
||||
vault_pass = os.getenv('ANSIBLE_VAULT_PASSWORD', '')
|
||||
if not vault_pass:
|
||||
raise Exception("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()
|
||||
|
||||
cmd = [
|
||||
'ansible-playbook',
|
||||
str(CLEANUP_PLAYBOOK),
|
||||
'-i', str(INVENTORY_FILE),
|
||||
'-e', f'subscription_id={sub_id}',
|
||||
'--vault-password-file', vault_pass_file.name,
|
||||
'-vvv'
|
||||
]
|
||||
|
||||
logger.debug(f"Running command: {' '.join(cmd)}")
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
logger.error(f"Cleanup playbook failed: {result.stderr}")
|
||||
else:
|
||||
logger.debug(f"Cleanup playbook output: {result.stdout}")
|
||||
|
||||
os.unlink(vault_pass_file.name)
|
||||
return result
|
||||
|
||||
def calculate_notification_times(start_time, end_time):
|
||||
"""
|
||||
Calculate warning and grace period times based on subscription duration
|
||||
|
||||
Returns:
|
||||
tuple: (warning_time, grace_end_time)
|
||||
"""
|
||||
duration = end_time - start_time
|
||||
|
||||
# Handle extremely short subscriptions
|
||||
if duration < NOTIFICATION_THRESHOLDS['minimum_duration']:
|
||||
warning_time = start_time # Immediate warning
|
||||
grace_end_time = end_time # No grace period
|
||||
return warning_time, grace_end_time
|
||||
|
||||
# Short-term subscriptions (less than 1 day)
|
||||
if duration <= NOTIFICATION_THRESHOLDS['short_term']['max_duration']:
|
||||
warning_delta = duration * NOTIFICATION_THRESHOLDS['short_term']['warning_fraction']
|
||||
grace_delta = duration * NOTIFICATION_THRESHOLDS['short_term']['grace_fraction']
|
||||
warning_time = end_time - warning_delta
|
||||
grace_end_time = end_time + grace_delta
|
||||
|
||||
# Medium-term subscriptions (1-7 days)
|
||||
elif duration <= NOTIFICATION_THRESHOLDS['medium_term']['max_duration']:
|
||||
warning_delta = duration * NOTIFICATION_THRESHOLDS['medium_term']['warning_fraction']
|
||||
grace_hours = NOTIFICATION_THRESHOLDS['medium_term']['grace_hours']
|
||||
warning_time = end_time - warning_delta
|
||||
grace_end_time = end_time + datetime.timedelta(hours=grace_hours)
|
||||
|
||||
# Long-term subscriptions (> 7 days)
|
||||
else:
|
||||
warning_days = NOTIFICATION_THRESHOLDS['long_term']['warning_days']
|
||||
grace_days = NOTIFICATION_THRESHOLDS['long_term']['grace_days']
|
||||
warning_time = end_time - datetime.timedelta(days=warning_days)
|
||||
grace_end_time = end_time + datetime.timedelta(days=grace_days)
|
||||
|
||||
return warning_time, grace_end_time
|
||||
|
||||
def get_notification_message(sub_data, remaining_time):
|
||||
"""Generate appropriate notification message based on subscription duration"""
|
||||
if remaining_time < datetime.timedelta(hours=1):
|
||||
return f"Your VPN subscription expires in {int(remaining_time.total_seconds() / 60)} minutes!"
|
||||
elif remaining_time < datetime.timedelta(days=1):
|
||||
return f"Your VPN subscription expires in {int(remaining_time.total_seconds() / 3600)} hours!"
|
||||
else:
|
||||
return f"Your VPN subscription expires in {remaining_time.days} days!"
|
||||
|
||||
def notify_user(user_id, message):
|
||||
"""Send notification to user about subscription status"""
|
||||
try:
|
||||
subscriptions = load_subscriptions()
|
||||
sub_data = subscriptions.get(user_id)
|
||||
|
||||
if not sub_data or 'email' not in sub_data:
|
||||
logger.error(f"No email found for user {user_id}")
|
||||
return False
|
||||
|
||||
# Import BTCPayHandler here to avoid circular imports
|
||||
from app.handlers.payment_handler import BTCPayHandler
|
||||
|
||||
btcpay_handler = BTCPayHandler()
|
||||
email_sent = btcpay_handler.send_confirmation_email(
|
||||
sub_data['email'],
|
||||
f"""
|
||||
VPN Subscription Update
|
||||
|
||||
{message}
|
||||
|
||||
If you wish to continue using the VPN service, please renew your subscription.
|
||||
"""
|
||||
)
|
||||
|
||||
if email_sent:
|
||||
logger.info(f"Sent notification to {sub_data['email']}: {message}")
|
||||
else:
|
||||
logger.warning(f"Failed to send notification to {sub_data['email']}")
|
||||
|
||||
return email_sent
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending notification: {str(e)}")
|
||||
return False
|
||||
|
||||
def check_subscriptions():
|
||||
"""Check subscription status and clean up expired ones"""
|
||||
logger.info("Starting subscription check")
|
||||
|
||||
subscriptions = load_subscriptions()
|
||||
now = datetime.datetime.now()
|
||||
modified = False
|
||||
|
||||
logger.info(f"Checking {len(subscriptions)} subscriptions")
|
||||
|
||||
for sub_id, sub_data in list(subscriptions.items()):
|
||||
try:
|
||||
logger.debug(f"Processing subscription {sub_id}")
|
||||
|
||||
if sub_data.get('status') != 'Active':
|
||||
logger.debug(f"Skipping inactive subscription {sub_id}")
|
||||
continue
|
||||
|
||||
start_time = parse(sub_data['start_time'])
|
||||
expiry = parse(sub_data['expiry'])
|
||||
warning_time, grace_end_time = calculate_notification_times(start_time, expiry)
|
||||
|
||||
# Calculate remaining time
|
||||
remaining_time = expiry - now
|
||||
logger.debug(f"Subscription {sub_id} has {remaining_time} remaining")
|
||||
|
||||
# Handle warnings
|
||||
if now >= warning_time and not sub_data.get('warning_sent'):
|
||||
message = get_notification_message(sub_data, remaining_time)
|
||||
logger.info(f"Sending notification for subscription {sub_id}: {message}")
|
||||
|
||||
notify_user(sub_id, message)
|
||||
|
||||
sub_data['warning_sent'] = True
|
||||
modified = True
|
||||
|
||||
# Handle expiration
|
||||
if now >= grace_end_time and sub_data.get('status') == 'Active':
|
||||
logger.info(f"Processing expiration for subscription {sub_id}")
|
||||
|
||||
try:
|
||||
logger.debug(f"Running cleanup playbook for {sub_id}")
|
||||
result = run_cleanup_playbook(sub_id)
|
||||
|
||||
if result.returncode == 0:
|
||||
sub_data['status'] = 'Expired'
|
||||
sub_data['cleanup_date'] = now.isoformat()
|
||||
modified = True
|
||||
logger.info(f"Successfully cleaned up subscription {sub_id}")
|
||||
else:
|
||||
logger.error(f"Cleanup failed: {result.stderr}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during cleanup: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing subscription {sub_id}: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
continue
|
||||
|
||||
if modified:
|
||||
save_subscriptions(subscriptions)
|
||||
logger.info("Updated subscription database")
|
||||
|
||||
logger.info("Subscription check completed")
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
check_subscriptions()
|
||||
except Exception as e:
|
||||
logger.error(f"Subscription checker failed: {str(e)}")
|
||||
raise
|
1
vault_pass.txt
Normal file
1
vault_pass.txt
Normal file
@ -0,0 +1 @@
|
||||
Q1w2e3r4t5
|
8
venv/bin/normalizer
Executable file
8
venv/bin/normalizer
Executable file
@ -0,0 +1,8 @@
|
||||
#!/home/hashgate/vpn-btcpay-provisioner/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from charset_normalizer.cli import cli_detect
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(cli_detect())
|
Loading…
Reference in New Issue
Block a user