Files
SaaS-PDF/backend/app/routes/account.py
Your Name 314f847ece fix: Add scrollable container to ToolSelectorModal for small screens
- Add max-h-[90vh] and flex-col to modal content container
- Wrap tools grid in max-h-[50vh] overflow-y-auto container
- Add overscroll-contain for smooth scroll behavior on mobile
- Fixes issue where 21 PDF tools overflow viewport on small screens
2026-04-01 22:22:48 +02:00

243 lines
7.7 KiB
Python

"""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/<int:key_id>", 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