Backend (FastAPI): - REST API: auth, plants, beds, plantings - CRUD layer with CRUDBase - Pydantic v2 schemas for all entities - Alembic migration: complete schema + all enums - Seed data: 28 global plants + 15 compatibilities Frontend (Vue 3 + PrimeVue): - Axios client with JWT interceptor + auto-refresh - Pinia stores: auth, beds, plants - Views: Login, Beds, BedDetail, PlantLibrary - Components: AppLayout, BedForm, PlantingForm, PlantForm Docker: - docker-compose.yml (production) - docker-compose.dev.yml (development with hot-reload) - Nginx config with SPA fallback + API proxy - Multi-stage frontend Dockerfile - .env.example, .gitignore Version: 1.0.0-alpha
69 lines
2.4 KiB
Python
69 lines
2.4 KiB
Python
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from jose import JWTError
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.deps import CurrentUser, get_session
|
|
from app.core.security import TOKEN_TYPE_REFRESH, create_access_token, create_refresh_token, decode_token
|
|
from app.crud.user import crud_user
|
|
from app.schemas.auth import AccessTokenResponse, LoginRequest, RefreshRequest, TokenResponse
|
|
from app.schemas.user import UserRead
|
|
|
|
router = APIRouter(prefix="/auth", tags=["Authentifizierung"])
|
|
|
|
|
|
@router.post("/login", response_model=TokenResponse)
|
|
async def login(
|
|
body: LoginRequest,
|
|
db: Annotated[AsyncSession, Depends(get_session)],
|
|
) -> TokenResponse:
|
|
user = await crud_user.authenticate(db, email=body.email, password=body.password)
|
|
if not user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="E-Mail oder Passwort falsch.",
|
|
)
|
|
if not user.is_active:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Benutzerkonto ist deaktiviert.",
|
|
)
|
|
tenants = await crud_user.get_tenants(db, user_id=user.id)
|
|
return TokenResponse(
|
|
access_token=create_access_token(str(user.id)),
|
|
refresh_token=create_refresh_token(str(user.id)),
|
|
user=UserRead.model_validate(user),
|
|
tenants=tenants,
|
|
)
|
|
|
|
|
|
@router.post("/refresh", response_model=AccessTokenResponse)
|
|
async def refresh_token(
|
|
body: RefreshRequest,
|
|
db: Annotated[AsyncSession, Depends(get_session)],
|
|
) -> AccessTokenResponse:
|
|
try:
|
|
payload = decode_token(body.refresh_token)
|
|
if payload.get("type") != TOKEN_TYPE_REFRESH:
|
|
raise JWTError("Falscher Token-Typ")
|
|
user_id: str = payload["sub"]
|
|
except JWTError:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Ungültiger oder abgelaufener Refresh-Token.",
|
|
)
|
|
from uuid import UUID
|
|
user = await crud_user.get(db, id=UUID(user_id))
|
|
if not user or not user.is_active:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Benutzer nicht gefunden oder deaktiviert.",
|
|
)
|
|
return AccessTokenResponse(access_token=create_access_token(user_id))
|
|
|
|
|
|
@router.get("/me", response_model=UserRead)
|
|
async def get_me(current_user: CurrentUser) -> UserRead:
|
|
return UserRead.model_validate(current_user)
|