rapid interations of server side app and firefox extension
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
"""Credential management API endpoints."""
|
||||
|
||||
from datetime import datetime
|
||||
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.config import get_settings
|
||||
from app.utils.encryption import encrypt_data, decrypt_data
|
||||
|
||||
bp = Blueprint("credentials", __name__)
|
||||
|
||||
VALID_PLATFORMS = ["patreon", "subscribestar", "hentaifoundry", "discord"]
|
||||
|
||||
|
||||
@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()
|
||||
|
||||
return jsonify({
|
||||
"items": [c.to_dict(include_data=False) for c in credentials]
|
||||
})
|
||||
|
||||
|
||||
@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
|
||||
credential.last_verified = datetime.utcnow()
|
||||
await session.commit()
|
||||
|
||||
return jsonify({
|
||||
"platform": platform,
|
||||
"is_valid": True,
|
||||
"last_verified": credential.last_verified.isoformat(),
|
||||
})
|
||||
Reference in New Issue
Block a user