254 lines
8.3 KiB
Python
254 lines
8.3 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from flask import jsonify, request
|
|
|
|
from outlook_web.repositories import settings as settings_repo
|
|
from outlook_web.security.auth import api_key_required, get_external_api_consumer
|
|
from outlook_web.security.external_api_guard import external_api_guards
|
|
from outlook_web.services import external_api as external_api_service
|
|
from outlook_web.services.pool import (
|
|
PoolServiceError,
|
|
claim_random,
|
|
complete_claim,
|
|
get_pool_stats,
|
|
release_claim,
|
|
)
|
|
|
|
|
|
def _audit(endpoint: str, status: str, *, details: dict[str, Any], email_addr: str = "") -> None:
|
|
external_api_service.audit_external_api_access(
|
|
action="external_api_access",
|
|
email_addr=email_addr,
|
|
endpoint=endpoint,
|
|
status=status,
|
|
details=details,
|
|
)
|
|
|
|
|
|
def _pool_error_code(error_code: str) -> str:
|
|
return str(error_code or "INTERNAL_ERROR").upper()
|
|
|
|
|
|
def _error_response(endpoint: str, exc: PoolServiceError):
|
|
code = _pool_error_code(exc.error_code)
|
|
_audit(endpoint, "error", details={"code": code})
|
|
return jsonify(external_api_service.fail(code, str(exc))), exc.http_status
|
|
|
|
|
|
def _check_pool_external_enabled(endpoint: str):
|
|
if settings_repo.get_pool_external_enabled():
|
|
return None
|
|
_audit(
|
|
endpoint,
|
|
"error",
|
|
details={"code": "FEATURE_DISABLED", "feature": "external_pool"},
|
|
)
|
|
return (
|
|
jsonify(
|
|
external_api_service.fail(
|
|
"FEATURE_DISABLED",
|
|
"功能 external_pool 当前未启用",
|
|
data={"feature": "external_pool"},
|
|
)
|
|
),
|
|
403,
|
|
)
|
|
|
|
|
|
def _check_pool_access(endpoint: str):
|
|
consumer = get_external_api_consumer() or {}
|
|
if bool(consumer.get("is_legacy")) or bool(consumer.get("pool_access")):
|
|
return None
|
|
_audit(
|
|
endpoint,
|
|
"error",
|
|
details={
|
|
"code": "FORBIDDEN",
|
|
"feature": "external_pool",
|
|
"reason": "pool_access_required",
|
|
},
|
|
)
|
|
return (
|
|
jsonify(
|
|
external_api_service.fail(
|
|
"FORBIDDEN",
|
|
"当前 API Key 无权访问 external pool",
|
|
data={"feature": "external_pool", "reason": "pool_access_required"},
|
|
)
|
|
),
|
|
403,
|
|
)
|
|
|
|
|
|
@api_key_required
|
|
@external_api_guards(feature="pool_claim_random")
|
|
def api_external_pool_claim_random():
|
|
endpoint = "/api/external/pool/claim-random"
|
|
disabled_resp = _check_pool_external_enabled(endpoint)
|
|
if disabled_resp is not None:
|
|
return disabled_resp
|
|
access_resp = _check_pool_access(endpoint)
|
|
if access_resp is not None:
|
|
return access_resp
|
|
body = request.get_json(silent=True) or {}
|
|
caller_id = body.get("caller_id", "")
|
|
task_id = body.get("task_id", "")
|
|
provider = body.get("provider")
|
|
|
|
try:
|
|
account = claim_random(
|
|
caller_id=caller_id,
|
|
task_id=task_id,
|
|
provider=provider,
|
|
)
|
|
data = {
|
|
"account_id": account["id"],
|
|
"email": account["email"],
|
|
"claim_token": account["claim_token"],
|
|
"lease_expires_at": account["lease_expires_at"],
|
|
}
|
|
_audit(
|
|
endpoint,
|
|
"ok",
|
|
details={"provider": provider or "", "account_id": data["account_id"]},
|
|
)
|
|
return jsonify(external_api_service.ok(data))
|
|
except PoolServiceError as exc:
|
|
return _error_response(endpoint, exc)
|
|
except Exception as exc:
|
|
_audit(
|
|
endpoint,
|
|
"error",
|
|
details={"code": "INTERNAL_ERROR", "err": type(exc).__name__},
|
|
)
|
|
return jsonify(external_api_service.fail("INTERNAL_ERROR", "服务内部错误")), 500
|
|
|
|
|
|
@api_key_required
|
|
@external_api_guards(feature="pool_claim_release")
|
|
def api_external_pool_claim_release():
|
|
endpoint = "/api/external/pool/claim-release"
|
|
disabled_resp = _check_pool_external_enabled(endpoint)
|
|
if disabled_resp is not None:
|
|
return disabled_resp
|
|
access_resp = _check_pool_access(endpoint)
|
|
if access_resp is not None:
|
|
return access_resp
|
|
body = request.get_json(silent=True) or {}
|
|
account_id = body.get("account_id")
|
|
claim_token = body.get("claim_token", "")
|
|
caller_id = body.get("caller_id", "")
|
|
task_id = body.get("task_id", "")
|
|
reason = body.get("reason")
|
|
|
|
if account_id is None:
|
|
_audit(endpoint, "error", details={"code": "ACCOUNT_ID_MISSING"})
|
|
return jsonify(external_api_service.fail("ACCOUNT_ID_MISSING", "account_id 不能为空")), 400
|
|
try:
|
|
account_id = int(account_id)
|
|
except (TypeError, ValueError):
|
|
_audit(endpoint, "error", details={"code": "ACCOUNT_ID_INVALID"})
|
|
return jsonify(external_api_service.fail("ACCOUNT_ID_INVALID", "account_id 必须为整数")), 400
|
|
|
|
try:
|
|
release_claim(
|
|
account_id=account_id,
|
|
claim_token=claim_token,
|
|
caller_id=caller_id,
|
|
task_id=task_id,
|
|
reason=reason,
|
|
)
|
|
_audit(endpoint, "ok", details={"account_id": account_id})
|
|
return jsonify(external_api_service.ok({"account_id": account_id, "pool_status": "available"}))
|
|
except PoolServiceError as exc:
|
|
return _error_response(endpoint, exc)
|
|
except Exception as exc:
|
|
_audit(
|
|
endpoint,
|
|
"error",
|
|
details={"code": "INTERNAL_ERROR", "err": type(exc).__name__},
|
|
)
|
|
return jsonify(external_api_service.fail("INTERNAL_ERROR", "服务内部错误")), 500
|
|
|
|
|
|
@api_key_required
|
|
@external_api_guards(feature="pool_claim_complete")
|
|
def api_external_pool_claim_complete():
|
|
endpoint = "/api/external/pool/claim-complete"
|
|
disabled_resp = _check_pool_external_enabled(endpoint)
|
|
if disabled_resp is not None:
|
|
return disabled_resp
|
|
access_resp = _check_pool_access(endpoint)
|
|
if access_resp is not None:
|
|
return access_resp
|
|
body = request.get_json(silent=True) or {}
|
|
account_id = body.get("account_id")
|
|
claim_token = body.get("claim_token", "")
|
|
caller_id = body.get("caller_id", "")
|
|
task_id = body.get("task_id", "")
|
|
result = body.get("result", "")
|
|
detail = body.get("detail")
|
|
|
|
if account_id is None:
|
|
_audit(endpoint, "error", details={"code": "ACCOUNT_ID_MISSING"})
|
|
return jsonify(external_api_service.fail("ACCOUNT_ID_MISSING", "account_id 不能为空")), 400
|
|
try:
|
|
account_id = int(account_id)
|
|
except (TypeError, ValueError):
|
|
_audit(endpoint, "error", details={"code": "ACCOUNT_ID_INVALID"})
|
|
return jsonify(external_api_service.fail("ACCOUNT_ID_INVALID", "account_id 必须为整数")), 400
|
|
|
|
try:
|
|
new_status = complete_claim(
|
|
account_id=account_id,
|
|
claim_token=claim_token,
|
|
caller_id=caller_id,
|
|
task_id=task_id,
|
|
result=result,
|
|
detail=detail,
|
|
)
|
|
_audit(
|
|
endpoint,
|
|
"ok",
|
|
details={
|
|
"account_id": account_id,
|
|
"result": result,
|
|
"pool_status": new_status,
|
|
},
|
|
)
|
|
return jsonify(external_api_service.ok({"account_id": account_id, "pool_status": new_status}))
|
|
except PoolServiceError as exc:
|
|
return _error_response(endpoint, exc)
|
|
except Exception as exc:
|
|
_audit(
|
|
endpoint,
|
|
"error",
|
|
details={"code": "INTERNAL_ERROR", "err": type(exc).__name__},
|
|
)
|
|
return jsonify(external_api_service.fail("INTERNAL_ERROR", "服务内部错误")), 500
|
|
|
|
|
|
@api_key_required
|
|
@external_api_guards(feature="pool_stats")
|
|
def api_external_pool_stats():
|
|
endpoint = "/api/external/pool/stats"
|
|
disabled_resp = _check_pool_external_enabled(endpoint)
|
|
if disabled_resp is not None:
|
|
return disabled_resp
|
|
access_resp = _check_pool_access(endpoint)
|
|
if access_resp is not None:
|
|
return access_resp
|
|
try:
|
|
stats = get_pool_stats()
|
|
_audit(endpoint, "ok", details={"snapshot": True})
|
|
return jsonify(external_api_service.ok(stats))
|
|
except Exception as exc:
|
|
_audit(
|
|
endpoint,
|
|
"error",
|
|
details={"code": "INTERNAL_ERROR", "err": type(exc).__name__},
|
|
)
|
|
return jsonify(external_api_service.fail("INTERNAL_ERROR", "服务内部错误")), 500
|