Arag / app /services /platform_api_service.py
AuthorBot
SuperAdmin QoL: persistent session, TOTP session, shared provider keys.
65a6194
Raw
History Blame Contribute Delete
6.53 kB
"""SuperAdmin service for platform API credential management."""
from __future__ import annotations
import structlog
from fastapi import HTTPException
from redis.asyncio import Redis
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.crypto.credential_crypto import encrypt_secret, mask_secret
from app.models.user import User
from app.repositories.audit_repo import AuditRepository
from app.repositories.platform_api_credential_repo import PlatformApiCredentialRepository
from app.services.platform_api_registry import (
PLATFORM_API_META,
PROVIDER_DEFINITIONS,
build_credential_views,
build_provider_views,
invalidate_credential_cache,
is_provider_scope,
provider_scope_id,
)
logger = structlog.get_logger(__name__)
class PlatformApiService:
"""Manage optional retailer API keys with audit logging."""
def __init__(self, db: AsyncSession, redis: Redis | None = None) -> None:
self._db = db
self._redis = redis
self._repo = PlatformApiCredentialRepository(db)
self._audit = AuditRepository(db)
async def list_credentials(self) -> dict:
"""Return provider-scoped masked credential views."""
rows = await self._repo.list_all()
return build_provider_views(rows)
async def upsert_provider_credential(
self,
actor: User,
*,
provider_id: str,
api_key: str,
api_secret: str | None,
is_enabled: bool,
) -> dict:
"""Create or update a shared provider API key."""
if provider_id not in PROVIDER_DEFINITIONS:
raise HTTPException(400, f"Unknown provider: {provider_id}")
if not api_key.strip():
raise HTTPException(400, "API key is required")
scope_id = provider_scope_id(provider_id)
await self._repo.upsert(
platform_id=scope_id,
provider=provider_id,
api_key_encrypted=encrypt_secret(api_key.strip()),
api_secret_encrypted=encrypt_secret(api_secret.strip()) if api_secret else None,
is_enabled=is_enabled,
updated_by_id=actor.id,
updated_by_email=actor.email,
)
await self._audit.log(
actor_id=actor.id,
actor_email=actor.email,
action="upsert_platform_api",
target_type="platform_api",
target_id=scope_id,
details={
"provider": provider_id,
"scope": "provider",
"is_enabled": is_enabled,
"api_key_hint": mask_secret(api_key.strip()),
},
)
await invalidate_credential_cache(self._redis, scope_id)
logger.info("Provider API credential saved", provider_id=provider_id)
return build_provider_views(await self._repo.list_all())
async def upsert_credential(
self,
actor: User,
*,
platform_id: str,
provider: str,
api_key: str,
api_secret: str | None,
is_enabled: bool,
) -> dict:
"""Create or update encrypted platform API credential (legacy per-platform)."""
if is_provider_scope(platform_id):
return await self.upsert_provider_credential(
actor,
provider_id=provider,
api_key=api_key,
api_secret=api_secret,
is_enabled=is_enabled,
)
meta = PLATFORM_API_META.get(platform_id)
if not meta:
raise HTTPException(400, f"Unknown platform: {platform_id}")
if provider not in meta["providers"]:
raise HTTPException(
400,
f"Provider '{provider}' not allowed for {platform_id}. "
f"Allowed: {', '.join(meta['providers']) or 'none'}",
)
if not api_key.strip():
raise HTTPException(400, "API key is required")
await self._repo.upsert(
platform_id=platform_id,
provider=provider,
api_key_encrypted=encrypt_secret(api_key.strip()),
api_secret_encrypted=encrypt_secret(api_secret.strip()) if api_secret else None,
is_enabled=is_enabled,
updated_by_id=actor.id,
updated_by_email=actor.email,
)
await self._audit.log(
actor_id=actor.id,
actor_email=actor.email,
action="upsert_platform_api",
target_type="platform_api",
target_id=platform_id,
details={
"provider": provider,
"is_enabled": is_enabled,
"api_key_hint": mask_secret(api_key.strip()),
},
)
await invalidate_credential_cache(self._redis, platform_id)
logger.info("Platform API credential saved", platform_id=platform_id, provider=provider)
return build_provider_views(await self._repo.list_all())
async def disable_credential(self, actor: User, scope_or_platform_id: str) -> dict:
"""Disable SuperAdmin credential (falls back to env/free logic)."""
row = await self._repo.get_by_platform(scope_or_platform_id)
if not row:
raise HTTPException(404, "No credential configured")
row.is_enabled = False
row.updated_by_id = actor.id
row.updated_by_email = actor.email
await self._db.flush()
await self._audit.log(
actor_id=actor.id,
actor_email=actor.email,
action="disable_platform_api",
target_type="platform_api",
target_id=scope_or_platform_id,
details={"provider": row.provider},
)
await invalidate_credential_cache(self._redis, scope_or_platform_id)
return build_provider_views(await self._repo.list_all())
async def delete_credential(self, actor: User, scope_or_platform_id: str) -> dict:
"""Remove SuperAdmin credential entirely."""
deleted = await self._repo.delete(scope_or_platform_id)
if not deleted:
raise HTTPException(404, "No credential configured")
await self._audit.log(
actor_id=actor.id,
actor_email=actor.email,
action="delete_platform_api",
target_type="platform_api",
target_id=scope_or_platform_id,
details={},
)
await invalidate_credential_cache(self._redis, scope_or_platform_id)
return build_provider_views(await self._repo.list_all())