This repository has been archived on 2026-05-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
GallerySubscriber/backend/app/api/credentials.py
T
2026-02-02 20:17:13 -05:00

188 lines
6.4 KiB
Python

"""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("/<platform>", 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(),
})