v0.6.0: Sobors.hu parser, HTTP auth, recipe validation, UI polish

- New sobors.hu parser with ingredient groups and section headers
- Incomplete recipe warnings (missing ingredients/instructions)
- Optional HTTP Basic Auth (configurable on settings page)
- Brand text: "Recept" in white, "Importáló" in blue
- Larger logo (36px), favicon using logo_notext.svg

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 18:07:05 +01:00
parent 76290770f4
commit a0bcb62588
7 changed files with 237 additions and 12 deletions
+63 -2
View File
@@ -1,9 +1,11 @@
"""Flask application — recipe importer web UI."""
import hashlib
import os
import secrets
import traceback
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, send_from_directory
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, send_from_directory, Response
from app import config
from app.scraper import scrape, supported_sites
@@ -20,6 +22,42 @@ app.secret_key = os.environ.get("SECRET_KEY", "recipe-importer-dev-key")
VERSION = os.environ.get("VERSION", "dev")
# ---------------------------------------------------------------------------
# Basic auth middleware
# ---------------------------------------------------------------------------
_AUTH_EXEMPT = {"/health", "/assets/logo.svg", "/assets/logo_notext.svg"}
@app.before_request
def _check_basic_auth():
"""Enforce HTTP Basic Auth if configured."""
if request.path in _AUTH_EXEMPT or request.path.startswith("/assets/"):
return None
cfg = config.load()
auth_user = cfg.get("auth_username", "").strip()
auth_hash = cfg.get("auth_password_hash", "").strip()
if not auth_user or not auth_hash:
return None # auth not configured — allow all
auth = request.authorization
if (auth
and auth.type == "basic"
and auth.username == auth_user
and _hash_password(auth.password) == auth_hash):
return None # credentials match
return Response(
"Hitelesítés szükséges.", 401,
{"WWW-Authenticate": 'Basic realm="Recept Importáló"'},
)
def _hash_password(password: str) -> str:
"""SHA-256 hex digest of password."""
return hashlib.sha256(password.encode("utf-8")).hexdigest()
# ---------------------------------------------------------------------------
# Routes
# ---------------------------------------------------------------------------
@@ -46,6 +84,20 @@ def settings():
cfg["mealie_api_key"] = request.form.get("mealie_api_key", "").strip()
cfg["tandoor_url"] = request.form.get("tandoor_url", "").strip().rstrip("/")
cfg["tandoor_api_key"] = request.form.get("tandoor_api_key", "").strip()
# Auth settings
new_user = request.form.get("auth_username", "").strip()
new_pass = request.form.get("auth_password", "").strip()
if new_user:
cfg["auth_username"] = new_user
if new_pass:
# Only update password hash if a new password was provided
cfg["auth_password_hash"] = _hash_password(new_pass)
else:
# Clear auth if username removed
cfg.pop("auth_username", None)
cfg.pop("auth_password_hash", None)
config.save(cfg)
flash("Beállítások mentve.", "success")
return redirect(url_for("settings"))
@@ -125,8 +177,17 @@ def scrape_url():
except Exception:
pass # non-fatal
# Validate completeness
warnings = []
real_ingredients = [i for i in data.get("ingredients", []) if "group" not in i]
if not real_ingredients:
warnings.append("A recept nem tartalmaz hozzávalókat.")
if not data.get("instructions"):
warnings.append("A recept nem tartalmaz elkészítési lépéseket.")
return jsonify({"ok": True, "data": data, "duplicate": duplicate,
"tandoor_duplicate": tandoor_duplicate})
"tandoor_duplicate": tandoor_duplicate,
"warnings": warnings})
except Exception as exc:
return jsonify({"ok": False, "error": str(exc), "trace": traceback.format_exc()})