From ecc3fb35c3601dcb93ff2295beecc08e7f4c5115 Mon Sep 17 00:00:00 2001 From: David Rodenkirchen Date: Wed, 27 May 2026 19:36:33 +0200 Subject: [PATCH] add USer Admin Page --- src/elm/components/AccountInfoBox.py | 28 ++-- src/elm/components/UserNavigation.py | 32 +++- src/elm/pages/UserAdminPage.py | 209 +++++++++++++++++++++++++++ src/elm/services/UserService.py | 2 + 4 files changed, 253 insertions(+), 18 deletions(-) create mode 100644 src/elm/pages/UserAdminPage.py diff --git a/src/elm/components/AccountInfoBox.py b/src/elm/components/AccountInfoBox.py index c4b6a68..ca102af 100644 --- a/src/elm/components/AccountInfoBox.py +++ b/src/elm/components/AccountInfoBox.py @@ -4,7 +4,7 @@ from bson import ObjectId from rio import Component, Rectangle, Column, Text, Row, PointerEventListener, TextInput from rio.event import on_populate -from elm.types import UserSession, Ticket, Seat +from elm.types import UserSession, Ticket, Seat, User from elm.components import ElmButton from elm.services import UserService @@ -18,19 +18,25 @@ class AccountInfoBox(Component): password_change_in_progress: bool = False ticket: Optional[Ticket] = None seat: Optional[Seat] = None + fixed_user: Optional[User] = None @on_populate async def on_populate(self) -> None: - try: - user = await self.session[UserService].get_user(self.session[UserSession].user_name) - if user: - self.mail = user.user_mail - self.ticket = await Ticket.find_one({"owner.$id": user.id}) - self.seat = await Seat.find_one({"user.$id": ObjectId(user.id)}) - else: + if self.fixed_user is None: + try: + user = await self.session[UserService].get_user(self.session[UserSession].user_name) + if user: + self.mail = user.user_mail + self.ticket = await Ticket.find_one({"owner.$id": user.id}) + self.seat = await Seat.find_one({"user.$id": ObjectId(user.id)}) + else: + self.session.navigate_to("./login") + except KeyError: self.session.navigate_to("./login") - except KeyError: - self.session.navigate_to("./login") + else: + self.mail = self.fixed_user.user_mail + self.ticket = await Ticket.find_one({"owner.$id": self.fixed_user.id}) + self.seat = await Seat.find_one({"user.$id": ObjectId(self.fixed_user.id)}) async def set_new_password(self) -> None: self.password_change_in_progress = True @@ -86,7 +92,7 @@ class AccountInfoBox(Component): stroke_color=self.session.theme.box_border_color, ), Column( - TextInput(text=self.session[UserSession].user_name, label="Nutzername", is_sensitive=False), + TextInput(text=self.fixed_user.user_name if self.fixed_user is not None else self.session[UserSession].user_name, label="Nutzername", is_sensitive=False), TextInput(text=self.mail, label="E-Mail Adresse", is_sensitive=False), row_col( PointerEventListener( diff --git a/src/elm/components/UserNavigation.py b/src/elm/components/UserNavigation.py index 0fc7694..2c59e07 100644 --- a/src/elm/components/UserNavigation.py +++ b/src/elm/components/UserNavigation.py @@ -87,15 +87,33 @@ class UserNavigation(Component): async def on_populate(self) -> None: self.session.create_task(self.update_balance()) + def show_admin_navigation(self) -> bool: + try: + user_session = self.session[UserSession] + except KeyError: + return False + + return user_session.is_team_member + def build(self) -> Component: + base_nav = [ + UserNavigationButton(f"Guthaben: {self.session[AccountingService].make_euro_string_from_decimal(self.balance)}", "/balance", self.close_navigation), + UserNavigationButton("Meine Bestellungen", "/my-orders", self.close_navigation), + UserNavigationButton("Mein Profil", "/my-profile", self.close_navigation) + ] + + if self.show_admin_navigation(): + base_nav.extend([ + UserNavigationButton("Admin: Benutzer", "/user-admin", self.close_navigation), + UserNavigationButton("Admin: Catering", "/catering-admin", self.close_navigation) + ]) + + base_nav.append( + UserNavigationButton("Ausloggen", "/logout", self.close_navigation) + ) + return Rectangle( - content=Column( - UserNavigationButton(f"Guthaben: {self.session[AccountingService].make_euro_string_from_decimal(self.balance)}", "/balance", self.close_navigation), - UserNavigationButton("Meine Bestellungen", "/my-orders", self.close_navigation), - UserNavigationButton("Mein Profil", "/my-profile", self.close_navigation), - # UserNavigationButton("Mein Clan", "/my-clans", self.close_navigation), ToDo: Implement - UserNavigationButton("Ausloggen", "/logout", self.close_navigation) - ), + content=Column(*base_nav), min_width=3.5, min_height=3.5, fill=self.session.theme.background_color diff --git a/src/elm/pages/UserAdminPage.py b/src/elm/pages/UserAdminPage.py new file mode 100644 index 0000000..686a304 --- /dev/null +++ b/src/elm/pages/UserAdminPage.py @@ -0,0 +1,209 @@ +from __future__ import annotations + +import logging +from functools import partial +from typing import Optional +from decimal import Decimal + +from rio import Component, Column, Row, Text, Spacer, page, Rectangle, TextInput, GuardEvent, Button, TextInputChangeEvent, NumberInput +from rio.event import on_populate + +from elm.types import UserSession, User, Transaction +from elm.services import AccountingService, MailingService +from elm.components import AccountInfoBox + +logger = logging.getLogger(__name__.split(".")[-1]) + +def user_admin_page_guard(event: GuardEvent) -> Optional[str]: + try: + if event.session[UserSession].is_team_member: + return None + return "/" + except KeyError: + return "/" + +@page(name="Benutzerverwaltung", url_segment="user-admin", guard=user_admin_page_guard) +class UserAdminPage(Component): + all_users: list[User] = list() + user_list: list[User] = list() + search_bar_text: str = "" + active_user: Optional[User] = None + transaction_value: float = 0.0 + transaction_reason: str = "" + active_user_balance: str = "0.00 €" + + @on_populate + async def on_populate(self) -> None: + user_list = await User.find_all().to_list() + self.all_users = sorted(user_list, key=lambda u: u.user_name) + self.user_list = sorted(user_list, key=lambda u: u.user_name) + + async def on_search_bar_text_changed(self, e: TextInputChangeEvent) -> None: + self.user_list = list(filter(lambda user: (e.text.lower() in user.user_name.lower()), self.all_users)) + + async def on_user_clicked(self, user: User) -> None: + self.active_user = user + self.active_user_balance = self.session[AccountingService].make_euro_string_from_decimal( + await self.session[AccountingService].get_balance(user.user_name) + ) + + async def create_debit_transaction(self) -> None: + if not self.active_user: + return + logger.info(f"Trying to remove {self.transaction_value} to {self.active_user.user_name} ({self.transaction_reason})") + new_transaction = Transaction( + user_name=self.active_user.user_name, + value=Decimal(str(self.transaction_value)), + is_debit=True, + title=self.transaction_reason + ) + try: + await new_transaction.save() + except Exception as e: + logger.error(e) + + self.transaction_value = 0.0 + self.transaction_reason = "" + self.active_user_balance = self.session[AccountingService].make_euro_string_from_decimal( + await self.session[AccountingService].get_balance(self.active_user.user_name) + ) + + async def create_credit_transaction(self) -> None: + if not self.active_user: + return + logger.info(f"Trying to add {self.transaction_value} to {self.active_user.user_name} ({self.transaction_reason})") + value = Decimal(str(self.transaction_value)) + new_transaction = Transaction( + user_name=self.active_user.user_name, + value=value, + is_debit=False, + title=self.transaction_reason + ) + try: + await new_transaction.save() + except Exception as e: + logger.error(e) + + self.transaction_value = 0.0 + self.transaction_reason = "" + total_balance = await self.session[AccountingService].get_balance(self.active_user.user_name) + self.active_user_balance = self.session[AccountingService].make_euro_string_from_decimal(total_balance) + + self.session.create_task(self.session[MailingService].send_email( + subject="Dein Guthaben wurde aufgeladen!", + body=self.session[MailingService].generate_account_balance_added_mail_body(user=self.active_user, added_balance=value, total_balance=total_balance), + receiver=self.active_user.user_mail + )) + + def build(self) -> Component: + right_panel_contents = [] + if not self.active_user: + right_panel_contents.append(Spacer()) + else: + right_panel_contents.extend([ + AccountInfoBox(fixed_user=self.active_user), + Rectangle( + content=Column( + Rectangle( + content=Rectangle( + content=Text(f"LAN Konto - Kontostand: {self.active_user_balance}", margin=0.5, selectable=False, overflow="wrap"), + fill=self.session.theme.header_box_background_color, + margin=0.4 + ), + stroke_width=0.1, + stroke_color=self.session.theme.box_border_color, + ), + Column( + NumberInput( + value=self.bind().transaction_value, + label="Betrag", + suffix_text="€", + decimals=2, + margin=1, + margin_bottom=0 + ), + TextInput( + text=self.bind().transaction_reason, + label="Beschreibung", + margin=1, + margin_bottom=0 + ), + Row( + Button( + content="Entfernen", + shape="rectangle", + color="danger", + margin=1, + on_press=self.create_debit_transaction + ), + Button( + content="Hinzufügen", + shape="rectangle", + color="success", + margin=1, + on_press=self.create_credit_transaction + ) + ), + margin=1, + spacing=1 + ), + Spacer() + ), + fill=self.session.theme.box_color, + stroke_width=0.1, + stroke_color=self.session.theme.box_border_color + ), + ]) + + + + return Row( + Rectangle( + content=Column( + Rectangle( + content=Rectangle( + content=Text("Nutzerliste", margin=0.5, selectable=False, overflow="wrap"), + fill=self.session.theme.header_box_background_color, + margin=0.4 + ), + stroke_width=0.1, + stroke_color=self.session.theme.box_border_color, + ), + Column( + TextInput(label="Nutzername", text=self.bind().search_bar_text, on_change=self.on_search_bar_text_changed, margin_bottom=1), + *[Button(content=user.user_name, shape="rectangle", style="plain-text", on_press=partial(self.on_user_clicked, user)) for user in self.user_list], + margin=1 + ), + Spacer() + ), + fill=self.session.theme.box_color, + stroke_width=0.1, + stroke_color=self.session.theme.box_border_color, + min_width=25 + ), + Rectangle( + content=Column( + Rectangle( + content=Rectangle( + content=Text(f"Nutzer bearbeiten{': ' if self.active_user else ''}{self.active_user.user_name if self.active_user else ''}", margin=0.5, selectable=False, overflow="wrap"), + fill=self.session.theme.header_box_background_color, + margin=0.4 + ), + stroke_width=0.1, + stroke_color=self.session.theme.box_border_color, + ), + Column( + *right_panel_contents, + margin=1, + spacing=1 + ), + Spacer() + ), + fill=self.session.theme.box_color, + stroke_width=0.1, + stroke_color=self.session.theme.box_border_color, + grow_x=True + ), + spacing=1, + margin=1 + ) diff --git a/src/elm/services/UserService.py b/src/elm/services/UserService.py index 3dc3d4a..d34d9eb 100644 --- a/src/elm/services/UserService.py +++ b/src/elm/services/UserService.py @@ -69,6 +69,8 @@ class UserService: async def is_login_valid(self, user_name: str, password_clear_text: str) -> bool: user = await self.get_user(user_name) + if not user: + user = await self.get_user(user_name.lower()) # Migrated users had all lowercase names user_password_hash = sha256(password_clear_text.encode(encoding="utf-8")).hexdigest() if not user: return False