add USer Admin Page

This commit is contained in:
David Rodenkirchen
2026-05-27 19:36:33 +02:00
parent adec26a67b
commit ecc3fb35c3
4 changed files with 253 additions and 18 deletions
+8 -2
View File
@@ -4,7 +4,7 @@ from bson import ObjectId
from rio import Component, Rectangle, Column, Text, Row, PointerEventListener, TextInput from rio import Component, Rectangle, Column, Text, Row, PointerEventListener, TextInput
from rio.event import on_populate 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.components import ElmButton
from elm.services import UserService from elm.services import UserService
@@ -18,9 +18,11 @@ class AccountInfoBox(Component):
password_change_in_progress: bool = False password_change_in_progress: bool = False
ticket: Optional[Ticket] = None ticket: Optional[Ticket] = None
seat: Optional[Seat] = None seat: Optional[Seat] = None
fixed_user: Optional[User] = None
@on_populate @on_populate
async def on_populate(self) -> None: async def on_populate(self) -> None:
if self.fixed_user is None:
try: try:
user = await self.session[UserService].get_user(self.session[UserSession].user_name) user = await self.session[UserService].get_user(self.session[UserSession].user_name)
if user: if user:
@@ -31,6 +33,10 @@ class AccountInfoBox(Component):
self.session.navigate_to("./login") self.session.navigate_to("./login")
except KeyError: except KeyError:
self.session.navigate_to("./login") 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: async def set_new_password(self) -> None:
self.password_change_in_progress = True self.password_change_in_progress = True
@@ -86,7 +92,7 @@ class AccountInfoBox(Component):
stroke_color=self.session.theme.box_border_color, stroke_color=self.session.theme.box_border_color,
), ),
Column( 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), TextInput(text=self.mail, label="E-Mail Adresse", is_sensitive=False),
row_col( row_col(
PointerEventListener( PointerEventListener(
+23 -5
View File
@@ -87,15 +87,33 @@ class UserNavigation(Component):
async def on_populate(self) -> None: async def on_populate(self) -> None:
self.session.create_task(self.update_balance()) 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: def build(self) -> Component:
return Rectangle( base_nav = [
content=Column(
UserNavigationButton(f"Guthaben: {self.session[AccountingService].make_euro_string_from_decimal(self.balance)}", "/balance", self.close_navigation), 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("Meine Bestellungen", "/my-orders", self.close_navigation),
UserNavigationButton("Mein Profil", "/my-profile", self.close_navigation), UserNavigationButton("Mein Profil", "/my-profile", self.close_navigation)
# UserNavigationButton("Mein Clan", "/my-clans", self.close_navigation), ToDo: Implement ]
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) UserNavigationButton("Ausloggen", "/logout", self.close_navigation)
), )
return Rectangle(
content=Column(*base_nav),
min_width=3.5, min_width=3.5,
min_height=3.5, min_height=3.5,
fill=self.session.theme.background_color fill=self.session.theme.background_color
+209
View File
@@ -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
)
+2
View File
@@ -69,6 +69,8 @@ class UserService:
async def is_login_valid(self, user_name: str, password_clear_text: str) -> bool: async def is_login_valid(self, user_name: str, password_clear_text: str) -> bool:
user = await self.get_user(user_name) 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() user_password_hash = sha256(password_clear_text.encode(encoding="utf-8")).hexdigest()
if not user: if not user:
return False return False