feat(auth): required login barrier with 1-click demo access and session management

This commit is contained in:
2026-09-21 10:28:08 +02:00
parent 8200857754
commit 4f5ef164c3
6 changed files with 547 additions and 46 deletions
+26
View File
@@ -0,0 +1,26 @@
import hashlib
import os
import secrets
def hash_password(password: str) -> str:
salt = secrets.token_hex(16)
key = hashlib.pbkdf2_hmac(
'sha256',
password.encode('utf-8'),
salt.encode('utf-8'),
100000
)
return f"{salt}${key.hex()}"
def verify_password(password: str, hashed: str) -> bool:
try:
salt, key_hex = hashed.split('$')
computed_key = hashlib.pbkdf2_hmac(
'sha256',
password.encode('utf-8'),
salt.encode('utf-8'),
100000
)
return secrets.compare_digest(computed_key.hex(), key_hex)
except Exception:
return False
+199 -30
View File
@@ -1,29 +1,36 @@
import os
from datetime import datetime
from typing import Optional
from typing import Optional, Dict
from decimal import Decimal
from fastapi import FastAPI, Request, Depends, HTTPException, Form, status
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from starlette.middleware.sessions import SessionMiddleware
from sqlalchemy.orm import Session
from .database import get_db
from .models import Tenant, User, Product, Event, Order, OrderItem
from .auth import hash_password, verify_password
app = FastAPI(title="Schmankerl – Das moderne Brotzeitportal")
# Session Middleware für sichere Login-Cookies
app.add_middleware(
SessionMiddleware,
secret_key=os.getenv("SECRET_KEY", "schmankerl-secret-key-waiswuaschd-2026!"),
session_cookie="schmankerl_session",
max_age=86400 * 30 # 30 Tage
)
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
app.mount("/static", StaticFiles(directory=os.path.join(BASE_DIR, "static")), name="static")
templates = Jinja2Templates(directory=os.path.join(BASE_DIR, "templates"))
def resolve_tenant(request: Request, db: Session) -> Optional[Tenant]:
"""
Ermittelt den Mandanten anhand des Host-Headers (Subdomain)
z.B. demo.dev.waiswuaschd.de -> slug='demo'
"""
"""Ermittelt den Mandanten anhand der Subdomain (Host-Header)."""
host = request.headers.get("host", "").split(":")[0].lower()
parts = host.split(".")
@@ -41,12 +48,55 @@ def resolve_tenant(request: Request, db: Session) -> Optional[Tenant]:
return tenant
def get_current_user(request: Request, db: Session) -> Optional[User]:
"""Liest den eingeloggten Benutzer aus der Session."""
user_id = request.session.get("user_id")
if not user_id:
return None
return db.query(User).filter(User.id == user_id, User.is_active == True).first()
def seed_demo_users_if_needed(tenant: Tenant, db: Session):
"""Stellt sicher, dass die Standard-Accounts für die Demo existieren."""
demo_user = db.query(User).filter(User.tenant_id == tenant.id, User.email == "maxi@waiswuaschd.de").first()
if not demo_user:
demo_user = User(
tenant_id=tenant.id,
email="maxi@waiswuaschd.de",
username="maxi",
first_name="Maxi",
last_name="Mustermann",
password_hash=hash_password("demo123"),
role="user"
)
db.add(demo_user)
admin_user = db.query(User).filter(User.tenant_id == tenant.id, User.email == "admin@waiswuaschd.de").first()
if not admin_user:
admin_user = User(
tenant_id=tenant.id,
email="admin@waiswuaschd.de",
username="admin",
first_name="Franz",
last_name="Xaver",
password_hash=hash_password("admin123"),
role="admin"
)
db.add(admin_user)
db.commit()
@app.get("/", response_class=HTMLResponse)
def index(request: Request, db: Session = Depends(get_db)):
tenant = resolve_tenant(request, db)
if not tenant:
return templates.TemplateResponse(request, "not_found_tenant.html", context={})
current_user = get_current_user(request, db)
if not current_user:
# Require Login: Weiterleitung zur Anmeldeseite
return RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER)
# Aktuelle offene Runde suchen
event = db.query(Event).filter(
Event.tenant_id == tenant.id,
@@ -59,33 +109,154 @@ def index(request: Request, db: Session = Depends(get_db)):
Product.is_active == True
).order_by(Product.sort_order.asc(), Product.id.asc()).all()
# Bestehende Bestellungen für die Event-Runde zählen
# Bestehende Bestellung des Users in diesem Event
existing_order = None
user_items: Dict[int, int] = {}
if event:
existing_order = db.query(Order).filter(
Order.tenant_id == tenant.id,
Order.event_id == event.id,
Order.user_id == current_user.id
).first()
if existing_order:
for it in existing_order.items:
user_items[it.product_id] = it.quantity
# Bestellungen insgesamt zählen
order_count = 0
if event:
order_count = db.query(Order).filter(Order.event_id == event.id).count()
return templates.TemplateResponse(request, "index.html", context={
"tenant": tenant,
"current_user": current_user,
"event": event,
"products": products,
"existing_order": existing_order,
"user_items": user_items,
"order_count": order_count,
"now": datetime.now()
})
@app.post("/order")
async def place_order(
@app.get("/login", response_class=HTMLResponse)
def login_page(request: Request, db: Session = Depends(get_db), error: Optional[str] = None, success: Optional[str] = None):
tenant = resolve_tenant(request, db)
if tenant and tenant.slug == "demo":
seed_demo_users_if_needed(tenant, db)
current_user = get_current_user(request, db)
if current_user:
return RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER)
return templates.TemplateResponse(request, "login.html", context={
"tenant": tenant,
"error": error,
"success": success,
"current_user": None
})
@app.post("/login")
def login(
request: Request,
first_name: str = Form(...),
last_name: str = Form(...),
email: str = Form(...),
event_id: int = Form(...),
password: str = Form(...),
db: Session = Depends(get_db)
):
tenant = resolve_tenant(request, db)
clean_email = email.strip().lower()
user = db.query(User).filter(User.tenant_id == tenant.id, User.email == clean_email).first()
if not user or not verify_password(password, user.password_hash):
return templates.TemplateResponse(request, "login.html", context={
"tenant": tenant,
"error": "Ungültige E-Mail-Adresse oder falsches Passwort.",
"current_user": None
}, status_code=400)
# Session setzen
request.session["user_id"] = user.id
return RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER)
@app.post("/login/quick")
def quick_demo_login(
request: Request,
role: str = Form(...),
db: Session = Depends(get_db)
):
tenant = resolve_tenant(request, db)
if not tenant:
raise HTTPException(status_code=404, detail="Mandant nicht gefunden")
seed_demo_users_if_needed(tenant, db)
target_email = "admin@waiswuaschd.de" if role == "admin" else "maxi@waiswuaschd.de"
user = db.query(User).filter(User.tenant_id == tenant.id, User.email == target_email).first()
if not user:
raise HTTPException(status_code=500, detail="Demo-Benutzer konnte nicht geladen werden")
request.session["user_id"] = user.id
if role == "admin":
return RedirectResponse(url="/admin", status_code=status.HTTP_303_SEE_OTHER)
return RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER)
@app.post("/register")
def register(
request: Request,
first_name: str = Form(...),
last_name: str = Form(...),
email: str = Form(...),
password: str = Form(...),
db: Session = Depends(get_db)
):
tenant = resolve_tenant(request, db)
clean_email = email.strip().lower()
existing = db.query(User).filter(User.tenant_id == tenant.id, User.email == clean_email).first()
if existing:
return templates.TemplateResponse(request, "login.html", context={
"tenant": tenant,
"error": "Diese E-Mail-Adresse ist bereits registriert. Bitte melde dich an.",
"current_user": None
}, status_code=400)
username = clean_email.split("@")[0]
new_user = User(
tenant_id=tenant.id,
email=clean_email,
username=username,
first_name=first_name.strip(),
last_name=last_name.strip(),
password_hash=hash_password(password),
role="user"
)
db.add(new_user)
db.commit()
request.session["user_id"] = new_user.id
return RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER)
@app.get("/logout")
def logout(request: Request):
request.session.clear()
return RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER)
@app.post("/order")
async def place_order(
request: Request,
event_id: int = Form(...),
db: Session = Depends(get_db)
):
tenant = resolve_tenant(request, db)
current_user = get_current_user(request, db)
if not current_user:
return RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER)
event = db.query(Event).filter(Event.id == event_id, Event.tenant_id == tenant.id).first()
if not event or event.status != "open":
raise HTTPException(status_code=400, detail="Diese Bestellrunde ist bereits geschlossen")
@@ -93,23 +264,7 @@ async def place_order(
if event.deadline < datetime.now():
raise HTTPException(status_code=400, detail="Die Bestellfrist ist leider abgelaufen!")
# User ermitteln oder anlegen
clean_email = email.strip().lower()
user = db.query(User).filter(User.tenant_id == tenant.id, User.email == clean_email).first()
if not user:
username = clean_email.split("@")[0]
user = User(
tenant_id=tenant.id,
email=clean_email,
username=username,
first_name=first_name.strip(),
last_name=last_name.strip(),
role="user"
)
db.add(user)
db.flush()
# Formulardaten auslesen (Mengen je Produkt)
# Formulardaten auslesen
form_data = await request.form()
order_items = []
total_price = Decimal("0.00")
@@ -138,7 +293,7 @@ async def place_order(
existing_order = db.query(Order).filter(
Order.tenant_id == tenant.id,
Order.event_id == event.id,
Order.user_id == user.id
Order.user_id == current_user.id
).first()
if existing_order:
@@ -153,7 +308,7 @@ async def place_order(
new_order = Order(
tenant_id=tenant.id,
event_id=event.id,
user_id=user.id,
user_id=current_user.id,
total_price=total_price,
is_paid=False
)
@@ -171,12 +326,17 @@ async def place_order(
@app.get("/order/{order_id}/success", response_class=HTMLResponse)
def order_success(order_id: int, request: Request, db: Session = Depends(get_db)):
tenant = resolve_tenant(request, db)
current_user = get_current_user(request, db)
if not current_user:
return RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER)
order = db.query(Order).filter(Order.id == order_id).first()
if not order:
raise HTTPException(status_code=404, detail="Bestellung nicht gefunden")
return templates.TemplateResponse(request, "order_success.html", context={
"tenant": tenant,
"current_user": current_user,
"order": order
})
@@ -187,6 +347,10 @@ def admin_dashboard(request: Request, db: Session = Depends(get_db)):
if not tenant:
return templates.TemplateResponse(request, "not_found_tenant.html", context={})
current_user = get_current_user(request, db)
if not current_user:
return RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER)
# Neuestes Event finden
event = db.query(Event).filter(Event.tenant_id == tenant.id).order_by(Event.id.desc()).first()
@@ -211,6 +375,7 @@ def admin_dashboard(request: Request, db: Session = Depends(get_db)):
return templates.TemplateResponse(request, "admin.html", context={
"tenant": tenant,
"current_user": current_user,
"event": event,
"orders": orders,
"item_sums": item_sums,
@@ -221,6 +386,10 @@ def admin_dashboard(request: Request, db: Session = Depends(get_db)):
@app.post("/admin/toggle-paid/{order_id}")
def toggle_paid(order_id: int, request: Request, db: Session = Depends(get_db)):
current_user = get_current_user(request, db)
if not current_user:
return RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER)
order = db.query(Order).filter(Order.id == order_id).first()
if order:
order.is_paid = not order.is_paid
+178 -2
View File
@@ -40,7 +40,7 @@ body {
}
.container.narrow {
max-width: 600px;
max-width: 580px;
}
/* Navbar */
@@ -87,7 +87,7 @@ body {
.nav-links {
display: flex;
align-items: center;
gap: 20px;
gap: 18px;
}
.nav-links a {
@@ -102,6 +102,20 @@ body {
color: var(--text-main);
}
.user-greeting {
color: var(--text-main);
font-size: 0.85rem;
font-weight: 600;
background: rgba(255, 255, 255, 0.05);
padding: 4px 10px;
border-radius: 6px;
}
.nav-logout {
color: #ef4444 !important;
font-size: 0.85rem !important;
}
.nav-links a.nav-admin {
background: rgba(251, 191, 36, 0.12);
color: var(--accent-gold);
@@ -267,6 +281,31 @@ body {
backdrop-filter: blur(12px);
}
.logged-in-badge {
display: flex;
align-items: center;
gap: 12px;
background: rgba(11, 15, 25, 0.8);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 12px 16px;
margin-bottom: 16px;
}
.user-avatar {
font-size: 1.5rem;
}
.notice-box {
background: rgba(56, 189, 248, 0.1);
border: 1px solid rgba(56, 189, 248, 0.25);
border-radius: var(--radius-md);
padding: 12px 14px;
color: var(--text-main);
font-size: 0.85rem;
margin-bottom: 16px;
}
.input-group {
margin-bottom: 16px;
}
@@ -344,6 +383,143 @@ body {
margin-top: 12px;
}
/* Login Page */
.login-box {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 36px;
backdrop-filter: blur(12px);
}
.login-header {
text-align: center;
margin-bottom: 24px;
}
.login-icon {
font-size: 2.5rem;
margin-bottom: 12px;
}
.login-header h1 {
font-size: 1.6rem;
margin-bottom: 6px;
}
.login-sub {
color: var(--text-muted);
font-size: 0.95rem;
margin-bottom: 14px;
}
.privacy-note {
background: rgba(56, 189, 248, 0.08);
border: 1px solid rgba(56, 189, 248, 0.2);
border-radius: var(--radius-md);
padding: 10px 14px;
color: var(--text-muted);
font-size: 0.8rem;
line-height: 1.4;
}
.demo-login-card {
background: rgba(11, 15, 25, 0.9);
border: 1px solid rgba(251, 191, 36, 0.3);
border-radius: var(--radius-md);
padding: 18px;
margin-bottom: 28px;
}
.demo-login-card h3 {
font-size: 1rem;
color: var(--accent-gold);
margin-bottom: 4px;
}
.demo-btn-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-top: 12px;
}
.btn-demo {
width: 100%;
padding: 12px 10px;
background: rgba(56, 189, 248, 0.1);
border: 1px solid rgba(56, 189, 248, 0.3);
border-radius: var(--radius-md);
color: var(--text-main);
cursor: pointer;
text-align: center;
transition: all 0.2s;
}
.btn-demo:hover {
background: rgba(56, 189, 248, 0.2);
border-color: var(--accent);
}
.btn-demo.admin {
background: rgba(251, 191, 36, 0.1);
border-color: rgba(251, 191, 36, 0.3);
}
.btn-demo.admin:hover {
background: rgba(251, 191, 36, 0.2);
border-color: var(--accent-gold);
}
.btn-demo small {
color: var(--text-muted);
font-size: 0.75rem;
}
.auth-tabs {
display: flex;
border-bottom: 1px solid var(--border-color);
margin-bottom: 20px;
}
.auth-tab {
flex: 1;
background: transparent;
border: none;
color: var(--text-muted);
font-size: 0.95rem;
font-weight: 600;
padding: 12px;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.2s;
}
.auth-tab.active {
color: var(--text-main);
border-bottom-color: var(--accent);
}
.alert-danger {
background: rgba(239, 68, 68, 0.15);
border: 1px solid var(--danger);
color: #fca5a5;
padding: 10px 14px;
border-radius: var(--radius-md);
font-size: 0.85rem;
margin-bottom: 18px;
}
.alert-success {
background: rgba(34, 197, 94, 0.15);
border: 1px solid var(--success);
color: #86efac;
padding: 10px 14px;
border-radius: var(--radius-md);
font-size: 0.85rem;
margin-bottom: 18px;
}
/* Success Card */
.success-card {
background: var(--bg-card);
+8
View File
@@ -15,8 +15,16 @@
🥨 <span>Schmankerl</span> <small class="tenant-badge">{{ tenant.name if tenant else 'Portal' }}</small>
</a>
<nav class="nav-links">
{% if current_user %}
<a href="/">Brotzeit bestellen</a>
{% if current_user.role == 'admin' %}
<a href="/admin" class="nav-admin">👨‍🍳 Einkäufer-Cockpit</a>
{% endif %}
<span class="user-greeting">👤 {{ current_user.first_name }}</span>
<a href="/logout" class="nav-logout">Abmelden</a>
{% else %}
<a href="/login" class="nav-login">Anmelden</a>
{% endif %}
<a href="https://new.dev.waiswuaschd.de" target="_blank" class="nav-home">Zur Plattform</a>
</nav>
</div>
+23 -14
View File
@@ -13,7 +13,7 @@
Bestellschluss: <strong>{{ event.deadline.strftime('%d.%m.%Y um %H:%M Uhr') }}</strong>
</p>
<div class="banner-stats">
<span>👥 Bereits <strong>{{ order_count }}</strong> Bestellungen eingegangen</span>
<span>👥 Bereits <strong>{{ order_count }}</strong> Bestellungen von Kollegen eingegangen</span>
</div>
</div>
@@ -27,6 +27,7 @@
<h2>1. Brotzeit auswählen</h2>
<div class="product-list">
{% for product in products %}
{% set current_qty = user_items.get(product.id, 0) %}
<div class="product-card" data-price="{{ product.price }}" data-id="{{ product.id }}">
<div class="product-info">
<h3>{{ product.name }}</h3>
@@ -35,7 +36,7 @@
</div>
<div class="quantity-control">
<button type="button" class="qty-btn" onclick="changeQty({{ product.id }}, -1)">-</button>
<input type="number" id="qty_{{ product.id }}" name="qty_{{ product.id }}" value="0" min="0" max="50" class="qty-input" onchange="updateTotal()">
<input type="number" id="qty_{{ product.id }}" name="qty_{{ product.id }}" value="{{ current_qty }}" min="0" max="50" class="qty-input" onchange="updateTotal()">
<button type="button" class="qty-btn" onclick="changeQty({{ product.id }}, 1)">+</button>
</div>
</div>
@@ -46,19 +47,21 @@
<!-- Right: User Details & Summary -->
<div class="summary-section">
<div class="summary-card">
<h2>2. Deine Angaben</h2>
<div class="input-group">
<label for="first_name">Vorname</label>
<input type="text" id="first_name" name="first_name" required placeholder="z. B. Maxi">
<h2>2. Zusammenfassung</h2>
<div class="logged-in-badge">
<div class="user-avatar">👤</div>
<div>
<strong>{{ current_user.first_name }} {{ current_user.last_name }}</strong><br>
<small class="text-muted">{{ current_user.email }}</small>
</div>
</div>
<div class="input-group">
<label for="last_name">Nachname</label>
<input type="text" id="last_name" name="last_name" required placeholder="z. B. Muster">
</div>
<div class="input-group">
<label for="email">E-Mail-Adresse</label>
<input type="email" id="email" name="email" required placeholder="maxi@beispiel.de">
{% if existing_order %}
<div class="notice-box">
ℹ️ Du hast bereits für diese Runde bestellt. Du kannst deine Mengen hier anpassen und aktualisieren.
</div>
{% endif %}
<hr class="divider">
@@ -68,9 +71,13 @@
</div>
<button type="submit" class="btn-submit" id="submitBtn">
{% if existing_order %}
🥨 Bestellung aktualisieren
{% else %}
🥨 Bestellung verbindlich abschicken
{% endif %}
</button>
<small class="form-hint">Du kannst deine Bestellung bis zum Stichtag jederzeit mit derselben E-Mail-Adresse ändern.</small>
<small class="form-hint">Du kannst deine Bestellung bis zum Stichtag jederzeit hier im Portal anpassen.</small>
</div>
</div>
</div>
@@ -81,7 +88,9 @@
<div class="empty-icon">🏖️</div>
<h2>Aktuell keine offene Bestellrunde</h2>
<p>Für dieses Team ist im Moment keine Brotzeit geplant.</p>
{% if current_user.role == 'admin' %}
<a href="/admin" class="btn-primary">Neue Bestellrunde im Cockpit anlegen</a>
{% endif %}
</div>
{% endif %}
+113
View File
@@ -0,0 +1,113 @@
{% extends "base.html" %}
{% block title %}Anmelden – Schmankerl{% endblock %}
{% block content %}
<div class="container narrow">
<div class="login-box">
<div class="login-header">
<div class="login-icon">🥨</div>
<h1>Anmeldung erforderlich</h1>
<p class="login-sub">
Portal: <strong>{{ tenant.name if tenant else 'Schmankerl' }}</strong>
</p>
<div class="privacy-note">
🔒 Aus Datenschutzgründen (DSGVO) sind Bestelllisten und Teilnehmerdaten nur für angemeldete Teammitglieder sichtbar.
</div>
</div>
{% if error %}
<div class="alert-danger">{{ error }}</div>
{% endif %}
{% if success %}
<div class="alert-success">{{ success }}</div>
{% endif %}
<!-- 1-Click Demo Login Box (For easy review & testing) -->
<div class="demo-login-card">
<h3>🚀 1-Klick Demo-Zugang</h3>
<p class="card-hint">Wähle eine Rolle zum sofortigen Ausprobieren:</p>
<div class="demo-btn-grid">
<form action="/login/quick" method="POST">
<input type="hidden" name="role" value="user">
<button type="submit" class="btn-demo">
👤 <strong>Mitarbeiter</strong><br>
<small>Brotzeit bestellen & Übersicht</small>
</button>
</form>
<form action="/login/quick" method="POST">
<input type="hidden" name="role" value="admin">
<button type="submit" class="btn-demo admin">
👨‍🍳 <strong>Einkäufer / Admin</strong><br>
<small>Sammelliste & Kasse verwalten</small>
</button>
</form>
</div>
</div>
<!-- Tab Navigation -->
<div class="auth-tabs">
<button class="auth-tab active" id="tabLoginBtn" onclick="switchAuthTab('login')">Anmelden</button>
<button class="auth-tab" id="tabRegisterBtn" onclick="switchAuthTab('register')">Neu registrieren</button>
</div>
<!-- Login Form -->
<form action="/login" method="POST" id="formLogin" class="auth-form">
<div class="input-group">
<label for="login_email">E-Mail-Adresse</label>
<input type="email" id="login_email" name="email" required placeholder="name@firma.de">
</div>
<div class="input-group">
<label for="login_password">Passwort</label>
<input type="password" id="login_password" name="password" required placeholder="••••••••">
</div>
<button type="submit" class="btn-submit">Anmelden</button>
</form>
<!-- Register Form (Hidden by default) -->
<form action="/register" method="POST" id="formRegister" class="auth-form" style="display: none;">
<div class="input-group">
<label for="reg_first_name">Vorname</label>
<input type="text" id="reg_first_name" name="first_name" required placeholder="z. B. Anna">
</div>
<div class="input-group">
<label for="reg_last_name">Nachname</label>
<input type="text" id="reg_last_name" name="last_name" required placeholder="z. B. Huber">
</div>
<div class="input-group">
<label for="reg_email">E-Mail-Adresse</label>
<input type="email" id="reg_email" name="email" required placeholder="anna@firma.de">
</div>
<div class="input-group">
<label for="reg_password">Passwort festlegen</label>
<input type="password" id="reg_password" name="password" required minlength="6" placeholder="Mindestens 6 Zeichen">
</div>
<button type="submit" class="btn-submit">Konto erstellen & anmelden</button>
</form>
</div>
</div>
<script>
function switchAuthTab(tab) {
const loginForm = document.getElementById('formLogin');
const regForm = document.getElementById('formRegister');
const loginBtn = document.getElementById('tabLoginBtn');
const regBtn = document.getElementById('tabRegisterBtn');
if (tab === 'login') {
loginForm.style.display = 'block';
regForm.style.display = 'none';
loginBtn.classList.add('active');
regBtn.classList.remove('active');
} else {
loginForm.style.display = 'none';
regForm.style.display = 'block';
loginBtn.classList.remove('active');
regBtn.classList.add('active');
}
}
</script>
{% endblock %}