Untitled
public
Jul 27, 2024
Never
19
1 from flask import Flask, request, jsonify, url_for, redirect, flash, send_from_directory 2 from flask_sqlalchemy import SQLAlchemy 3 from flask_mail import Mail 4 from itsdangerous import URLSafeTimedSerializer, SignatureExpired, BadTimeSignature 5 from datetime import datetime 6 from flask_wtf.csrf import CSRFProtect, generate_csrf 7 from flask_cors import CORS 8 from dotenv import load_dotenv 9 import os 10 import logging 11 import requests 12 from sqlalchemy.exc import IntegrityError 13 14 load_dotenv() 15 16 app = Flask(__name__) 17 CORS(app, resources={r"/*": {"origins": "https://pnyx.nu"}}, supports_credentials=True) 18 19 app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'your-default-secret-key') 20 app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL').replace('postgres://', 'postgresql://') 21 app.config['MAILGUN_DOMAIN'] = os.getenv('MAILGUN_DOMAIN', 'your-mailgun-domain') 22 app.config['MAILGUN_API_KEY'] = os.getenv('MAILGUN_API_KEY', 'your-mailgun-api-key') 23 24 app.config['MAIL_SERVER'] = 'smtp.mailgun.org' 25 app.config['MAIL_PORT'] = 587 26 app.config['MAIL_USERNAME'] = os.getenv('MAIL_USERNAME', 'your-mail-username') 27 app.config['MAIL_PASSWORD'] = os.getenv('MAIL_PASSWORD', 'your-mail-password') 28 app.config['MAIL_USE_TLS'] = True 29 app.config['MAIL_USE_SSL'] = False 30 31 db = SQLAlchemy(app) 32 mail = Mail(app) 33 csrf = CSRFProtect(app) 34 serializer = URLSafeTimedSerializer(app.config['SECRET_KEY']) 35 36 # Configure logging 37 logging.basicConfig(level=logging.INFO) 38 logger = logging.getLogger(__name__) 39 40 class Registration(db.Model): 41 id = db.Column(db.Integer, primary_key=True) 42 firstname = db.Column(db.String(100), nullable=False) 43 lastname = db.Column(db.String(100), nullable=False) 44 email = db.Column(db.String(120), nullable=False, unique=True) 45 phone = db.Column(db.String(20), nullable=False) 46 birthday = db.Column(db.Date, nullable=False) 47 confirmed = db.Column(db.Boolean, default=False) 48 49 @app.route('/register', methods=['POST', 'OPTIONS']) 50 @csrf.exempt 51 def register(): 52 if request.method == 'OPTIONS': 53 response = jsonify({'message': 'CORS preflight'}) 54 response.headers.add('Access-Control-Allow-Origin', 'https://pnyx.nu') 55 response.headers.add('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE, PUT') 56 response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization,X-Requested-With,X-CSRFToken,Accept') 57 response.headers.add('Access-Control-Allow-Credentials', 'true') 58 return response 59 60 data = request.get_json() 61 62 logger.info('Received registration data: %s', data) 63 64 if not data or not all(key in data for key in ['firstname', 'lastname', 'email', 'phone', 'countryCode', 'birthday']): 65 logger.warning('Missing data in the request') 66 return jsonify({'message': 'Missing data'}), 400 67 68 token = serializer.dumps(data['email'], salt='email-confirm-salt') 69 confirm_url = url_for('confirm_email', token=token, _external=True) 70 71 try: 72 birthday = datetime.strptime(data['birthday'], '%Y-%m-%d').date() 73 except ValueError: 74 logger.warning('Invalid date format: %s', data['birthday']) 75 return jsonify({'message': 'Invalid date format. Please use YYYY-MM-DD.'}), 400 76 77 new_registration = Registration( 78 firstname=data['firstname'], 79 lastname=data['lastname'], 80 email=data['email'], 81 phone=data['countryCode'] + data['phone'], 82 birthday=birthday 83 ) 84 85 try: 86 db.session.add(new_registration) 87 db.session.commit() 88 logger.info('Registration saved: %s', new_registration) 89 except IntegrityError as e: 90 db.session.rollback() 91 if 'duplicate key value violates unique constraint "registration_email_key"' in str(e): 92 logger.warning('Duplicate email: %s', data['email']) 93 return jsonify({'message': 'This email is already registered.'}), 400 94 logger.error('Error saving registration: %s', str(e)) 95 return jsonify({'message': 'Error saving registration. Please try again later.'}), 500 96 97 send_confirmation_email(data['email'], confirm_url) 98 return jsonify({'message': 'Please check your email to confirm your registration.'}), 200 99 100 def send_confirmation_email(to_email, confirm_url): 101 subject = "Please confirm your registration" 102 body = f"Thank you for registering. Please confirm your registration by clicking the following link: {confirm_url}" 103 104 logger.info('Sending email to: %s', to_email) 105 logger.info('Using MAILGUN_DOMAIN: %s', app.config['MAILGUN_DOMAIN']) 106 logger.info('Using MAILGUN_API_KEY: %s', app.config['MAILGUN_API_KEY']) 107 logger.info('Using MAILGUN_DEFAULT_FROM: %s', app.config['MAILGUN_DEFAULT_FROM']) 108 109 response = requests.post( 110 f"https://api.eu.mailgun.net/v3/{app.config['MAILGUN_DOMAIN']}/messages", 111 auth=("api", app.config['MAILGUN_API_KEY']), 112 data={"from": app.config['MAILGUN_DEFAULT_FROM'], 113 "to": [to_email], 114 "subject": subject, 115 "text": body}) 116 117 logger.info('Mailgun response status code: %s', response.status_code) 118 logger.info('Mailgun response text: %s', response.text) 119 120 if response.status_code != 200: 121 logger.error("Failed to send email: %s", response.text) 122 123 @app.route('/confirm/<token>') 124 def confirm_email(token): 125 try: 126 email = serializer.loads(token, salt='email-confirm-salt', max_age=3600) 127 except (SignatureExpired, BadTimeSignature) as e: 128 logger.error('Error confirming email: %s', e) 129 flash('The confirmation link is invalid or has expired.', 'danger') 130 return redirect(url_for('index')) 131 132 registration = Registration.query.filter_by(email=email).first_or_404() 133 if registration and not registration.confirmed: 134 registration.confirmed = True 135 db.session.commit() 136 logger.info('Email confirmed for: %s', email) 137 return redirect("https://pnyx.nu/confirmation_success.html") 138 else: 139 flash('Email is already confirmed.', 'success') 140 return redirect(url_for('index')) 141 142 @app.route('/') 143 def index(): 144 user_agent = request.headers.get('User-Agent') 145 if "Mobi" in user_agent or "Tablet" in user_agent: 146 return redirect(url_for('index_mobile')) 147 return send_from_directory('.', 'index.html') 148 149 @app.route('/index.mobile.html') 150 def index_mobile(): 151 return send_from_directory('.', 'index.mobile.html') 152 153 @app.route('/get_csrf_token', methods=['GET']) 154 @csrf.exempt 155 def get_csrf_token(): 156 response = jsonify({'csrf_token': generate_csrf()}) 157 response.headers.add('Access-Control-Allow-Origin', 'https://pnyx.nu') 158 response.headers.add('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE, PUT') 159 response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization,X-Requested-With,X-CSRFToken,Accept') 160 response.headers.add('Access-Control-Allow-Credentials', 'true') 161 return response 162 163 @app.after_request 164 def add_security_headers(response): 165 response.headers['Content-Security-Policy'] = ( 166 "default-src 'self'; " 167 "script-src 'self' 'sha256-Bed9qzUU2IVkAAxp73fh88GAnMchALEvifOr4XcPQ/4=' 'sha256-rx35CH5hONLWpWC9d43ABhaVMBiWHzn6/Npxn7D7UT0=' 'sha256-HC/7mAU+xhanEo18GAjE8J4Wnp9Fo4+i/e2PeNW2VeQ=' 'sha256-66gRKVjRFyDmaZVN1QTwH4ThLwRwMwvOZGYWKJ8uXP4=' 'unsafe-eval' https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js; " 168 "style-src 'self' 'sha256-WmyMLJokW+oizRe7FT8qTk+WRdfJE6vhzFX5xSPiGiw=' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css; " 169 "font-src 'self' https://fonts.gstatic.com data:; " 170 "connect-src 'self' https://pnyx-0fe8297d9f8b.herokuapp.com; " 171 "object-src 'none'; " 172 "base-uri 'self';" 173 ) 174 response.headers['Referrer-Policy'] = 'no-referrer' 175 response.headers['X-Content-Type-Options'] = 'nosniff' 176 response.headers['X-Frame-Options'] = 'DENY' 177 response.headers['X-XSS-Protection'] = '1; mode=block' 178 response.headers.add('X-CSRF-TOKEN', generate_csrf()) 179 return response 180 181 if __name__ == '__main__': 182 app.run(debug=False, ssl_context='adhoc')