forked from retoor/devplacepy
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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user