"""Credential management API endpoints.""" from quart import Blueprint, request, jsonify, current_app from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models.credential import Credential, CredentialType from app.models.setting import Setting, EXTENSION_API_KEY_SETTING, generate_api_key from app.models.base import utcnow from app.config import get_settings from app.utils.encryption import encrypt_data, decrypt_data bp = Blueprint("credentials", __name__) VALID_PLATFORMS = ["patreon", "subscribestar", "hentaifoundry", "discord", "pixiv", "deviantart"] @bp.route("", methods=["GET"]) async def list_credentials(): """List all credential status (without sensitive data).""" async with current_app.db_engine.connect() as conn: session = AsyncSession(bind=conn) result = await session.execute(select(Credential)) credentials = result.scalars().all() # Also return a simple map of platform -> has_credentials for UI convenience platforms_with_creds = {c.platform: True for c in credentials} return jsonify({ "items": [c.to_dict(include_data=False) for c in credentials], "platforms": platforms_with_creds, }) @bp.route("", methods=["POST"]) async def upload_credentials(): """Upload credentials (cookies or token). Can be called by: 1. Firefox extension (with X-Extension-Key header) 2. Web UI manual upload (with session auth) """ settings = get_settings() # Check extension API key if present extension_key = request.headers.get("X-Extension-Key") if extension_key: # Look up API key from database async with current_app.db_engine.connect() as conn: session = AsyncSession(bind=conn) result = await session.execute( select(Setting).where(Setting.key == EXTENSION_API_KEY_SETTING) ) setting = result.scalar() stored_key = setting.value if setting else None if not stored_key or extension_key != stored_key: return jsonify({"error": "Invalid extension API key"}), 401 data = await request.get_json() # Validate required fields if not data.get("platform"): return jsonify({"error": "Platform is required"}), 400 if not data.get("credential_type"): return jsonify({"error": "Credential type is required"}), 400 if not data.get("data"): return jsonify({"error": "Credential data is required"}), 400 platform = data["platform"] cred_type = data["credential_type"] cred_data = data["data"] # Validate platform if platform not in VALID_PLATFORMS: return jsonify({"error": f"Invalid platform. Must be one of: {', '.join(VALID_PLATFORMS)}"}), 400 # Validate credential type if cred_type not in [CredentialType.COOKIES, CredentialType.TOKEN]: return jsonify({"error": "Invalid credential type. Must be 'cookies' or 'token'"}), 400 async with current_app.db_engine.connect() as conn: session = AsyncSession(bind=conn) # Check for existing credential result = await session.execute( select(Credential).where(Credential.platform == platform) ) credential = result.scalar() # Encrypt the credential data encrypted = encrypt_data(cred_data, settings.secret_key) if credential: # Update existing credential.credential_type = cred_type credential.data = encrypted credential.expires_at = data.get("expires_at") credential.last_verified = None # Reset verification else: # Create new credential = Credential( platform=platform, credential_type=cred_type, data=encrypted, expires_at=data.get("expires_at"), ) session.add(credential) await session.commit() await session.refresh(credential) current_app.logger.info(f"Updated credentials for platform: {platform}") return jsonify({ "message": "Credentials updated", "platform": platform, "credential_type": cred_type, "expires_at": credential.expires_at.isoformat() if credential.expires_at else None, }) @bp.route("/", methods=["DELETE"]) async def delete_credentials(platform: str): """Delete credentials for a platform.""" if platform not in VALID_PLATFORMS: return jsonify({"error": f"Invalid platform. Must be one of: {', '.join(VALID_PLATFORMS)}"}), 400 async with current_app.db_engine.connect() as conn: session = AsyncSession(bind=conn) result = await session.execute( select(Credential).where(Credential.platform == platform) ) credential = result.scalar() if not credential: return jsonify({"error": "Credentials not found for this platform"}), 404 await session.delete(credential) await session.commit() current_app.logger.info(f"Deleted credentials for platform: {platform}") return "", 204 @bp.route("/verify", methods=["POST"]) async def verify_credentials(): """Verify that credentials are working. Attempts to make a test request to the platform. """ data = await request.get_json() platform = data.get("platform") if not platform: return jsonify({"error": "Platform is required"}), 400 if platform not in VALID_PLATFORMS: return jsonify({"error": f"Invalid platform. Must be one of: {', '.join(VALID_PLATFORMS)}"}), 400 async with current_app.db_engine.connect() as conn: session = AsyncSession(bind=conn) result = await session.execute( select(Credential).where(Credential.platform == platform) ) credential = result.scalar() if not credential: return jsonify({ "platform": platform, "is_valid": False, "error": "No credentials stored for this platform", }) # TODO: Implement actual verification by making test request # For now, just mark as verified verified_at = utcnow() credential.last_verified = verified_at await session.commit() return jsonify({ "platform": platform, "is_valid": True, "last_verified": verified_at.isoformat(), })