Initial commit: Phase 1 – Projektstruktur, DB-Schema, Core-API

- 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>
This commit is contained in:
Audiolib
2026-05-26 11:43:35 +02:00
commit 14ffee3051
56 changed files with 3220 additions and 0 deletions

102
backend/app/routers/auth.py Normal file
View File

@@ -0,0 +1,102 @@
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"}