212 lines
8.9 KiB
Python
212 lines
8.9 KiB
Python
|
|
"""
|
||
|
|
TMS Permission Management Routes
|
||
|
|
Handles submission creation, listing, deletion, retry, and queue display.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from fastapi import APIRouter, HTTPException, Request
|
||
|
|
from pydantic import BaseModel
|
||
|
|
from typing import List
|
||
|
|
import logging
|
||
|
|
|
||
|
|
from ..services import supabase_service
|
||
|
|
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
router = APIRouter(prefix="/api", tags=["TMS"])
|
||
|
|
|
||
|
|
|
||
|
|
# ==================== REQUEST MODELS ====================
|
||
|
|
|
||
|
|
class SubmissionCreate(BaseModel):
|
||
|
|
submission_id: str
|
||
|
|
usernames: List[str]
|
||
|
|
ge_input: str
|
||
|
|
|
||
|
|
|
||
|
|
# ==================== SUBMISSION ENDPOINTS ====================
|
||
|
|
|
||
|
|
@router.post("/submissions")
|
||
|
|
def create_submission(payload: SubmissionCreate):
|
||
|
|
try:
|
||
|
|
created = supabase_service.create_submission_supabase(
|
||
|
|
payload.submission_id,
|
||
|
|
payload.usernames,
|
||
|
|
payload.ge_input
|
||
|
|
)
|
||
|
|
return created
|
||
|
|
except Exception as e:
|
||
|
|
raise HTTPException(status_code=400, detail=str(e))
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/submissions")
|
||
|
|
def list_submissions(limit: int = 50):
|
||
|
|
submissions = supabase_service.get_submissions_supabase(limit=limit)
|
||
|
|
return {"success": True, "submissions": submissions}
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/submissions/{submission_id}")
|
||
|
|
def get_submission(submission_id: str):
|
||
|
|
sub_list = supabase_service.get_submissions_supabase(limit=100)
|
||
|
|
sub = next((item for item in sub_list if item.get("submission_id") == submission_id), None)
|
||
|
|
if not sub:
|
||
|
|
raise HTTPException(status_code=404, detail="Submission not found")
|
||
|
|
return {"success": True, "submission": sub}
|
||
|
|
|
||
|
|
|
||
|
|
@router.delete("/submissions/{submission_id}")
|
||
|
|
def delete_submission(submission_id: str):
|
||
|
|
ok = supabase_service.delete_submission_supabase(submission_id)
|
||
|
|
if not ok:
|
||
|
|
raise HTTPException(status_code=404, detail="Submission not found or could not be deleted")
|
||
|
|
return {"success": True}
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("/submissions/{submission_id}/retry")
|
||
|
|
def retry_submission(submission_id: str, payload: dict = None): # type: ignore
|
||
|
|
"""Retry a submission with only the error GE IDs and error usernames.
|
||
|
|
|
||
|
|
If payload.errorGeIds and payload.errorUsernames are provided, create a new submission
|
||
|
|
with only those GE IDs and usernames that had errors.
|
||
|
|
Otherwise, reset the original submission to pending (legacy behavior).
|
||
|
|
"""
|
||
|
|
if payload and payload.get('errorGeIds') and payload.get('errorUsernames'):
|
||
|
|
error_ge_ids = payload['errorGeIds']
|
||
|
|
error_usernames = payload['errorUsernames']
|
||
|
|
|
||
|
|
# Create new submission with only error GE IDs and error usernames
|
||
|
|
new_ge_id_and_lang = '\n'.join(error_ge_ids)
|
||
|
|
username_str = ','.join(error_usernames)
|
||
|
|
|
||
|
|
created = supabase_service.create_retry_submission(username_str, new_ge_id_and_lang)
|
||
|
|
if not created:
|
||
|
|
raise HTTPException(status_code=500, detail="Failed to create retry submission")
|
||
|
|
return {"success": True, "newSubmissionId": created.get("id")}
|
||
|
|
else:
|
||
|
|
# Legacy behavior: reset status to pending
|
||
|
|
ok = supabase_service.update_submission_supabase(submission_id, status="pending")
|
||
|
|
if not ok:
|
||
|
|
raise HTTPException(status_code=404, detail="Submission not found or could not be updated")
|
||
|
|
return {"success": True}
|
||
|
|
|
||
|
|
|
||
|
|
# ==================== DRIVER MANAGEMENT ====================
|
||
|
|
|
||
|
|
@router.post('/driver/close')
|
||
|
|
def close_driver(request: Request):
|
||
|
|
"""Close the global Selenium WebDriver if it's running.
|
||
|
|
|
||
|
|
Security policy:
|
||
|
|
- If environment variable DRIVER_ADMIN_TOKEN is set, require header X-Admin-Token matching it.
|
||
|
|
- If DRIVER_ADMIN_TOKEN is not set, only allow requests from localhost (127.0.0.1 or ::1).
|
||
|
|
"""
|
||
|
|
import os
|
||
|
|
try:
|
||
|
|
admin_token = os.environ.get('DRIVER_ADMIN_TOKEN')
|
||
|
|
header_token = request.headers.get('x-admin-token')
|
||
|
|
client_host = request.client.host if request.client else ''
|
||
|
|
|
||
|
|
if admin_token:
|
||
|
|
if not header_token or header_token != admin_token:
|
||
|
|
raise HTTPException(status_code=401, detail='Invalid or missing admin token')
|
||
|
|
else:
|
||
|
|
# allow only requests originating from localhost when no token configured
|
||
|
|
if client_host not in ('127.0.0.1', '::1', 'localhost'):
|
||
|
|
raise HTTPException(status_code=403, detail='Driver close is restricted to localhost')
|
||
|
|
|
||
|
|
# TMS permission automation is handled by TypeScript backend now
|
||
|
|
# This endpoint is kept for backward compatibility but does nothing
|
||
|
|
return {'success': True, 'message': 'Driver management moved to TypeScript backend'}
|
||
|
|
except HTTPException:
|
||
|
|
raise
|
||
|
|
except Exception as e:
|
||
|
|
raise HTTPException(status_code=500, detail=str(e))
|
||
|
|
|
||
|
|
|
||
|
|
# ==================== USERNAME MANAGEMENT ====================
|
||
|
|
|
||
|
|
@router.get('/usernames')
|
||
|
|
def get_usernames():
|
||
|
|
usernames = supabase_service.get_userslist()
|
||
|
|
return {"success": True, "usernames": usernames}
|
||
|
|
|
||
|
|
|
||
|
|
@router.get('/usernames/search')
|
||
|
|
def search_usernames(q: str = ""):
|
||
|
|
"""Search usernames by query string (case-insensitive, contains match)."""
|
||
|
|
all_usernames = supabase_service.get_userslist()
|
||
|
|
if not q:
|
||
|
|
return {"success": True, "suggestions": all_usernames[:20]}
|
||
|
|
|
||
|
|
q_lower = q.lower()
|
||
|
|
suggestions = [u for u in all_usernames if q_lower in u.lower()]
|
||
|
|
return {"success": True, "suggestions": suggestions[:20]}
|
||
|
|
|
||
|
|
|
||
|
|
@router.post('/usernames')
|
||
|
|
def add_username(payload: dict):
|
||
|
|
new_username = payload.get('username') if isinstance(payload, dict) else None
|
||
|
|
if not new_username:
|
||
|
|
raise HTTPException(status_code=400, detail='username is required')
|
||
|
|
return supabase_service.add_username(new_username)
|
||
|
|
|
||
|
|
|
||
|
|
@router.delete('/usernames')
|
||
|
|
def delete_username(payload: dict):
|
||
|
|
username = payload.get('username') if isinstance(payload, dict) else None
|
||
|
|
if not username:
|
||
|
|
raise HTTPException(status_code=400, detail='username is required')
|
||
|
|
return supabase_service.delete_username(username)
|
||
|
|
|
||
|
|
|
||
|
|
# ==================== QUEUE DISPLAY ====================
|
||
|
|
|
||
|
|
@router.get('/queue')
|
||
|
|
def get_queue(limit: int = 100, all: bool = False):
|
||
|
|
"""Return a flattened list of GE items built from pending submissions in Supabase.
|
||
|
|
Each pending submission's `input.ge_input` (newline separated) is split into GE ID and lang
|
||
|
|
and turned into an item consumable by the frontend `QueueStatus` component.
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
# By default include only pending and processing submissions so UI can show the single processing submission
|
||
|
|
# If caller passes all=true, include completed and failed as well (useful when a single endpoint should provide history)
|
||
|
|
all_subs = supabase_service.get_submissions_supabase(limit=1000) or []
|
||
|
|
allowed = ('pending', 'processing') if not all else ('pending', 'processing', 'completed', 'failed')
|
||
|
|
subs = [d for d in all_subs if str(d.get('status', '')).lower() in allowed]
|
||
|
|
items = []
|
||
|
|
for doc in subs:
|
||
|
|
submission_id = doc.get('submission_id')
|
||
|
|
usernames = doc.get('input', {}).get('usernames', []) if isinstance(doc.get('input'), dict) else []
|
||
|
|
usernames_str = '\n'.join(usernames) if isinstance(usernames, list) else (usernames or '')
|
||
|
|
ge_input = doc.get('input', {}).get('ge_input', '') if isinstance(doc.get('input'), dict) else ''
|
||
|
|
# split lines and create items
|
||
|
|
lines = [l.strip() for l in str(ge_input).splitlines() if l and l.strip()]
|
||
|
|
for idx, line in enumerate(lines):
|
||
|
|
parts = line.split() # expect e.g. "1000 de" or "696 us"
|
||
|
|
ge_id = parts[0] if len(parts) > 0 else line
|
||
|
|
lang = parts[1] if len(parts) > 1 else ''
|
||
|
|
key = f"{submission_id}:{idx}"
|
||
|
|
raw_status = str(doc.get('status', 'pending')).lower()
|
||
|
|
# map backend status to frontend status labels
|
||
|
|
if raw_status == 'pending':
|
||
|
|
mapped_status = 'waiting'
|
||
|
|
elif raw_status == 'processing':
|
||
|
|
mapped_status = 'processing'
|
||
|
|
elif raw_status == 'completed':
|
||
|
|
# when requesting all, represent completed as done
|
||
|
|
mapped_status = 'done'
|
||
|
|
elif raw_status == 'failed':
|
||
|
|
mapped_status = 'error'
|
||
|
|
else:
|
||
|
|
mapped_status = raw_status
|
||
|
|
items.append({
|
||
|
|
'key': key,
|
||
|
|
'id': str(ge_id),
|
||
|
|
'lang': str(lang),
|
||
|
|
'status': mapped_status,
|
||
|
|
'usernames': usernames_str,
|
||
|
|
'submission_id': submission_id
|
||
|
|
})
|
||
|
|
# respect limit on resulting GE items
|
||
|
|
return {'success': True, 'queue': items[:limit]}
|
||
|
|
except Exception as e:
|
||
|
|
raise HTTPException(status_code=500, detail=str(e))
|