initial release

This commit is contained in:
Justin Visser 2026-03-03 16:16:36 +01:00
parent e156de22c3
commit b3ac11a85e
37 changed files with 3649 additions and 669 deletions

104
backend/app/routes/pages.py Normal file
View file

@ -0,0 +1,104 @@
import threading
from pathlib import Path
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, Response
from app.auth import require_admin
from app.config import get_settings
from app.database import init_db
from app.models import (
Page,
PageCreateRequest,
PageReorderRequest,
PageUpdateRequest,
SegmentResponse,
)
from app.services import page_service, segment_service
router = APIRouter(prefix="/api/pages", tags=["pages"])
_write_lock = threading.Lock()
def get_db_path() -> Path:
data_dir = get_settings().data_dir
db_path = Path(data_dir) / "handin.db"
init_db(db_path)
return db_path
DbPath = Annotated[Path, Depends(get_db_path)]
Admin = Annotated[None, Depends(require_admin)]
@router.get("/")
def list_pages(db_path: DbPath) -> list[Page]:
return page_service.list_pages(db_path)
@router.post("/", status_code=201)
def create_page(request: PageCreateRequest, _: Admin, db_path: DbPath) -> Page:
with _write_lock:
# Check slug uniqueness
existing = page_service.get_page_by_slug(db_path, request.slug)
if existing:
raise HTTPException(status_code=409, detail="A page with this slug already exists")
return page_service.create_page(db_path, request)
@router.put("/reorder")
def reorder_pages(request: PageReorderRequest, _: Admin, db_path: DbPath) -> list[Page]:
with _write_lock:
try:
return page_service.reorder_pages(db_path, request.page_ids)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e)) from e
@router.get("/{page_id}")
def get_page(page_id: str, db_path: DbPath) -> Page:
page = page_service.get_page(db_path, page_id)
if page is None:
raise HTTPException(status_code=404, detail="Page not found")
return page
@router.get("/by-slug/{slug}")
def get_page_by_slug(slug: str, db_path: DbPath) -> Page:
page = page_service.get_page_by_slug(db_path, slug)
if page is None:
raise HTTPException(status_code=404, detail="Page not found")
return page
@router.patch("/{page_id}")
def update_page(page_id: str, request: PageUpdateRequest, _: Admin, db_path: DbPath) -> Page:
if request.slug is not None:
existing = page_service.get_page_by_slug(db_path, request.slug)
if existing and existing.id != page_id:
raise HTTPException(status_code=409, detail="A page with this slug already exists")
page = page_service.update_page(db_path, page_id, request)
if page is None:
raise HTTPException(status_code=404, detail="Page not found")
return page
@router.delete("/{page_id}", status_code=204)
def delete_page(page_id: str, _: Admin, db_path: DbPath) -> Response:
deleted, error = page_service.delete_page(db_path, page_id)
if not deleted:
if error == "not_found":
raise HTTPException(status_code=404, detail="Page not found")
if error == "system_page":
raise HTTPException(status_code=403, detail="System pages cannot be deleted")
return Response(status_code=204)
@router.get("/{page_id}/segments")
def list_page_segments(page_id: str, db_path: DbPath) -> list[SegmentResponse]:
page = page_service.get_page(db_path, page_id)
if page is None:
raise HTTPException(status_code=404, detail="Page not found")
segments = segment_service.list_segments(db_path, page_id=page_id)
return [SegmentResponse.model_validate(s.model_dump()) for s in segments]

View file

@ -3,7 +3,7 @@ from pathlib import Path
from typing import Annotated
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Response
from fastapi import APIRouter, Depends, HTTPException, Query, Response
from app.auth import require_admin
from app.config import get_settings
@ -34,8 +34,11 @@ Admin = Annotated[None, Depends(require_admin)]
@router.get("/")
def list_segments(db_path: DbPath) -> list[SegmentResponse]:
segments = segment_service.list_segments(db_path)
def list_segments(
db_path: DbPath,
page_id: Annotated[str | None, Query()] = None,
) -> list[SegmentResponse]:
segments = segment_service.list_segments(db_path, page_id=page_id)
return [SegmentResponse.model_validate(s.model_dump()) for s in segments]

View file

@ -0,0 +1,56 @@
import threading
from pathlib import Path
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, Response
from app.auth import require_admin
from app.config import get_settings
from app.database import init_db
from app.models import TeamMember, TeamMemberCreateRequest, TeamMemberReorderRequest
from app.services import team_service
router = APIRouter(prefix="/api/team", tags=["team"])
_write_lock = threading.Lock()
def get_db_path() -> Path:
data_dir = get_settings().data_dir
db_path = Path(data_dir) / "handin.db"
init_db(db_path)
return db_path
DbPath = Annotated[Path, Depends(get_db_path)]
Admin = Annotated[None, Depends(require_admin)]
@router.get("/")
def list_members(db_path: DbPath) -> list[TeamMember]:
return team_service.list_members(db_path)
@router.post("/", status_code=201)
def add_member(request: TeamMemberCreateRequest, _: Admin, db_path: DbPath) -> TeamMember:
with _write_lock:
return team_service.add_member(db_path, request)
@router.put("/reorder")
def reorder_members(
request: TeamMemberReorderRequest, _: Admin, db_path: DbPath
) -> list[TeamMember]:
with _write_lock:
try:
return team_service.reorder_members(db_path, request.member_ids)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e)) from e
@router.delete("/{member_id}", status_code=204)
def delete_member(member_id: str, _: Admin, db_path: DbPath) -> Response:
deleted = team_service.delete_member(db_path, member_id)
if not deleted:
raise HTTPException(status_code=404, detail="Team member not found")
return Response(status_code=204)