feat: Initialize frontend with React, Vite, and Tailwind CSS

- Set up main entry point for React application.
- Create About, Home, NotFound, Privacy, and Terms pages with SEO support.
- Implement API service for file uploads and task management.
- Add global styles using Tailwind CSS.
- Create utility functions for SEO and text processing.
- Configure Vite for development and production builds.
- Set up Nginx configuration for serving frontend and backend.
- Add scripts for cleanup of expired files and sitemap generation.
- Implement deployment script for production environment.
This commit is contained in:
Your Name
2026-02-28 23:31:19 +02:00
parent 3b84ebb916
commit 85d98381df
93 changed files with 5940 additions and 0 deletions

View File

@@ -0,0 +1 @@
"""Backend application routes."""

View File

@@ -0,0 +1,47 @@
"""PDF compression routes."""
from flask import Blueprint, request, jsonify
from app.extensions import limiter
from app.utils.file_validator import validate_file, FileValidationError
from app.utils.sanitizer import generate_safe_path
from app.tasks.compress_tasks import compress_pdf_task
compress_bp = Blueprint("compress", __name__)
@compress_bp.route("/pdf", methods=["POST"])
@limiter.limit("10/minute")
def compress_pdf_route():
"""
Compress a PDF file.
Accepts: multipart/form-data with 'file' field (PDF)
Optional form field 'quality': "low", "medium", "high" (default: "medium")
Returns: JSON with task_id for polling
"""
if "file" not in request.files:
return jsonify({"error": "No file provided."}), 400
file = request.files["file"]
quality = request.form.get("quality", "medium")
# Validate quality parameter
if quality not in ("low", "medium", "high"):
quality = "medium"
try:
original_filename, ext = validate_file(file, allowed_types=["pdf"])
except FileValidationError as e:
return jsonify({"error": e.message}), e.code
# Save file to temp location
task_id, input_path = generate_safe_path(ext, folder_type="upload")
file.save(input_path)
# Dispatch async task
task = compress_pdf_task.delay(input_path, task_id, original_filename, quality)
return jsonify({
"task_id": task.id,
"message": "Compression started. Poll /api/tasks/{task_id}/status for progress.",
}), 202

View File

@@ -0,0 +1,73 @@
"""PDF conversion routes (PDF↔Word)."""
from flask import Blueprint, request, jsonify
from app.extensions import limiter
from app.utils.file_validator import validate_file, FileValidationError
from app.utils.sanitizer import generate_safe_path
from app.tasks.convert_tasks import convert_pdf_to_word, convert_word_to_pdf
convert_bp = Blueprint("convert", __name__)
@convert_bp.route("/pdf-to-word", methods=["POST"])
@limiter.limit("10/minute")
def pdf_to_word_route():
"""
Convert a PDF file to Word (DOCX).
Accepts: multipart/form-data with 'file' field (PDF)
Returns: JSON with task_id for polling
"""
if "file" not in request.files:
return jsonify({"error": "No file provided."}), 400
file = request.files["file"]
try:
original_filename, ext = validate_file(file, allowed_types=["pdf"])
except FileValidationError as e:
return jsonify({"error": e.message}), e.code
# Save file to temp location
task_id, input_path = generate_safe_path(ext, folder_type="upload")
file.save(input_path)
# Dispatch async task
task = convert_pdf_to_word.delay(input_path, task_id, original_filename)
return jsonify({
"task_id": task.id,
"message": "Conversion started. Poll /api/tasks/{task_id}/status for progress.",
}), 202
@convert_bp.route("/word-to-pdf", methods=["POST"])
@limiter.limit("10/minute")
def word_to_pdf_route():
"""
Convert a Word (DOC/DOCX) file to PDF.
Accepts: multipart/form-data with 'file' field (DOC/DOCX)
Returns: JSON with task_id for polling
"""
if "file" not in request.files:
return jsonify({"error": "No file provided."}), 400
file = request.files["file"]
try:
original_filename, ext = validate_file(
file, allowed_types=["doc", "docx"]
)
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 = convert_word_to_pdf.delay(input_path, task_id, original_filename)
return jsonify({
"task_id": task.id,
"message": "Conversion started. Poll /api/tasks/{task_id}/status for progress.",
}), 202

View File

@@ -0,0 +1,35 @@
"""Local file download route — used when S3 is not configured."""
import os
from flask import Blueprint, send_file, abort, request, current_app
download_bp = Blueprint("download", __name__)
@download_bp.route("/<task_id>/<filename>", methods=["GET"])
def download_file(task_id: str, filename: str):
"""
Serve a processed file from local filesystem.
Only active in development (when S3 is not configured).
"""
# Security: sanitize inputs
# Only allow UUID-style task IDs and safe filenames
if ".." in task_id or "/" in task_id or "\\" in task_id:
abort(400, "Invalid task ID.")
if ".." in filename or "/" in filename or "\\" in filename:
abort(400, "Invalid filename.")
output_dir = current_app.config["OUTPUT_FOLDER"]
file_path = os.path.join(output_dir, task_id, filename)
if not os.path.isfile(file_path):
abort(404, "File not found or expired.")
download_name = request.args.get("name", filename)
return send_file(
file_path,
as_attachment=True,
download_name=download_name,
)

View File

@@ -0,0 +1,14 @@
"""Health check endpoint."""
from flask import Blueprint, jsonify
health_bp = Blueprint("health", __name__)
@health_bp.route("/health", methods=["GET"])
def health_check():
"""Simple health check — returns 200 if the service is running."""
return jsonify({
"status": "healthy",
"service": "SaaS-PDF API",
"version": "1.0.0",
})

122
backend/app/routes/image.py Normal file
View File

@@ -0,0 +1,122 @@
"""Image processing routes."""
from flask import Blueprint, request, jsonify
from app.extensions import limiter
from app.utils.file_validator import validate_file, FileValidationError
from app.utils.sanitizer import generate_safe_path
from app.tasks.image_tasks import convert_image_task, resize_image_task
image_bp = Blueprint("image", __name__)
ALLOWED_IMAGE_TYPES = ["png", "jpg", "jpeg", "webp"]
ALLOWED_OUTPUT_FORMATS = ["jpg", "png", "webp"]
@image_bp.route("/convert", methods=["POST"])
@limiter.limit("10/minute")
def convert_image_route():
"""
Convert an image to a different format.
Accepts: multipart/form-data with:
- 'file': Image file (PNG, JPG, JPEG, WebP)
- 'format': Target format ("jpg", "png", "webp")
- 'quality' (optional): Quality 1-100 (default: 85)
Returns: JSON with task_id for polling
"""
if "file" not in request.files:
return jsonify({"error": "No file provided."}), 400
file = request.files["file"]
output_format = request.form.get("format", "").lower()
quality = request.form.get("quality", "85")
# Validate output format
if output_format not in ALLOWED_OUTPUT_FORMATS:
return jsonify({
"error": f"Invalid format. Supported: {', '.join(ALLOWED_OUTPUT_FORMATS)}"
}), 400
# Validate quality
try:
quality = max(1, min(100, int(quality)))
except ValueError:
quality = 85
try:
original_filename, ext = validate_file(file, allowed_types=ALLOWED_IMAGE_TYPES)
except FileValidationError as e:
return jsonify({"error": e.message}), e.code
# Save file
task_id, input_path = generate_safe_path(ext, folder_type="upload")
file.save(input_path)
# Dispatch task
task = convert_image_task.delay(
input_path, task_id, original_filename, output_format, quality
)
return jsonify({
"task_id": task.id,
"message": "Image conversion started. Poll /api/tasks/{task_id}/status for progress.",
}), 202
@image_bp.route("/resize", methods=["POST"])
@limiter.limit("10/minute")
def resize_image_route():
"""
Resize an image.
Accepts: multipart/form-data with:
- 'file': Image file
- 'width' (optional): Target width
- 'height' (optional): Target height
- 'quality' (optional): Quality 1-100 (default: 85)
Returns: JSON with task_id for polling
"""
if "file" not in request.files:
return jsonify({"error": "No file provided."}), 400
file = request.files["file"]
width = request.form.get("width")
height = request.form.get("height")
quality = request.form.get("quality", "85")
# Validate dimensions
try:
width = int(width) if width else None
height = int(height) if height else None
except ValueError:
return jsonify({"error": "Width and height must be integers."}), 400
if width is None and height is None:
return jsonify({"error": "At least one of width or height is required."}), 400
if width and (width < 1 or width > 10000):
return jsonify({"error": "Width must be between 1 and 10000."}), 400
if height and (height < 1 or height > 10000):
return jsonify({"error": "Height must be between 1 and 10000."}), 400
try:
quality = max(1, min(100, int(quality)))
except ValueError:
quality = 85
try:
original_filename, ext = validate_file(file, allowed_types=ALLOWED_IMAGE_TYPES)
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 = resize_image_task.delay(
input_path, task_id, original_filename, width, height, quality
)
return jsonify({
"task_id": task.id,
"message": "Image resize started. Poll /api/tasks/{task_id}/status for progress.",
}), 202

View File

@@ -0,0 +1,39 @@
"""Task status polling endpoint."""
from flask import Blueprint, jsonify
from celery.result import AsyncResult
from app.extensions import celery
tasks_bp = Blueprint("tasks", __name__)
@tasks_bp.route("/<task_id>/status", methods=["GET"])
def get_task_status(task_id: str):
"""
Get the status of an async task.
Returns:
JSON with task state and result (if completed)
"""
result = AsyncResult(task_id, app=celery)
response = {
"task_id": task_id,
"state": result.state,
}
if result.state == "PENDING":
response["progress"] = "Task is waiting in queue..."
elif result.state == "PROCESSING":
meta = result.info or {}
response["progress"] = meta.get("step", "Processing...")
elif result.state == "SUCCESS":
task_result = result.result or {}
response["result"] = task_result
elif result.state == "FAILURE":
response["error"] = str(result.info) if result.info else "Task failed."
return jsonify(response)

View File

@@ -0,0 +1,70 @@
"""Video processing routes."""
from flask import Blueprint, request, jsonify
from app.extensions import limiter
from app.utils.file_validator import validate_file, FileValidationError
from app.utils.sanitizer import generate_safe_path
from app.tasks.video_tasks import create_gif_task
video_bp = Blueprint("video", __name__)
ALLOWED_VIDEO_TYPES = ["mp4", "webm"]
@video_bp.route("/to-gif", methods=["POST"])
@limiter.limit("5/minute")
def video_to_gif_route():
"""
Convert a video clip to an animated GIF.
Accepts: multipart/form-data with:
- 'file': Video file (MP4, WebM, max 50MB)
- 'start_time' (optional): Start time in seconds (default: 0)
- 'duration' (optional): Duration in seconds, max 15 (default: 5)
- 'fps' (optional): Frames per second, max 20 (default: 10)
- 'width' (optional): Output width, max 640 (default: 480)
Returns: JSON with task_id for polling
"""
if "file" not in request.files:
return jsonify({"error": "No file provided."}), 400
file = request.files["file"]
# Parse and validate parameters
try:
start_time = float(request.form.get("start_time", 0))
duration = float(request.form.get("duration", 5))
fps = int(request.form.get("fps", 10))
width = int(request.form.get("width", 480))
except (ValueError, TypeError):
return jsonify({"error": "Invalid parameters. Must be numeric."}), 400
# Enforce limits
if start_time < 0:
return jsonify({"error": "Start time cannot be negative."}), 400
if duration <= 0 or duration > 15:
return jsonify({"error": "Duration must be between 0.5 and 15 seconds."}), 400
if fps < 1 or fps > 20:
return jsonify({"error": "FPS must be between 1 and 20."}), 400
if width < 100 or width > 640:
return jsonify({"error": "Width must be between 100 and 640 pixels."}), 400
try:
original_filename, ext = validate_file(file, allowed_types=ALLOWED_VIDEO_TYPES)
except FileValidationError as e:
return jsonify({"error": e.message}), e.code
# Save file
task_id, input_path = generate_safe_path(ext, folder_type="upload")
file.save(input_path)
# Dispatch task
task = create_gif_task.delay(
input_path, task_id, original_filename,
start_time, duration, fps, width,
)
return jsonify({
"task_id": task.id,
"message": "GIF creation started. Poll /api/tasks/{task_id}/status for progress.",
}), 202