Add stories/ dir, /stories API endpoints, dev.sh blog symlink
This commit is contained in:
+56
-32
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user