Sprint 0: FastAPI-Skeleton, vollständiges DB-Schema (Alembic), Auth (JWT/bcrypt), Rollen-Dependency, CI
CI / backend-tests (push) Failing after 2s
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:
co-authored by
Claude Sonnet 5
parent
e4ee4e5458
commit
cedc39e075
@@ -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"])
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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}
|
||||
Reference in New Issue
Block a user