add auth
This commit is contained in:
76
backend/app/routers/auth.py
Normal file
76
backend/app/routers/auth.py
Normal 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()
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user