Add stories/ dir, /stories API endpoints, dev.sh blog symlink

This commit is contained in:
brutsalvadi
2026-04-23 17:03:34 +02:00
parent aba80ee49e
commit db265fc8d1
7 changed files with 333 additions and 35 deletions
+56 -32
View File
@@ -21,6 +21,7 @@ from pydantic import BaseModel
# Resolved at startup relative to the project root (one level above this file)
_ROOT = Path(__file__).parent.parent
pages_dir: Path = _ROOT / os.environ.get("WIKI_PAGES_DIR", "pages")
stories_dir: Path = _ROOT / os.environ.get("WIKI_STORIES_DIR", "stories")
site_dir: Path = _ROOT / "site"
_DB_PATH = _ROOT / "data" / "wiki.db"
@@ -165,13 +166,13 @@ async def me(user: dict = Depends(require_auth)) -> JSONResponse:
return JSONResponse({"username": user["username"]})
# ── Page helpers ──────────────────────────────────────────────────────────────
# ── File helpers ──────────────────────────────────────────────────────────────
def _slug_to_path(slug: str) -> Path:
def _slug_to_path(slug: str, base: Path) -> Path:
if not _SAFE_SLUG.match(slug):
raise HTTPException(400, "Invalid page slug — only alphanumeric, hyphens, underscores, and slashes allowed")
resolved = (pages_dir / f"{slug}.md").resolve()
if not str(resolved).startswith(str(pages_dir.resolve())):
raise HTTPException(400, "Invalid slug — only alphanumeric, hyphens, underscores, and slashes allowed")
resolved = (base / f"{slug}.md").resolve()
if not str(resolved).startswith(str(base.resolve())):
raise HTTPException(400, "Path traversal detected")
return resolved
@@ -182,44 +183,67 @@ class PageBody(BaseModel):
# ── Page endpoints (all require auth) ────────────────────────────────────────
@app.get("/pages")
async def list_pages(user: dict = Depends(require_auth)) -> JSONResponse:
"""Return all wiki page slugs."""
if not pages_dir.exists():
return JSONResponse({"pages": []})
slugs = [
str(p.relative_to(pages_dir).with_suffix(""))
for p in sorted(pages_dir.rglob("*.md"))
]
return JSONResponse({"pages": slugs})
def _list(base: Path) -> list[str]:
if not base.exists():
return []
return [str(p.relative_to(base).with_suffix("")) for p in sorted(base.rglob("*.md"))]
@app.get("/pages/{slug:path}")
async def get_page(slug: str, user: dict = Depends(require_auth)) -> JSONResponse:
"""Return raw markdown content for a page."""
path = _slug_to_path(slug)
def _get(slug: str, base: Path) -> JSONResponse:
path = _slug_to_path(slug, base)
if not path.exists():
raise HTTPException(404, "Page not found")
raise HTTPException(404, "Not found")
return JSONResponse({"slug": slug, "content": path.read_text(encoding="utf-8")})
@app.post("/pages/{slug:path}")
async def save_page(slug: str, body: PageBody, user: dict = Depends(require_auth)) -> JSONResponse:
"""Create or update a wiki page."""
path = _slug_to_path(slug)
def _save(slug: str, body: PageBody, base: Path) -> JSONResponse:
path = _slug_to_path(slug, base)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(body.content, encoding="utf-8")
return JSONResponse({"slug": slug, "saved": True})
def _delete(slug: str, base: Path) -> JSONResponse:
path = _slug_to_path(slug, base)
if not path.exists():
raise HTTPException(404, "Not found")
path.unlink()
return JSONResponse({"slug": slug, "deleted": True})
# ── Page endpoints ────────────────────────────────────────────────────────────
@app.get("/pages")
async def list_pages(user: dict = Depends(require_auth)) -> JSONResponse:
return JSONResponse({"pages": _list(pages_dir)})
@app.get("/pages/{slug:path}")
async def get_page(slug: str, user: dict = Depends(require_auth)) -> JSONResponse:
return _get(slug, pages_dir)
@app.post("/pages/{slug:path}")
async def save_page(slug: str, body: PageBody, user: dict = Depends(require_auth)) -> JSONResponse:
return _save(slug, body, pages_dir)
@app.delete("/pages/{slug:path}")
async def delete_page(slug: str, user: dict = Depends(require_auth)) -> JSONResponse:
"""Delete a wiki page."""
path = _slug_to_path(slug)
if not path.exists():
raise HTTPException(404, "Page not found")
path.unlink()
return JSONResponse({"slug": slug, "deleted": True})
return _delete(slug, pages_dir)
# ── Story endpoints ───────────────────────────────────────────────────────────
@app.get("/stories")
async def list_stories(user: dict = Depends(require_auth)) -> JSONResponse:
return JSONResponse({"stories": _list(stories_dir)})
@app.get("/stories/{slug:path}")
async def get_story(slug: str, user: dict = Depends(require_auth)) -> JSONResponse:
return _get(slug, stories_dir)
@app.post("/stories/{slug:path}")
async def save_story(slug: str, body: PageBody, user: dict = Depends(require_auth)) -> JSONResponse:
return _save(slug, body, stories_dir)
@app.delete("/stories/{slug:path}")
async def delete_story(slug: str, user: dict = Depends(require_auth)) -> JSONResponse:
return _delete(slug, stories_dir)
@app.post("/rebuild")