diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b5f36ae --- /dev/null +++ b/.vscode/settings.json @@ -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 +} diff --git a/auth/oidc.py b/auth/oidc.py index aef1650..21b25c0 100644 --- a/auth/oidc.py +++ b/auth/oidc.py @@ -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") diff --git a/auth/session.py b/auth/session.py index c47312a..d67fd95 100644 --- a/auth/session.py +++ b/auth/session.py @@ -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 diff --git a/db/models.py b/db/models.py index c1d2842..da22e9a 100644 --- a/db/models.py +++ b/db/models.py @@ -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 diff --git a/main.py b/main.py index 2c71518..6266c53 100644 --- a/main.py +++ b/main.py @@ -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__": diff --git a/templates/base.html b/templates/base.html index 192c094..80d03de 100644 --- a/templates/base.html +++ b/templates/base.html @@ -15,8 +15,7 @@
- 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 %} – Logout
{% endif %} @@ -105,18 +104,20 @@| ID | Username | -Role | Money (€) | +Activated | +last drink |
|---|---|---|---|---|---|
| - {{ db_user.username }} + {{ db_user_i.id }} | -{{ db_user.role }} | - {{ db_user.money / 100 }} + {{ db_user_i.username }} + | ++ {{ db_user_i.money / 100 }} + | ++ {{ db_user_i.activated }} + | ++ {{ db_user_i.last_drink }} |