210 lines
7.5 KiB
Python
210 lines
7.5 KiB
Python
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, token_required
|
|
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
|
|
@token_required
|
|
def admin(token):
|
|
active_course = get_course(token.active_course)
|
|
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/<new_course>", methods=["POST"])
|
|
@flask_login.login_required
|
|
@token_required
|
|
def switch_course_admin(new_course: int, token):
|
|
user = flask_login.current_user
|
|
if get_course(new_course):
|
|
token.active_course = new_course
|
|
db.session.commit()
|
|
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/<int:deck_id>/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/<int:password_id>/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/<int:banner_id>", 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'
|
|
)
|