Add sql syntax and refactor postpaid users and databases

This commit is contained in:
2025-05-17 00:03:05 +02:00
parent 167101d917
commit d4e01d5109
6 changed files with 461 additions and 125 deletions

14
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,14 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"python.analysis.typeCheckingMode": "standard",
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": false
},
"[python]": {
"editor.defaultFormatter": "ms-python.python",
"editor.formatOnSave": true
},
"cSpell.enabled": false
}

View File

@@ -1,31 +1,49 @@
from fastapi import APIRouter, Depends, HTTPException
"""
This module handles OpenID Connect (OIDC) authentication using FastAPI and Authlib.
Routes:
/login/oidc (GET): Initiates the OIDC login flow by redirecting the user to the identity provider's authorization endpoint.
/authorize (ANY): Handles the callback from the identity provider, exchanges the authorization code for tokens, retrieves user profile information, stores it in the session, and ensures the user exists in the database.
/logout (GET): Logs the user out by clearing session data and redirecting to the identity provider's logout endpoint.
Environment Variables:
OIDC_CLIENT_ID: The client ID for the OIDC application.
OIDC_CLIENT_SECRET: The client secret for the OIDC application.
OIDC_CONFIG_URL: The OIDC discovery document URL.
OIDC_REDIRECT_URL: The redirect URI registered with the OIDC provider.
OIDC_LOGOUT_URL: The logout endpoint URL (optional fallback).
Dependencies:
- FastAPI
- Authlib
- SQLAlchemy
- python-dotenv
- Starlette
Session Keys:
SESSION_KEY: Key for storing user profile information in the session ("user_authentik").
"user_db_id": Key for storing the user's database ID in the session.
Database:
Uses SQLAlchemy to check for and create users in the 'users_postpaid' table.
Functions:
login(request): Starts the OIDC login process.
authorize(request): Handles the OIDC callback, processes user info, and manages user records.
logout(request): Logs the user out and redirects to the identity provider's logout endpoint.
"""
import os
from fastapi import APIRouter
from fastapi.responses import RedirectResponse
from authlib.integrations.starlette_client import OAuth
from starlette.requests import Request
from db.models import User, SessionLocal
from sqlalchemy import text
from dotenv import load_dotenv
import os
from db.models import (
engine,
create_postpaid_user,
)
oauth = OAuth()
router = APIRouter()
SESSION_KEY = "user_authentik"
# OIDC-Provider konfigurieren (hier als Beispiel Auth0)
# oauth.register(
# name="auth0",
# client_id="ZUZYpdYmqjMwdBDb2GdWWX4xkASNe2gsYqLlF9dy",
# client_secret="o6LXTspeaiAMhvPyX2vplQ0RUsRGhthFadg1M5LOJylpQXm9A0d8YJ4CeNwq0kAg2BrdCM7UyfZFOlnVjrJS2o4fBvvhLWDfbd7LhScCzde4Heh5P3C26ZWCRGQppJhb",
# authorize_url="https://login.fs.cs.uni-frankfurt.de/application/o/authorize/",
# authorize_params=None,
# access_token_url="https://login.fs.cs.uni-frankfurt.de/application/o/token/",
# access_token_params=None,
# client_kwargs={"scope": "openid profile email"},
# server_metadata_url="https://login.fs.cs.uni-frankfurt.de/application/o/testkicker/.well-known/openid-configuration",
# api_base_url="https://login.fs.cs.uni-frankfurt.de/application/o/testkicker/",
# jwks_uri="https://login.fs.cs.uni-frankfurt.de/application/o/testkicker/jwks/",
# userinfo_endpoint="https://login.fs.cs.uni-frankfurt.de/application/o/userinfo/",
# )
load_dotenv()
@@ -41,43 +59,74 @@ oauth.register(
@router.get("/login/oidc")
async def login(request: Request):
"""
Initiates the OAuth2 login flow using Auth0 and redirects the user to the Auth0 authorization endpoint.
Args:
request (Request): The incoming HTTP request object.
Returns:
Response: A redirect response to the Auth0 authorization URL.
Raises:
Exception: If the Auth0 client cannot be created or the redirect fails.
Environment Variables:
OIDC_REDIRECT_URL: The URL to which Auth0 should redirect after authentication.
"""
auth0_client = oauth.create_client("auth0")
redirect_uri = os.getenv("OIDC_REDIRECT_URL")
return await auth0_client.authorize_redirect(request, redirect_uri)
@router.route("/authorize")
async def authorize(request: Request):
"""
Handles the OAuth2 authorization callback, retrieves the user's profile from the identity provider,
stores user information in the session, checks if the user exists in the database (and creates them if not),
and redirects to the home page.
Args:
request (Request): The incoming HTTP request containing the OAuth2 callback.
Returns:
RedirectResponse: A redirect response to the home page after successful authorization and user handling.
Side Effects:
- Stores user profile and database user ID in the session.
- May create a new user in the database if not already present.
"""
token = await oauth.auth0.authorize_access_token(request)
userinfo_endpoint = oauth.auth0.server_metadata.get("userinfo_endpoint")
resp = await oauth.auth0.get(userinfo_endpoint, token=token)
resp.raise_for_status()
profile = resp.json()
print("Profile:", profile)
# save user info in session
request.session["user"] = profile
request.session[SESSION_KEY] = profile
# check if user is already in the database
db = SessionLocal()
user_db = db.query(User).filter(User.username == profile["preferred_username"]).first()
if not user_db:
print("Create User in DB")
user_db = User(
username=profile["preferred_username"],
role="user" # Default role
)
db.add(user_db)
db.commit()
db.refresh(user_db)
db.close()
print("User in DB:", user_db)
with engine.connect() as conn:
t = text("SELECT id FROM users_postpaid WHERE username = :username")
result = conn.execute(t, {"username": profile["preferred_username"]}).fetchone()
if result:
user_db_id = result[0]
else:
print("Create User in DB")
user_db_id = create_postpaid_user(profile["preferred_username"])
request.session["user_db_id"] = user_db_id
return RedirectResponse(url="/", status_code=303)
@router.get("/logout")
async def logout(request: Request):
request.session.pop("user", None)
"""
Logs out the current user by clearing session data and redirecting to the OIDC provider's logout endpoint.
Args:
request (Request): The incoming HTTP request containing the user's session.
Returns:
RedirectResponse: A response that redirects the user to the OIDC provider's logout URL with a 303 status code.
Notes:
- Removes the authentication session key and user database information from the session.
- Determines the logout URL from the OIDC provider's metadata or an environment variable.
"""
request.session.pop(SESSION_KEY, None)
request.session.pop("user_db", None)
logout_url = oauth.auth0.server_metadata.get("end_session_endpoint")
if not logout_url:
logout_url = os.getenv("OIDC_LOGOUT_URL")

View File

@@ -1,31 +1,78 @@
from fastapi import Depends, HTTPException
"""
This module provides session management utilities for postpaid users in a FastAPI application.
It includes functions to log in a user by username, log out the current user, and retrieve the
currently logged-in user from the session.
Functions:
login_postpaid_user(request: Request, username: str):
Raises HTTPException if the user is not found.
logout_user(request: Request):
get_current_user(request: Request):
Retrieves the current user from the session, returning the user object if present.
"""
from fastapi import HTTPException
from starlette.requests import Request
from db.models import User, SessionLocal
from sqlalchemy.orm import Session
from sqlalchemy import text
from starlette.requests import Request
from db.models import User, get_db
from sqlalchemy.orm import Session
SESSION_KEY = "user"
from db.models import engine, get_postpaid_user
def login_user(request: Request, db: Session, username: str):
user = db.query(User).filter(User.username == username).first()
if user:
request.session[SESSION_KEY] = user.id
return user
SESSION_KEY = "user_db_id"
def login_postpaid_user(request: Request, username: str):
"""
Logs in a postpaid user by their username and stores their user ID in the session.
Args:
request (Request): The incoming HTTP request object, which contains the session.
username (str): The username of the postpaid user to log in.
Returns:
int or None: The user ID if the user is found and logged in; otherwise, None.
Raises:
HTTPException: If the user with the given username is not found (404 error).
"""
t = text("SELECT id FROM users_postpaid WHERE username = :username")
with engine.connect() as conn:
result = conn.execute(t, {"username": username}).fetchone()
if result:
user_id = result[0]
else:
raise HTTPException(status_code=404, detail="User nicht gefunden")
if user_id:
request.session[SESSION_KEY] = user_id
return user_id
return None
def logout_user(request: Request):
"""
Logs out the current user by removing their session data.
This function removes the user's session key and any associated user database information
from the session, effectively logging the user out.
Args:
request (Request): The incoming request object containing the session.
Returns:
None
"""
request.session.pop(SESSION_KEY, None)
request.session.pop("user_db", None)
def get_current_user(request: Request, db: Session = Depends(get_db)):
def get_current_user(request: Request):
"""
Retrieve the current user from the session in the given request.
Args:
request (Request): The incoming HTTP request containing the session.
Returns:
User or None: The user object associated with the session if present, otherwise None.
"""
user_id = request.session.get(SESSION_KEY)
# if user_id is None:
# raise HTTPException(status_code=401, detail="Nicht eingeloggt")
# user = db.query(User).filter(User.id == user_id).first()
user = user_id
if not user_id:
return None
user = get_postpaid_user(user_id)
return user

View File

@@ -1,38 +1,189 @@
from sqlalchemy import Column, Integer, String, Float
from sqlalchemy.orm import declarative_base, sessionmaker, Session
from sqlalchemy import create_engine
from fastapi import Depends
"""
This module defines database models and utility functions for managing users and drinks in a beverage tracking application using SQLAlchemy and FastAPI.
Tables:
- users_postpaid: Stores postpaid user accounts.
- users_prepaid: Stores prepaid user accounts, linked to postpaid users.
- drinks: Records drink transactions for both postpaid and prepaid users.
Functions:
- create_postpaid_user(username: str) -> int:
Creates a new postpaid user with the specified username. Raises HTTPException if the user already exists or if a database error occurs. Returns the ID of the newly created user.
- get_postpaid_user(user_id: int) -> dict:
Retrieves a postpaid user's information by their user ID. Returns a dictionary with user details. Raises HTTPException if the user is not found.
- get_postpaid_user_by_username(username: str) -> dict:
Retrieves a postpaid user's information by their username. Returns a dictionary with user details. Raises HTTPException if the user is not found.
- set_postpaid_user_money(user_id: int, money: float) -> int:
Updates the 'money' balance for a postpaid user. Raises HTTPException if the user is not found. Returns the number of rows affected.
- drink_postpaid_user(user_id: int) -> int:
Deducts 100 units from the specified postpaid user's balance and records a drink entry. Raises HTTPException if the user is not found or if the drink entry could not be created. Returns the number of rows affected by the drink entry insertion.
"""
from sqlalchemy import create_engine, text
from fastapi import HTTPException
DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(bind=engine)
Base = declarative_base()
with engine.connect() as conn:
# Create a new table for postpaid users
conn.execute(text("""
CREATE TABLE IF NOT EXISTS users_postpaid (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
money INT DEFAULT 0,
activated BOOLEAN DEFAULT 0,
last_drink TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS users_prepaid (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
user_key TEXT NOT NULL UNIQUE,
postpaid_user_id INTEGER NOT NULL,
money INT DEFAULT 0,
activated BOOLEAN DEFAULT 0,
last_drink TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (postpaid_user_id) REFERENCES users_postpaid(id)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS drinks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
postpaid_user_id INTEGER,
prepaid_user_id INTEGER,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
drink_type TEXT,
FOREIGN KEY (postpaid_user_id) REFERENCES users_postpaid(id),
FOREIGN KEY (prepaid_user_id) REFERENCES users_prepaid(id)
)
"""))
conn.commit()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
username = Column(String, unique=True, index=True)
role = Column(String) # z.B. "admin" oder "user"
money = Column(Float, default=0) # money of user
def create_postpaid_user(username: str):
"""
Creates a new postpaid user with the given username in the users_postpaid table.
Args:
username (str): The username of the user to be created.
Raises:
HTTPException: If the user already exists (status_code=400).
HTTPException: If the user could not be created due to a database error (status_code=500).
Returns:
int: The ID of the newly created user.
"""
def create_user(db: Session, username: str, role: str):
db_user = User(username=username, role=role)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
t = text("INSERT INTO users_postpaid (username) VALUES (:username)")
with engine.connect() as connection:
t = text("SELECT * FROM users_postpaid WHERE username = :username")
if connection.execute(t, {"username": username}).fetchone():
raise HTTPException(status_code=400, detail="User already exists")
try:
res = connection.execute(t, {"username": username})
if res.rowcount == 0:
raise HTTPException(status_code=500, detail="Failed to create user")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") from e
connection.commit()
def get_user(db: Session, user_id: int):
return db.query(User).filter(User.id == user_id).first()
return res.lastrowid
def get_postpaid_user(user_id: int):
"""
Retrieve a postpaid user's information from the database by user ID.
Args:
user_id (int): The unique identifier of the user to retrieve.
Returns:
dict: A dictionary containing the user's id, username, money, activated status, and last_drink timestamp.
Raises:
HTTPException: If no user with the given ID is found, raises a 404 HTTPException.
"""
t = text("SELECT id, username, money, activated, last_drink FROM users_postpaid WHERE id = :id")
user_db = {}
with engine.connect() as connection:
result = connection.execute(t, {"id": user_id}).fetchone()
if result:
user_db["id"] = result[0]
user_db["username"] = result[1]
user_db["money"] = result[2]
user_db["activated"] = result[3]
user_db["last_drink"] = result[4]
else:
raise HTTPException(status_code=404, detail="User not found")
return user_db
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
def get_postpaid_user_by_username(username: str):
"""
Retrieve a postpaid user from the database by their username.
Args:
username (str): The username of the user to retrieve.
Returns:
dict: A dictionary containing the user's id, username, money, activated status, and last_drink timestamp.
Raises:
HTTPException: If no user with the given username is found, raises a 404 HTTPException.
"""
t = text("SELECT id, username, money, activated, last_drink FROM users_postpaid WHERE username = :username")
user_db = {}
with engine.connect() as connection:
result = connection.execute(t, {"username": username}).fetchone()
if result:
user_db["id"] = result[0]
user_db["username"] = result[1]
user_db["money"] = result[2]
user_db["activated"] = result[3]
user_db["last_drink"] = result[4]
else:
raise HTTPException(status_code=404, detail="User not found")
return user_db
def set_postpaid_user_money(user_id: int, money: float):
"""
Updates the 'money' value for a postpaid user in the database.
Args:
user_id (int): The ID of the user whose balance is to be updated.
money (float): The new balance to set for the user.
Raises:
HTTPException: If no user with the given ID is found (404 error).
Returns:
int: The number of rows affected by the update operation.
"""
print(f"set_postpaid_user_money: {user_id}, {money}")
t = text("UPDATE users_postpaid SET money = :money WHERE id = :id")
with engine.connect() as connection:
result = connection.execute(t, {"id": user_id, "money": money})
if result.rowcount == 0:
raise HTTPException(status_code=404, detail="User not found")
connection.commit()
return result.rowcount
def drink_postpaid_user(user_id: int):
"""
Deducts 100 units from the specified postpaid user's balance and records a drink entry.
Args:
user_id (int): The ID of the postpaid user.
Raises:
HTTPException: If the user is not found (404) or if the drink entry could not be created (500).
Returns:
int: The number of rows affected by the drink entry insertion (should be 1 on success).
"""
prev_money = get_postpaid_user(user_id)["money"]
t = text("UPDATE users_postpaid SET money = :money, last_drink = CURRENT_TIMESTAMP WHERE id = :id")
with engine.connect() as connection:
result = connection.execute(t, {"id": user_id, "money": prev_money - 100})
if result.rowcount == 0:
raise HTTPException(status_code=404, detail="User not found")
connection.commit()
with engine.connect() as connection:
t = text("INSERT INTO drinks (postpaid_user_id, timestamp) VALUES (:postpaid_user_id, CURRENT_TIMESTAMP)")
result = connection.execute(t, {"postpaid_user_id": user_id})
if result.rowcount == 0:
raise HTTPException(status_code=500, detail="Failed to create drink entry")
connection.commit()
return result.rowcount

136
main.py
View File

@@ -4,14 +4,19 @@ from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from starlette.middleware.sessions import SessionMiddleware
from db.models import Base, engine, get_db, User
import uvicorn
from sqlalchemy import text
from db.models import engine
from db.models import get_postpaid_user
from db.models import get_postpaid_user_by_username
from db.models import set_postpaid_user_money
from db.models import drink_postpaid_user
from auth.session import get_current_user
from auth import oidc
import uvicorn
from sqlalchemy.orm import Session
ADMIN_GROUP = "Fachschaft Admins"
@@ -23,60 +28,121 @@ app.include_router(oidc.router)
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
# DB
Base.metadata.create_all(bind=engine)
@app.get("/", response_class=HTMLResponse)
def home(request: Request, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
if not user:
def home(request: Request):
user_db_id = request.session.get("user_db_id")
user_authentik = request.session.get("user_authentik")
if not user_db_id or not user_authentik:
return RedirectResponse(url="/login", status_code=303)
db_user = db.query(User).filter_by(username=user["preferred_username"]).first()
if not db_user:
user_db_id = request.session.get("user_db_id")
user_authentik = request.session.get("user_authentik")
if not user_db_id or not user_authentik:
raise HTTPException(status_code=404, detail="User nicht gefunden")
users = None
if ADMIN_GROUP in user["groups"]:
users = db.query(User).all()
return templates.TemplateResponse("index.html", {"request": request, "user": user, "users": users, "db_user": db_user})
if ADMIN_GROUP in user_authentik["groups"]:
with engine.connect() as conn:
t = text("SELECT id FROM users_postpaid")
result = conn.execute(t).fetchall()
if result:
users = []
for row in result:
user_db = get_postpaid_user(row[0])
if user_db:
users.append(user_db)
db_user = get_postpaid_user(user_db_id)
return templates.TemplateResponse("index.html", {"request": request, "user": user_authentik, "users": users, "user_db_id": user_db_id, "db_user": db_user})
@app.get("/login", response_class=HTMLResponse)
def login_form(request: Request):
"""
Renders the login form template.
Args:
request (Request): The incoming HTTP request object.
Returns:
TemplateResponse: The rendered login.html template with the request context.
"""
return templates.TemplateResponse("login.html", {"request": request})
@app.post("/set_money")
def set_money(request: Request, username: str = Form(...), money: float = Form(...), db: Session = Depends(get_db), user: User = Depends(get_current_user)):
if not user or ADMIN_GROUP not in user["groups"]:
@app.post("/set_money_postpaid")
def set_money_postpaid(request: Request, username = Form(...), money: float = Form(...)):
"""
Handles a POST request to set the postpaid money balance for a specified user.
Args:
request (Request): The incoming HTTP request, containing session information for authentication.
username (str, Form): The username of the user whose postpaid balance is to be set (provided via form data).
money (float, Form): The new balance amount to set for the user (provided via form data).
Raises:
HTTPException:
- 403 if the current user is not authenticated as an admin.
- 404 if the specified user does not exist.
Returns:
RedirectResponse: Redirects to the home page ("/") with a 303 status code upon successful update.
"""
user_authentik = request.session.get("user_authentik")
if not user_authentik or ADMIN_GROUP not in user_authentik["groups"]:
raise HTTPException(status_code=403, detail="Nicht erlaubt")
db_user = db.query(User).filter_by(username=username).first()
if not db_user:
raise HTTPException(status_code=404, detail="User nicht gefunden")
db_user.money = money*100
db.commit()
with engine.connect() as conn:
t = text("SELECT id FROM users_postpaid WHERE username = :username")
result = conn.execute(t, {"username": username}).fetchone()
if result:
requested_user_id = result[0]
else:
raise HTTPException(status_code=404, detail="User nicht gefunden")
set_postpaid_user_money(requested_user_id, money*100)
return RedirectResponse(url="/", status_code=303)
@app.post("/drink")
def drink(request: Request, db: Session = Depends(get_db), user: User = Depends(get_current_user)):
if not user or ADMIN_GROUP not in user["groups"]:
def drink(request: Request):
"""
Handles a drink purchase request for a user.
Checks if the user is authenticated and belongs to the admin group. If not, raises a 403 error.
Verifies that the user's database ID is present in the session; if not, raises a 404 error.
Retrieves the current user's balance and processes the drink purchase.
Redirects the user to the homepage after processing.
Args:
request (Request): The incoming HTTP request containing session data.
Raises:
HTTPException: If the user is not authenticated as an admin (403).
HTTPException: If the user's database ID is not found in the session (404).
Returns:
RedirectResponse: Redirects to the homepage after processing the drink purchase.
"""
user_authentik = request.session.get("user_authentik")
if not user_authentik or ADMIN_GROUP not in user_authentik["groups"]:
raise HTTPException(status_code=403, detail="Nicht erlaubt")
db_user = db.query(User).filter_by(username=user["preferred_username"]).first()
if not db_user:
user_db_id = request.session.get("user_db_id")
if not user_db_id:
raise HTTPException(status_code=404, detail="User nicht gefunden")
db_user.money -= 100
db.commit()
drink_postpaid_user(user_db_id)
return RedirectResponse(url="/", status_code=303)
@app.post("/payup")
def payup(request: Request, username: str = Form(...), money: float = Form(...), db: Session = Depends(get_db), user: User = Depends(get_current_user)):
if not user or ADMIN_GROUP not in user["groups"]:
def payup(request: Request, username: str = Form(...), money: float = Form(...)):
user_auth = request.session.get("user_authentik")
if not user_auth or ADMIN_GROUP not in user_auth["groups"]:
raise HTTPException(status_code=403, detail="Nicht erlaubt")
db_user = db.query(User).filter_by(username=username).first()
if not db_user:
user_db_id = get_postpaid_user_by_username(username)["id"]
if not user_db_id:
raise HTTPException(status_code=404, detail="User nicht gefunden")
db_user.money += money*100
current_user = db.query(User).filter_by(username=user["preferred_username"]).first()
if not current_user:
curr_user_money = get_postpaid_user(user_db_id)["money"]
set_postpaid_user_money(user_db_id, curr_user_money + money*100)
current_user_db_id = request.session.get("user_db_id")
if not current_user_db_id:
raise HTTPException(status_code=404, detail="Aktueller User nicht gefunden")
current_user.money -= money*100
db.commit()
current_user_money = get_postpaid_user(current_user_db_id)["money"]
set_postpaid_user_money(current_user_db_id, current_user_money - money*100)
return RedirectResponse(url="/", status_code=303)
if __name__ == "__main__":

View File

@@ -15,8 +15,7 @@
<h1>Getränkeliste</h1>
{% if user %}
<p>
Angemeldet als {{ user.preferred_username }}{% if 'Fachschaft
Admins' in user.groups %} (Admin){% endif %}
Angemeldet als {{ user.preferred_username }}{% if 'Fachschaft Admins' in user.groups %} (Admin){% endif %}
<a href="/logout">Logout</a>
</p>
{% endif %}
@@ -105,18 +104,20 @@
<table>
<thead>
<tr>
<th style="padding: 0.5em 1em">ID</th>
<th style="padding: 0.5em 1em">Username</th>
<th style="padding: 0.5em 1em">Role</th>
<th style="padding: 0.5em 1em">Money (€)</th>
<th style="padding: 0.5em 1em">Activated</th>
<th style="padding: 0.5em 1em">last drink</th>
</tr>
</thead>
<tbody>
{% for db_user in users %}
{% for db_user_i in users %}
<tr
{%
if
db_user.money
<="-5000"
db_user_i.money
<=-5000
%}
style="background-color: rgba(179, 6, 44, 0.5)"
{%
@@ -124,11 +125,19 @@
%}
>
<td style="padding: 0.5em 1em">
{{ db_user.username }}
{{ db_user_i.id }}
</td>
<td style="padding: 0.5em 1em">{{ db_user.role }}</td>
<td style="padding: 0.5em 1em">
{{ db_user.money / 100 }}
{{ db_user_i.username }}
</td>
<td style="padding: 0.5em 1em">
{{ db_user_i.money / 100 }}
</td>
<td style="padding: 0.5em 1em">
{{ db_user_i.activated }}
</td>
<td style="padding: 0.5em 1em">
{{ db_user_i.last_drink }}
</td>
</tr>
{% endfor %}
@@ -137,7 +146,7 @@
<p>Set user money:</p>
<form
method="post"
action="/set_money"
action="/set_money_postpaid"
style="
display: flex;
gap: 1em;