"""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())