198 lines
8.4 KiB
Python
198 lines
8.4 KiB
Python
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/<token>')
|
|
@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/<token>')
|
|
@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
|