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
This commit is contained in:
@@ -6,15 +6,24 @@ 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
|
||||
from app.utils.auth import get_current_user_id, has_session_task_access
|
||||
import stripe
|
||||
import logging
|
||||
|
||||
@@ -38,6 +47,19 @@ def get_usage_route():
|
||||
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():
|
||||
@@ -159,3 +181,62 @@ def revoke_api_key_route(key_id: int):
|
||||
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
|
||||
|
||||
@@ -52,7 +52,7 @@ def generate_barcode_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="barcode")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ def compress_pdf_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="compress-pdf")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ def compress_image_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="compress-image")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ def pdf_to_word_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="pdf-to-word")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
@@ -75,7 +75,7 @@ def word_to_pdf_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="word-to-pdf")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
|
||||
@@ -34,6 +34,13 @@ def download_file(task_id: str, filename: str):
|
||||
assert_api_task_access(actor, task_id)
|
||||
else:
|
||||
actor = resolve_web_actor()
|
||||
# Download gate: anonymous users must register before downloading
|
||||
if actor.actor_type == "anonymous":
|
||||
return (
|
||||
{"error": "signup_required",
|
||||
"message": "Create a free account to download your file."},
|
||||
401,
|
||||
)
|
||||
assert_web_task_access(actor, task_id)
|
||||
except PolicyError as exc:
|
||||
abort(exc.status_code, exc.message)
|
||||
|
||||
@@ -39,7 +39,7 @@ def extract_flowchart_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="pdf-flowchart")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
@@ -75,7 +75,7 @@ def extract_sample_flowchart_route():
|
||||
"""
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="pdf-flowchart-sample")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ def html_to_pdf_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="html-to-pdf")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ def convert_image_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="image-convert")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
@@ -127,7 +127,7 @@ def resize_image_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="image-resize")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
@@ -180,7 +180,7 @@ def convert_image_to_svg_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="image-to-svg")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ def crop_image_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="image-crop")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
@@ -120,7 +120,7 @@ def rotate_flip_image_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="image-rotate-flip")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ def ocr_image_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="ocr-image")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
@@ -102,7 +102,7 @@ def ocr_pdf_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="ocr-pdf")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@ from app.services.policy_service import (
|
||||
resolve_web_actor,
|
||||
validate_actor_file,
|
||||
)
|
||||
from app.services.translation_guardrails import (
|
||||
check_page_admission,
|
||||
TranslationAdmissionError,
|
||||
)
|
||||
from app.utils.file_validator import FileValidationError
|
||||
from app.utils.sanitizer import generate_safe_path
|
||||
from app.tasks.pdf_ai_tasks import (
|
||||
@@ -48,7 +52,7 @@ def chat_pdf_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="chat-pdf")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
@@ -104,7 +108,7 @@ def summarize_pdf_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="summarize-pdf")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
@@ -161,7 +165,7 @@ def translate_pdf_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="translate-pdf")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
@@ -175,6 +179,12 @@ def translate_pdf_route():
|
||||
task_id, input_path = generate_safe_path(ext, folder_type="upload")
|
||||
file.save(input_path)
|
||||
|
||||
# ── Page-count admission guard ──
|
||||
try:
|
||||
page_count = check_page_admission(input_path, actor.plan)
|
||||
except TranslationAdmissionError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
task = translate_pdf_task.delay(
|
||||
input_path,
|
||||
task_id,
|
||||
@@ -213,7 +223,7 @@ def extract_tables_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="extract-tables")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ def pdf_to_pptx_route():
|
||||
file = request.files["file"]
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="pdf-to-pptx")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
@@ -77,7 +77,7 @@ def excel_to_pdf_route():
|
||||
file = request.files["file"]
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="excel-to-pdf")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
@@ -116,7 +116,7 @@ def pptx_to_pdf_route():
|
||||
file = request.files["file"]
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="pptx-to-pdf")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
@@ -167,7 +167,7 @@ def sign_pdf_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="sign-pdf")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ def edit_pdf_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="pdf-edit")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ def crop_pdf_route():
|
||||
file = request.files["file"]
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="crop-pdf")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
@@ -89,7 +89,7 @@ def flatten_pdf_route():
|
||||
file = request.files["file"]
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="flatten-pdf")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
@@ -126,7 +126,7 @@ def repair_pdf_route():
|
||||
file = request.files["file"]
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="repair-pdf")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
@@ -184,7 +184,7 @@ def edit_metadata_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="edit-metadata")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ def pdf_to_excel_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="pdf-to-excel")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ def merge_pdfs_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="merge-pdf")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
@@ -123,7 +123,7 @@ def split_pdf_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="split-pdf")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
@@ -183,7 +183,7 @@ def rotate_pdf_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="rotate-pdf")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
@@ -246,7 +246,7 @@ def add_page_numbers_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="page-numbers")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
@@ -304,7 +304,7 @@ def pdf_to_images_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="pdf-to-images")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
@@ -353,7 +353,7 @@ def images_to_pdf_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="images-to-pdf")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
@@ -424,7 +424,7 @@ def watermark_pdf_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="watermark-pdf")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
@@ -480,7 +480,7 @@ def protect_pdf_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="protect-pdf")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
@@ -532,7 +532,7 @@ def unlock_pdf_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="unlock-pdf")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
@@ -579,7 +579,7 @@ def remove_watermark_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="remove-watermark")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
@@ -638,7 +638,7 @@ def reorder_pdf_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="reorder-pdf")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
@@ -690,7 +690,7 @@ def extract_pages_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="extract-pages")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ def generate_qr_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="qr-code")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ def remove_bg_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="remove-bg")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ def video_to_gif_route():
|
||||
|
||||
actor = resolve_web_actor()
|
||||
try:
|
||||
assert_quota_available(actor)
|
||||
assert_quota_available(actor, tool="video-frames")
|
||||
except PolicyError as e:
|
||||
return jsonify({"error": e.message}), e.status_code
|
||||
|
||||
|
||||
Reference in New Issue
Block a user