diff --git a/auth/__init__.py b/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/auth/login.py b/auth/login.py new file mode 100644 index 0000000..4250dae --- /dev/null +++ b/auth/login.py @@ -0,0 +1,56 @@ +from fastapi.responses import RedirectResponse +from nicegui import APIRouter, app, ui + + +#TODO +passwords = {"a": "a", "b": "b", "c": "c"} +pgroups = {"a": {"p11111", "p22222"}, "b": {"p33333", "p44444"}} + + +router = APIRouter() + + +@router.page("/login") +def login(redirect_to: str = "/") -> RedirectResponse | None: + if app.storage.user.get("authenticated", False): + return RedirectResponse("/") + + def try_login(): # local function to avoid passing username and password as arguments + if not username.value: + ui.notify("Missing username", color="negative") + focus(username) + return + + if not password.value: + focus(password) + return + + if passwords.get(username.value) != password.value: + ui.notify("Wrong username or password", color="negative") + return + + app.storage.user.update( + username=username.value, + authenticated=True, + pgroups=list(pgroups.get(username.value, set())) + ) + + ui.navigate.to(redirect_to) # go back to where the user wanted to go + + with ui.card().classes("absolute-center items-stretch"): + ui.image("icon.png") + username = ui.input("Username").props("autofocus") + password = ui.input("Password", password=True, password_toggle_button=True) + username.on("keydown.enter", try_login) + password.on("keydown.enter", try_login) + ui.button("log in", icon="login", on_click=try_login) + + return None + + + +def focus(element): + ui.run_javascript(f"getElement({element.id}).$refs.qRef.focus()") + + + diff --git a/auth/logout.py b/auth/logout.py new file mode 100644 index 0000000..ea653f5 --- /dev/null +++ b/auth/logout.py @@ -0,0 +1,9 @@ +from nicegui import app, ui + + +def logout(): + app.storage.user.clear() + ui.navigate.to("/login") + + + diff --git a/auth/mw.py b/auth/mw.py new file mode 100644 index 0000000..7950c1a --- /dev/null +++ b/auth/mw.py @@ -0,0 +1,29 @@ +from fastapi import Request +from fastapi.responses import RedirectResponse +from nicegui import app +from starlette.middleware.base import BaseHTTPMiddleware + + +unrestricted_page_routes = {"/login"} + + +class AuthMiddleware(BaseHTTPMiddleware): + """ + This middleware restricts access to all NiceGUI pages. + It redirects the user to the login page if they are not authenticated. + """ + + async def dispatch(self, request: Request, call_next): + path = request.url.path + + if ( + app.storage.user.get("authenticated", False) + or path in unrestricted_page_routes + or path.startswith("/_nicegui") + ): + return await call_next(request) + + return RedirectResponse(f"/login?redirect_to={path}") + + + diff --git a/authenticated.py b/authenticated.py new file mode 100755 index 0000000..b649b9f --- /dev/null +++ b/authenticated.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 + +from nicegui import app, ui + +from auth.login import router as login_router +from auth.logout import logout +from auth.mw import AuthMiddleware + + +app.include_router(login_router) +app.add_middleware(AuthMiddleware) + + +@ui.page("/") +def main_page(): + with ui.column().classes("absolute-center items-center"): + username = app.storage.user.get("username", "unknown user") + ui.label(f"Hello {username}!").classes("text-2xl") + ui.button("log out", icon="logout", on_click=logout) + + pgroups = app.storage.user.get("pgroups", set()) + ui.select( + label="pgroup", + options=sorted(pgroups), + with_input=True, + on_change=lambda e: ui.navigate.to(e.value) + ) + + + +#TODO: the below is a dummy + +from typing import Annotated +from fastapi import Path + +PGroup = Annotated[str, Path(pattern=r"^p\d{5}$")] + + +@ui.page("/{pgroup}") +def table(pgroup: PGroup): + pgroups = app.storage.user.get("pgroups", set()) + if pgroup in pgroups: + table_show(pgroup) + else: + table_deny(pgroup) + +def table_show(pgroup): + with ui.column().classes("absolute-center items-center gap-8"): + ui.icon("sym_o_thumb_up", size="xl") + ui.label(f"{pgroup} erlaubt!").classes("text-2xl") + +def table_deny(pgroup): + with ui.column().classes("absolute-center items-center gap-8"): + ui.icon("sym_o_front_hand", size="xl") + ui.label(f"{pgroup} verboten!").classes("text-2xl") + + + +ui.run(storage_secret="THIS_NEEDS_TO_BE_CHANGED", dark=True) #TODO: storage_secret? + + +