finished auth overhaul and started tenants
This commit is contained in:
@@ -9,7 +9,7 @@ users: Dict[str, User]
|
|||||||
|
|
||||||
def init_auth(app: Flask):
|
def init_auth(app: Flask):
|
||||||
login_manager.init_app(app)
|
login_manager.init_app(app)
|
||||||
login_manager.login_view = "main.login" # type: ignore
|
login_manager.login_view = "main.admin_login" # type: ignore
|
||||||
global users
|
global users
|
||||||
users = {"admin": User("admin", app.config["ADMIN_PASSWORD"])}
|
users = {"admin": User("admin", app.config["ADMIN_PASSWORD"])}
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ from typing import List, Optional
|
|||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
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
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
db : SQLAlchemy = SQLAlchemy()
|
db : SQLAlchemy = SQLAlchemy()
|
||||||
|
|
||||||
deck_password = db.Table(
|
deck_password = db.Table(
|
||||||
@@ -69,19 +68,25 @@ class Deck(db.Model):
|
|||||||
|
|
||||||
passwords : Mapped[List[Password]] = relationship("Password", secondary=deck_password, back_populates='decks')
|
passwords : Mapped[List[Password]] = relationship("Password", secondary=deck_password, back_populates='decks')
|
||||||
tokens : Mapped[List[Token]] = relationship("Token", secondary=deck_token, 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.name = name
|
||||||
self.deck = deck
|
self.deck = deck
|
||||||
self.description = description
|
self.description = description
|
||||||
|
self.course = course
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<Deck(name={self.name}, deck={self.deck})>"
|
return f"<Deck(name={self.name}, deck={self.deck}, course={self.course})>"
|
||||||
|
|
||||||
class Token(db.Model):
|
class Token(db.Model):
|
||||||
__tablename__ = "token"
|
__tablename__ = "token"
|
||||||
id : Mapped[int] = mapped_column(primary_key=True)
|
id : Mapped[int] = mapped_column(primary_key=True)
|
||||||
name: Mapped[str] = mapped_column(String(60), unique=True, nullable=False)
|
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')
|
decks : Mapped[List[Deck]]= relationship("Deck", secondary=deck_token, back_populates='tokens')
|
||||||
|
|
||||||
def __init__(self, name: str) -> None:
|
def __init__(self, name: str) -> None:
|
||||||
@@ -93,6 +98,19 @@ class Token(db.Model):
|
|||||||
def is_unlocked(self, deck: str) -> bool :
|
def is_unlocked(self, deck: str) -> bool :
|
||||||
return deck in [deck.deck for deck in self.decks]
|
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"<Course(id={self.id}, name={self.name})>"
|
||||||
|
|
||||||
|
|
||||||
class InfoBanner(db.Model):
|
class InfoBanner(db.Model):
|
||||||
__tablename__ = "info_banner"
|
__tablename__ = "info_banner"
|
||||||
id : Mapped[int] = mapped_column(primary_key=True)
|
id : Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ from flask import (
|
|||||||
abort,
|
abort,
|
||||||
current_app,
|
current_app,
|
||||||
flash,
|
flash,
|
||||||
jsonify,
|
|
||||||
make_response,
|
make_response,
|
||||||
redirect,
|
redirect,
|
||||||
render_template,
|
render_template,
|
||||||
@@ -29,8 +28,8 @@ from .models import Deck, InfoBanner, Password, db
|
|||||||
from .utils import (
|
from .utils import (
|
||||||
create_token,
|
create_token,
|
||||||
generate_password,
|
generate_password,
|
||||||
generate_token,
|
|
||||||
get_active_banner,
|
get_active_banner,
|
||||||
|
get_all_courses,
|
||||||
get_available_decks,
|
get_available_decks,
|
||||||
get_token,
|
get_token,
|
||||||
get_unlocked_decks,
|
get_unlocked_decks,
|
||||||
@@ -38,6 +37,7 @@ from .utils import (
|
|||||||
retrieve_token,
|
retrieve_token,
|
||||||
serialize_token,
|
serialize_token,
|
||||||
update_token,
|
update_token,
|
||||||
|
get_course
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -98,9 +98,39 @@ def inject_confused_button(response):
|
|||||||
def index():
|
def index():
|
||||||
token = get_token()
|
token = get_token()
|
||||||
if not token:
|
if not token:
|
||||||
return redirect(url_for("main.register"))
|
return redirect(url_for("main.set_token"))
|
||||||
else:
|
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'])
|
@main.route('/register', methods=['POST', 'GET'])
|
||||||
def register():
|
def register():
|
||||||
@@ -134,15 +164,34 @@ def register():
|
|||||||
|
|
||||||
resp = make_response(redirect(url_for("main.index")))
|
resp = make_response(redirect(url_for("main.index")))
|
||||||
resp.set_cookie('access_token', serialize_token(token), httponly=True)
|
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
|
return resp
|
||||||
|
|
||||||
# If GET request, render the form
|
# If GET request, render the form
|
||||||
return render_template('register.html')
|
return render_template('register.html')
|
||||||
|
|
||||||
@main.route('/login', methods=['POST', 'GET'])
|
@main.route('/logout', methods=['POST'])
|
||||||
def login():
|
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/<new_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()
|
form = LoginForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
assert isinstance(form.user_name.data, str)
|
assert isinstance(form.user_name.data, str)
|
||||||
@@ -152,8 +201,6 @@ def login():
|
|||||||
user = user_loader(user_name)
|
user = user_loader(user_name)
|
||||||
if user and check_password(user, password):
|
if user and check_password(user, password):
|
||||||
flask_login.login_user(user)
|
flask_login.login_user(user)
|
||||||
flash('Login successful!', 'success')
|
|
||||||
|
|
||||||
next = request.args.get('next')
|
next = request.args.get('next')
|
||||||
if next is None:
|
if next is None:
|
||||||
return redirect(url_for('main.index'))
|
return redirect(url_for('main.index'))
|
||||||
@@ -164,7 +211,7 @@ def login():
|
|||||||
return redirect(next)
|
return redirect(next)
|
||||||
else:
|
else:
|
||||||
flash('Incorrect username or password. Please try again.', 'error')
|
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'])
|
@main.route('/access', methods=['POST'])
|
||||||
@@ -174,7 +221,7 @@ def access():
|
|||||||
token = get_token()
|
token = get_token()
|
||||||
if password is not None and not password.is_expired():
|
if password is not None and not password.is_expired():
|
||||||
update_token(password.decks)
|
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))
|
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()
|
token = get_token()
|
||||||
if password is not None and not password.is_expired():
|
if password is not None and not password.is_expired():
|
||||||
update_token(password.decks)
|
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))
|
return render_template('index.html', error="Invalid password.", decks=get_unlocked_decks(token))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@main.route('/confused', methods=['POST'])
|
@main.route('/confused', methods=['POST'])
|
||||||
def confused():
|
def confused():
|
||||||
|
|
||||||
socketio.emit(
|
socketio.emit(
|
||||||
"confused",
|
"confused",
|
||||||
{
|
{
|
||||||
@@ -202,18 +248,6 @@ def confused():
|
|||||||
)
|
)
|
||||||
return ("", 204)
|
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/<deck>/')
|
@main.route('/slides/<deck>/')
|
||||||
@main.route('/slides/<deck>/<path:path>')
|
@main.route('/slides/<deck>/<path:path>')
|
||||||
def serve_deck(deck, path='index.html'):
|
def serve_deck(deck, path='index.html'):
|
||||||
@@ -221,7 +255,7 @@ def serve_deck(deck, path='index.html'):
|
|||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
if not is_deck_unlocked(deck):
|
if not is_deck_unlocked(deck):
|
||||||
return redirect('/')
|
return redirect(url_for('main.index'))
|
||||||
|
|
||||||
slides_dir = current_app.config['SLIDES_DIR']
|
slides_dir = current_app.config['SLIDES_DIR']
|
||||||
deck_path = os.path.join(slides_dir, deck)
|
deck_path = os.path.join(slides_dir, deck)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<body class="bg-gray-900 text-gray-100 min-h-screen flex items-center justify-center">
|
<body class="bg-gray-900 text-gray-100 min-h-screen flex items-center justify-center">
|
||||||
|
|
||||||
<div class="w-full max-w-sm p-6 bg-gray-800 rounded-2xl shadow-lg">
|
<div class="w-full max-w-sm p-6 bg-gray-800 rounded-2xl shadow-lg">
|
||||||
<h2 class="text-2xl font-bold mb-6 text-center text-white">Login</h2>
|
<h2 class="text-2xl font-bold mb-6 text-center text-white">Admin Login</h2>
|
||||||
|
|
||||||
<!-- Display Flash Messages -->
|
<!-- Display Flash Messages -->
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
@@ -41,59 +41,64 @@
|
|||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="bg-surface border-b border-border shadow-sm">
|
<header class="bg-surface border-b border-border shadow-sm">
|
||||||
<div
|
<div
|
||||||
class="max-w-7xl mx-auto px-6 py-4
|
class="w-full px-6 py-4
|
||||||
grid grid-cols-2 items-center gap-4"
|
flex items-center justify-between"
|
||||||
>
|
|
||||||
<!-- 1) form in column 1 -->
|
|
||||||
<form
|
|
||||||
method="POST"
|
|
||||||
action="/retrieve-token"
|
|
||||||
class="relative z-10 flex flex-col space-y-2"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="user_name"
|
|
||||||
value="{{ user_name }}"
|
|
||||||
class="bg-transparent text-muted text-sm font-medium w-auto min-w-max whitespace-nowrap px-2 py-1 rounded-md border border-transparent focus:border-primary focus:outline-none focus:bg-background"
|
|
||||||
size="{{ user_name|length }}"
|
|
||||||
disabled
|
|
||||||
id="usernameInput"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="w-auto min-w-max whitespace-nowrap">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onclick="enableUsernameEdit()"
|
|
||||||
class="text-sm text-white bg-primary px-3 py-1 rounded-md hover:bg-blue-700"
|
|
||||||
id="editBtn"
|
|
||||||
>
|
>
|
||||||
Username ändern
|
<div class="relative z-20">
|
||||||
|
<button id="menu-toggle" class="text-white hover:text-gray-300 focus:outline-none p-2 rounded-md">
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="hidden text-sm text-white bg-primary px-3 py-1 rounded-md hover:bg-blue-700"
|
|
||||||
id="saveBtn"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- 2) admin button in column 2 -->
|
<div class="hamburger-menu">
|
||||||
<div class="flex justify-end">
|
<button class="menu-toggle" aria-controls="course-selector">Menu</button>
|
||||||
<a href="/admin">
|
|
||||||
<button class="bg-primary hover:bg-blue-700 text-white px-3 py-1 rounded-md transition">
|
<select id="course-selector" name="course_selection" onchange="this.form.submit()">
|
||||||
Admin
|
<option value="" disabled selected>Select a Course</option>
|
||||||
</button>
|
|
||||||
</a>
|
{% for course in courses %}
|
||||||
|
<option value="{{ course.id }}">
|
||||||
|
{{ course.display_name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
|
<div class="flex items-center space-x-1 relative z-10">
|
||||||
|
<span class="text-white text-base font-medium whitespace-nowrap py-1">
|
||||||
|
User: {{ user_name }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<form method="POST" action="/logout">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="text-sm text-white bg-red-600 px-3 py-1 rounded-md hover:bg-red-700 transition"
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<a href="/admin" class="ml-4">
|
||||||
|
<button class="bg-primary hover:bg-blue-700 text-white px-3 py-1 rounded-md transition">
|
||||||
|
Admin
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('menu-toggle').addEventListener('click', function() {
|
||||||
|
document.getElementById('course-menu').classList.toggle('hidden');
|
||||||
|
});
|
||||||
|
</script> <!-- Main Content -->
|
||||||
|
|
||||||
<main class="max-w-5xl mx-auto px-6 py-10 space-y-12">
|
<main class="max-w-5xl mx-auto px-6 py-10 space-y-12">
|
||||||
<h3 class="text-3xl font-bold text-center mb-8">GRNVS Tutorium SS 25</h3>
|
<h3 class="text-3xl font-bold text-center mb-8">{{course_display_name}}</h3>
|
||||||
|
|
||||||
|
|
||||||
{% if active_banner %}
|
{% if active_banner %}
|
||||||
|
|||||||
@@ -20,6 +20,19 @@
|
|||||||
<p class="text-muted text-sm">Choose a username to get started.</p>
|
<p class="text-muted text-sm">Choose a username to get started.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="space-y-3 mb-6">
|
||||||
|
{% 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' %}
|
||||||
|
<div class="p-3 border rounded-lg {{ alert_class }}" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
<form action="/register" method="POST" class="space-y-6">
|
<form action="/register" method="POST" class="space-y-6">
|
||||||
<!-- Flask-WTF form fields will be injected here -->
|
<!-- Flask-WTF form fields will be injected here -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
@@ -46,7 +59,7 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p class="text-center mt-4 text-sm">
|
<p class="text-center mt-4 text-sm">
|
||||||
Already have an account? <a href="/login" class="text-blue-400 hover:underline">Log In</a>
|
Already have an account? <a href="/set_token" class="text-blue-400 hover:underline">Log In</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
53
app/templates/set_token.html
Normal file
53
app/templates/set_token.html
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Register for Tutortool</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
@apply bg-background text-text font-sans;
|
||||||
|
background-color: #1a202c;
|
||||||
|
color: #a0aec0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="flex items-center justify-center min-h-screen">
|
||||||
|
<div class="bg-gray-800 p-8 rounded-lg shadow-lg max-w-md w-full border border-gray-700">
|
||||||
|
<div class="text-center mb-6">
|
||||||
|
<h1 class="text-3xl font-bold text-white mb-2">Login</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="space-y-3 mb-6">
|
||||||
|
{% 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' %}
|
||||||
|
<div class="p-3 border rounded-lg {{ alert_class }}" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<form action="/set_token" method="POST" class="space-y-6">
|
||||||
|
<!-- Flask-WTF form fields will be injected here -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="username" class="block text-sm font-medium text-gray-400 mb-2">Username</label>
|
||||||
|
<input type="text" id="username" name="username" placeholder="Your unique username" required class="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-lg transition-colors">
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="text-center mt-4 text-sm">
|
||||||
|
Do not have an Account? <a href="/register" class="text-blue-400 hover:underline">Create one</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
11
app/utils.py
11
app/utils.py
@@ -5,7 +5,7 @@ from typing import cast
|
|||||||
from itsdangerous import BadSignature, URLSafeSerializer
|
from itsdangerous import BadSignature, URLSafeSerializer
|
||||||
from xkcdpass import xkcd_password
|
from xkcdpass import xkcd_password
|
||||||
|
|
||||||
from .models import Deck, InfoBanner, Token, db
|
from .models import Deck, InfoBanner, Token, db, Course
|
||||||
|
|
||||||
def init_password_generator():
|
def init_password_generator():
|
||||||
wordfile = xkcd_password.locate_wordfile()
|
wordfile = xkcd_password.locate_wordfile()
|
||||||
@@ -57,7 +57,7 @@ def get_token() -> (Token | None):
|
|||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
id = serializer.loads(token)
|
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
|
return token
|
||||||
except BadSignature:
|
except BadSignature:
|
||||||
return None
|
return None
|
||||||
@@ -65,7 +65,6 @@ def get_token() -> (Token | None):
|
|||||||
def retrieve_token(username : str):
|
def retrieve_token(username : str):
|
||||||
return db.session.execute(db.select(Token).filter_by(name=username)).scalar_one_or_none()
|
return db.session.execute(db.select(Token).filter_by(name=username)).scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
def get_unlocked_decks(token: (Token|None)):
|
def get_unlocked_decks(token: (Token|None)):
|
||||||
if token is None:
|
if token is None:
|
||||||
return []
|
return []
|
||||||
@@ -90,3 +89,9 @@ def update_token(decks: list[Deck]):
|
|||||||
else:
|
else:
|
||||||
token.decks = list(set(decks + token.decks))
|
token.decks = list(set(decks + token.decks))
|
||||||
db.session.commit()
|
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()
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ class Config:
|
|||||||
SQLALCHEMY_DATABASE_URI =
|
SQLALCHEMY_DATABASE_URI =
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS =
|
SQLALCHEMY_TRACK_MODIFICATIONS =
|
||||||
ADMIN_PASSWORD =
|
ADMIN_PASSWORD =
|
||||||
|
COURSE_MAP =
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user