from flask import Blueprint, render_template, request, flash, redirect, url_for, current_app from flask_mail import Message from itsdangerous import URLSafeTimedSerializer, SignatureExpired, BadSignature from app.extensions import db, mail, limiter from app.models import Participant import traceback bp = Blueprint('main', __name__) def get_confirmed_count(): return Participant.query.filter_by(confirmed=True).count() def add_user(name, email, looking_for_team, on_waitlist = False): new_user = Participant(name=name, email=email, looking_for_team=looking_for_team) if on_waitlist: new_user.on_waitlist = on_waitlist db.session.add(new_user) db.session.commit() return new_user def send_verification_email(user, is_new=True): if user.num_emails_sent < 5: s = URLSafeTimedSerializer(current_app.config['SECRET_KEY']) register_token = s.dumps(user.email, salt='email-confirm-h4tum-ctf-2025') remove_token = s.dumps(user.email, salt='email-remove-h4tum-ctf-2026') register_link = url_for('main.verify_email', token=register_token, _external=True) remove_link = url_for('main.remove_registration', token=remove_token, _external=True) logo = url_for('static', filename='h4tum_white.png', _external=True) msg = Message('Verify your CTF Registration', recipients=[user.email]) msg.html = render_template("email.html", user_name=user.name, register_link=register_link, remove_link=remove_link, logo_url=logo) mail.send(msg) current_app.logger.info(f"Sent verification email to {user.email}") user.num_emails_sent += 1 db.session.commit() if is_new: flash("Registration started! Check your email to confirm your spot.", "success") else: flash("Confirmation email resent. Please check your inbox.", "info") else: flash("Email limit reached", "error") def send_waitlist_email(user): msg = Message('h4tum CTF 2026 Registration', recipients=[user.email]) logo = url_for('static', filename='h4tum_white.png', _external=True) msg.html = render_template("waitlist_email.html", logo_url=logo, user_name=user.name) current_app.logger.info(msg) mail.send(msg) def send_waitlist_newsletter(): results = db.session.query(Participant.email).filter(Participant.on_waitlist == True).all() waitlist_emails = [r[0] for r in results] logo = url_for('static', filename='h4tum_white.png', _external=True) s = URLSafeTimedSerializer(current_app.config['SECRET_KEY']) for email in waitlist_emails: register_token = s.dumps(email, salt='email-confirm-h4tum-ctf-2025') register_link = url_for('main.verify_email', token=register_token, _external=True) remove_token = s.dumps(email, salt='email-remove-h4tum-ctf-2026') remove_link = url_for('main.remove_registration', token=remove_token, _external=True) msg = Message('h4tum CTF 2026 Free Spot available', recipients=[email]) msg.html = render_template("waitlist_maillist_email.html", logo_url=logo, register_link=register_link, remove_link=remove_link) mail.send(msg) current_app.logger.info(f"Sent waitlist newsletter email to {email}") @bp.route('/', methods=['GET', 'POST']) @limiter.limit("5 per hour", methods=["POST"]) def index(): confirmed_count = get_confirmed_count() max_spots = current_app.config['MAX_SPOTS'] spots_left = max_spots - confirmed_count if request.method == 'POST': name = request.form['name'] email = request.form['email'].lower().strip() looking_for_team = True if request.form.get('looking_for_team') else False tum_domains = current_app.config['TUM_DOMAINS'] if not email.endswith(tum_domains): domains_str = ", ".join(tum_domains) flash(f"Error: You must use one of these email addresses: {domains_str}", "error") return redirect(url_for('main.index')) existing_user = Participant.query.filter_by(email=email).first() if existing_user: if existing_user.confirmed: flash("You are already registered and confirmed!", "info") elif existing_user.on_waitlist: if confirmed_count < max_spots: existing_user.on_waitlist = False db.session.commit() send_verification_email(existing_user) else: flash("You are already on the waitlist", "info") else: send_verification_email(existing_user, is_new=False) return redirect(url_for('main.index')) else: new_user = add_user(name, email, looking_for_team) if confirmed_count < max_spots: send_verification_email(new_user) else: new_user.on_waitlist = True db.session.commit() send_waitlist_email(new_user) flash("The event is already full!\nYou have been placed on the waitlist", "info") return redirect(url_for('main.index')) return render_template('index.html', spots_left=spots_left, max_spots=max_spots) @bp.route('/verify/') @limiter.limit("50 per hour") def verify_email(token): s = URLSafeTimedSerializer(current_app.config['SECRET_KEY']) try: email = s.loads(token, salt='email-confirm-h4tum-ctf-2025', max_age=1209600) except SignatureExpired: return render_template("error.html", error_message="Token expired. Please register again.") except BadSignature: return render_template("error.html", error_message="Invalid token.") user = Participant.query.filter_by(email=email).first_or_404() if get_confirmed_count() >= current_app.config['MAX_SPOTS']: if user.confirmed: return render_template('success.html', message="You are confirmed", discord_link=current_app.config['DISCORD_LINK'], discord_token=user.discord_token) else: return render_template("error.html", error_message="Sorry! The event filled up while you were claiming the last spot.") else: user.confirmed = True if user.on_waitlist: user.on_waitlist = False db.session.commit() discord_link = current_app.config['DISCORD_LINK'] discord_token = user.discord_token msg = "You are confirmed!" if user.looking_for_team: msg += " Since you need a team, please join the #team-search channel." return render_template('success.html', message=msg, discord_link=discord_link, discord_token=discord_token) @bp.route('/remove/') @limiter.limit("5 per hour") def remove_registration(token): s = URLSafeTimedSerializer(current_app.config['SECRET_KEY']) try: email = s.loads(token, salt='email-remove-h4tum-ctf-2026', max_age=1209600) except SignatureExpired: return render_template("error.html", error_message="Token expired. Please register again.") except BadSignature: return render_template("error.html", error_message="Invalid token.") user = Participant.query.filter_by(email=email).first_or_404() if not user: return render_template("error.html", error_message="No user found.") db.session.delete(user) db.session.commit() if get_confirmed_count() == current_app.config['MAX_SPOTS'] - 1: send_waitlist_newsletter() return render_template("deregistered.html", message="Sad to see you go. Comeback next time :D") @bp.route('/dsgvo') @limiter.exempt def dsgvo(): return render_template('dsgvo.html') @bp.route('/impressum') @limiter.exempt def impressum(): return render_template('impressum.html') @bp.route('/rules') @limiter.exempt def rules(): return render_template('rules.html') @bp.errorhandler(429) def ratelimit_handler(e): msg = Message('429 Ratelimit hit', recipients=["simon.bussmann@tum.de"]) msg.body = f"IP {request.remote_addr} triggered the rate limit" mail.send(msg) return render_template("ratelimit_error.html", error_message=e.description), 429 @bp.errorhandler(500) def server_error(e): msg = Message('500 Error on registration Website', recipients=["simon.bussmann@tum.de"]) full_traceback = traceback.format_exc() msg.body = f"An error occurred during registration:\n\n{full_traceback}" mail.send(msg) return render_template("error.html", error_message="Internal Server Error. Admin has been notified!"), 500