- FastAPI-Backend mit vollständiger ABS v2.x API-Kompatibilität - SQLAlchemy-Models: User, Library, LibraryItem, BookFile, Chapter, Podcast, PodcastEpisode, MediaProgress, Bookmark, PlaybackSession - Auth: JWT-Login (/login, /logout, /api/authorize) - Library + Items Endpoints inkl. camelCase ABS-Response-Format - HLS-Streaming via FFmpeg (POST /api/items/:id/play, Session-Sync) - Me/Progress Endpoints + Lesezeichen - User-Management + Server-Settings (Admin) - Library-Scanner (MP3/WAV Discovery, Hintergrund-Task) - File Watcher (watchdog, 30s Debounce) - Matching-Skelett (MusicBrainz, OpenLibrary, Google Books – Phase 5) - Docker-Setup: backend (Python 3.12+FFmpeg), frontend (React/Vite), nginx Reverse-Proxy auf Port 3000 - setup.sh: Installiert Docker auf Debian/Ubuntu, richtet .env ein Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
103 lines
3.3 KiB
Python
103 lines
3.3 KiB
Python
from datetime import datetime
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select
|
|
from ..database import AsyncSessionLocal
|
|
from ..dependencies import get_db, get_current_user
|
|
from ..models.user import User
|
|
from ..models.library import Library
|
|
from ..models.session import ServerSetting
|
|
from ..services.auth import verify_password, create_token
|
|
from ..schemas.auth import LoginRequest, LoginResponse, AuthorizeResponse
|
|
from ..schemas.user import UserOut, UserSettings, ServerSettingsOut
|
|
|
|
router = APIRouter(tags=["auth"])
|
|
|
|
|
|
def _build_user_out(user: User) -> UserOut:
|
|
raw_settings = user.settings or {}
|
|
settings = UserSettings(**{k: v for k, v in raw_settings.items() if k in UserSettings.model_fields})
|
|
return UserOut(
|
|
id=user.id,
|
|
username=user.username,
|
|
email=user.email,
|
|
is_admin=user.is_admin,
|
|
is_active=user.is_active,
|
|
last_seen=user.last_seen,
|
|
created_at=user.created_at,
|
|
token=user.token,
|
|
settings=settings,
|
|
type="root" if user.is_admin else "user",
|
|
permissions={
|
|
"download": True,
|
|
"update": user.is_admin,
|
|
"delete": user.is_admin,
|
|
"upload": user.is_admin,
|
|
"access_all_libraries": True,
|
|
"access_explicit_content": True,
|
|
},
|
|
)
|
|
|
|
|
|
@router.post("/login", response_model=LoginResponse)
|
|
async def login(body: LoginRequest, db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(
|
|
select(User).where(User.username == body.username, User.is_active == True)
|
|
)
|
|
user = result.scalar_one_or_none()
|
|
if not user or not verify_password(body.password, user.password_hash):
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
|
|
|
|
token = create_token(user.id)
|
|
user.token = token
|
|
user.last_seen = datetime.utcnow()
|
|
await db.commit()
|
|
|
|
# Erste Library als Default zurückgeben
|
|
lib_result = await db.execute(select(Library).limit(1))
|
|
first_lib = lib_result.scalar_one_or_none()
|
|
|
|
return LoginResponse(
|
|
user=_build_user_out(user),
|
|
user_default_library_id=first_lib.id if first_lib else None,
|
|
server_settings=ServerSettingsOut(),
|
|
)
|
|
|
|
|
|
@router.post("/logout")
|
|
async def logout(current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
|
current_user.token = None
|
|
await db.commit()
|
|
return {"success": True}
|
|
|
|
|
|
@router.get("/api/authorize", response_model=AuthorizeResponse)
|
|
async def authorize(current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
|
current_user.last_seen = datetime.utcnow()
|
|
await db.commit()
|
|
|
|
lib_result = await db.execute(select(Library))
|
|
libraries = lib_result.scalars().all()
|
|
|
|
from ..routers.libraries import _library_to_out
|
|
libs_out = [_library_to_out(lib) for lib in libraries]
|
|
|
|
first_lib_id = libraries[0].id if libraries else None
|
|
|
|
return AuthorizeResponse(
|
|
user=_build_user_out(current_user),
|
|
libraries=libs_out,
|
|
user_default_library_id=first_lib_id,
|
|
server_settings=ServerSettingsOut(),
|
|
)
|
|
|
|
|
|
@router.get("/ping")
|
|
async def ping():
|
|
return {"success": True}
|
|
|
|
|
|
@router.get("/health")
|
|
async def health():
|
|
return {"status": "ok"}
|