feat: add wildcard token support for allowed extensions and access token CLI commands

Introduce WILDCARD_TOKENS set in attachments.py to treat "*", ".*", "*.*" as wildcards that fall back to ALLOWED_UPLOAD_TYPES. Add cmd_token_issue and cmd_token_list CLI commands for issuing and listing DevPlace access tokens. Register access_tokens table in SOFT_DELETE_TABLES and initialize its columns and indexes in init_db. Replace Form() with Depends(json_or_form(...)) in admin backup, container, notification, settings, and user routers to support both JSON and form data.
This commit is contained in:
2026-06-17 17:10:52 +00:00
parent 217210e02f
commit 9598a1b867
45 changed files with 436 additions and 284 deletions
+3 -24
View File
@@ -4,7 +4,7 @@ import json
import logging
from typing import Annotated
from fastapi import APIRouter, Form, Request, WebSocket, WebSocketDisconnect
from fastapi import Depends, APIRouter, Request, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse, JSONResponse, Response
from devplacepy import database
@@ -21,6 +21,7 @@ from devplacepy.services.jobs.deepsearch.service import DeepsearchService
from devplacepy.services.manager import service_manager
from devplacepy.templating import templates
from devplacepy.utils import generate_uid, get_current_user, is_admin, not_found, track_action
from devplacepy.dependencies import json_or_form
from ._shared import owner_for
@@ -33,21 +34,17 @@ CONTROL_STATES = ("running", "paused", "cancelled")
TOUCH_EXTEND_SECONDS = 3600
_service = DeepsearchService()
def _collection_for(uid: str) -> str:
return _service.collection_name(uid)
def _control_path(uid: str):
return DEEPSEARCH_DIR / uid / "control.json"
def _set_control(uid: str, state: str) -> None:
path = _control_path(uid)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps({"state": state}), encoding="utf-8")
def _owns(request: Request, session: dict) -> bool:
owner_kind, owner_id = owner_for(request)
return (
@@ -55,7 +52,6 @@ def _owns(request: Request, session: dict) -> bool:
and session.get("owner_id") == owner_id
)
def _job_payload(job: dict) -> dict:
result = job.get("result", {})
payload = job.get("payload", {})
@@ -81,7 +77,6 @@ def _job_payload(job: dict) -> dict:
"completed_at": job.get("completed_at") or None,
}
@router.get("", response_class=HTMLResponse)
async def deepsearch_page(request: Request):
user = get_current_user(request)
@@ -111,9 +106,8 @@ async def deepsearch_page(request: Request):
{**seo_ctx, "request": request, "user": user},
)
@router.post("/run")
async def deepsearch_run(request: Request, data: Annotated[DeepsearchRunForm, Form()]):
async def deepsearch_run(request: Request, data: Annotated[DeepsearchRunForm, Depends(json_or_form(DeepsearchRunForm))]):
owner_kind, owner_id = owner_for(request)
from devplacepy.services.audit import record as audit
@@ -174,7 +168,6 @@ async def deepsearch_run(request: Request, data: Annotated[DeepsearchRunForm, Fo
}
)
def _enqueue(uid: str, payload: dict, owner_kind: str, owner_id: str, query: str) -> None:
from datetime import datetime, timezone
@@ -204,7 +197,6 @@ def _enqueue(uid: str, payload: dict, owner_kind: str, owner_id: str, query: str
}
)
@router.get("/{uid}")
async def deepsearch_status(request: Request, uid: str):
job = queue.get_job(uid)
@@ -214,13 +206,11 @@ async def deepsearch_status(request: Request, uid: str):
DeepsearchJobOut.model_validate(_job_payload(job)).model_dump(mode="json")
)
def _report_for(job: dict) -> dict:
if job.get("status") != queue.DONE:
return {}
return job.get("result", {}).get("report", {})
def _session_context(request: Request, uid: str, job: dict, session: dict) -> dict:
report = _report_for(job)
user = get_current_user(request)
@@ -255,7 +245,6 @@ def _session_context(request: Request, uid: str, job: dict, session: dict) -> di
"meta_robots": "noindex,nofollow",
}
@router.get("/{uid}/session")
async def deepsearch_session(request: Request, uid: str):
job = queue.get_job(uid)
@@ -266,22 +255,18 @@ async def deepsearch_session(request: Request, uid: str):
context = _session_context(request, uid, job, session)
return respond(request, "tools/deepsearch_session.html", context, model=DeepsearchSessionOut)
@router.post("/{uid}/pause")
async def deepsearch_pause(request: Request, uid: str):
return _control(request, uid, "paused")
@router.post("/{uid}/resume")
async def deepsearch_resume(request: Request, uid: str):
return _control(request, uid, "running")
@router.post("/{uid}/cancel")
async def deepsearch_cancel(request: Request, uid: str):
return _control(request, uid, "cancelled")
def _control(request: Request, uid: str, state: str):
session = database.get_deepsearch_session(uid)
job = queue.get_job(uid)
@@ -295,7 +280,6 @@ def _control(request: Request, uid: str, state: str):
_set_control(uid, state)
return JSONResponse({"ok": True, "state": state})
def _export_report(uid: str) -> dict | None:
job = queue.get_job(uid)
if not job or job.get("kind") != "deepsearch" or job.get("status") != queue.DONE:
@@ -303,7 +287,6 @@ def _export_report(uid: str) -> dict | None:
queue.touch_job(uid, TOUCH_EXTEND_SECONDS)
return job.get("result", {}).get("report", {})
@router.get("/{uid}/export.md")
async def deepsearch_export_md(request: Request, uid: str):
report = _export_report(uid)
@@ -311,7 +294,6 @@ async def deepsearch_export_md(request: Request, uid: str):
raise not_found("Report not ready")
return Response(content=to_markdown(report), media_type="text/markdown")
@router.get("/{uid}/export.json")
async def deepsearch_export_json(request: Request, uid: str):
report = _export_report(uid)
@@ -319,7 +301,6 @@ async def deepsearch_export_json(request: Request, uid: str):
raise not_found("Report not ready")
return Response(content=to_json(report), media_type="application/json")
@router.get("/{uid}/export.pdf")
async def deepsearch_export_pdf(request: Request, uid: str):
report = _export_report(uid)
@@ -327,7 +308,6 @@ async def deepsearch_export_pdf(request: Request, uid: str):
raise not_found("Report not ready")
return Response(content=to_pdf(report), media_type="application/pdf")
@router.websocket("/{uid}/ws")
async def deepsearch_ws(websocket: WebSocket, uid: str):
await websocket.accept()
@@ -366,7 +346,6 @@ async def deepsearch_ws(websocket: WebSocket, uid: str):
finally:
hub.unsubscribe(uid, listener)
@router.websocket("/{uid}/chat")
async def deepsearch_chat_ws(websocket: WebSocket, uid: str):
await websocket.accept()