from flask import Flask, request, jsonify, render_template import logging import os from pathlib import Path from .handlers.webhook_handler import handle_payment_webhook from .handlers.payment_handler import BTCPayHandler from .utils.db.operations import DatabaseManager logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) app = Flask(__name__) app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', 'dev-secret-key') btcpay_handler = BTCPayHandler() # Register blueprints from .routes.user import user_bp app.register_blueprint(user_bp, url_prefix='/user') # 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(): try: logger.info("=== Price Calculation Started ===") hours = request.json.get('hours', 0) logger.debug(f"Calculating price for {hours} hours") # Validate input if not isinstance(hours, int) or hours < 1: logger.error(f"Invalid duration value: {hours}") return jsonify({'error': 'Invalid duration'}), 400 # Basic price calculation - adjust formula as needed base_price = hours * 100 # 100 sats per hour # Apply volume discounts original_price = base_price if hours >= 2160: # 3 months base_price = base_price * 0.75 # 25% discount logger.info(f"Applied 3-month discount: {original_price} -> {base_price}") elif hours >= 720: # 1 month base_price = base_price * 0.85 # 15% discount logger.info(f"Applied 1-month discount: {original_price} -> {base_price}") elif hours >= 168: # 1 week base_price = base_price * 0.90 # 10% discount logger.info(f"Applied 1-week discount: {original_price} -> {base_price}") elif hours >= 24: # 1 day base_price = base_price * 0.95 # 5% discount logger.info(f"Applied 1-day discount: {original_price} -> {base_price}") final_price = int(base_price) logger.info(f"Final price calculation: {final_price} sats for {hours} hours") logger.info("=== Price Calculation Completed ===") return jsonify({ 'price': final_price, 'duration': hours, 'original_price': original_price, 'discount_applied': original_price - final_price }) except Exception as e: logger.error(f"Error in price calculation: {str(e)}") logger.error("Traceback:", exc_info=True) return jsonify({'error': 'Failed to calculate price'}), 500 try: logger.info("=== Create Invoice Request Started ===") logger.info(f"Received invoice creation request with data: {request.json}") data = request.json logger.debug(f"Request data: {data}") # Validate input data duration_hours = data.get('duration') user_id = data.get('user_id') public_key = data.get('public_key') is_renewal = data.get('is_renewal', False) prev_sub_id = data.get('previous_subscription_id') logger.info(f"Validating request parameters: duration={duration_hours}, user_id={user_id}, " \ f"has_public_key={bool(public_key)}, is_renewal={is_renewal}") # Validate required fields if not duration_hours: logger.error("Duration missing from request") return jsonify({'error': 'Duration is required'}), 400 if not user_id: logger.error("User ID missing from request") return jsonify({'error': 'User ID is required'}), 400 if not public_key: logger.error("Public key missing from request") return jsonify({'error': 'Public key is required'}), 400 try: duration_hours = int(duration_hours) if duration_hours < 1 or duration_hours > 2160: logger.error(f"Invalid duration value: {duration_hours}") return jsonify({'error': 'Invalid duration value'}), 400 logger.info(f"Converted duration to integer: {duration_hours}") except ValueError: logger.error(f"Invalid duration value: {duration_hours}") return jsonify({'error': 'Invalid duration value'}), 400 # Get or validate user user = DatabaseManager.get_user_by_uuid(user_id) if not user and not is_renewal: user = DatabaseManager.create_user(user_id) logger.info(f"Created new user with ID: {user_id}") elif not user: logger.error(f"User not found for renewal: {user_id}") return jsonify({'error': 'User not found'}), 404 # Calculate price price_response = calculate_price() if isinstance(price_response, tuple): logger.error("Price calculation failed") return price_response amount_sats = price_response.json['price'] logger.info(f"Calculated price: {amount_sats} sats for {duration_hours} hours") # Prepare metadata for invoice metadata = { 'is_renewal': is_renewal, 'duration_hours': duration_hours, 'user_id': user_id } if is_renewal and prev_sub_id: metadata['previous_subscription_id'] = prev_sub_id # Validate previous subscription prev_sub = DatabaseManager.get_subscription_by_id(prev_sub_id) if not prev_sub: logger.error(f"Previous subscription not found: {prev_sub_id}") return jsonify({'error': 'Previous subscription not found'}), 404 metadata['previous_expiry'] = prev_sub.expiry_time.isoformat() # Create BTCPay invoice logger.info("Creating BTCPay invoice") invoice_data = btcpay_handler.create_invoice( amount_sats=amount_sats, duration_hours=duration_hours, user_id=user_id, public_key=public_key, metadata=metadata ) 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')}") logger.info("=== Create Invoice Request Completed ===") return jsonify(invoice_data) except Exception as e: logger.error(f"Error in create_invoice endpoint: {str(e)}") logger.error(f"Traceback: ", exc_info=True) return jsonify({'error': str(e)}), 500 @app.route('/create-invoice', methods=['POST']) def create_invoice(): try: logger.info("=== Create Invoice Request Started ===") logger.info(f"Received invoice creation request with data: {request.json}") data = request.json # Validate input data duration_hours = data.get('duration') user_id = data.get('user_id') public_key = data.get('public_key') is_renewal = data.get('is_renewal', False) prev_sub_id = data.get('previous_subscription_id') logger.info(f"Validating request parameters: duration={duration_hours}, user_id={user_id}, " \ f"has_public_key={bool(public_key)}, is_renewal={is_renewal}") # Validate required fields if not duration_hours: logger.error("Duration missing from request") return jsonify({'error': 'Duration is required'}), 400 if not user_id: logger.error("User ID missing from request") return jsonify({'error': 'User ID is required'}), 400 if not public_key: logger.error("Public key missing from request") return jsonify({'error': 'Public key is required'}), 400 try: duration_hours = int(duration_hours) if duration_hours < 1 or duration_hours > 2160: logger.error(f"Invalid duration value: {duration_hours}") return jsonify({'error': 'Invalid duration value'}), 400 logger.info(f"Converted duration to integer: {duration_hours}") except ValueError: logger.error(f"Invalid duration value: {duration_hours}") return jsonify({'error': 'Invalid duration value'}), 400 # Calculate price directly here base_price = duration_hours * 100 original_price = base_price if duration_hours >= 2160: base_price = base_price * 0.75 elif duration_hours >= 720: base_price = base_price * 0.85 elif duration_hours >= 168: base_price = base_price * 0.90 elif duration_hours >= 24: base_price = base_price * 0.95 amount_sats = int(base_price) logger.info(f"Calculated price: {amount_sats} sats for {duration_hours} hours") # Get or validate user user = DatabaseManager.get_user_by_uuid(user_id) if not user and not is_renewal: user = DatabaseManager.create_user(user_id) logger.info(f"Created new user with ID: {user_id}") elif not user: logger.error(f"User not found for renewal: {user_id}") return jsonify({'error': 'User not found'}), 404 # Prepare metadata for invoice metadata = { 'is_renewal': is_renewal, 'duration_hours': duration_hours, 'user_id': user_id } if is_renewal and prev_sub_id: metadata['previous_subscription_id'] = prev_sub_id prev_sub = DatabaseManager.get_subscription_by_id(prev_sub_id) if not prev_sub: logger.error(f"Previous subscription not found: {prev_sub_id}") return jsonify({'error': 'Previous subscription not found'}), 404 metadata['previous_expiry'] = prev_sub.expiry_time.isoformat() # Create BTCPay invoice logger.info("Creating BTCPay invoice") invoice_data = btcpay_handler.create_invoice( amount_sats=amount_sats, duration_hours=duration_hours, user_id=user_id, public_key=public_key ) 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')}") logger.info("=== Create Invoice Request Completed ===") return jsonify(invoice_data) except Exception as e: logger.error(f"Error in create_invoice endpoint: {str(e)}") logger.error(f"Traceback: ", exc_info=True) return jsonify({'error': str(e)}), 500 @app.route('/api/vpn-config/') def get_vpn_config(user_id): try: logger.info(f"Fetching VPN config for user: {user_id}") subscription = DatabaseManager.get_active_subscription_for_user(user_id) if not subscription: logger.error(f"No active subscription found for user {user_id}") return jsonify({"error": "No active subscription found"}), 404 # Get the config based on test or production path base_path = Path('/etc/wireguard') if subscription.invoice_id.startswith('__test__'): config_path = base_path / 'test_clients' / subscription.invoice_id / 'wg0.conf' else: config_path = base_path / 'clients' / subscription.invoice_id / 'wg0.conf' logger.info(f"Looking for config at: {config_path}") if not config_path.exists(): logger.error(f"Configuration file not found at {config_path}") return jsonify({"error": "Configuration file not found"}), 404 with open(config_path) as f: config_text = f.read() # Check if this is a renewed subscription renewal_info = None if hasattr(subscription, 'metadata') and subscription.metadata: if subscription.metadata.get('is_renewal'): prev_sub_id = subscription.metadata.get('previous_subscription_id') if prev_sub_id: prev_sub = DatabaseManager.get_subscription_by_id(prev_sub_id) if prev_sub: renewal_info = { 'previous_id': prev_sub_id, 'previous_expiry': prev_sub.expiry_time.isoformat() } logger.info(f"Successfully retrieved config for user {user_id}") return jsonify({ "configText": config_text, "status": "active", "expiryTime": subscription.expiry_time.isoformat() if subscription.expiry_time else None, "renewalInfo": renewal_info }) except Exception as e: logger.error(f"Error retrieving VPN config: {str(e)}") logger.error("Traceback:", exc_info=True) return jsonify({"error": "Failed to retrieve configuration"}), 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)