from typing import cast, List from flask import render_template, current_app, redirect, url_for, request, abort, session, flash from app.utils import get_active_banner, get_course, generate_password, get_available_decks, get_passwords, get_all_courses, is_safe_url, get_token, create_token, retrieve_token, set_cookie from app.models import db, InfoBanner, Password, Deck from app.forms import InfoBannerForm, DeckForm, PasswordForm, LoginForm from app.auth import user_loader, check_password from datetime import datetime import pytz from . import admin_bp import flask_login @admin_bp.route("/login", methods=["POST", "GET"]) def admin_login(): form = LoginForm() if form.validate_on_submit(): assert isinstance(form.user_name.data, str) assert isinstance(form.password.data, str) user_name: str = form.user_name.data password: str = form.password.data user = user_loader(user_name) if user and check_password(user, password): flask_login.login_user(user) current_app.logger.info(f"Admin login successful: {user_name}") next_page = request.args.get("next") if next_page and not is_safe_url(next_page): current_app.logger.warning( f"Blocked unsafe redirect attempt to: {next_page}" ) return abort(400) if next_page: session["next_url"] = next_page if not get_token(): token = retrieve_token(user_name) if not token: token = create_token(user_name) return set_cookie(token) return redirect(next_page or url_for("admin.admin")) else: current_app.logger.warning(f"Failed admin login attempt: {user_name}") flash("Incorrect username or password. Please try again.", "error") return render_template("admin_login.html", form=form) @admin_bp.route("/", methods=["GET", "POST"]) @flask_login.login_required def admin(): active_course = get_course(flask_login.current_user.active_course_id) if active_course is None: current_app.logger.error( f"Admin user {flask_login.current_user.id} has no active_course_id." ) raise Exception deck_form = DeckForm() password_form = PasswordForm() if request.method == "GET": password_form.value.data = generate_password() deck_form.deck.choices = get_available_decks(active_course) decks = active_course.decks password_form.decks.choices = [(deck.id, deck.name) for deck in decks] if request.method == "POST": if "submit_deck" in request.form and deck_form.validate(): deck = Deck( name=cast(str, deck_form.name.data), deck=deck_form.deck.data, course=active_course, description=cast(str, deck_form.description.data), ) db.session.add(deck) db.session.commit() current_app.logger.info(f"Admin created deck: {deck.name}") if "submit_password" in request.form: if password_form.validate(): value: str = password_form.value.data expires_at: datetime | None = password_form.expires_at.data if expires_at: tz = pytz.timezone("Europe/Berlin") utc = pytz.utc expires_at = tz.localize(expires_at).astimezone(utc) password = Password(value=value, expires_at=expires_at) decks = db.session.execute( db.select(Deck).filter( Deck.id.in_(cast(List[Deck], password_form.decks.data)) ) ).scalars() password.decks.extend(decks) db.session.add(password) db.session.commit() current_app.logger.info( f"Admin created password: {value} (Expires: {expires_at})" ) else: # Form was submitted but validation failed for field, errors in password_form.errors.items(): for error in errors: current_app.logger.warning( f"Admin form validation error ({field}): {error}" ) return redirect(url_for("admin.admin")) return render_template( "admin.html", deck_form=deck_form, password_form=password_form, decks=decks, passwords=get_passwords(active_course), courses=get_all_courses(), active_course_name=active_course.name, ) @admin_bp.route("/switch_course/", methods=["POST"]) @flask_login.login_required def switch_course_admin(new_course: int): user = flask_login.current_user if get_course(new_course): user.active_course_id = new_course current_app.logger.info(f"Admin {user.id} switched to course ID {new_course}") return redirect(url_for("admin.admin")) @admin_bp.route("/banner", methods=["POST", "GET"]) @flask_login.login_required def banner(): banner_form = InfoBannerForm() if banner_form.validate_on_submit(): assert isinstance(banner_form.content.data, str) content: str = banner_form.content.data banner = InfoBanner(content=content) db.session.add(banner) db.session.commit() current_app.logger.info(f"Admin added banner: {content[:30]}...") return render_template( "banner.html", banner_form=banner_form, banners=db.session.execute(db.select(InfoBanner)).scalars(), ) @admin_bp.route("/decks//delete", methods=["POST"]) @flask_login.login_required def delete_deck(deck_id: int): deck = Deck.query.get_or_404(deck_id) current_app.logger.info(f"Admin deleted deck: {deck.name}") db.session.delete(deck) db.session.commit() return redirect(url_for("admin.admin")) @admin_bp.route("/passwords//delete", methods=["POST"]) @flask_login.login_required def delete_password(password_id: int): password = Password.query.get_or_404(password_id) current_app.logger.info(f"Admin deleted password ID {password_id}") db.session.delete(password) db.session.commit() return redirect(url_for("admin.admin")) @admin_bp.route("/banner/show/", methods=["POST"]) @flask_login.login_required def set_active_banner(banner_id): banner = InfoBanner.query.get(banner_id) if banner: # ensures that only one banner is active at all times active_banner = get_active_banner() if active_banner: active_banner.is_active = False banner.is_active = True db.session.commit() current_app.logger.info(f"Admin activated banner ID {banner_id}") return redirect(url_for("admin.banner")) @admin_bp.route("/banner/hide", methods=["POST"]) @flask_login.login_required def hide_active_banner(): banner = get_active_banner() if banner: banner.is_active = False db.session.commit() current_app.logger.info("Admin hid active banner") return redirect(url_for("admin.banner")) @admin_bp.route("/confused") @flask_login.login_required def confused_notifications(): return render_template("confused.html") @admin_bp.route("/dashboard") @flask_login.login_required def dashboard(): return render_template( 'dashboard.html', grafana_url='/dashboard/d/tutor-tool' )