improve docs
This commit is contained in:
+96
-28
@@ -49,6 +49,73 @@ from bincio.serve.db import (
|
||||
use_invite,
|
||||
)
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
# ── Pydantic request/response models ─────────────────────────────────────────
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
handle: str = Field(..., description="User handle (username)")
|
||||
password: str = Field(..., description="User password")
|
||||
|
||||
|
||||
class LoginResponse(BaseModel):
|
||||
ok: bool = Field(True, description="Success flag")
|
||||
handle: str = Field(..., description="User handle")
|
||||
display_name: str = Field(..., description="User's display name")
|
||||
|
||||
|
||||
class ResetPasswordRequest(BaseModel):
|
||||
handle: str = Field(..., description="User handle")
|
||||
code: str = Field(..., description="Reset code (24 hours valid)")
|
||||
password: str = Field(..., description="New password (min 8 chars)")
|
||||
|
||||
|
||||
class RegisterRequest(BaseModel):
|
||||
code: str = Field(..., description="Invite code")
|
||||
handle: str = Field(..., description="Desired username (lowercase, 1-30 chars)")
|
||||
password: str = Field(..., description="Password (min 8 characters)")
|
||||
display_name: str = Field(default="", description="Full name (optional, defaults to handle)")
|
||||
|
||||
|
||||
class RegisterResponse(BaseModel):
|
||||
ok: bool = Field(True, description="Success flag")
|
||||
handle: str = Field(..., description="New user's handle")
|
||||
|
||||
|
||||
class CurrentUserResponse(BaseModel):
|
||||
handle: str = Field(..., description="User handle")
|
||||
display_name: str = Field(..., description="User's display name")
|
||||
is_admin: bool = Field(..., description="Whether user is an admin")
|
||||
store_originals_default: bool = Field(
|
||||
default=True,
|
||||
description="Instance-wide default for storing original files"
|
||||
)
|
||||
|
||||
|
||||
class ActivityEditRequest(BaseModel):
|
||||
title: str | None = Field(default=None, description="Activity title")
|
||||
description: str | None = Field(default=None, description="Activity description (markdown)")
|
||||
sport: str | None = Field(default=None, description="Sport type")
|
||||
private: bool | None = Field(default=None, description="Hide from public feed")
|
||||
highlight: bool | None = Field(default=None, description="Mark as favorite")
|
||||
gear: str | None = Field(default=None, description="Gear used (e.g., 'Trek Domane')")
|
||||
|
||||
|
||||
class ActivityEditResponse(BaseModel):
|
||||
ok: bool = Field(True, description="Success flag")
|
||||
|
||||
|
||||
class ResetPasswordCodeResponse(BaseModel):
|
||||
ok: bool = Field(True, description="Success flag")
|
||||
code: str = Field(..., description="One-time reset code")
|
||||
expires_in_hours: int = Field(24, description="Code validity period in hours")
|
||||
|
||||
|
||||
class GenericResponse(BaseModel):
|
||||
ok: bool = Field(True, description="Success flag")
|
||||
|
||||
|
||||
# ── Active job tracker ───────────────────────────────────────────────────────
|
||||
# Tracks in-progress upload/processing jobs so admins can see what's running.
|
||||
# Jobs are added when a streaming upload starts and removed when it finishes.
|
||||
@@ -132,7 +199,7 @@ def _get_data_dir() -> Path:
|
||||
|
||||
# ── App ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
app = FastAPI(title="BincioActivity Serve", docs_url=None, redoc_url=None)
|
||||
app = FastAPI(title="BincioActivity Serve")
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
@@ -326,7 +393,7 @@ def _trigger_rebuild(handle: str) -> None:
|
||||
|
||||
# ── Auth endpoints ────────────────────────────────────────────────────────────
|
||||
|
||||
@app.get("/api/me")
|
||||
@app.get("/api/me", response_model=CurrentUserResponse)
|
||||
async def me(bincio_session: Optional[str] = Cookie(default=None)) -> JSONResponse:
|
||||
user = _current_user(bincio_session)
|
||||
if not user:
|
||||
@@ -361,14 +428,16 @@ async def stats() -> JSONResponse:
|
||||
})
|
||||
|
||||
|
||||
@app.post("/api/auth/login")
|
||||
async def login(request: Request) -> JSONResponse:
|
||||
@app.post("/api/auth/login", response_model=LoginResponse)
|
||||
async def login(
|
||||
login_req: LoginRequest,
|
||||
request: Request,
|
||||
) -> JSONResponse:
|
||||
ip = request.client.host if request.client else "unknown"
|
||||
_check_rate_limit(ip, _login_attempts, _LOGIN_RATE_LIMIT, "Too many login attempts. Try again later.")
|
||||
|
||||
body = await request.json()
|
||||
handle = body.get("handle", "").strip().lower()
|
||||
password = body.get("password", "")
|
||||
handle = login_req.handle.strip().lower()
|
||||
password = login_req.password
|
||||
|
||||
user = authenticate(_get_db(), handle, password)
|
||||
if not user:
|
||||
@@ -380,7 +449,7 @@ async def login(request: Request) -> JSONResponse:
|
||||
return resp
|
||||
|
||||
|
||||
@app.post("/api/auth/logout")
|
||||
@app.post("/api/auth/logout", response_model=GenericResponse)
|
||||
async def logout(bincio_session: Optional[str] = Cookie(default=None)) -> JSONResponse:
|
||||
if bincio_session:
|
||||
delete_session(_get_db(), bincio_session)
|
||||
@@ -389,16 +458,13 @@ async def logout(bincio_session: Optional[str] = Cookie(default=None)) -> JSONRe
|
||||
return resp
|
||||
|
||||
|
||||
@app.post("/api/auth/reset-password")
|
||||
async def reset_password(request: Request) -> JSONResponse:
|
||||
@app.post("/api/auth/reset-password", response_model=GenericResponse)
|
||||
async def reset_password(reset_req: ResetPasswordRequest) -> JSONResponse:
|
||||
"""Validate a reset code and set a new password. Public endpoint."""
|
||||
from bincio.serve.db import use_reset_code, change_password
|
||||
body = await request.json()
|
||||
handle = (body.get("handle") or "").strip().lower()
|
||||
code = (body.get("code") or "").strip().upper()
|
||||
new_pw = body.get("password") or ""
|
||||
if not handle or not code or not new_pw:
|
||||
raise HTTPException(400, "handle, code, and password are required")
|
||||
handle = reset_req.handle.strip().lower()
|
||||
code = reset_req.code.strip().upper()
|
||||
new_pw = reset_req.password
|
||||
if len(new_pw) < 8:
|
||||
raise HTTPException(400, "Password must be at least 8 characters")
|
||||
db = _get_db()
|
||||
@@ -410,16 +476,18 @@ async def reset_password(request: Request) -> JSONResponse:
|
||||
|
||||
# ── Registration ──────────────────────────────────────────────────────────────
|
||||
|
||||
@app.post("/api/register")
|
||||
async def register(request: Request) -> JSONResponse:
|
||||
@app.post("/api/register", response_model=RegisterResponse)
|
||||
async def register(
|
||||
register_req: RegisterRequest,
|
||||
request: Request,
|
||||
) -> JSONResponse:
|
||||
ip = request.client.host if request.client else "unknown"
|
||||
_check_rate_limit(ip, _register_attempts, _REGISTER_RATE_LIMIT, "Too many registration attempts. Try again later.")
|
||||
|
||||
body = await request.json()
|
||||
code = body.get("code", "").strip().upper()
|
||||
handle = body.get("handle", "").strip().lower()
|
||||
password = body.get("password", "")
|
||||
display = body.get("display_name", "").strip() or handle
|
||||
code = register_req.code.strip().upper()
|
||||
handle = register_req.handle.strip().lower()
|
||||
password = register_req.password
|
||||
display = register_req.display_name.strip() or handle
|
||||
|
||||
if not _VALID_HANDLE.match(handle):
|
||||
raise HTTPException(400, "Invalid handle (lowercase letters, numbers, _ - only; max 30 chars)")
|
||||
@@ -568,7 +636,7 @@ async def admin_disk(bincio_session: Optional[str] = Cookie(default=None)) -> JS
|
||||
})
|
||||
|
||||
|
||||
@app.post("/api/admin/users/{handle}/reset-password-code")
|
||||
@app.post("/api/admin/users/{handle}/reset-password-code", response_model=ResetPasswordCodeResponse)
|
||||
async def admin_reset_password_code(
|
||||
handle: str,
|
||||
bincio_session: Optional[str] = Cookie(default=None),
|
||||
@@ -1171,10 +1239,10 @@ async def get_activity(
|
||||
return JSONResponse(json.loads(path.read_text()))
|
||||
|
||||
|
||||
@app.post("/api/activity/{activity_id}")
|
||||
@app.post("/api/activity/{activity_id}", response_model=ActivityEditResponse)
|
||||
async def post_activity(
|
||||
activity_id: str,
|
||||
request: Request,
|
||||
edit_req: ActivityEditRequest,
|
||||
bincio_session: Optional[str] = Cookie(default=None),
|
||||
) -> JSONResponse:
|
||||
user = _require_user(bincio_session)
|
||||
@@ -1185,13 +1253,13 @@ async def post_activity(
|
||||
raise HTTPException(404, "Activity not found")
|
||||
|
||||
from bincio.edit.ops import apply_sidecar_edit
|
||||
body = await request.json()
|
||||
body = edit_req.model_dump(exclude_none=True)
|
||||
# apply_sidecar_edit already calls merge_one internally — no full rebuild needed.
|
||||
apply_sidecar_edit(activity_id, body, dd)
|
||||
return JSONResponse({"ok": True})
|
||||
|
||||
|
||||
@app.delete("/api/activity/{activity_id}")
|
||||
@app.delete("/api/activity/{activity_id}", response_model=GenericResponse)
|
||||
async def delete_activity(
|
||||
activity_id: str,
|
||||
bincio_session: Optional[str] = Cookie(default=None),
|
||||
|
||||
Reference in New Issue
Block a user