diff --git a/meals.py b/meals.py index 1f16ec9..482c38a 100644 --- a/meals.py +++ b/meals.py @@ -342,6 +342,33 @@ def ntfy_callback(): return {"message": "Response recorded", "status": status}, 200 +# ── Auto-login from ntfy notification click ────────────────────────────────── + +@meals_bp.route("/auto-login/") +def auto_login(token): + """Log in a user via their ntfy callback token, then redirect to dashboard. + + Clicking an ntfy notification opens this URL. The callback token + identifies the user, so they don't need to re-enter credentials. + """ + user = get_user_by_callback_token(token) + if user is None: + flash(t("Invalid login link."), "error") + return redirect(url_for("auth.login")) + + session["user_id"] = user["id"] + session["username"] = user["username"] + session["is_admin"] = bool(user["is_admin"]) + session["household_id"] = user["household_id"] + session["household_name"] = user["household_name"] or "" + + date_param = request.args.get("date", "") + target = url_for("meals.dashboard") + if date_param: + target += f"?date={date_param}" + return redirect(target) + + # ── Cron tick (called by meal-cron container) ───────────────────────────────── REMINDER_HOURS = {"lunch": 10, "dinner": 16} diff --git a/ntfy.py b/ntfy.py index f12dec5..9fd14f0 100644 --- a/ntfy.py +++ b/ntfy.py @@ -8,9 +8,9 @@ import urllib.error from datetime import date -def _ntfy_headers(token: str) -> dict[str, str]: - """Build Authorization header if token is provided.""" - headers = {"Content-Type": "application/json"} +def _ntfy_headers(token: str, extra: dict[str, str] | None = None) -> dict[str, str]: + """Build headers for ntfy request, optionally with Authorization.""" + headers = dict(extra) if extra else {} if token.strip(): headers["Authorization"] = f"Bearer {token.strip()}" return headers @@ -39,49 +39,54 @@ def send_meal_reminder( """ date_str = for_date.isoformat() meal_label = "Lunch" if meal_type == "lunch" else "Dinner" - emoji = "🌤" if meal_type == "lunch" else "🌙" callback_url = f"{app_base_url.rstrip('/')}/api/ntfy-callback" - payload = { - "topic": ntfy_url.rstrip("/").rsplit("/", 1)[-1], - "title": f"{emoji} {meal_label} — Meal Reminder", - "message": f"Will you be home for {meal_label.lower()} on {date_str}?", - "priority": 4, - "tags": ["plate_with_cutlery"], - "actions": [ - { - "action": "http", - "label": "🏠 Home", - "method": "POST", - "url": callback_url, - "headers": {"Content-Type": "application/json"}, - "body": json.dumps({ - "token": callback_token, - "meal_type": meal_type, - "date": date_str, - "status": "yes", - }), - }, - { - "action": "http", - "label": "🍽 Out", - "method": "POST", - "url": callback_url, - "headers": {"Content-Type": "application/json"}, - "body": json.dumps({ - "token": callback_token, - "meal_type": meal_type, - "date": date_str, - "status": "no", - }), - }, - ], - "click": f"{app_base_url.rstrip('/')}/dashboard?date={date_str}", - } + topic = ntfy_url.rstrip("/").rsplit("/", 1)[-1] + message = f"Will you be home for {meal_label.lower()} on {date_str}?" - data = json.dumps(payload).encode("utf-8") - req = urllib.request.Request(ntfy_url, data=data, headers=_ntfy_headers(ntfy_token)) + home_body = json.dumps({ + "token": callback_token, + "meal_type": meal_type, + "date": date_str, + "status": "yes", + }) + out_body = json.dumps({ + "token": callback_token, + "meal_type": meal_type, + "date": date_str, + "status": "no", + }) + + actions = json.dumps([ + { + "action": "http", + "label": "Home", + "method": "POST", + "url": callback_url, + "headers": {"Content-Type": "application/json"}, + "body": home_body, + }, + { + "action": "http", + "label": "Out", + "method": "POST", + "url": callback_url, + "headers": {"Content-Type": "application/json"}, + "body": out_body, + }, + ]) + + headers = _ntfy_headers(ntfy_token, { + "Title": f"{meal_label} - Meal Reminder", + "Priority": "4", + "Tags": "plate_with_cutlery", + "Actions": actions, + "Click": f"{app_base_url.rstrip('/')}/auto-login/{callback_token}?date={date_str}", + }) + + data = message.encode("utf-8") + req = urllib.request.Request(ntfy_url, data=data, headers=headers) try: with urllib.request.urlopen(req, timeout=10) as resp: @@ -93,16 +98,16 @@ def send_meal_reminder( def send_test_notification(ntfy_url: str, ntfy_token: str) -> bool: """Send a simple test notification to verify ntfy configuration.""" - payload = { - "topic": ntfy_url.rstrip("/").rsplit("/", 1)[-1], - "title": "✅ Meal Tracker — Test", - "message": "Your ntfy integration is working! You'll receive meal reminders here.", - "priority": 3, - "tags": ["white_check_mark"], - } + topic = ntfy_url.rstrip("/").rsplit("/", 1)[-1] - data = json.dumps(payload).encode("utf-8") - req = urllib.request.Request(ntfy_url, data=data, headers=_ntfy_headers(ntfy_token)) + headers = _ntfy_headers(ntfy_token, { + "Title": "Meal Tracker - Test", + "Priority": "3", + "Tags": "white_check_mark", + }) + + data = "Your ntfy integration is working! You'll receive meal reminders here.".encode("utf-8") + req = urllib.request.Request(ntfy_url, data=data, headers=headers) try: with urllib.request.urlopen(req, timeout=10) as resp: