diff --git a/app/main.py b/app/main.py index d0984a3..f7fa70d 100644 --- a/app/main.py +++ b/app/main.py @@ -1,3 +1,5 @@ +import importlib +import logging from contextlib import asynccontextmanager from fastapi import Depends, FastAPI, Request, WebSocket @@ -7,15 +9,71 @@ from fastapi.templating import Jinja2Templates from app.core.config import get_settings from app.core.database import get_db -from app.modules.auth.dependencies import RequiresLoginException, get_current_user +from app.modules.auth.dependencies import ( + RequiresLoginException, + check_permission, + get_current_user, + get_current_user_optional, +) from app.modules.auth.router import router as auth_router +logger = logging.getLogger(__name__) settings = get_settings() +MODULES = [ + { + "slug": "rss", + "icon": "📡", + "name": "RSS-Feed Server", + "description": "Aggregiert und verteilt Neuigkeiten der Fakultät als standardkonformen RSS 2.0 Feed.", + "status": "active", + "permission": ["authenticated"], + }, + { + "slug": "kalender", + "icon": "📅", + "name": "Kalender", + "description": "Veranstaltungen, Prüfungstermine und Fristen im iCal-Format.", + "status": "planned", + "permission": ["authenticated"], + }, + { + "slug": "benachrichtigungen", + "icon": "🔔", + "name": "Benachrichtigungen", + "description": "Push-Nachrichten und WebSocket-basierte Echtzeit-Alerts für Studierende.", + "status": "planned", + "permission": ["authenticated"], + }, + { + "slug": "auslastung", + "icon": "📊", + "name": "Auslastung", + "description": "Raum- und Ressourcenauslastung der Fakultät in Echtzeit.", + "status": "planned", + "permission": ["authenticated"], + }, + { + "slug": "lehrveranstaltungen", + "icon": "📚", + "name": "Lehrveranstaltungen", + "description": "Stundenplan-API und Kursinformationen aus dem Campus-System.", + "status": "planned", + "permission": ["authenticated"], + }, +] + +NAV_ITEMS = [ + {"label": "Übersicht", "url": "/", "active": True}, + {"label": "RSS-Feeds", "url": "/rss", "active": False}, + {"label": "Kalender", "url": "/kalender", "active": False}, + {"label": "Docs", "url": "/docs", "active": False}, +] + def _reset_admin() -> None: from app.core.database import Base, SessionLocal, engine - from app.modules.auth.models import User # noqa: F401 — registers table + from app.modules.auth.models import User # noqa: F401 from app.modules.auth.service import get_user, hash_password Base.metadata.create_all(bind=engine) @@ -47,66 +105,34 @@ templates = Jinja2Templates(directory="app/templates") app.include_router(auth_router) +for _module in MODULES: + if _module["status"] == "active": + try: + _mod = importlib.import_module(f"app.modules.{_module['slug']}.router") + app.include_router(_mod.router, prefix=f"/{_module['slug']}") + except ModuleNotFoundError: + logger.warning("Module router not found: app.modules.%s.router", _module["slug"]) + @app.exception_handler(RequiresLoginException) async def requires_login_handler(request: Request, exc: RequiresLoginException): return RedirectResponse(url="/auth/login", status_code=307) -MODULES = [ - { - "icon": "📡", - "name": "RSS-Feed Server", - "description": "Aggregiert und verteilt Neuigkeiten der Fakultät als standardkonformen RSS 2.0 Feed.", - "status": "active", - }, - { - "icon": "📅", - "name": "Kalender", - "description": "Veranstaltungen, Prüfungstermine und Fristen im iCal-Format.", - "status": "planned", - }, - { - "icon": "🔔", - "name": "Benachrichtigungen", - "description": "Push-Nachrichten und WebSocket-basierte Echtzeit-Alerts für Studierende.", - "status": "planned", - }, - { - "icon": "📊", - "name": "Auslastung", - "description": "Raum- und Ressourcenauslastung der Fakultät in Echtzeit.", - "status": "planned", - }, - { - "icon": "📚", - "name": "Lehrveranstaltungen", - "description": "Stundenplan-API und Kursinformationen aus dem Campus-System.", - "status": "planned", - }, -] - -NAV_ITEMS = [ - {"label": "Übersicht", "url": "/", "active": True}, - {"label": "RSS-Feeds", "url": "/rss", "active": False}, - {"label": "Kalender", "url": "/kalender", "active": False}, - {"label": "Docs", "url": "/docs", "active": False}, -] - - def _db_mode() -> str: url = settings.DATABASE_URL return "SQLite (dev)" if url.startswith("sqlite") else "MariaDB (prod)" @app.get("/", response_class=HTMLResponse) -async def root(request: Request, current_user=Depends(get_current_user)): +async def root(request: Request, current_user=Depends(get_current_user_optional)): + visible = [m for m in MODULES if check_permission(current_user, m["permission"])] return templates.TemplateResponse( request, "index.html", { "nav_items": NAV_ITEMS, - "modules": MODULES, + "modules": visible, "db_mode": _db_mode(), "app_version": "0.1.0", "current_user": current_user, diff --git a/tests/test_landing.py b/tests/test_landing.py index d16ba05..57498f4 100644 --- a/tests/test_landing.py +++ b/tests/test_landing.py @@ -42,31 +42,36 @@ def auth_cookies(override_db): return {"access_token": token} -def test_landing_without_auth_redirects(client): +@pytest.fixture +def admin_cookies(override_db): + user = User(username="adminuser", full_name="Admin User", is_admin=True, pw_hash=hash_password("pw")) + override_db.add(user) + override_db.commit() + token = create_access_token(username="adminuser", is_admin=True) + return {"access_token": token} + + +def test_landing_accessible_without_auth(client): response = client.get("/") - assert response.status_code == 307 - assert "/auth/login" in response.headers["location"] - - -def test_landing_returns_html(client, auth_cookies): - response = client.get("/", cookies=auth_cookies) assert response.status_code == 200 assert "text/html" in response.headers["content-type"] -def test_landing_contains_title(client, auth_cookies): - response = client.get("/", cookies=auth_cookies) - assert "University Process Hub" in response.text +def test_landing_shows_no_modules_for_anonymous(client): + response = client.get("/") + assert response.status_code == 200 + assert "RSS-Feed Server" not in response.text -def test_landing_contains_rss_module(client, auth_cookies): +def test_landing_shows_modules_for_authenticated(client, auth_cookies): response = client.get("/", cookies=auth_cookies) + assert response.status_code == 200 assert "RSS-Feed Server" in response.text -def test_landing_navbar_links_present(client, auth_cookies): - response = client.get("/", cookies=auth_cookies) - assert "Übersicht" in response.text +def test_landing_contains_title(client): + response = client.get("/") + assert "University Process Hub" in response.text def test_landing_info_strip_shows_db_mode(client, auth_cookies):