331 lines
13 KiB
Python
331 lines
13 KiB
Python
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/<user_id>')
|
|
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) |