- auth: username lookup is now case-insensitive via func.lower() - scanner: trigger match_audiobook for each newly found item after scan - matcher: read match_sources from library settings; refactored to loop over configured sources in priority order instead of hardcoded sequence - schemas/routers: expose matchSources in LibraryOut API response - Admin UI: pill-toggle for MusicBrainz/Open Library/Google Books per library 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, func
|
|
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(func.lower(User.username) == body.username.lower(), 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"}
|