- User-Modell (username, password_hash, role admin/user, is_active) - Standard-Admin-Benutzer wird beim ersten Start automatisch angelegt - JWT-Tokens (HS256) für Benutzer-Sessions, konfigurierbare Ablaufzeit - API-Key bleibt für service-to-service-Calls (backward-compatible) - POST /api/v1/auth/login → JWT-Token - GET /api/v1/auth/me → aktueller Benutzer - CRUD /api/v1/users/ → Benutzerverwaltung (nur Admin) - TUI zeigt Login-Screen beim Start; nach Erfolg → MainScreen - Passwort-Hashing mit bcrypt (python-jose für JWT)
72 lines
2.6 KiB
Python
72 lines
2.6 KiB
Python
from __future__ import annotations
|
||
|
||
from textual.app import ComposeResult
|
||
from textual.binding import Binding
|
||
from textual.containers import Vertical
|
||
from textual.screen import Screen
|
||
from textual.widgets import Button, Footer, Header, Input, Label, Static
|
||
|
||
|
||
class LoginScreen(Screen):
|
||
"""Anmeldebildschirm – erscheint beim Start der TUI."""
|
||
|
||
BINDINGS = [Binding("ctrl+c", "quit_app", "Beenden")]
|
||
|
||
def compose(self) -> ComposeResult:
|
||
yield Header(show_clock=True)
|
||
with Vertical(id="login-container"):
|
||
yield Static("Anmeldung", id="login-title")
|
||
yield Label("Benutzername:")
|
||
yield Input(placeholder="admin", id="username-input")
|
||
yield Label("Passwort:")
|
||
yield Input(placeholder="", password=True, id="password-input")
|
||
yield Static("", id="login-error")
|
||
yield Button("Anmelden", variant="primary", id="login-btn")
|
||
yield Footer()
|
||
|
||
def on_mount(self) -> None:
|
||
self.query_one("#username-input", Input).focus()
|
||
|
||
def on_input_submitted(self, event: Input.Submitted) -> None:
|
||
if event.input.id == "username-input":
|
||
self.query_one("#password-input", Input).focus()
|
||
elif event.input.id == "password-input":
|
||
self._do_login()
|
||
|
||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||
if event.button.id == "login-btn":
|
||
self._do_login()
|
||
|
||
def _do_login(self) -> None:
|
||
import asyncio
|
||
asyncio.create_task(self._login_async())
|
||
|
||
async def _login_async(self) -> None:
|
||
username = self.query_one("#username-input", Input).value.strip()
|
||
password = self.query_one("#password-input", Input).value
|
||
if not username or not password:
|
||
self.query_one("#login-error", Static).update("Benutzername und Passwort eingeben.")
|
||
return
|
||
|
||
btn = self.query_one("#login-btn", Button)
|
||
btn.disabled = True
|
||
self.query_one("#login-error", Static).update("")
|
||
|
||
try:
|
||
ok = await self.app._api_client.login(username, password) # type: ignore[attr-defined]
|
||
except Exception:
|
||
ok = False
|
||
|
||
btn.disabled = False
|
||
|
||
if ok:
|
||
from tui.screens.main_screen import MainScreen
|
||
self.app.switch_screen(MainScreen())
|
||
else:
|
||
self.query_one("#login-error", Static).update("Ungültige Zugangsdaten oder Server nicht erreichbar.")
|
||
self.query_one("#password-input", Input).clear()
|
||
self.query_one("#password-input", Input).focus()
|
||
|
||
def action_quit_app(self) -> None:
|
||
self.app.exit()
|