538 lines
23 KiB
Python
538 lines
23 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import io
|
|
import json
|
|
import re
|
|
import uuid
|
|
import zipfile
|
|
from datetime import datetime
|
|
from typing import Any, Literal
|
|
|
|
from fastapi import APIRouter, Header, HTTPException
|
|
from fastapi.concurrency import run_in_threadpool
|
|
from fastapi.responses import Response
|
|
from pydantic import BaseModel, Field
|
|
|
|
from services.auth_service import auth_service
|
|
|
|
from api.support import (
|
|
require_admin,
|
|
sanitize_cpa_pool,
|
|
sanitize_cpa_pools,
|
|
sanitize_sub2api_server,
|
|
sanitize_sub2api_servers,
|
|
)
|
|
from services.account_service import account_service
|
|
from services.cpa_service import cpa_config, cpa_import_service, list_remote_files
|
|
from services.oauth_login_service import OAuthLoginError, oauth_login_service
|
|
from services.sub2api_service import (
|
|
list_remote_accounts as sub2api_list_remote_accounts,
|
|
list_remote_groups as sub2api_list_remote_groups,
|
|
sub2api_config,
|
|
sub2api_import_service,
|
|
)
|
|
|
|
|
|
|
|
class UserKeyCreateRequest(BaseModel):
|
|
name: str = ""
|
|
|
|
|
|
class UserKeyUpdateRequest(BaseModel):
|
|
name: str | None = None
|
|
enabled: bool | None = None
|
|
key: str | None = None
|
|
|
|
|
|
class AccountCreateRequest(BaseModel):
|
|
tokens: list[str] = Field(default_factory=list)
|
|
accounts: list[dict[str, Any]] = Field(default_factory=list)
|
|
|
|
|
|
class AccountDeleteRequest(BaseModel):
|
|
tokens: list[str] = Field(default_factory=list)
|
|
|
|
|
|
class AccountRefreshRequest(BaseModel):
|
|
access_tokens: list[str] = Field(default_factory=list)
|
|
|
|
|
|
class AccountExportRequest(BaseModel):
|
|
access_tokens: list[str] = Field(default_factory=list)
|
|
format: Literal["json", "zip"] = "json"
|
|
|
|
|
|
class AccountUpdateRequest(BaseModel):
|
|
access_token: str = ""
|
|
type: str | None = None
|
|
status: str | None = None
|
|
quota: int | None = None
|
|
proxy: str | None = None
|
|
|
|
|
|
class CPAPoolCreateRequest(BaseModel):
|
|
name: str = ""
|
|
base_url: str = ""
|
|
secret_key: str = ""
|
|
|
|
|
|
class CPAPoolUpdateRequest(BaseModel):
|
|
name: str | None = None
|
|
base_url: str | None = None
|
|
secret_key: str | None = None
|
|
|
|
|
|
class CPAImportRequest(BaseModel):
|
|
names: list[str] = Field(default_factory=list)
|
|
|
|
|
|
class Sub2APIServerCreateRequest(BaseModel):
|
|
name: str = ""
|
|
base_url: str = ""
|
|
email: str = ""
|
|
password: str = ""
|
|
api_key: str = ""
|
|
group_id: str = ""
|
|
|
|
|
|
class Sub2APIServerUpdateRequest(BaseModel):
|
|
name: str | None = None
|
|
base_url: str | None = None
|
|
email: str | None = None
|
|
password: str | None = None
|
|
api_key: str | None = None
|
|
group_id: str | None = None
|
|
|
|
|
|
class Sub2APIImportRequest(BaseModel):
|
|
account_ids: list[str] = Field(default_factory=list)
|
|
|
|
|
|
class OAuthLoginStartRequest(BaseModel):
|
|
"""起始 OAuth 桥。email_hint 可选,仅用于让 OpenAI 登录页预填邮箱。"""
|
|
email_hint: str = ""
|
|
|
|
|
|
class OAuthLoginFinishRequest(BaseModel):
|
|
"""提交 callback。callback 既可以是完整 URL 也可以只填 code。"""
|
|
session_id: str = ""
|
|
callback: str = ""
|
|
|
|
|
|
def _account_payload_token(item: dict[str, Any]) -> str:
|
|
return str(item.get("access_token") or item.get("accessToken") or "").strip()
|
|
|
|
|
|
def _unique_tokens(tokens: list[str]) -> list[str]:
|
|
return list(dict.fromkeys(str(token or "").strip() for token in tokens if str(token or "").strip()))
|
|
|
|
|
|
def _download_timestamp() -> str:
|
|
return datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
|
|
|
|
def _safe_export_name(value: str, fallback: str) -> str:
|
|
clean = re.sub(r"[^A-Za-z0-9._-]+", "-", value.strip()).strip("-._")
|
|
return (clean or fallback)[:80]
|
|
|
|
|
|
def _account_zip_bytes(items: list[dict[str, str]]) -> bytes:
|
|
buf = io.BytesIO()
|
|
used_names: set[str] = set()
|
|
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as archive:
|
|
for index, item in enumerate(items, start=1):
|
|
raw_name = item.get("email") or item.get("account_id") or f"account-{index:03d}"
|
|
base_name = _safe_export_name(raw_name, f"account-{index:03d}")
|
|
name = base_name
|
|
suffix = 2
|
|
while name in used_names:
|
|
name = f"{base_name}-{suffix}"
|
|
suffix += 1
|
|
used_names.add(name)
|
|
archive.writestr(
|
|
f"{name}.json",
|
|
json.dumps(item, ensure_ascii=False, indent=2) + "\n",
|
|
)
|
|
return buf.getvalue()
|
|
|
|
|
|
def create_router() -> APIRouter:
|
|
router = APIRouter()
|
|
|
|
@router.get("/api/auth/users")
|
|
async def list_user_keys(authorization: str | None = Header(default=None)):
|
|
require_admin(authorization)
|
|
return {"items": auth_service.list_keys(role="user")}
|
|
|
|
@router.post("/api/auth/users")
|
|
async def create_user_key(body: UserKeyCreateRequest, authorization: str | None = Header(default=None)):
|
|
require_admin(authorization)
|
|
try:
|
|
item, raw_key = auth_service.create_key(role="user", name=body.name)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc
|
|
return {"item": item, "key": raw_key, "items": auth_service.list_keys(role="user")}
|
|
|
|
@router.post("/api/auth/users/{key_id}")
|
|
async def update_user_key(
|
|
key_id: str,
|
|
body: UserKeyUpdateRequest,
|
|
authorization: str | None = Header(default=None),
|
|
):
|
|
require_admin(authorization)
|
|
updates = {
|
|
key: value
|
|
for key, value in {
|
|
"name": body.name,
|
|
"enabled": body.enabled,
|
|
"key": body.key,
|
|
}.items()
|
|
if value is not None
|
|
}
|
|
if not updates:
|
|
raise HTTPException(status_code=400, detail={"error": "还没有检测到改动,请修改后再保存"})
|
|
try:
|
|
item = auth_service.update_key(key_id, updates, role="user")
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc
|
|
if item is None:
|
|
raise HTTPException(status_code=404, detail={"error": "这条用户密钥不存在,可能已经被删除"})
|
|
return {"item": item, "items": auth_service.list_keys(role="user")}
|
|
|
|
@router.delete("/api/auth/users/{key_id}")
|
|
async def delete_user_key(key_id: str, authorization: str | None = Header(default=None)):
|
|
require_admin(authorization)
|
|
if not auth_service.delete_key(key_id, role="user"):
|
|
raise HTTPException(status_code=404, detail={"error": "这条用户密钥不存在,可能已经被删除"})
|
|
return {"items": auth_service.list_keys(role="user")}
|
|
|
|
@router.get("/api/accounts")
|
|
async def get_accounts(authorization: str | None = Header(default=None)):
|
|
require_admin(authorization)
|
|
return {"items": account_service.list_accounts()}
|
|
|
|
@router.post("/api/accounts")
|
|
async def create_accounts(body: AccountCreateRequest, authorization: str | None = Header(default=None)):
|
|
require_admin(authorization)
|
|
account_payloads = [item for item in body.accounts if isinstance(item, dict)]
|
|
payload_tokens = [_account_payload_token(item) for item in account_payloads]
|
|
tokens = _unique_tokens([*body.tokens, *payload_tokens])
|
|
if not tokens:
|
|
raise HTTPException(status_code=400, detail={"error": "tokens is required"})
|
|
if account_payloads:
|
|
result = account_service.add_account_items(account_payloads)
|
|
payload_token_set = set(_unique_tokens(payload_tokens))
|
|
extra_tokens = [token for token in tokens if token not in payload_token_set]
|
|
if extra_tokens:
|
|
extra_result = account_service.add_accounts(extra_tokens)
|
|
result["added"] = int(result.get("added") or 0) + int(extra_result.get("added") or 0)
|
|
result["skipped"] = int(result.get("skipped") or 0) + int(extra_result.get("skipped") or 0)
|
|
else:
|
|
result = account_service.add_accounts(tokens)
|
|
refresh_result = account_service.refresh_accounts(tokens)
|
|
return {
|
|
**result,
|
|
"refreshed": refresh_result.get("refreshed", 0),
|
|
"errors": refresh_result.get("errors", []),
|
|
"items": refresh_result.get("items", result.get("items", [])),
|
|
}
|
|
|
|
@router.delete("/api/accounts")
|
|
async def delete_accounts(body: AccountDeleteRequest, authorization: str | None = Header(default=None)):
|
|
require_admin(authorization)
|
|
tokens = [str(token or "").strip() for token in body.tokens if str(token or "").strip()]
|
|
if not tokens:
|
|
raise HTTPException(status_code=400, detail={"error": "tokens is required"})
|
|
return account_service.delete_accounts(tokens)
|
|
|
|
@router.post("/api/accounts/refresh")
|
|
async def refresh_accounts(body: AccountRefreshRequest, authorization: str | None = Header(default=None)):
|
|
require_admin(authorization)
|
|
access_tokens = [str(token or "").strip() for token in body.access_tokens if str(token or "").strip()]
|
|
if not access_tokens:
|
|
access_tokens = account_service.list_tokens()
|
|
if not access_tokens:
|
|
raise HTTPException(status_code=400, detail={"error": "access_tokens is required"})
|
|
|
|
progress_id = str(uuid.uuid4())
|
|
|
|
async def _do_refresh():
|
|
try:
|
|
await run_in_threadpool(account_service.refresh_accounts, access_tokens, progress_id, False)
|
|
except Exception as e:
|
|
account_service.finish_refresh_progress(progress_id, error=str(e))
|
|
|
|
asyncio.create_task(_do_refresh())
|
|
|
|
return {"progress_id": progress_id}
|
|
|
|
@router.get("/api/accounts/refresh/progress/{progress_id}")
|
|
async def get_refresh_progress(progress_id: str, authorization: str | None = Header(default=None)):
|
|
require_admin(authorization)
|
|
progress = account_service.get_refresh_progress(progress_id)
|
|
if progress is None:
|
|
raise HTTPException(status_code=404, detail={"error": "progress not found"})
|
|
return progress
|
|
|
|
@router.post("/api/accounts/re-login")
|
|
async def re_login_accounts(body: AccountRefreshRequest, authorization: str | None = Header(default=None)):
|
|
"""对选中账号执行密码重新登录流程(密码登录→验证码登录→刷新token)。"""
|
|
require_admin(authorization)
|
|
access_tokens = [str(token or "").strip() for token in body.access_tokens if str(token or "").strip()]
|
|
if not access_tokens:
|
|
raise HTTPException(status_code=400, detail={"error": "access_tokens is required"})
|
|
|
|
progress_id = str(uuid.uuid4())
|
|
|
|
async def _do_relogin():
|
|
try:
|
|
await run_in_threadpool(account_service.re_login_accounts, access_tokens, progress_id)
|
|
except Exception as e:
|
|
account_service.finish_relogin_progress(progress_id, error=str(e))
|
|
|
|
asyncio.create_task(_do_relogin())
|
|
|
|
return {"progress_id": progress_id}
|
|
|
|
@router.get("/api/accounts/re-login/progress/{progress_id}")
|
|
async def get_relogin_progress(progress_id: str, authorization: str | None = Header(default=None)):
|
|
require_admin(authorization)
|
|
progress = account_service.get_relogin_progress(progress_id)
|
|
if progress is None:
|
|
raise HTTPException(status_code=404, detail={"error": "progress not found"})
|
|
return progress
|
|
|
|
@router.post("/api/accounts/export")
|
|
async def export_accounts(body: AccountExportRequest, authorization: str | None = Header(default=None)):
|
|
require_admin(authorization)
|
|
access_tokens = _unique_tokens(body.access_tokens)
|
|
items = account_service.build_export_items(access_tokens)
|
|
if not items:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={"error": "没有可导出的完整账号,需要同时有 access_token、refresh_token 和 id_token"},
|
|
)
|
|
|
|
timestamp = _download_timestamp()
|
|
if body.format == "zip":
|
|
content = _account_zip_bytes(items)
|
|
return Response(
|
|
content,
|
|
media_type="application/zip",
|
|
headers={"Content-Disposition": f'attachment; filename="codex-accounts-{timestamp}.zip"'},
|
|
)
|
|
|
|
payload: dict[str, str] | list[dict[str, str]] = items[0] if len(items) == 1 else items
|
|
return Response(
|
|
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
|
|
media_type="application/json",
|
|
headers={"Content-Disposition": f'attachment; filename="codex-accounts-{timestamp}.json"'},
|
|
)
|
|
|
|
@router.post("/api/accounts/update")
|
|
async def update_account(body: AccountUpdateRequest, authorization: str | None = Header(default=None)):
|
|
require_admin(authorization)
|
|
access_token = str(body.access_token or "").strip()
|
|
if not access_token:
|
|
raise HTTPException(status_code=400, detail={"error": "access_token is required"})
|
|
updates = {key: value for key, value in {"type": body.type, "status": body.status, "quota": body.quota, "proxy": body.proxy}.items() if value is not None}
|
|
if not updates:
|
|
raise HTTPException(status_code=400, detail={"error": "还没有检测到改动,请修改后再保存"})
|
|
account = account_service.update_account(access_token, updates, manual=True)
|
|
if account is None:
|
|
raise HTTPException(status_code=404, detail={"error": "account not found"})
|
|
return {"item": account, "items": account_service.list_accounts()}
|
|
|
|
@router.post("/api/accounts/oauth/start")
|
|
async def start_oauth_login(
|
|
body: OAuthLoginStartRequest,
|
|
authorization: str | None = Header(default=None),
|
|
):
|
|
"""登记一次 PKCE 会话,返回可让用户浏览器打开的 authorize URL。"""
|
|
require_admin(authorization)
|
|
try:
|
|
return await run_in_threadpool(oauth_login_service.start, body.email_hint)
|
|
except OAuthLoginError as exc:
|
|
raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc
|
|
|
|
@router.post("/api/accounts/oauth/finish")
|
|
async def finish_oauth_login(
|
|
body: OAuthLoginFinishRequest,
|
|
authorization: str | None = Header(default=None),
|
|
):
|
|
"""收用户从浏览器抓回的 callback URL / code,换出 token 三件套并落盘。"""
|
|
require_admin(authorization)
|
|
# 入参日志:截断敏感字段,仅保留前几位,方便排错而不泄密
|
|
cb_preview = (body.callback or "")[:80]
|
|
sid_preview = (body.session_id or "")[:8]
|
|
print(
|
|
f"[oauth-login] finish called: session_id={sid_preview}..., callback_preview={cb_preview!r}",
|
|
flush=True,
|
|
)
|
|
try:
|
|
tokens = await run_in_threadpool(oauth_login_service.finish, body.session_id, body.callback)
|
|
except OAuthLoginError as exc:
|
|
print(f"[oauth-login] finish rejected: {exc}", flush=True)
|
|
raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc
|
|
|
|
payload = {
|
|
"access_token": tokens["access_token"],
|
|
"refresh_token": tokens["refresh_token"],
|
|
"id_token": tokens["id_token"],
|
|
"source_type": "oauth_login",
|
|
}
|
|
add_result = await run_in_threadpool(account_service.add_account_items, [payload])
|
|
refresh_result = await run_in_threadpool(
|
|
account_service.refresh_accounts, [tokens["access_token"]]
|
|
)
|
|
return {
|
|
**add_result,
|
|
"refreshed": refresh_result.get("refreshed", 0),
|
|
"errors": refresh_result.get("errors", []),
|
|
"items": refresh_result.get("items", add_result.get("items", [])),
|
|
}
|
|
|
|
@router.get("/api/cpa/pools")
|
|
async def list_cpa_pools(authorization: str | None = Header(default=None)):
|
|
require_admin(authorization)
|
|
return {"pools": sanitize_cpa_pools(cpa_config.list_pools())}
|
|
|
|
@router.post("/api/cpa/pools")
|
|
async def create_cpa_pool(body: CPAPoolCreateRequest, authorization: str | None = Header(default=None)):
|
|
require_admin(authorization)
|
|
if not body.base_url.strip():
|
|
raise HTTPException(status_code=400, detail={"error": "base_url is required"})
|
|
if not body.secret_key.strip():
|
|
raise HTTPException(status_code=400, detail={"error": "secret_key is required"})
|
|
pool = cpa_config.add_pool(name=body.name, base_url=body.base_url, secret_key=body.secret_key)
|
|
return {"pool": sanitize_cpa_pool(pool), "pools": sanitize_cpa_pools(cpa_config.list_pools())}
|
|
|
|
@router.post("/api/cpa/pools/{pool_id}")
|
|
async def update_cpa_pool(pool_id: str, body: CPAPoolUpdateRequest, authorization: str | None = Header(default=None)):
|
|
require_admin(authorization)
|
|
pool = cpa_config.update_pool(pool_id, body.model_dump(exclude_none=True))
|
|
if pool is None:
|
|
raise HTTPException(status_code=404, detail={"error": "pool not found"})
|
|
return {"pool": sanitize_cpa_pool(pool), "pools": sanitize_cpa_pools(cpa_config.list_pools())}
|
|
|
|
@router.delete("/api/cpa/pools/{pool_id}")
|
|
async def delete_cpa_pool(pool_id: str, authorization: str | None = Header(default=None)):
|
|
require_admin(authorization)
|
|
if not cpa_config.delete_pool(pool_id):
|
|
raise HTTPException(status_code=404, detail={"error": "pool not found"})
|
|
return {"pools": sanitize_cpa_pools(cpa_config.list_pools())}
|
|
|
|
@router.get("/api/cpa/pools/{pool_id}/files")
|
|
async def cpa_pool_files(pool_id: str, authorization: str | None = Header(default=None)):
|
|
require_admin(authorization)
|
|
pool = cpa_config.get_pool(pool_id)
|
|
if pool is None:
|
|
raise HTTPException(status_code=404, detail={"error": "pool not found"})
|
|
return {"pool_id": pool_id, "files": await run_in_threadpool(list_remote_files, pool)}
|
|
|
|
@router.post("/api/cpa/pools/{pool_id}/import")
|
|
async def cpa_pool_import(pool_id: str, body: CPAImportRequest, authorization: str | None = Header(default=None)):
|
|
require_admin(authorization)
|
|
pool = cpa_config.get_pool(pool_id)
|
|
if pool is None:
|
|
raise HTTPException(status_code=404, detail={"error": "pool not found"})
|
|
try:
|
|
job = cpa_import_service.start_import(pool, body.names)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc
|
|
return {"import_job": job}
|
|
|
|
@router.get("/api/cpa/pools/{pool_id}/import")
|
|
async def cpa_pool_import_progress(pool_id: str, authorization: str | None = Header(default=None)):
|
|
require_admin(authorization)
|
|
pool = cpa_config.get_pool(pool_id)
|
|
if pool is None:
|
|
raise HTTPException(status_code=404, detail={"error": "pool not found"})
|
|
return {"import_job": pool.get("import_job")}
|
|
|
|
@router.get("/api/sub2api/servers")
|
|
async def list_sub2api_servers(authorization: str | None = Header(default=None)):
|
|
require_admin(authorization)
|
|
return {"servers": sanitize_sub2api_servers(sub2api_config.list_servers())}
|
|
|
|
@router.post("/api/sub2api/servers")
|
|
async def create_sub2api_server(body: Sub2APIServerCreateRequest, authorization: str | None = Header(default=None)):
|
|
require_admin(authorization)
|
|
if not body.base_url.strip():
|
|
raise HTTPException(status_code=400, detail={"error": "base_url is required"})
|
|
has_login = body.email.strip() and body.password.strip()
|
|
has_api_key = bool(body.api_key.strip())
|
|
if not has_login and not has_api_key:
|
|
raise HTTPException(status_code=400, detail={"error": "email+password or api_key is required"})
|
|
server = sub2api_config.add_server(
|
|
name=body.name,
|
|
base_url=body.base_url,
|
|
email=body.email,
|
|
password=body.password,
|
|
api_key=body.api_key,
|
|
group_id=body.group_id,
|
|
)
|
|
return {"server": sanitize_sub2api_server(server), "servers": sanitize_sub2api_servers(sub2api_config.list_servers())}
|
|
|
|
@router.post("/api/sub2api/servers/{server_id}")
|
|
async def update_sub2api_server(server_id: str, body: Sub2APIServerUpdateRequest, authorization: str | None = Header(default=None)):
|
|
require_admin(authorization)
|
|
server = sub2api_config.update_server(server_id, body.model_dump(exclude_none=True))
|
|
if server is None:
|
|
raise HTTPException(status_code=404, detail={"error": "server not found"})
|
|
return {"server": sanitize_sub2api_server(server), "servers": sanitize_sub2api_servers(sub2api_config.list_servers())}
|
|
|
|
@router.delete("/api/sub2api/servers/{server_id}")
|
|
async def delete_sub2api_server(server_id: str, authorization: str | None = Header(default=None)):
|
|
require_admin(authorization)
|
|
if not sub2api_config.delete_server(server_id):
|
|
raise HTTPException(status_code=404, detail={"error": "server not found"})
|
|
return {"servers": sanitize_sub2api_servers(sub2api_config.list_servers())}
|
|
|
|
@router.get("/api/sub2api/servers/{server_id}/groups")
|
|
async def sub2api_server_groups(server_id: str, authorization: str | None = Header(default=None)):
|
|
require_admin(authorization)
|
|
server = sub2api_config.get_server(server_id)
|
|
if server is None:
|
|
raise HTTPException(status_code=404, detail={"error": "server not found"})
|
|
try:
|
|
groups = await run_in_threadpool(sub2api_list_remote_groups, server)
|
|
except Exception as exc:
|
|
raise HTTPException(status_code=502, detail={"error": str(exc)}) from exc
|
|
return {"server_id": server_id, "groups": groups}
|
|
|
|
@router.get("/api/sub2api/servers/{server_id}/accounts")
|
|
async def sub2api_server_accounts(server_id: str, authorization: str | None = Header(default=None)):
|
|
require_admin(authorization)
|
|
server = sub2api_config.get_server(server_id)
|
|
if server is None:
|
|
raise HTTPException(status_code=404, detail={"error": "server not found"})
|
|
try:
|
|
accounts = await run_in_threadpool(sub2api_list_remote_accounts, server)
|
|
except Exception as exc:
|
|
raise HTTPException(status_code=502, detail={"error": str(exc)}) from exc
|
|
return {"server_id": server_id, "accounts": accounts}
|
|
|
|
@router.post("/api/sub2api/servers/{server_id}/import")
|
|
async def sub2api_server_import(server_id: str, body: Sub2APIImportRequest, authorization: str | None = Header(default=None)):
|
|
require_admin(authorization)
|
|
server = sub2api_config.get_server(server_id)
|
|
if server is None:
|
|
raise HTTPException(status_code=404, detail={"error": "server not found"})
|
|
try:
|
|
job = sub2api_import_service.start_import(server, body.account_ids)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc
|
|
return {"import_job": job}
|
|
|
|
@router.get("/api/sub2api/servers/{server_id}/import")
|
|
async def sub2api_server_import_progress(server_id: str, authorization: str | None = Header(default=None)):
|
|
require_admin(authorization)
|
|
server = sub2api_config.get_server(server_id)
|
|
if server is None:
|
|
raise HTTPException(status_code=404, detail={"error": "server not found"})
|
|
return {"import_job": server.get("import_job")}
|
|
|
|
return router
|