From 02fc60151fb39da6f7a7b728bbe56f629f1e1dc3 Mon Sep 17 00:00:00 2001 From: cato447 Date: Tue, 14 Oct 2025 01:30:22 +0200 Subject: [PATCH] finished auth overhaul and started tenants --- app/auth.py | 2 +- app/models.py | 26 ++++- app/routes.py | 88 ++++++++++++----- .../{login.html => admin_login.html} | 2 +- app/templates/index.html | 99 ++++++++++--------- app/templates/register.html | 17 +++- app/templates/set_token.html | 53 ++++++++++ app/utils.py | 11 ++- config.py.example | 1 + debug.py | 2 +- 10 files changed, 215 insertions(+), 86 deletions(-) rename app/templates/{login.html => admin_login.html} (99%) create mode 100644 app/templates/set_token.html diff --git a/app/auth.py b/app/auth.py index 79b7c0d..350422c 100644 --- a/app/auth.py +++ b/app/auth.py @@ -9,7 +9,7 @@ users: Dict[str, User] def init_auth(app: Flask): login_manager.init_app(app) - login_manager.login_view = "main.login" # type: ignore + login_manager.login_view = "main.admin_login" # type: ignore global users users = {"admin": User("admin", app.config["ADMIN_PASSWORD"])} diff --git a/app/models.py b/app/models.py index b00d937..3ff3ad9 100644 --- a/app/models.py +++ b/app/models.py @@ -5,9 +5,8 @@ from typing import List, Optional import pytz from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import Boolean, Column, DateTime, String, TypeDecorator +from sqlalchemy import Boolean, Column, DateTime, String, TypeDecorator, ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship - db : SQLAlchemy = SQLAlchemy() deck_password = db.Table( @@ -69,19 +68,25 @@ class Deck(db.Model): passwords : Mapped[List[Password]] = relationship("Password", secondary=deck_password, back_populates='decks') tokens : Mapped[List[Token]] = relationship("Token", secondary=deck_token, back_populates='decks') + course_id: Mapped[int] = mapped_column(ForeignKey("course.id")) + course: Mapped['Course'] = relationship("Course", back_populates='decks') - def __init__(self, name: str, deck: str, description: str = "") -> None: + + def __init__(self, name: str, deck: str, course: Course, description: str = "") -> None: self.name = name self.deck = deck self.description = description + self.course = course def __repr__(self) -> str: - return f"" + return f"" class Token(db.Model): __tablename__ = "token" id : Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(60), unique=True, nullable=False) + active_course_id: Mapped[int] = mapped_column(ForeignKey("course.id"), nullable=True) + active_course: Mapped[Course] = relationship("Course", foreign_keys=[active_course_id]) decks : Mapped[List[Deck]]= relationship("Deck", secondary=deck_token, back_populates='tokens') def __init__(self, name: str) -> None: @@ -93,6 +98,19 @@ class Token(db.Model): def is_unlocked(self, deck: str) -> bool : return deck in [deck.deck for deck in self.decks] +class Course(db.Model): + __tablename__ = "course" + id : Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(60), unique=True, nullable=False) + decks: Mapped[List['Deck']] = relationship(back_populates='course') + + def __init__(self, name: str) -> None: + self.name = name + + def __repr__(self) -> str: + return f"" + + class InfoBanner(db.Model): __tablename__ = "info_banner" id : Mapped[int] = mapped_column(primary_key=True) diff --git a/app/routes.py b/app/routes.py index f16d22b..1b0a0a7 100644 --- a/app/routes.py +++ b/app/routes.py @@ -12,7 +12,6 @@ from flask import ( abort, current_app, flash, - jsonify, make_response, redirect, render_template, @@ -29,8 +28,8 @@ from .models import Deck, InfoBanner, Password, db from .utils import ( create_token, generate_password, - generate_token, get_active_banner, + get_all_courses, get_available_decks, get_token, get_unlocked_decks, @@ -38,6 +37,7 @@ from .utils import ( retrieve_token, serialize_token, update_token, + get_course ) @@ -98,9 +98,39 @@ def inject_confused_button(response): def index(): token = get_token() if not token: - return redirect(url_for("main.register")) + return redirect(url_for("main.set_token")) else: - return render_template('index.html', active_banner=get_active_banner(), decks=get_unlocked_decks(token), user_name=token.name) + return render_template('index.html', active_banner=get_active_banner(), decks=get_unlocked_decks(token), user_name=token.name, active_course_id=token.active_course_id, courses=get_all_courses()) + +@main.route('/set_token', methods=['POST', 'GET']) +def set_token(): + """ + Handles. + GET: Renders the registration form. + POST: Processes the form submission. + """ + if request.method == 'POST': + # Get form data + user_name = request.form.get('username') + + # Simple validation + if not user_name: + flash('Username is required.', 'error') + return render_template('set_token.html') + + token = retrieve_token(user_name) + + if not token: + flash(f'The username "{user_name}" does not exist.', 'error') + return render_template('set_token.html') + + resp = make_response(redirect(url_for("main.index"))) + resp.set_cookie('access_token', serialize_token(token), httponly=True) + return resp + + # If GET request, render the form + return render_template('set_token.html') + @main.route('/register', methods=['POST', 'GET']) def register(): @@ -134,15 +164,34 @@ def register(): resp = make_response(redirect(url_for("main.index"))) resp.set_cookie('access_token', serialize_token(token), httponly=True) - flash(f'Registration successful for {user_name}!', 'success') - # Redirect to the login page after successful registration return resp # If GET request, render the form return render_template('register.html') -@main.route('/login', methods=['POST', 'GET']) -def login(): +@main.route('/logout', methods=['POST']) +def logout(): + resp = make_response(redirect(url_for('main.index'))) # Redirect to a safe page, e.g., index or login + resp.set_cookie('access_token', '', expires=0) # Clears the cookie + return resp + +@main.route('/switch_course/', methods=['POST']) +def switch_course(new_course: int): + token = get_token() + + if not token: + return redirect(url_for('main.set_token')) + + switched = get_course(new_course) + + if switched: + token.active_course = switched + db.session.commit() + + return redirect(url_for("main.index")) + +@main.route('/admin/login', methods=['POST', 'GET']) +def admin_login(): form = LoginForm() if form.validate_on_submit(): assert isinstance(form.user_name.data, str) @@ -152,8 +201,6 @@ def login(): user = user_loader(user_name) if user and check_password(user, password): flask_login.login_user(user) - flash('Login successful!', 'success') - next = request.args.get('next') if next is None: return redirect(url_for('main.index')) @@ -164,7 +211,7 @@ def login(): return redirect(next) else: flash('Incorrect username or password. Please try again.', 'error') - return render_template("login.html", form=form) + return render_template("admin_login.html", form=form) @main.route('/access', methods=['POST']) @@ -174,7 +221,7 @@ def access(): token = get_token() if password is not None and not password.is_expired(): update_token(password.decks) - return redirect('/') + return redirect(url_for('main.index')) return render_template('index.html', error="Invalid password.", decks=get_unlocked_decks(token)) @@ -185,14 +232,13 @@ def access_link(access_code: str): token = get_token() if password is not None and not password.is_expired(): update_token(password.decks) - return redirect('/') + return redirect(url_for('main.index')) return render_template('index.html', error="Invalid password.", decks=get_unlocked_decks(token)) @main.route('/confused', methods=['POST']) def confused(): - socketio.emit( "confused", { @@ -202,18 +248,6 @@ def confused(): ) return ("", 204) - -@main.route('/retrieve-token', methods=['POST']) -def set_username_token(): - new_username : str = request.form.get("user_name", "").strip() - resp = make_response(redirect('/')) - if new_username: - token = retrieve_token(new_username) - if token: - resp.set_cookie('access_token', serialize_token(token), httponly=True) - return resp - - @main.route('/slides//') @main.route('/slides//') def serve_deck(deck, path='index.html'): @@ -221,7 +255,7 @@ def serve_deck(deck, path='index.html'): abort(404) if not is_deck_unlocked(deck): - return redirect('/') + return redirect(url_for('main.index')) slides_dir = current_app.config['SLIDES_DIR'] deck_path = os.path.join(slides_dir, deck) diff --git a/app/templates/login.html b/app/templates/admin_login.html similarity index 99% rename from app/templates/login.html rename to app/templates/admin_login.html index 166a68a..6082245 100644 --- a/app/templates/login.html +++ b/app/templates/admin_login.html @@ -17,7 +17,7 @@
-

Login

+

Admin Login

{% with messages = get_flashed_messages(with_categories=true) %} diff --git a/app/templates/index.html b/app/templates/index.html index 33f0696..79a0427 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -41,59 +41,64 @@
-
- -
- - -
- - -
-
- -
- - - +
+ + +
-
-
+ + +
+ + User: {{ user_name }} + + +
+ +
+ + + + +
+
+ + + -
-

GRNVS Tutorium SS 25

+

{{course_display_name}}

{% if active_banner %} diff --git a/app/templates/register.html b/app/templates/register.html index cf7729d..a8fdea0 100644 --- a/app/templates/register.html +++ b/app/templates/register.html @@ -19,7 +19,20 @@

Create an Account

Choose a username to get started.

- + + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} + {% set alert_class = 'bg-red-900 border-red-400 text-red-100' if category == 'error' else 'bg-green-900 border-green-400 text-green-100' %} + + {% endfor %} +
+ {% endif %} + {% endwith %} +
@@ -46,7 +59,7 @@

- Already have an account? Log In + Already have an account? Log In

diff --git a/app/templates/set_token.html b/app/templates/set_token.html new file mode 100644 index 0000000..2780d7a --- /dev/null +++ b/app/templates/set_token.html @@ -0,0 +1,53 @@ + + + + + + Register for Tutortool + + + + +
+
+

Login

+
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} + {% set alert_class = 'bg-red-900 border-red-400 text-red-100' if category == 'error' else 'bg-green-900 border-green-400 text-green-100' %} + + {% endfor %} +
+ {% endif %} + {% endwith %} + +
+ +
+ + +
+ + +
+ +

+ Do not have an Account? Create one +

+
+ + + diff --git a/app/utils.py b/app/utils.py index 2b00875..abc94a6 100644 --- a/app/utils.py +++ b/app/utils.py @@ -5,7 +5,7 @@ from typing import cast from itsdangerous import BadSignature, URLSafeSerializer from xkcdpass import xkcd_password -from .models import Deck, InfoBanner, Token, db +from .models import Deck, InfoBanner, Token, db, Course def init_password_generator(): wordfile = xkcd_password.locate_wordfile() @@ -57,7 +57,7 @@ def get_token() -> (Token | None): return None try: id = serializer.loads(token) - token = db.session.execute(db.select(Token).filter_by(id=id)).scalar_one_or_none() + token = db.session.get(Token, id) return token except BadSignature: return None @@ -65,7 +65,6 @@ def get_token() -> (Token | None): def retrieve_token(username : str): return db.session.execute(db.select(Token).filter_by(name=username)).scalar_one_or_none() - def get_unlocked_decks(token: (Token|None)): if token is None: return [] @@ -90,3 +89,9 @@ def update_token(decks: list[Deck]): else: token.decks = list(set(decks + token.decks)) db.session.commit() + +def get_course(id: int) -> (Course | None): + return db.session.get(Course, id) + +def get_all_courses() -> list[Course]: + return db.session.execute(db.select(Course).order_by(Course.id)).scalars().all() diff --git a/config.py.example b/config.py.example index 62c3849..9ac9e7d 100644 --- a/config.py.example +++ b/config.py.example @@ -4,4 +4,5 @@ class Config: SQLALCHEMY_DATABASE_URI = SQLALCHEMY_TRACK_MODIFICATIONS = ADMIN_PASSWORD = + COURSE_MAP = diff --git a/debug.py b/debug.py index 00faad1..bc0b0f7 100644 --- a/debug.py +++ b/debug.py @@ -3,4 +3,4 @@ from app import create_app, socketio app = create_app() if __name__ == '__main__': - socketio.run(app, host="127.0.0.1", port=5000, debug=True) + socketio.run(app, host="0.0.0.0", port=5000, debug=True)