This commit is contained in:
Garret Patti
2026-06-29 10:41:29 -04:00
parent d48c1e973e
commit 2b0b19eb91
23 changed files with 969 additions and 44 deletions

View File

@@ -0,0 +1,76 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.database import get_db
from app.models import User
from app.schemas import LoginRequest, TokenResponse, UserOut, UserCreate
from app.auth import (
verify_password,
hash_password,
create_access_token,
get_current_user,
get_current_admin_user,
)
router = APIRouter(prefix="/auth", tags=["auth"])
@router.post("/login", response_model=TokenResponse)
async def login(body: LoginRequest, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(User).where(User.username == body.username))
user = result.scalars().first()
if not user or not verify_password(body.password, user.password_hash):
raise HTTPException(401, "Invalid username or password")
token = create_access_token({"sub": str(user.id)})
return TokenResponse(access_token=token)
@router.get("/me", response_model=UserOut)
async def get_me(current_user: User = Depends(get_current_user)):
return current_user
@router.get("/users", response_model=list[UserOut])
async def list_users(
db: AsyncSession = Depends(get_db),
_admin: User = Depends(get_current_admin_user),
):
result = await db.execute(select(User).order_by(User.username))
return result.scalars().all()
@router.post("/users", response_model=UserOut, status_code=201)
async def create_user(
body: UserCreate,
db: AsyncSession = Depends(get_db),
_admin: User = Depends(get_current_admin_user),
):
existing = await db.execute(select(User).where(User.username == body.username))
if existing.scalars().first():
raise HTTPException(409, "Username already exists")
user = User(
username=body.username,
password_hash=hash_password(body.password),
is_admin=False,
)
db.add(user)
await db.commit()
await db.refresh(user)
return user
@router.delete("/users/{user_id}", status_code=204)
async def delete_user(
user_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_admin_user),
):
if user_id == current_user.id:
raise HTTPException(400, "Cannot delete your own account")
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalars().first()
if not user:
raise HTTPException(404, "User not found")
await db.delete(user)
await db.commit()

View File

@@ -5,15 +5,19 @@ from sqlalchemy import select
from sqlalchemy.orm import selectinload
from app.database import get_db
from app.models import Library, MediaItem
from app.models import Library, MediaItem, User
from app.schemas import LibraryCreate, LibraryOut, MediaItemOut, BrowseResult, BrowseEntry
from app.services import scanner, watcher as watcher_service
from app.auth import get_current_user
router = APIRouter(prefix="/libraries", tags=["libraries"])
@router.get("", response_model=list[LibraryOut])
async def list_libraries(db: AsyncSession = Depends(get_db)):
async def list_libraries(
db: AsyncSession = Depends(get_db),
_user: User = Depends(get_current_user),
):
result = await db.execute(select(Library))
return result.scalars().all()
@@ -23,6 +27,7 @@ async def create_library(
body: LibraryCreate,
background_tasks: BackgroundTasks,
db: AsyncSession = Depends(get_db),
_user: User = Depends(get_current_user),
):
path = Path(body.path)
if not path.is_dir():
@@ -44,7 +49,10 @@ async def create_library(
@router.get("/{library_id}/scan-status")
async def get_scan_status(library_id: int):
async def get_scan_status(
library_id: int,
_user: User = Depends(get_current_user),
):
return {"scanning": scanner.is_scanning(library_id)}
@@ -53,6 +61,7 @@ async def rescan_library(
library_id: int,
background_tasks: BackgroundTasks,
db: AsyncSession = Depends(get_db),
_user: User = Depends(get_current_user),
):
result = await db.execute(select(Library).where(Library.id == library_id))
lib = result.scalars().first()
@@ -69,6 +78,7 @@ async def doom_scroll(
library_id: int,
path: str = "",
db: AsyncSession = Depends(get_db),
_user: User = Depends(get_current_user),
):
result = await db.execute(select(Library).where(Library.id == library_id))
if not result.scalars().first():
@@ -87,7 +97,11 @@ async def doom_scroll(
@router.delete("/{library_id}", status_code=204)
async def delete_library(library_id: int, db: AsyncSession = Depends(get_db)):
async def delete_library(
library_id: int,
db: AsyncSession = Depends(get_db),
_user: User = Depends(get_current_user),
):
result = await db.execute(select(Library).where(Library.id == library_id))
lib = result.scalars().first()
if not lib:
@@ -98,7 +112,12 @@ async def delete_library(library_id: int, db: AsyncSession = Depends(get_db)):
@router.get("/{library_id}/browse", response_model=BrowseResult)
async def browse_library(library_id: int, path: str = "", db: AsyncSession = Depends(get_db)):
async def browse_library(
library_id: int,
path: str = "",
db: AsyncSession = Depends(get_db),
_user: User = Depends(get_current_user),
):
result = await db.execute(select(Library).where(Library.id == library_id))
lib = result.scalars().first()
if not lib:

View File

@@ -5,9 +5,10 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.database import get_db
from app.models import Library, MediaItem, Tag
from app.models import Library, MediaItem, Tag, User
from app.schemas import MediaItemOut, TagIdList
from app.services.thumbnails import get_or_create_thumbnail
from app.auth import get_current_user, get_current_user_from_query_or_header
router = APIRouter(prefix="/media", tags=["media"])
@@ -33,7 +34,11 @@ def _resolve_safe(lib: Library, item: MediaItem) -> Path:
@router.get("/{media_id}", response_model=MediaItemOut)
async def get_media_item(media_id: int, db: AsyncSession = Depends(get_db)):
async def get_media_item(
media_id: int,
db: AsyncSession = Depends(get_db),
_user: User = Depends(get_current_user),
):
result = await db.execute(
select(MediaItem).where(MediaItem.id == media_id)
)
@@ -46,7 +51,11 @@ async def get_media_item(media_id: int, db: AsyncSession = Depends(get_db)):
@router.get("/{media_id}/file")
async def serve_file(media_id: int, db: AsyncSession = Depends(get_db)):
async def serve_file(
media_id: int,
db: AsyncSession = Depends(get_db),
_user: User = Depends(get_current_user_from_query_or_header),
):
item, lib = await _get_item_and_lib(media_id, db)
if item.missing:
raise HTTPException(404, "File is missing from disk")
@@ -57,7 +66,11 @@ async def serve_file(media_id: int, db: AsyncSession = Depends(get_db)):
@router.get("/{media_id}/thumbnail")
async def serve_thumbnail(media_id: int, db: AsyncSession = Depends(get_db)):
async def serve_thumbnail(
media_id: int,
db: AsyncSession = Depends(get_db),
_user: User = Depends(get_current_user_from_query_or_header),
):
item, lib = await _get_item_and_lib(media_id, db)
abs_path = _resolve_safe(lib, item)
thumb = get_or_create_thumbnail(media_id, str(abs_path), item.media_type)
@@ -67,7 +80,12 @@ async def serve_thumbnail(media_id: int, db: AsyncSession = Depends(get_db)):
@router.put("/{media_id}/tags", response_model=MediaItemOut)
async def set_tags(media_id: int, body: TagIdList, db: AsyncSession = Depends(get_db)):
async def set_tags(
media_id: int,
body: TagIdList,
db: AsyncSession = Depends(get_db),
_user: User = Depends(get_current_user),
):
result = await db.execute(select(MediaItem).where(MediaItem.id == media_id))
item = result.scalars().first()
if not item:

View File

@@ -4,8 +4,9 @@ from sqlalchemy import select
from sqlalchemy.orm import selectinload
from app.database import get_db
from app.models import MediaItem, media_item_tags
from app.models import MediaItem, media_item_tags, User
from app.schemas import MediaItemOut
from app.auth import get_current_user
router = APIRouter(prefix="/search", tags=["search"])
@@ -16,6 +17,7 @@ async def search(
tags: str = Query(default=""),
library_id: int | None = Query(default=None),
db: AsyncSession = Depends(get_db),
_user: User = Depends(get_current_user),
):
stmt = (
select(MediaItem)

View File

@@ -3,14 +3,18 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.database import get_db
from app.models import Tag
from app.models import Tag, User
from app.schemas import TagCreate, TagOut, TagsByCategory
from app.auth import get_current_user
router = APIRouter(prefix="/tags", tags=["tags"])
@router.get("", response_model=list[TagsByCategory])
async def list_tags(db: AsyncSession = Depends(get_db)):
async def list_tags(
db: AsyncSession = Depends(get_db),
_user: User = Depends(get_current_user),
):
result = await db.execute(select(Tag).order_by(Tag.category, Tag.name))
tags = result.scalars().all()
@@ -22,7 +26,11 @@ async def list_tags(db: AsyncSession = Depends(get_db)):
@router.post("", response_model=TagOut, status_code=201)
async def create_tag(body: TagCreate, db: AsyncSession = Depends(get_db)):
async def create_tag(
body: TagCreate,
db: AsyncSession = Depends(get_db),
_user: User = Depends(get_current_user),
):
existing = await db.execute(
select(Tag).where(Tag.name == body.name, Tag.category == body.category)
)
@@ -36,7 +44,11 @@ async def create_tag(body: TagCreate, db: AsyncSession = Depends(get_db)):
@router.delete("/{tag_id}", status_code=204)
async def delete_tag(tag_id: int, db: AsyncSession = Depends(get_db)):
async def delete_tag(
tag_id: int,
db: AsyncSession = Depends(get_db),
_user: User = Depends(get_current_user),
):
result = await db.execute(select(Tag).where(Tag.id == tag_id))
tag = result.scalars().first()
if not tag: