Sprint 0: FastAPI-Skeleton, vollständiges DB-Schema (Alembic), Auth (JWT/bcrypt), Rollen-Dependency, CI
CI / backend-tests (push) Failing after 2s

- Vollständiges Ziel-Schema aus Prompt 20 als initiale Migration (inkl. Karte-13-Vorbereitung: UUID-PKs, systemknoten)
- Seed-Migration für Hauptserver-Datensatz (Sprintplan E6)
- JWT-Login + /auth/me, require_roles-Dependency (Prompt 05 Berechtigungsmatrix)
- Tests: health, login/me, Rollen-Ablehnung/-Zulassung (S0-Abnahmekriterien)
- Gitea-Actions-CI: install -> migrate -> pytest mit Coverage-Gate 50%

Nur Code/Config erzeugt, nicht lokal installiert oder ausgeführt (Deployment-Regel).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt
This commit is contained in:
sysops
2026-09-03 22:32:54 +02:00
co-authored by Claude Sonnet 5
parent e4ee4e5458
commit cedc39e075
34 changed files with 1006 additions and 0 deletions
View File
+7
View File
@@ -0,0 +1,7 @@
from fastapi import APIRouter
from app.api.v1.endpoints import auth, health
api_router = APIRouter()
api_router.include_router(health.router, tags=["health"])
api_router.include_router(auth.router, tags=["auth"])
+55
View File
@@ -0,0 +1,55 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_user
from app.core.security import create_access_token, verify_password
from app.db.session import get_db
from app.models.auth import Benutzer
router = APIRouter()
class TokenResponse(BaseModel):
access_token: str
token_type: str = "bearer"
class MeResponse(BaseModel):
id: int
name: str
login: str
rollen: list[str]
@router.post("/auth/login", response_model=TokenResponse)
async def login(
form_data: OAuth2PasswordRequestForm = Depends(),
db: AsyncSession = Depends(get_db),
) -> TokenResponse:
result = await db.execute(select(Benutzer).where(Benutzer.login == form_data.username))
benutzer = result.scalar_one_or_none()
if (
benutzer is None
or not benutzer.aktiv
or not verify_password(form_data.password, benutzer.passwort_hash)
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Login oder Passwort falsch",
headers={"WWW-Authenticate": "Bearer"},
)
token = create_access_token(subject=benutzer.login, roles=benutzer.rollen_namen)
return TokenResponse(access_token=token)
@router.get("/auth/me", response_model=MeResponse)
async def me(current_user: Benutzer = Depends(get_current_user)) -> MeResponse:
return MeResponse(
id=current_user.id,
name=current_user.name,
login=current_user.login,
rollen=current_user.rollen_namen,
)
+19
View File
@@ -0,0 +1,19 @@
from fastapi import APIRouter, Depends
from app.api.deps import require_roles
from app.models.auth import Benutzer, RolleTyp
router = APIRouter()
@router.get("/health")
async def health() -> dict:
return {"status": "ok"}
@router.get("/health/admin-only")
async def health_admin_only(
current_user: Benutzer = Depends(require_roles(RolleTyp.administration)),
) -> dict:
"""Nachweis, dass die Rollen-Dependency (Prompt 05) greift kein Fachfeature."""
return {"status": "ok", "login": current_user.login}