Files
Audiolib/backend/app/routers/users.py
Audiolib 14ffee3051 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>
2026-05-26 11:43:35 +02:00

161 lines
5.0 KiB
Python

import uuid
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from ..dependencies import get_db, get_current_user, require_admin
from ..models.user import User
from ..models.session import PlaybackSession
from ..services.auth import hash_password
from ..schemas.user import UserCreate, UserUpdate, UserOut, UserSettings
from ..routers.auth import _build_user_out
router = APIRouter(prefix="/api/users", tags=["users"])
@router.get("")
async def list_users(
admin: User = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(User))
users = result.scalars().all()
return [_build_user_out(u).model_dump(by_alias=True) for u in users]
@router.post("")
async def create_user(
body: UserCreate,
admin: User = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
existing = await db.execute(select(User).where(User.username == body.username))
if existing.scalar_one_or_none():
raise HTTPException(status_code=400, detail="Username already exists")
user = User(
id=str(uuid.uuid4()),
username=body.username,
email=body.email,
password_hash=hash_password(body.password),
is_admin=body.is_admin,
)
db.add(user)
await db.commit()
await db.refresh(user)
return _build_user_out(user).model_dump(by_alias=True)
@router.get("/{user_id}")
async def get_user(
user_id: str,
admin: User = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=404, detail="User not found")
return _build_user_out(user).model_dump(by_alias=True)
@router.patch("/{user_id}")
async def update_user(
user_id: str,
body: UserUpdate,
admin: User = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=404, detail="User not found")
if body.email is not None:
user.email = body.email
if body.password is not None:
user.password_hash = hash_password(body.password)
if body.is_admin is not None:
user.is_admin = body.is_admin
if body.is_active is not None:
user.is_active = body.is_active
if body.settings is not None:
user.settings = {**(user.settings or {}), **body.settings}
user.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(user)
return _build_user_out(user).model_dump(by_alias=True)
@router.delete("/{user_id}")
async def delete_user(
user_id: str,
admin: User = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=404, detail="User not found")
if user.is_admin:
# Sicherstellen dass mindestens ein Admin übrig bleibt
admin_count_result = await db.execute(
select(User).where(User.is_admin == True)
)
admins = admin_count_result.scalars().all()
if len(admins) <= 1:
raise HTTPException(status_code=400, detail="Cannot delete the last admin")
await db.delete(user)
await db.commit()
return {"success": True}
@router.get("/{user_id}/listening-sessions")
async def get_listening_sessions(
user_id: str,
admin: User = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(PlaybackSession)
.where(PlaybackSession.user_id == user_id)
.order_by(PlaybackSession.updated_at.desc())
.limit(100)
)
sessions = result.scalars().all()
return {
"sessions": [
{
"id": s.id,
"userId": s.user_id,
"libraryItemId": s.library_item_id,
"episodeId": s.episode_id,
"mediaType": s.media_type,
"currentTime": s.current_time,
"duration": s.duration,
"startedAt": int(s.started_at.timestamp() * 1000) if s.started_at else 0,
"updatedAt": int(s.updated_at.timestamp() * 1000) if s.updated_at else 0,
}
for s in sessions
]
}
@router.get("/{user_id}/listening-stats")
async def get_listening_stats(
user_id: str,
admin: User = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(PlaybackSession).where(PlaybackSession.user_id == user_id)
)
sessions = result.scalars().all()
total_time = sum(s.duration for s in sessions if s.duration)
return {
"totalTime": total_time,
"numSessions": len(sessions),
"days": {},
}