diff --git a/edit/server.py b/edit/server.py index a35ea02..61a7c3e 100644 --- a/edit/server.py +++ b/edit/server.py @@ -41,9 +41,10 @@ _SESSION_DOMAIN = os.environ.get("SESSION_DOMAIN") or None _SESSION_TTL = 30 * 24 * 3600 # 30 days (matches bincio_activity) _SESSION_COOKIE = "bincio_session" -_SAFE_SLUG = re.compile(r"^[a-zA-Z0-9_][a-zA-Z0-9_\-/]*$") -_SAFE_HANDLE = re.compile(r"^[a-z][a-z0-9_-]{1,19}$") -_SAFE_HASH = re.compile(r"^[0-9a-f]{4,40}$") +_SAFE_SLUG = re.compile(r"^[a-zA-Z0-9_][a-zA-Z0-9_\-/]*$") +_SAFE_HANDLE = re.compile(r"^[a-z][a-z0-9_-]{1,19}$") +_SAFE_HASH = re.compile(r"^[0-9a-f]{4,40}$") +_SAFE_REL_PATH = re.compile(r"^(pages|blog)/[a-zA-Z0-9_][a-zA-Z0-9_\-/]*\.md$") _ALLOWED_IMAGE_TYPES = {"image/jpeg", "image/png", "image/webp", "image/gif"} _MAX_IMAGE_BYTES = 10 * 1024 * 1024 # 10 MB @@ -288,12 +289,15 @@ async def get_wiki_log(user: User = Depends(require_auth)) -> JSONResponse: @app.get("/api/diff/{commit_hash}") -async def get_diff(commit_hash: str, user: User = Depends(require_auth)) -> JSONResponse: +async def get_diff(commit_hash: str, file: Optional[str] = None, user: User = Depends(require_auth)) -> JSONResponse: if not _SAFE_HASH.match(commit_hash): raise HTTPException(status_code=400, detail="invalid hash") + if file is not None and not _SAFE_REL_PATH.match(file): + raise HTTPException(status_code=400, detail="invalid file path") + paths = [file] if file else ["pages/", "blog/"] env = _git_env() proc = await asyncio.create_subprocess_exec( - "git", "show", commit_hash, "-p", "--no-color", "--format=", "--", "pages/", "blog/", + "git", "show", commit_hash, "-p", "--no-color", "--format=", "--", *paths, cwd=str(_ROOT), env=env, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) @@ -303,6 +307,26 @@ async def get_diff(commit_hash: str, user: User = Depends(require_auth)) -> JSON return JSONResponse({"diff": stdout.decode(errors="replace")}) +@app.get("/api/history/{slug:path}") +async def get_history(slug: str, user: User = Depends(require_auth)) -> JSONResponse: + if not _SAFE_SLUG.match(slug): + raise HTTPException(status_code=400, detail="invalid slug") + env = _git_env() + proc = await asyncio.create_subprocess_exec( + "git", "log", "--format=%h|%ar|%aN|%s", "--author=@bincio.wiki", "-n", "50", + "--", f"pages/{slug}.md", f"blog/{slug}.md", + cwd=str(_ROOT), env=env, + stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL, + ) + stdout, _ = await proc.communicate() + entries = [] + for line in stdout.decode().strip().splitlines(): + parts = line.split("|", 3) + if len(parts) == 4: + entries.append({"hash": parts[0], "date": parts[1], "author": parts[2], "message": parts[3]}) + return JSONResponse({"log": entries}) + + @app.get("/api/me") async def me(user: User = Depends(require_auth)) -> JSONResponse: return JSONResponse({ diff --git a/site b/site index 64d16db..e6b9ba5 160000 --- a/site +++ b/site @@ -1 +1 @@ -Subproject commit 64d16dbbdf19dd16c210c606adfe0fb878a988dd +Subproject commit e6b9ba56b101592519de3dfe030d0ac92457f877