From bafca3c29195063f808c39ae351d831e653ab219 Mon Sep 17 00:00:00 2001 From: Niklas Meinzer Date: Sat, 13 Dec 2025 15:15:18 +0100 Subject: [PATCH] Start User management --- src/allmende_payment_system/api/admin.py | 26 ++++++++++ src/allmende_payment_system/app.py | 2 + src/allmende_payment_system/models.py | 52 +++++++++++++++++++ .../templates/base.html.jinja | 17 +++--- .../templates/users.html.jinja | 39 ++++++++++++++ test/test_models.py | 20 ++++++- 6 files changed, 145 insertions(+), 11 deletions(-) create mode 100644 src/allmende_payment_system/api/admin.py create mode 100644 src/allmende_payment_system/templates/users.html.jinja diff --git a/src/allmende_payment_system/api/admin.py b/src/allmende_payment_system/api/admin.py new file mode 100644 index 0000000..ec73c0c --- /dev/null +++ b/src/allmende_payment_system/api/admin.py @@ -0,0 +1,26 @@ +from decimal import Decimal + +from fastapi import APIRouter, HTTPException, Request +from sqlalchemy import select +from starlette import status +from starlette.responses import RedirectResponse + +from allmende_payment_system.api.dependencies import SessionDep, UserDep +from allmende_payment_system.models import Area, Order, OrderItem, Product, User +from allmende_payment_system.tools import get_jinja_renderer + +admin_router = APIRouter(prefix="/admin") + + +@admin_router.get("/users") +async def user_list(request: Request, session: SessionDep, user: UserDep): + if not user.has_permission("user", "edit"): + raise HTTPException(status_code=403, detail="Insufficient permissions") + + query = select(User) + users = session.scalars(query).all() + templates = get_jinja_renderer() + return templates.TemplateResponse( + "users.html.jinja", + context={"request": request, "users": users}, + ) diff --git a/src/allmende_payment_system/app.py b/src/allmende_payment_system/app.py index adcbbff..0b4a156 100644 --- a/src/allmende_payment_system/app.py +++ b/src/allmende_payment_system/app.py @@ -4,6 +4,7 @@ from fastapi import Depends, FastAPI from fastapi.staticfiles import StaticFiles from allmende_payment_system.api import root_router +from allmende_payment_system.api.admin import admin_router from allmende_payment_system.api.dependencies import get_user_object from allmende_payment_system.api.shop import shop_router @@ -20,3 +21,4 @@ app.mount( app.include_router(root_router) app.include_router(shop_router) +app.include_router(admin_router) diff --git a/src/allmende_payment_system/models.py b/src/allmende_payment_system/models.py index a856baa..cf56d9c 100644 --- a/src/allmende_payment_system/models.py +++ b/src/allmende_payment_system/models.py @@ -55,6 +55,12 @@ class User(Base): ) orders: Mapped[list["Order"]] = relationship("Order", back_populates="user") + user_groups: Mapped[list["UserGroup"]] = relationship( + "UserGroup", + secondary=TABLE_PREFIX + "user_user_group_association", + back_populates="users", + ) + @property def shopping_cart(self): for order in self.orders: @@ -68,6 +74,52 @@ class User(Base): return cart + def has_permission(self, scope: str, action: str) -> bool: + for group in self.user_groups: + for permission in group.permissions: + if permission.scope == scope and permission.action == action: + return True + return False + + +class UserGroup(Base): + __tablename__ = TABLE_PREFIX + "user_group" + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + name: Mapped[str] = mapped_column(nullable=False, unique=True) + description: Mapped[str] = mapped_column(nullable=True) + + permissions = relationship("Permission", back_populates="user_group") + + users: Mapped[list["User"]] = relationship( + "User", + secondary=TABLE_PREFIX + "user_user_group_association", + back_populates="user_groups", + ) + + +user_group_association = Table( + TABLE_PREFIX + "user_user_group_association", + Base.metadata, + Column("user_id", ForeignKey(TABLE_PREFIX + "user.id"), primary_key=True), + Column( + "user_group_id", ForeignKey(TABLE_PREFIX + "user_group.id"), primary_key=True + ), +) + + +class Permission(Base): + __tablename__ = TABLE_PREFIX + "permission" + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + scope: Mapped[str] = mapped_column(nullable=False) + action: Mapped[str] = mapped_column(nullable=False) + + user_group_id: Mapped[int] = mapped_column( + ForeignKey(TABLE_PREFIX + "user_group.id") + ) + user_group: Mapped["UserGroup"] = relationship( + "UserGroup", back_populates="permissions" + ) + class Area(Base): __tablename__ = TABLE_PREFIX + "area" diff --git a/src/allmende_payment_system/templates/base.html.jinja b/src/allmende_payment_system/templates/base.html.jinja index ead407e..b56a066 100644 --- a/src/allmende_payment_system/templates/base.html.jinja +++ b/src/allmende_payment_system/templates/base.html.jinja @@ -29,16 +29,13 @@ Einkaufen -{# #} -{# #} + {% if request.state.user.has_permission("user", "edit") %} + + {% endif %} diff --git a/src/allmende_payment_system/templates/users.html.jinja b/src/allmende_payment_system/templates/users.html.jinja new file mode 100644 index 0000000..aee8809 --- /dev/null +++ b/src/allmende_payment_system/templates/users.html.jinja @@ -0,0 +1,39 @@ +{% extends "base.html.jinja" %} +{% block content %} +
+

Benutzer verwalten

+
+ + {% if users|length == 0 %} +
Keine Benutzer vorhanden.
+ {% else %} +
+ + + + + + + + + + + + {% for user in users %} + + + + + + + + {% endfor %} + +
IDUsernameAzeigenameGruppen
{{ user.id }}{{ user.username }}{{ user.display_name }}{{ user.user_groups | map(attribute='name') | join(', ') }} + Bearbeiten + Löschen +
+
+ {% endif %} + +{% endblock %} \ No newline at end of file diff --git a/test/test_models.py b/test/test_models.py index 246986b..4685bdb 100644 --- a/test/test_models.py +++ b/test/test_models.py @@ -1,6 +1,6 @@ import pytest -from allmende_payment_system.models import Account, User +from allmende_payment_system.models import Account, Permission, User, UserGroup @pytest.fixture(scope="function") @@ -26,3 +26,21 @@ def test_user_shopping_cart_new(test_db, test_user): cart = test_user.shopping_cart assert len(cart.items) == 0 + + +def test_user_permissions(test_db): + user = User(username="normie", display_name="Normal User") + admin = User(username="admin", display_name="Admin User") + test_db.add(user) + + group = UserGroup(name="Admins", description="A group for admins") + group.permissions.append(Permission(scope="area", action="edit")) + test_db.add(group) + + admin.user_groups.append(group) + test_db.flush() + + assert len(admin.user_groups) == 1 + + assert not user.has_permission("area", "edit") + assert admin.has_permission("area", "edit")