diff --git a/app/modules/auth/router.py b/app/modules/auth/router.py new file mode 100644 index 0000000..75c7df3 --- /dev/null +++ b/app/modules/auth/router.py @@ -0,0 +1,57 @@ +from fastapi import APIRouter, Depends, Form, Request +from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi.templating import Jinja2Templates +from sqlalchemy.orm import Session + +from app.core.auth import clear_auth_cookie, create_access_token, set_auth_cookie +from app.core.config import get_settings +from app.core.database import get_db +from app.modules.auth.dependencies import get_current_user +from app.modules.auth.schemas import UserOut +from app.modules.auth.service import authenticate_user + +router = APIRouter(prefix="/auth", tags=["auth"]) +templates = Jinja2Templates(directory="app/templates") +settings = get_settings() + +_NAV: list[dict] = [] + + +@router.get("/login", response_class=HTMLResponse) +async def login_page(request: Request): + return templates.TemplateResponse( + request, "auth/login.html", {"nav_items": _NAV, "app_version": "0.1.0"} + ) + + +@router.post("/login", response_class=HTMLResponse) +async def login( + request: Request, + username: str = Form(...), + password: str = Form(...), + db: Session = Depends(get_db), +): + user = authenticate_user(db, username, password, ldap_enabled=settings.LDAP_ENABLED) + if user is None: + return templates.TemplateResponse( + request, + "auth/login.html", + {"nav_items": _NAV, "app_version": "0.1.0", "error": "Ungültige Zugangsdaten."}, + status_code=401, + ) + token = create_access_token(username=user.username, is_admin=user.is_admin) + response = RedirectResponse(url="/", status_code=303) + set_auth_cookie(response, token) + return response + + +@router.get("/logout") +async def logout(): + response = RedirectResponse(url="/auth/login", status_code=303) + clear_auth_cookie(response) + return response + + +@router.get("/me", response_model=UserOut) +async def me(user=Depends(get_current_user)): + return user diff --git a/app/templates/auth/login.html b/app/templates/auth/login.html new file mode 100644 index 0000000..adea061 --- /dev/null +++ b/app/templates/auth/login.html @@ -0,0 +1,54 @@ +{% extends "base.html" %} + +{% block title %}Anmelden · EFI Hub{% endblock %} + +{% block content %} +
+
+ +
+

+ Fakultät EFI · TH Nürnberg +

+

Anmelden

+

Bitte melde dich mit deinen Zugangsdaten an.

+
+ + {% if error %} +
+ {{ error }} +
+ {% endif %} + +
+
+ + +
+
+ + +
+ +
+ +
+
+{% endblock %} diff --git a/tests/test_auth_router.py b/tests/test_auth_router.py new file mode 100644 index 0000000..519e9fb --- /dev/null +++ b/tests/test_auth_router.py @@ -0,0 +1,72 @@ +import pytest +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import StaticPool + +from app.core.database import Base, get_db +from app.main import app +from app.modules.auth.models import User +from app.modules.auth.service import hash_password + + +@pytest.fixture(autouse=True) +def override_db(): + engine = create_engine( + "sqlite:///:memory:", + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) + Base.metadata.create_all(bind=engine) + Session = sessionmaker(bind=engine) + session = Session() + app.dependency_overrides[get_db] = lambda: session + yield session + app.dependency_overrides.clear() + session.close() + Base.metadata.drop_all(bind=engine) + + +@pytest.fixture +def client(): + return TestClient(app, follow_redirects=False) + + +@pytest.fixture +def alice(override_db): + user = User(username="alice", full_name="Alice Smith", pw_hash=hash_password("secret123")) + override_db.add(user) + override_db.commit() + return user + + +def test_get_login_page(client): + response = client.get("/auth/login") + assert response.status_code == 200 + assert "text/html" in response.headers["content-type"] + assert "Anmelden" in response.text + + +def test_login_correct_credentials_redirects(client, alice): + response = client.post("/auth/login", data={"username": "alice", "password": "secret123"}) + assert response.status_code in (302, 303, 307) + assert "access_token" in response.cookies + + +def test_login_wrong_password_shows_error(client, alice): + response = client.post("/auth/login", data={"username": "alice", "password": "wrong"}) + assert response.status_code == 200 + assert "Ungültige" in response.text + + +def test_login_unknown_user_shows_error(client): + response = client.post("/auth/login", data={"username": "ghost", "password": "any"}) + assert response.status_code == 200 + assert "Ungültige" in response.text + + +def test_logout_clears_cookie(client, alice): + client.post("/auth/login", data={"username": "alice", "password": "secret123"}) + response = client.get("/auth/logout") + assert response.status_code in (302, 303, 307) + assert response.cookies.get("access_token", "") == ""