تحويل لوحة الإدارة الداخلية من secret header إلى session auth حقيقي مع صلاحيات admin. إضافة دعم إدارة الأدوار من داخل لوحة الإدارة نفسها، مع حماية الحسابات المعتمدة عبر INTERNAL_ADMIN_EMAILS. تحسين بيانات المستخدم في الواجهة والباكند لتشمل role وis_allowlisted_admin. إضافة اختبار frontend مخصص لصفحة /internal/admin بدل الاعتماد فقط على build واختبار routes. تحسين إضافي في الأداء عبر إزالة الاعتماد على pdfjs-dist/pdf.worker في عدّ صفحات PDF واستبداله بمسار أخف باستخدام pdf-lib. تحسين تقسيم الـ chunks في build لتقليل أثر الحزم الكبيرة وفصل أجزاء مثل network, icons, pdf-core, وeditor. التحقق الذي تم: نجاح build للواجهة. نجاح اختبار صفحة الإدارة الداخلية في frontend. نجاح اختبارات auth/admin في backend. نجاح full backend suite مسبقًا مع EXIT:0. ولو تريد نسخة أقصر جدًا، استخدم هذه: آخر التحديثات: تم تحسين نظام الإدارة الداخلية ليعتمد على صلاحيات وجلسات حقيقية بدل secret header، مع إضافة إدارة أدوار من لوحة admin نفسها، وإضافة اختبارات frontend مخصصة للوحة، وتحسين أداء الواجهة عبر إزالة pdf.worker وتحسين تقسيم الـ chunks في build. جميع الاختبارات والتحققات الأساسية المطلوبة نجح
218 lines
7.4 KiB
Python
218 lines
7.4 KiB
Python
"""Routes for new PDF conversions — PDF↔PPTX, Excel→PDF, Sign PDF."""
|
|
import os
|
|
import uuid
|
|
|
|
from flask import Blueprint, request, jsonify, current_app
|
|
|
|
from app.extensions import limiter
|
|
from app.services.policy_service import (
|
|
assert_quota_available,
|
|
build_task_tracking_kwargs,
|
|
PolicyError,
|
|
record_accepted_usage,
|
|
resolve_web_actor,
|
|
validate_actor_file,
|
|
)
|
|
from app.utils.file_validator import FileValidationError
|
|
from app.utils.sanitizer import generate_safe_path
|
|
from app.tasks.pdf_convert_tasks import (
|
|
pdf_to_pptx_task,
|
|
excel_to_pdf_task,
|
|
pptx_to_pdf_task,
|
|
sign_pdf_task,
|
|
)
|
|
|
|
pdf_convert_bp = Blueprint("pdf_convert", __name__)
|
|
|
|
ALLOWED_IMAGE_TYPES = ["png", "jpg", "jpeg", "webp"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# PDF to PowerPoint — POST /api/convert/pdf-to-pptx
|
|
# ---------------------------------------------------------------------------
|
|
@pdf_convert_bp.route("/pdf-to-pptx", methods=["POST"])
|
|
@limiter.limit("10/minute")
|
|
def pdf_to_pptx_route():
|
|
"""Convert a PDF to PowerPoint (PPTX)."""
|
|
if "file" not in request.files:
|
|
return jsonify({"error": "No file provided."}), 400
|
|
|
|
file = request.files["file"]
|
|
actor = resolve_web_actor()
|
|
try:
|
|
assert_quota_available(actor)
|
|
except PolicyError as e:
|
|
return jsonify({"error": e.message}), e.status_code
|
|
|
|
try:
|
|
original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor)
|
|
except FileValidationError as e:
|
|
return jsonify({"error": e.message}), e.code
|
|
|
|
task_id, input_path = generate_safe_path(ext, folder_type="upload")
|
|
file.save(input_path)
|
|
|
|
task = pdf_to_pptx_task.delay(
|
|
input_path, task_id, original_filename,
|
|
**build_task_tracking_kwargs(actor),
|
|
)
|
|
record_accepted_usage(actor, "pdf-to-pptx", task.id)
|
|
|
|
return jsonify({
|
|
"task_id": task.id,
|
|
"message": "Conversion started. Poll /api/tasks/{task_id}/status for progress.",
|
|
}), 202
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Excel to PDF — POST /api/convert/excel-to-pdf
|
|
# ---------------------------------------------------------------------------
|
|
@pdf_convert_bp.route("/excel-to-pdf", methods=["POST"])
|
|
@limiter.limit("10/minute")
|
|
def excel_to_pdf_route():
|
|
"""Convert an Excel file to PDF."""
|
|
if "file" not in request.files:
|
|
return jsonify({"error": "No file provided."}), 400
|
|
|
|
file = request.files["file"]
|
|
actor = resolve_web_actor()
|
|
try:
|
|
assert_quota_available(actor)
|
|
except PolicyError as e:
|
|
return jsonify({"error": e.message}), e.status_code
|
|
|
|
try:
|
|
original_filename, ext = validate_actor_file(
|
|
file, allowed_types=["xlsx", "xls"], actor=actor
|
|
)
|
|
except FileValidationError as e:
|
|
return jsonify({"error": e.message}), e.code
|
|
|
|
task_id, input_path = generate_safe_path(ext, folder_type="upload")
|
|
file.save(input_path)
|
|
|
|
task = excel_to_pdf_task.delay(
|
|
input_path, task_id, original_filename,
|
|
**build_task_tracking_kwargs(actor),
|
|
)
|
|
record_accepted_usage(actor, "excel-to-pdf", task.id)
|
|
|
|
return jsonify({
|
|
"task_id": task.id,
|
|
"message": "Conversion started. Poll /api/tasks/{task_id}/status for progress.",
|
|
}), 202
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# PowerPoint to PDF — POST /api/convert/pptx-to-pdf
|
|
# ---------------------------------------------------------------------------
|
|
@pdf_convert_bp.route("/pptx-to-pdf", methods=["POST"])
|
|
@limiter.limit("10/minute")
|
|
def pptx_to_pdf_route():
|
|
"""Convert a PowerPoint file to PDF."""
|
|
if "file" not in request.files:
|
|
return jsonify({"error": "No file provided."}), 400
|
|
|
|
file = request.files["file"]
|
|
actor = resolve_web_actor()
|
|
try:
|
|
assert_quota_available(actor)
|
|
except PolicyError as e:
|
|
return jsonify({"error": e.message}), e.status_code
|
|
|
|
try:
|
|
original_filename, ext = validate_actor_file(
|
|
file, allowed_types=["pptx", "ppt"], actor=actor
|
|
)
|
|
except FileValidationError as e:
|
|
return jsonify({"error": e.message}), e.code
|
|
|
|
task_id, input_path = generate_safe_path(ext, folder_type="upload")
|
|
file.save(input_path)
|
|
|
|
task = pptx_to_pdf_task.delay(
|
|
input_path, task_id, original_filename,
|
|
**build_task_tracking_kwargs(actor),
|
|
)
|
|
record_accepted_usage(actor, "pptx-to-pdf", task.id)
|
|
|
|
return jsonify({
|
|
"task_id": task.id,
|
|
"message": "Conversion started. Poll /api/tasks/{task_id}/status for progress.",
|
|
}), 202
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Sign PDF — POST /api/pdf-tools/sign
|
|
# ---------------------------------------------------------------------------
|
|
@pdf_convert_bp.route("/sign", methods=["POST"])
|
|
@limiter.limit("10/minute")
|
|
def sign_pdf_route():
|
|
"""Sign a PDF by overlaying a signature image.
|
|
|
|
Accepts: multipart/form-data with:
|
|
- 'file': PDF file
|
|
- 'signature': Signature image (PNG/JPG)
|
|
- 'page' (optional): 1-based page number (default: 1)
|
|
- 'x', 'y' (optional): Position in points (default: 100, 100)
|
|
- 'width', 'height' (optional): Size in points (default: 200, 80)
|
|
"""
|
|
if "file" not in request.files:
|
|
return jsonify({"error": "No PDF file provided."}), 400
|
|
if "signature" not in request.files:
|
|
return jsonify({"error": "No signature image provided."}), 400
|
|
|
|
pdf_file = request.files["file"]
|
|
sig_file = request.files["signature"]
|
|
|
|
actor = resolve_web_actor()
|
|
try:
|
|
assert_quota_available(actor)
|
|
except PolicyError as e:
|
|
return jsonify({"error": e.message}), e.status_code
|
|
|
|
try:
|
|
original_filename, ext = validate_actor_file(pdf_file, allowed_types=["pdf"], actor=actor)
|
|
except FileValidationError as e:
|
|
return jsonify({"error": e.message}), e.code
|
|
|
|
try:
|
|
_, sig_ext = validate_actor_file(sig_file, allowed_types=ALLOWED_IMAGE_TYPES, actor=actor)
|
|
except FileValidationError as e:
|
|
return jsonify({"error": f"Signature image: {e.message}"}), e.code
|
|
|
|
# Parse position parameters
|
|
try:
|
|
page = max(1, int(request.form.get("page", 1))) - 1 # Convert to 0-based
|
|
x = float(request.form.get("x", 100))
|
|
y = float(request.form.get("y", 100))
|
|
width = float(request.form.get("width", 200))
|
|
height = float(request.form.get("height", 80))
|
|
except (ValueError, TypeError):
|
|
return jsonify({"error": "Invalid position parameters."}), 400
|
|
|
|
if width <= 0 or height <= 0:
|
|
return jsonify({"error": "Width and height must be positive."}), 400
|
|
|
|
task_id = str(uuid.uuid4())
|
|
upload_dir = os.path.join(current_app.config["UPLOAD_FOLDER"], task_id)
|
|
os.makedirs(upload_dir, exist_ok=True)
|
|
|
|
input_path = os.path.join(upload_dir, f"{uuid.uuid4()}.pdf")
|
|
pdf_file.save(input_path)
|
|
|
|
signature_path = os.path.join(upload_dir, f"{uuid.uuid4()}.{sig_ext}")
|
|
sig_file.save(signature_path)
|
|
|
|
task = sign_pdf_task.delay(
|
|
input_path, signature_path, task_id, original_filename,
|
|
page, x, y, width, height,
|
|
**build_task_tracking_kwargs(actor),
|
|
)
|
|
record_accepted_usage(actor, "sign-pdf", task.id)
|
|
|
|
return jsonify({
|
|
"task_id": task.id,
|
|
"message": "Signing started. Poll /api/tasks/{task_id}/status for progress.",
|
|
}), 202
|