From 66b44e5c78a7b91597004c0ce6efbd9a4e0b9385 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Fri, 12 Jun 2026 00:35:52 +0200
Subject: [PATCH] dead endpoint cleanup + admin sessions UX: remove unused
endpoints (billing/balances, files/attributes, files/bulk, voice-google
duplicates, workflow-automation/runs, jobs list), fix billing transaction
filter for enriched columns, admin sessions use revokeTokenById instead of
delete, add per-device revoke endpoint, add sessions nav item
Co-authored-by: Cursor
---
modules/datamodels/datamodelNavigation.py | 10 +
modules/routes/routeAdminSessions.py | 52 +++--
modules/routes/routeBilling.py | 246 +++-------------------
modules/routes/routeDataFiles.py | 229 --------------------
modules/routes/routeJobs.py | 32 +--
modules/routes/routeVoiceGoogle.py | 68 +-----
modules/routes/routeWorkflowAutomation.py | 21 --
7 files changed, 81 insertions(+), 577 deletions(-)
diff --git a/modules/datamodels/datamodelNavigation.py b/modules/datamodels/datamodelNavigation.py
index 101cef99..977e801c 100644
--- a/modules/datamodels/datamodelNavigation.py
+++ b/modules/datamodels/datamodelNavigation.py
@@ -320,6 +320,16 @@ NAVIGATION_SECTIONS = [
"adminOnly": True,
"sysAdminOnly": True,
},
+ {
+ "id": "admin-sessions",
+ "objectKey": "ui.admin.sessions",
+ "label": t("Sessions & Geräte"),
+ "icon": "FaDesktop",
+ "path": "/admin/sessions",
+ "order": 92,
+ "adminOnly": True,
+ "sysAdminOnly": True,
+ },
{
"id": "admin-languages",
"objectKey": "ui.admin.languages",
diff --git a/modules/routes/routeAdminSessions.py b/modules/routes/routeAdminSessions.py
index d962a9ea..7503cd59 100644
--- a/modules/routes/routeAdminSessions.py
+++ b/modules/routes/routeAdminSessions.py
@@ -81,23 +81,28 @@ def revokeSession(
sessionId: str,
currentUser: User = Depends(getCurrentUser),
) -> Dict[str, Any]:
- """Revoke a single session by sessionId."""
+ """Revoke a single session by sessionId (sets status=REVOKED, not delete)."""
_requireAdmin(currentUser)
rootInterface = getRootInterface()
+ adminId = str(currentUser.id)
tokens = rootInterface.db.getRecordset(
Token,
- recordFilter={"sessionId": sessionId, "tokenPurpose": TokenPurpose.AUTH_SESSION.value},
+ recordFilter={
+ "sessionId": sessionId,
+ "tokenPurpose": TokenPurpose.AUTH_SESSION.value,
+ "status": TokenStatus.ACTIVE.value,
+ },
)
count = 0
for t in tokens:
- rootInterface.db.recordDelete(Token, t["id"])
+ rootInterface.revokeTokenById(t["id"], revokedBy=adminId, reason="admin session revoke")
count += 1
if count == 0:
raise HTTPException(status_code=404, detail=routeApiMsg("Session not found"))
- logger.info(f"Admin {currentUser.username} revoked session {sessionId} ({count} token(s))")
+ logger.info("Admin %s revoked session %s (%d token(s))", currentUser.username, sessionId, count)
return {"revoked": count, "sessionId": sessionId}
@@ -111,20 +116,13 @@ def revokeAllSessions(
"""Revoke ALL active sessions for a user (force logout everywhere)."""
_requireAdmin(currentUser)
rootInterface = getRootInterface()
+ adminId = str(currentUser.id)
- tokens = rootInterface.db.getRecordset(
- Token,
- recordFilter={
- "userId": userId,
- "tokenPurpose": TokenPurpose.AUTH_SESSION.value,
- },
+ count = rootInterface.revokeTokensByUser(
+ userId, revokedBy=adminId, reason="admin revoke all sessions",
)
- count = 0
- for t in tokens:
- rootInterface.db.recordDelete(Token, t["id"])
- count += 1
- logger.info(f"Admin {currentUser.username} revoked all sessions for userId={userId} ({count} token(s))")
+ logger.info("Admin %s revoked all sessions for userId=%s (%d token(s))", currentUser.username, userId, count)
return {"revoked": count, "userId": userId}
@@ -156,7 +154,7 @@ def listTrustedDevices(
result = []
for d in devices:
result.append({
- "id": d.get("id", "")[:8] + "...",
+ "id": d.get("id", ""),
"trustedUntil": d.get("trustedUntil"),
"isExpired": d.get("trustedUntil", 0) < now,
"userAgent": d.get("userAgent"),
@@ -167,6 +165,26 @@ def listTrustedDevices(
return result
+@trustedDeviceRouter.delete("/{deviceId}")
+@limiter.limit("30/minute")
+def revokeTrustedDevice(
+ request: Request,
+ deviceId: str,
+ currentUser: User = Depends(getCurrentUser),
+) -> Dict[str, Any]:
+ """Revoke a single trusted device by ID."""
+ _requireAdmin(currentUser)
+ rootInterface = getRootInterface()
+
+ existing = rootInterface.db.getRecord(TrustedDevice, deviceId)
+ if not existing:
+ raise HTTPException(status_code=404, detail=routeApiMsg("Trusted device not found"))
+
+ rootInterface.db.recordDelete(TrustedDevice, deviceId)
+ logger.info("Admin %s revoked trusted device %s", currentUser.username, deviceId)
+ return {"revoked": 1, "deviceId": deviceId}
+
+
@trustedDeviceRouter.delete("")
@limiter.limit("10/minute")
def revokeAllTrustedDevices(
@@ -181,5 +199,5 @@ def revokeAllTrustedDevices(
from modules.auth.trustedDeviceService import revokeTrustedDevices
count = revokeTrustedDevices(userId, rootInterface.db)
- logger.info(f"Admin {currentUser.username} revoked all trusted devices for userId={userId} ({count})")
+ logger.info("Admin %s revoked all trusted devices for userId=%s (%d)", currentUser.username, userId, count)
return {"revoked": count, "userId": userId}
diff --git a/modules/routes/routeBilling.py b/modules/routes/routeBilling.py
index 1d1441c4..84dc5c7b 100644
--- a/modules/routes/routeBilling.py
+++ b/modules/routes/routeBilling.py
@@ -290,19 +290,6 @@ class MandateBalanceResponse(BaseModel):
warningThresholdPercent: float
-class UserBalanceResponse(BaseModel):
- """User-level balance summary."""
- accountId: str
- mandateId: str
- mandateName: str
- userId: str
- userName: str
- balance: float
- warningThreshold: float
- isWarning: bool
- enabled: bool
-
-
class UserTransactionResponse(BaseModel):
"""User-level transaction with user context."""
id: str
@@ -429,10 +416,6 @@ def _normalize_billing_tx_dict(t: Dict[str, Any]) -> Dict[str, Any]:
return r
-def _load_billing_user_transactions_normalized(billingService) -> List[Dict[str, Any]]:
- raw = billingService.getTransactionHistory(limit=5000)
- return [_normalize_billing_tx_dict(t) for t in raw]
-
def _view_user_transactions_filtered_list(
billing_interface,
@@ -464,147 +447,6 @@ def _view_user_transactions_filtered_list(
return all_items
-@router.get("/transactions")
-@limiter.limit("30/minute")
-def getTransactions(
- request: Request,
- limit: int = Query(default=50, ge=1, le=500),
- offset: int = Query(default=0, ge=0),
- pagination: Optional[str] = Query(
- None,
- description="JSON PaginationParams for table UI (filters, sort, viewKey, groupByLevels).",
- ),
- mode: Optional[str] = Query(None, description="'filterValues' | 'ids' with pagination"),
- column: Optional[str] = Query(None, description="Column for mode=filterValues"),
- ctx: RequestContext = Depends(getRequestContext),
-):
- """
- Get transaction history across all mandates the user belongs to.
-
- Without ``pagination`` query: legacy behaviour — returns a JSON array of
- transactions (`limit`/`offset` window).
-
- With ``pagination`` JSON: returns ``{ items, pagination, groupLayout?, appliedView? }``.
- Table list views use contextKey ``billing/transactions``.
- """
- try:
- billingService = getBillingService(
- ctx.user,
- ctx.mandateId,
- featureCode="billing",
- )
-
- if pagination:
- from modules.interfaces.interfaceTableHelpers import (
- applyViewToParams,
- buildGroupLayout,
- effective_group_by_levels,
- resolveView,
- )
- from modules.dbHelpers.paginationHelpers import (
- handleFilterValuesInMemory,
- handleIdsInMemory,
- )
- from modules.interfaces.interfaceDbApp import getInterface as getAppInterface
- from modules.interfaces.interfaceDbManagement import ComponentObjects
-
- CONTEXT_KEY = "billing/transactions"
-
- try:
- paginationDict = json.loads(pagination)
- if not paginationDict:
- raise ValueError("empty pagination")
- paginationDict = normalize_pagination_dict(paginationDict)
- paginationParams = PaginationParams(**paginationDict)
- except (json.JSONDecodeError, ValueError, TypeError) as e:
- raise HTTPException(status_code=400, detail=f"Invalid pagination parameter: {str(e)}")
-
- appInterface = getAppInterface(ctx.user)
- viewKey = paginationParams.viewKey
- viewConfig, viewDisplayName = resolveView(appInterface, CONTEXT_KEY, viewKey)
- viewMeta = AppliedViewMeta(viewKey=viewKey, displayName=viewDisplayName) if viewKey else None
- paginationParams = applyViewToParams(paginationParams, viewConfig)
- groupByLevels = effective_group_by_levels(paginationParams, viewConfig)
-
- all_items = _load_billing_user_transactions_normalized(billingService)
-
- if mode == "filterValues":
- if not column:
- raise HTTPException(status_code=400, detail="column parameter required for mode=filterValues")
- return handleFilterValuesInMemory(all_items, column, pagination)
-
- if mode == "ids":
- return handleIdsInMemory(all_items, pagination)
-
- comp = ComponentObjects()
- comp.setUserContext(ctx.user)
- if paginationParams.filters:
- all_items = comp._applyFilters(all_items, paginationParams.filters)
- if paginationParams.sort:
- all_items = comp._applySorting(all_items, paginationParams.sort)
-
- totalItems = len(all_items)
- totalPages = math.ceil(totalItems / paginationParams.pageSize) if totalItems > 0 else 0
-
- if not groupByLevels:
- pstart = (paginationParams.page - 1) * paginationParams.pageSize
- page_items = all_items[pstart : pstart + paginationParams.pageSize]
- group_layout = None
- else:
- page_items, group_layout = buildGroupLayout(
- all_items,
- groupByLevels,
- paginationParams.page,
- paginationParams.pageSize,
- )
-
- resp: Dict[str, Any] = {
- "items": page_items,
- "pagination": PaginationMetadata(
- currentPage=paginationParams.page,
- pageSize=paginationParams.pageSize,
- totalItems=totalItems,
- totalPages=totalPages,
- sort=paginationParams.sort,
- filters=paginationParams.filters,
- ).model_dump(),
- }
- if group_layout:
- resp["groupLayout"] = group_layout.model_dump()
- if viewMeta:
- resp["appliedView"] = viewMeta.model_dump()
- return JSONResponse(content=resp)
-
- transactions = billingService.getTransactionHistory(limit=offset + limit)
- result: List[TransactionResponse] = []
- for t in transactions[offset : offset + limit]:
- result.append(
- TransactionResponse(
- id=t.get("id"),
- accountId=t.get("accountId"),
- transactionType=TransactionTypeEnum(t.get("transactionType", "DEBIT")),
- amount=t.get("amount", 0.0),
- description=t.get("description", ""),
- referenceType=ReferenceTypeEnum(t["referenceType"]) if t.get("referenceType") else None,
- workflowId=t.get("workflowId"),
- featureCode=t.get("featureCode"),
- featureInstanceId=t.get("featureInstanceId"),
- aicoreProvider=t.get("aicoreProvider"),
- aicoreModel=t.get("aicoreModel"),
- createdByUserId=t.get("createdByUserId"),
- sysCreatedAt=t.get("sysCreatedAt"),
- mandateId=t.get("mandateId"),
- mandateName=t.get("mandateName"),
- )
- )
- return result
-
- except HTTPException:
- raise
- except Exception as e:
- logger.error(f"Error getting billing transactions: {e}")
- raise HTTPException(status_code=500, detail=str(e))
-
@router.get("/statistics", response_model=UsageReportResponse)
@limiter.limit("30/minute")
@@ -1373,51 +1215,6 @@ def getMandateViewTransactions(
# User View Endpoints (RBAC-based)
# =============================================================================
-@router.get("/view/users/balances", response_model=List[UserBalanceResponse])
-@limiter.limit("30/minute")
-def getUserViewBalances(
- request: Request,
- ctx: RequestContext = Depends(getRequestContext)
-):
- """
- Get user-level balances.
-
- RBAC filtering:
- - SysAdmin: sees all user balances across all mandates
- - Mandate-Admin: sees user balances for mandates they administrate
- - Regular user: sees only their own balances
- """
- try:
- billingInterface = getBillingInterface(ctx.user, ctx.mandateId)
-
- # Evaluate RBAC scope
- scope = _getBillingDataScope(ctx.user)
-
- # Determine mandate IDs for data loading
- if scope.isGlobalAdmin:
- mandateIds = None
- else:
- mandateIds = scope.adminMandateIds + scope.memberMandateIds
- if not mandateIds:
- return []
-
- allBalances = billingInterface.getUserBalancesForMandates(mandateIds)
-
- # RBAC filter: mandate admins see all in their mandates, regular users only own
- if not scope.isGlobalAdmin:
- adminMandateSet = set(scope.adminMandateIds)
- allBalances = [
- b for b in allBalances
- if b.get("mandateId") in adminMandateSet or b.get("userId") == scope.userId
- ]
-
- return [UserBalanceResponse(**b) for b in allBalances]
-
- except Exception as e:
- logger.error(f"Error getting user view balances: {e}")
- raise HTTPException(status_code=500, detail=str(e))
-
-
class ViewStatisticsResponse(BaseModel):
"""Aggregated statistics across all user's mandates."""
totalCost: float = 0.0
@@ -1730,25 +1527,48 @@ def getUserViewTransactions(
resp["appliedView"] = viewMeta.model_dump(mode="json")
return JSONResponse(content=resp)
- result = billingInterface.getTransactionsForMandatesPaginated(
- mandateIds=loadMandateIds,
- pagination=paginationParams,
- scope=effectiveScope,
- userId=personalUserId,
+ _ENRICHED_FILTER_COLS = {"mandateName", "userName", "mandateId", "userId"}
+ _hasEnrichedFilters = paginationParams.filters and any(
+ k in _ENRICHED_FILTER_COLS for k in paginationParams.filters
)
+ if _hasEnrichedFilters:
+ all_items = _view_user_transactions_filtered_list(
+ billingInterface,
+ loadMandateIds,
+ effectiveScope,
+ personalUserId,
+ paginationParams,
+ ctx.user,
+ )
+ totalItems = len(all_items)
+ totalPages = math.ceil(totalItems / paginationParams.pageSize) if totalItems > 0 else 0
+ pstart = (paginationParams.page - 1) * paginationParams.pageSize
+ page_items = all_items[pstart : pstart + paginationParams.pageSize]
+ else:
+ result = billingInterface.getTransactionsForMandatesPaginated(
+ mandateIds=loadMandateIds,
+ pagination=paginationParams,
+ scope=effectiveScope,
+ userId=personalUserId,
+ )
+ page_items = result.items
+ totalItems = result.totalItems
+ totalPages = result.totalPages
+
logger.debug(
- f"SQL-paginated {result.totalItems} transactions for user {ctx.user.id} "
- f"(scope={scope}, mandateId={mandateId}, page={paginationParams.page})"
+ f"Paginated {totalItems} transactions for user {ctx.user.id} "
+ f"(scope={scope}, mandateId={mandateId}, page={paginationParams.page}, "
+ f"enrichedFilter={_hasEnrichedFilters})"
)
return PaginatedResponse(
- items=[_toResponse(d) for d in result.items],
+ items=[_toResponse(d) for d in page_items],
pagination=PaginationMetadata(
currentPage=paginationParams.page,
pageSize=paginationParams.pageSize,
- totalItems=result.totalItems,
- totalPages=result.totalPages,
+ totalItems=totalItems,
+ totalPages=totalPages,
sort=paginationParams.sort,
filters=paginationParams.filters,
),
diff --git a/modules/routes/routeDataFiles.py b/modules/routes/routeDataFiles.py
index 745027bc..2d20f5c2 100644
--- a/modules/routes/routeDataFiles.py
+++ b/modules/routes/routeDataFiles.py
@@ -311,72 +311,6 @@ def get_folder_tree(
raise HTTPException(status_code=500, detail=str(e))
-@router.post("/attributes")
-@limiter.limit("120/minute")
-def getAttributesForIds(
- request: Request,
- body: Dict[str, Any] = Body(...),
- currentUser: User = Depends(getCurrentUser),
- context: RequestContext = Depends(getRequestContext),
-):
- """Return current attribute values (neutralize, scope, ragIndexEnabled) for
- a list of node IDs. For folder IDs, computes 'mixed' by checking direct
- children. The frontend sends this after every toggle to refresh visible
- nodes without reloading the tree structure."""
- ids = body.get("ids", [])
- if not isinstance(ids, list) or len(ids) == 0:
- return {}
- if len(ids) > 500:
- raise HTTPException(status_code=400, detail="Max 500 IDs per request")
-
- try:
- managementInterface = interfaceDbManagement.getInterface(
- currentUser,
- mandateId=str(context.mandateId) if context.mandateId else None,
- featureInstanceId=str(context.featureInstanceId) if context.featureInstanceId else None,
- )
- db = managementInterface.db
- userId = str(currentUser.id)
-
- allFolders = db.getRecordset(FileFolder, recordFilter={"sysCreatedBy": userId}) or []
- allFiles = db.getRecordset(FileItem, recordFilter={"sysCreatedBy": userId}) or []
-
- folderById = {f["id"]: f for f in allFolders}
- fileById = {f["id"]: f for f in allFiles}
-
- logger.info(
- "getAttributesForIds: %d ids requested, %d folders found, %d files found",
- len(ids), len(allFolders), len(allFiles),
- )
-
- result: Dict[str, Dict[str, Any]] = {}
-
- for nodeId in ids:
- if nodeId.startswith("__filesRoot:"):
- attrs = _computeSyntheticRootAttrs(allFolders, allFiles)
- result[nodeId] = attrs
- elif nodeId in folderById:
- folder = folderById[nodeId]
- attrs = _computeFolderAttrs(folder, allFolders, allFiles)
- result[nodeId] = attrs
- elif nodeId in fileById:
- f = fileById[nodeId]
- result[nodeId] = {
- "neutralize": bool(f.get("neutralize", False)),
- "scope": f.get("scope", "personal"),
- }
- else:
- logger.debug("getAttributesForIds: unknown id=%s", nodeId)
-
- logger.info("getAttributesForIds: returning %d entries", len(result))
- return result
- except HTTPException:
- raise
- except Exception as e:
- logger.error(f"getAttributesForIds error: {e}")
- raise HTTPException(status_code=500, detail=str(e))
-
-
def _enrichFoldersWithMixed(
db, userId: str, folders: List[Dict[str, Any]], ownerMode: str,
) -> None:
@@ -480,43 +414,6 @@ def _effectiveScope(
return childVals.pop()
-def _computeSyntheticRootAttrs(
- allFolders: List[Dict[str, Any]],
- allFiles: List[Dict[str, Any]],
-) -> Dict[str, Any]:
- """Compute attributes for the synthetic root by recursively checking the
- entire tree. If ANY item at any depth diverges, root shows 'mixed'."""
- topFolders = [f for f in allFolders if not f.get("parentId")]
- topFiles = [f for f in allFiles if not f.get("folderId")]
-
- neutralizeVals = set()
- scopeVals = set()
- for cf in topFolders:
- nEff = _effectiveNeutralize(cf["id"], allFolders, allFiles)
- if nEff == "mixed":
- neutralizeVals.add(True)
- neutralizeVals.add(False)
- else:
- neutralizeVals.add(nEff)
- sEff = _effectiveScope(cf["id"], allFolders, allFiles)
- if sEff == "mixed":
- scopeVals.add("__mixed_a__")
- scopeVals.add("__mixed_b__")
- else:
- scopeVals.add(sEff)
- for cf in topFiles:
- neutralizeVals.add(bool(cf.get("neutralize", False)))
- scopeVals.add(cf.get("scope", "personal"))
-
- if not neutralizeVals and not scopeVals:
- return {"neutralize": False, "scope": "personal"}
-
- return {
- "neutralize": "mixed" if len(neutralizeVals) > 1 else (neutralizeVals.pop() if neutralizeVals else False),
- "scope": "mixed" if len(scopeVals) > 1 else (scopeVals.pop() if scopeVals else "personal"),
- }
-
-
@router.post("/folders", status_code=status.HTTP_201_CREATED)
@limiter.limit("30/minute")
def create_folder(
@@ -1133,132 +1030,6 @@ def batchDownload(
raise HTTPException(status_code=500, detail=str(e))
-# ── Bulk file operations (replace former group-based bulk routes) ─────────────
-
-@router.post("/bulk/scope")
-@limiter.limit("30/minute")
-def bulk_set_scope(
- request: Request,
- body: dict = Body(...),
- currentUser: User = Depends(getCurrentUser),
- context: RequestContext = Depends(getRequestContext),
-):
- """Set scope for a list of files by their IDs."""
- fileIds: list = body.get("fileIds") or []
- scope: str = body.get("scope") or ""
- if not fileIds:
- raise HTTPException(status_code=400, detail="fileIds is required")
- validScopes = {"personal", "featureInstance", "mandate", "global"}
- if scope not in validScopes:
- raise HTTPException(status_code=400, detail=f"Invalid scope. Must be one of {validScopes}")
- if scope == "global" and not context.isSysAdmin:
- raise HTTPException(status_code=403, detail="Only sysadmins can set global scope")
- try:
- managementInterface = interfaceDbManagement.getInterface(
- currentUser,
- mandateId=str(context.mandateId) if context.mandateId else None,
- featureInstanceId=str(context.featureInstanceId) if context.featureInstanceId else None,
- )
- updated = 0
- for fid in fileIds:
- try:
- managementInterface.updateFile(fid, {"scope": scope})
- updated += 1
- except Exception as e:
- logger.error(f"bulk_set_scope: failed for file {fid}: {e}")
- return {"scope": scope, "filesUpdated": updated}
- except HTTPException:
- raise
- except Exception as e:
- logger.error(f"bulk_set_scope error: {e}")
- raise HTTPException(status_code=500, detail=str(e))
-
-
-@router.post("/bulk/neutralize")
-@limiter.limit("30/minute")
-def bulk_set_neutralize(
- request: Request,
- body: dict = Body(...),
- currentUser: User = Depends(getCurrentUser),
- context: RequestContext = Depends(getRequestContext),
-):
- """Set neutralize flag for a list of files by their IDs (incl. knowledge purge/reindex)."""
- fileIds: list = body.get("fileIds") or []
- neutralize = body.get("neutralize")
- if not fileIds:
- raise HTTPException(status_code=400, detail="fileIds is required")
- if neutralize is None:
- raise HTTPException(status_code=400, detail="neutralize is required")
- try:
- managementInterface = interfaceDbManagement.getInterface(
- currentUser,
- mandateId=str(context.mandateId) if context.mandateId else None,
- featureInstanceId=str(context.featureInstanceId) if context.featureInstanceId else None,
- )
- updated = 0
- for fid in fileIds:
- try:
- managementInterface.updateFile(fid, {"neutralize": neutralize})
- if not neutralize:
- try:
- kIface = interfaceDbKnowledge.getInterface(currentUser)
- kIface.purgeFileKnowledge(fid)
- except Exception as ke:
- logger.warning(f"bulk_set_neutralize: knowledge purge failed for {fid}: {ke}")
- updated += 1
- except Exception as e:
- logger.error(f"bulk_set_neutralize: failed for file {fid}: {e}")
- return {"neutralize": neutralize, "filesUpdated": updated}
- except HTTPException:
- raise
- except Exception as e:
- logger.error(f"bulk_set_neutralize error: {e}")
- raise HTTPException(status_code=500, detail=str(e))
-
-
-@router.post("/bulk/download-zip")
-@limiter.limit("10/minute")
-async def bulk_download_zip(
- request: Request,
- body: dict = Body(...),
- currentUser: User = Depends(getCurrentUser),
- context: RequestContext = Depends(getRequestContext),
-):
- """Download a list of files as a ZIP archive."""
- fileIds: list = body.get("fileIds") or []
- if not fileIds:
- raise HTTPException(status_code=400, detail="fileIds is required")
- try:
- managementInterface = interfaceDbManagement.getInterface(
- currentUser,
- mandateId=str(context.mandateId) if context.mandateId else None,
- featureInstanceId=str(context.featureInstanceId) if context.featureInstanceId else None,
- )
- buf = io.BytesIO()
- with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
- for fid in fileIds:
- try:
- fileMeta = managementInterface.getFile(fid)
- fileData = managementInterface.getFileData(fid)
- if fileMeta and fileData:
- name = (getattr(fileMeta, "fileName", None) or fid)
- zf.writestr(name, fileData)
- except Exception as fe:
- logger.warning(f"bulk_download_zip: skipping file {fid}: {fe}")
- buf.seek(0)
- from fastapi.responses import StreamingResponse
- return StreamingResponse(
- buf,
- media_type="application/zip",
- headers={"Content-Disposition": 'attachment; filename="files.zip"'},
- )
- except HTTPException:
- raise
- except Exception as e:
- logger.error(f"bulk_download_zip error: {e}")
- raise HTTPException(status_code=500, detail=str(e))
-
-
# ── Scope & neutralize tagging endpoints (before /{fileId} catch-all) ─────────
@router.patch("/{fileId}/scope")
diff --git a/modules/routes/routeJobs.py b/modules/routes/routeJobs.py
index c98c7bd0..7ef8c52b 100644
--- a/modules/routes/routeJobs.py
+++ b/modules/routes/routeJobs.py
@@ -4,7 +4,6 @@
Endpoints:
- GET /api/jobs/{jobId} -> single job status
-- GET /api/jobs -> list (filter by jobType, instanceId)
Access control: a caller may read a job iff they are a member of its mandate
(or PlatformAdmin). Jobs without a mandateId (system-wide) are restricted to
@@ -12,14 +11,13 @@ PlatformAdmin only.
"""
import logging
-from typing import Any, Dict, List, Optional
+from typing import Any, Dict, Optional
-from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request
+from fastapi import APIRouter, Depends, HTTPException, Path, Request
from modules.auth import getRequestContext, RequestContext, limiter
from modules.serviceCenter.services.serviceBackgroundJobs import (
getJobStatus,
- listJobs,
)
from modules.shared.i18nRegistry import apiRouteContext, resolveJobMessage
@@ -91,29 +89,3 @@ def get_job(
return _serialiseJob(job)
-@router.get("")
-@limiter.limit("30/minute")
-def list_jobs(
- request: Request,
- jobType: Optional[str] = Query(None),
- mandateId: Optional[str] = Query(None),
- instanceId: Optional[str] = Query(None, description="Feature instance scope"),
- limit: int = Query(20, ge=1, le=100),
- context: RequestContext = Depends(getRequestContext),
-) -> Dict[str, List[Dict[str, Any]]]:
- """List recent jobs filtered by scope. Newest first."""
- if mandateId is None:
- if not context or not context.isPlatformAdmin:
- raise HTTPException(
- status_code=400,
- detail=routeApiMsg("mandateId is required (only PlatformAdmin may list system-wide)"),
- )
- elif not _userHasMandateAccess(context, mandateId):
- raise HTTPException(status_code=403, detail=routeApiMsg("Access denied"))
- jobs = listJobs(
- mandateId=mandateId,
- featureInstanceId=instanceId,
- jobType=jobType,
- limit=limit,
- )
- return {"items": [_serialiseJob(j) for j in jobs]}
diff --git a/modules/routes/routeVoiceGoogle.py b/modules/routes/routeVoiceGoogle.py
index ada38504..addf7e47 100644
--- a/modules/routes/routeVoiceGoogle.py
+++ b/modules/routes/routeVoiceGoogle.py
@@ -14,10 +14,8 @@ import secrets
import time
from fastapi import APIRouter, Depends, HTTPException, Query, Request, WebSocket, WebSocketDisconnect
from typing import Optional, Dict, Any, List
-from modules.auth import getCurrentUser, getRequestContext, RequestContext, limiter
-from modules.datamodels.datamodelUam import User
+from modules.auth import getRequestContext, RequestContext, limiter
from modules.interfaces.interfaceVoiceObjects import getVoiceInterface, VoiceObjects
-from modules.shared.voiceCatalog import getCatalogPayload
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/voice-google", tags=["Voice Google"])
@@ -49,70 +47,6 @@ class ConnectionManager:
manager = ConnectionManager()
-def _getVoiceInterface(currentUser: User) -> VoiceObjects:
- """Get voice interface instance with user context."""
- try:
- return getVoiceInterface(currentUser)
- except Exception as e:
- logger.error(f"Failed to initialize voice interface: {e}")
- raise HTTPException(
- status_code=500,
- detail=f"Failed to initialize voice interface: {str(e)}"
- )
-
-@router.get("/languages")
-async def get_available_languages(currentUser: User = Depends(getCurrentUser)):
- """Return the curated voice/language catalog (single source of truth).
-
- Each entry: {bcp47, iso, label, flag, defaultVoice}. Same payload as
- /api/voice/languages — both endpoints back the same catalog.
- """
- return {
- "success": True,
- "languages": getCatalogPayload(),
- }
-
-@router.get("/voices")
-async def get_available_voices(
- languageCode: Optional[str] = None,
- language_code: Optional[str] = None, # Accept both camelCase and snake_case
- currentUser: User = Depends(getCurrentUser)
-):
- """
- Get available voices from Google Cloud Text-to-Speech.
- Accepts languageCode (camelCase) or language_code (snake_case) query parameter.
- """
- # Use language_code if provided (frontend sends this), otherwise use languageCode
- if language_code:
- languageCode = language_code
-
- try:
- logger.info(f"🎤 Getting available voices, language filter: {languageCode}")
-
- voiceInterface = _getVoiceInterface(currentUser)
- result = await voiceInterface.getAvailableVoices(languageCode=languageCode)
-
- if result["success"]:
- return {
- "success": True,
- "voices": result["voices"],
- "language_filter": languageCode
- }
- else:
- raise HTTPException(
- status_code=400,
- detail=f"Failed to get voices: {result.get('error', 'Unknown error')}"
- )
-
- except HTTPException:
- raise
- except Exception as e:
- logger.error(f"❌ Get voices error: {e}")
- raise HTTPException(
- status_code=500,
- detail=f"Failed to get available voices: {str(e)}"
- )
-
# =========================================================================
# STT Streaming WebSocket — generic, used by all features
# =========================================================================
diff --git a/modules/routes/routeWorkflowAutomation.py b/modules/routes/routeWorkflowAutomation.py
index 619928f9..09dc311d 100644
--- a/modules/routes/routeWorkflowAutomation.py
+++ b/modules/routes/routeWorkflowAutomation.py
@@ -207,27 +207,6 @@ async def _listRuns(
db.close()
-@router.get("/runs/{runId}")
-async def _getRun(
- runId: str,
- request: RequestContext = Depends(getRequestContext),
-):
- db = _getWorkflowAutomationDb()
- try:
- db._ensureTableExists(AutoRun)
- run = db.getRecord(AutoRun, runId)
- if not run:
- raise HTTPException(status_code=404, detail="Run not found")
-
- wfId = run.get("workflowId")
- if wfId:
- wf = db.getRecord(AutoWorkflow, wfId)
- _validateWorkflowAccess(request, wf, "read")
- return run
- finally:
- db.close()
-
-
# ---------------------------------------------------------------------------
# Tasks
# ---------------------------------------------------------------------------