feat: Multi-User-Unterstützung mit JWT-Authentifizierung
- 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)
This commit is contained in:
71
tui/screens/login_screen.py
Normal file
71
tui/screens/login_screen.py
Normal file
@@ -0,0 +1,71 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user