"""Authenticated account endpoints — usage summary and API key management.""" from flask import Blueprint, jsonify, request from app.extensions import limiter from app.services.account_service import ( create_api_key, get_user_by_id, has_task_access, list_api_keys, record_usage_event, revoke_api_key, ) from app.services.policy_service import get_usage_summary_for_user from app.services.credit_config import ( get_all_tool_costs, get_credits_for_plan, get_tool_credit_cost, CREDIT_WINDOW_DAYS, ) from app.services.credit_service import deduct_credits, get_credit_summary from app.services.stripe_service import ( is_stripe_configured, get_stripe_price_id, ) from app.utils.auth import get_current_user_id, has_session_task_access import stripe import logging logger = logging.getLogger(__name__) account_bp = Blueprint("account", __name__) @account_bp.route("/usage", methods=["GET"]) @limiter.limit("120/hour") def get_usage_route(): """Return plan, quota, and effective file-size cap summary for the current user.""" user_id = get_current_user_id() if user_id is None: return jsonify({"error": "Authentication required."}), 401 user = get_user_by_id(user_id) if user is None: return jsonify({"error": "User not found."}), 404 return jsonify(get_usage_summary_for_user(user_id, user["plan"])), 200 @account_bp.route("/credit-info", methods=["GET"]) @limiter.limit("60/hour") def get_credit_info_route(): """Return public credit/pricing info (no auth required).""" return jsonify({ "plans": { "free": {"credits": get_credits_for_plan("free"), "window_days": CREDIT_WINDOW_DAYS}, "pro": {"credits": get_credits_for_plan("pro"), "window_days": CREDIT_WINDOW_DAYS}, }, "tool_costs": get_all_tool_costs(), }), 200 @account_bp.route("/subscription", methods=["GET"]) @limiter.limit("60/hour") def get_subscription_status(): """Return subscription status for the authenticated user.""" user_id = get_current_user_id() if user_id is None: return jsonify({"error": "Authentication required."}), 401 user = get_user_by_id(user_id) if user is None: return jsonify({"error": "User not found."}), 404 # If Stripe is not configured, return basic info if not is_stripe_configured(): return jsonify( { "plan": user["plan"], "stripe_configured": False, "subscription": None, } ), 200 # Retrieve subscription info from Stripe if available subscription_info = None if user.get("stripe_subscription_id"): try: from app.services.stripe_service import get_stripe_secret_key stripe.api_key = get_stripe_secret_key() subscription = stripe.Subscription.retrieve(user["stripe_subscription_id"]) subscription_info = { "id": subscription.id, "status": subscription.status, "current_period_start": subscription.current_period_start, "current_period_end": subscription.current_period_end, "cancel_at_period_end": subscription.cancel_at_period_end, "items": [ { "price": item.price.id, "quantity": item.quantity, } for item in subscription.items.data ], } except Exception as e: logger.error( f"Failed to retrieve subscription {user['stripe_subscription_id']}: {e}" ) return jsonify( { "plan": user["plan"], "stripe_configured": True, "subscription": subscription_info, "pricing": { "monthly_price_id": get_stripe_price_id("monthly"), "yearly_price_id": get_stripe_price_id("yearly"), }, } ), 200 @account_bp.route("/api-keys", methods=["GET"]) @limiter.limit("60/hour") def list_api_keys_route(): """Return all API keys for the authenticated pro user.""" user_id = get_current_user_id() if user_id is None: return jsonify({"error": "Authentication required."}), 401 user = get_user_by_id(user_id) if user is None: return jsonify({"error": "User not found."}), 404 if user["plan"] != "pro": return jsonify({"error": "API key management requires a Pro plan."}), 403 return jsonify({"items": list_api_keys(user_id)}), 200 @account_bp.route("/api-keys", methods=["POST"]) @limiter.limit("20/hour") def create_api_key_route(): """Create a new API key for the authenticated pro user.""" user_id = get_current_user_id() if user_id is None: return jsonify({"error": "Authentication required."}), 401 user = get_user_by_id(user_id) if user is None: return jsonify({"error": "User not found."}), 404 if user["plan"] != "pro": return jsonify({"error": "API key management requires a Pro plan."}), 403 data = request.get_json(silent=True) or {} name = str(data.get("name", "")).strip() if not name: return jsonify({"error": "API key name is required."}), 400 try: result = create_api_key(user_id, name) except ValueError as exc: return jsonify({"error": str(exc)}), 400 return jsonify(result), 201 @account_bp.route("/api-keys/", methods=["DELETE"]) @limiter.limit("30/hour") def revoke_api_key_route(key_id: int): """Revoke one API key owned by the authenticated user.""" user_id = get_current_user_id() if user_id is None: return jsonify({"error": "Authentication required."}), 401 if not revoke_api_key(user_id, key_id): return jsonify({"error": "API key not found or already revoked."}), 404 return jsonify({"message": "API key revoked."}), 200 @account_bp.route("/claim-task", methods=["POST"]) @limiter.limit("60/hour") def claim_task_route(): """Adopt an anonymous task into the authenticated user's history. Called after a guest signs up or logs in to record the previously processed task in their account and deduct credits. """ user_id = get_current_user_id() if user_id is None: return jsonify({"error": "Authentication required."}), 401 data = request.get_json(silent=True) or {} task_id = str(data.get("task_id", "")).strip() tool = str(data.get("tool", "")).strip() if not task_id or not tool: return jsonify({"error": "task_id and tool are required."}), 400 # Verify this task belongs to the caller's session if not has_session_task_access(task_id): return jsonify({"error": "Task not found in your session."}), 403 # Skip if already claimed (idempotent) if has_task_access(user_id, "web", task_id): summary = get_credit_summary(user_id, "free") return jsonify({"claimed": True, "credits": summary}), 200 user = get_user_by_id(user_id) if user is None: return jsonify({"error": "User not found."}), 404 plan = user.get("plan", "free") cost = get_tool_credit_cost(tool) # Deduct credits try: deduct_credits(user_id, plan, tool) except ValueError: return jsonify({ "error": "Insufficient credits to claim this file.", "credits_required": cost, }), 429 # Record usage event so the task appears in history record_usage_event( user_id=user_id, source="web", tool=tool, task_id=task_id, event_type="accepted", api_key_id=None, cost_points=cost, ) summary = get_credit_summary(user_id, plan) return jsonify({"claimed": True, "credits": summary}), 200