import os import sys import secrets import tempfile import subprocess from flask import Flask, request, render_template_string, jsonify, send_file, redirect, url_for, flash from werkzeug.utils import secure_filename from flask_talisman import Talisman from flask_limiter import Limiter from flask_limiter.util import get_remote_address from flask_cors import CORS from functools import wraps from dotenv import load_dotenv from flask import send_file import json # --- ENV GENERATOR --- def generate_env(): secret_key = secrets.token_urlsafe(32) api_token = secrets.token_hex(20) with open(".env", "w") as f: f.write(f"FLASK_SECRET_KEY={secret_key}\n") f.write(f"FLASK_API_TOKEN={api_token}\n") print("✅ .env generated") print(f"FLASK_SECRET_KEY={secret_key}") print(f"FLASK_API_TOKEN={api_token}") print("⚠ Keep .env out of version control!") sys.exit(0) if "--genenv" in sys.argv: generate_env() # --- LOAD ENV --- load_dotenv() BASE_DIR = os.path.dirname(os.path.abspath(__file__)) MOJO_FMT_PATH = os.path.join(BASE_DIR, "mojofmt.py") SECRET_KEY = os.environ.get('FLASK_SECRET_KEY') API_TOKEN = os.environ.get('FLASK_API_TOKEN') if not SECRET_KEY or not API_TOKEN: raise RuntimeError("FLASK_SECRET_KEY and FLASK_API_TOKEN must be set (use --genenv to create .env)") app = Flask(__name__) app.secret_key = SECRET_KEY app.config.update( SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_SECURE=True, SESSION_COOKIE_SAMESITE='Lax' ) # Enable CORS for all routes CORS(app) # CSP — only self and cdn.jsdelivr.net for scripts/styles csp = { 'default-src': ["'self'"], 'script-src': ["'self'", 'https://cdn.jsdelivr.net', "'unsafe-inline'"], 'style-src': ["'self'", 'https://cdn.jsdelivr.net', "'unsafe-inline'"], } Talisman(app, content_security_policy=csp) limiter = Limiter(key_func=get_remote_address, app=app, default_limits=["100/hour"]) def get_formatter_version(): try: result = subprocess.run( ["python3", MOJO_FMT_PATH, "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) if result.returncode == 0: return result.stdout.strip() else: return "Unknown" except Exception as e: app.logger.warning(f"Could not get formatter version: {e}") return "Unknown" FORMATTER_VERSION = get_formatter_version() def require_api_token(f): @wraps(f) def decorated(*args, **kwargs): auth = request.headers.get('Authorization', '') if not auth.startswith('Bearer ') or auth[len('Bearer '):] != API_TOKEN: return jsonify({"error": "Unauthorized"}), 401 return f(*args, **kwargs) return decorated HTML_TEMPLATE = """ Mojolicious Template Code Formatter

Mojolicious Template Code Formatter v{{ formatter_version }}

""" def run_mojofmt(input_text: str) -> str: app.logger.debug("Running mojofmt") with tempfile.TemporaryDirectory() as tmpdir: in_path = os.path.join(tmpdir, "input.txt") out_path = os.path.join(tmpdir, "output.txt") with open(in_path, 'w', encoding='utf-8') as f: f.write(input_text) result = subprocess.run( ['python3', MOJO_FMT_PATH, '-o', out_path, in_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) if result.returncode != 0: raise RuntimeError(f"mojofmt failed:\n{result.stderr.strip()}") with open(out_path, 'r', encoding='utf-8') as f: return f.read() @app.route("/", methods=["GET", "POST"]) def index(): # Handle both GET and POST requests # POST requests are redirected to use AJAX instead if request.method == "POST": # If someone tries to submit the form traditionally, redirect to GET return redirect(url_for('index')) # Serve the HTML template return render_template_string( HTML_TEMPLATE, formatter_version=FORMATTER_VERSION ) @app.route("/api/format_ajax", methods=["POST"]) @limiter.limit("10/minute") def api_format_ajax(): """AJAX endpoint for formatting text""" if not request.is_json: return jsonify({"error": "JSON body required"}), 400 data = request.get_json() input_text = data.get("input_text", "") remove_empty = bool(data.get("remove_empty", False)) if not input_text.strip(): return jsonify({"error": "No input data provided."}), 400 try: formatted_text = run_mojofmt(input_text) if remove_empty: formatted_text = "\n".join( line for line in formatted_text.splitlines() if line.strip() ) return jsonify({"formatted_text": formatted_text}) except RuntimeError as e: return jsonify({"error": str(e)}), 500 @app.route("/api/format", methods=["POST"]) @limiter.limit("10/minute") @require_api_token def api_format(): """Original API endpoint with token authentication""" if not request.is_json: return jsonify({"error": "JSON body required"}), 400 data = request.get_json() text = data.get("text", "") remove_empty = bool(data.get("remove_empty")) if not text: return jsonify({"error": "Missing 'text'"}), 400 try: formatted = run_mojofmt(text) if remove_empty: formatted = "\n".join([line for line in formatted.splitlines() if line.strip()]) return jsonify({"formatted_text": formatted}) except RuntimeError as e: return jsonify({"error": str(e)}), 500 if __name__ == "__main__": app.run(host="0.0.0.0", port=8000, debug=True)