Add in-app Settings page for secrets; move Gmail creds out of source

- settings.py: JSON-on-volume store with env fallback; secrets never rendered.
- /settings page (⚙️ in nav) to set Gmail address + app password + reminder prefs;
  password fields are write-only (blank keeps existing).
- reminders.py reads Gmail creds from Settings instead of a hardcoded password
  (resolves #2 in code — note the old password is still in git history; rotate it).
- mac_notification no longer crashes off-macOS; FLASK_DEBUG gates debug (#7).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 16:38:20 -05:00
parent be74ca1eb7
commit 74d5e196c7
5 changed files with 159 additions and 8 deletions

26
app.py
View File

@@ -6,6 +6,7 @@ import re
from datetime import datetime, date
import calendar as cal_module
from storage import load_json, save_json
from settings import load_settings, save_settings, SECRET_KEYS
from categories import CATEGORIES, CATEGORY_COLORS
from bills import load_bills, save_bills, bills_for_month, calendar_grid, get_week_num
from sinking import (load_funds, save_funds, FUND_COLORS, monthly_needed,
@@ -2847,5 +2848,28 @@ def income_delete(entry_id):
return redirect(url_for("income_view", year=year, month=month))
@app.route("/settings", methods=["GET", "POST"])
def settings_view():
current = load_settings()
if request.method == "POST":
new = dict(current)
new["gmail_address"] = request.form.get("gmail_address", "").strip()
new["reminder_recipient"] = request.form.get("reminder_recipient", "").strip()
new["reminders_enabled"] = request.form.get("reminders_enabled") == "on"
# Secret fields: only overwrite when a new value is supplied; a blank
# field keeps the existing stored secret.
pw = request.form.get("gmail_app_password", "").strip()
if pw:
new["gmail_app_password"] = pw
save_settings(new)
return redirect(url_for("settings_view", saved=1))
# Never send secret values to the template — only whether each one is set.
safe = {k: ("" if k in SECRET_KEYS else v) for k, v in current.items()}
secrets_status = {k: bool(current.get(k)) for k in SECRET_KEYS}
return render_template("settings.html", settings=safe,
secrets_status=secrets_status,
saved=request.args.get("saved"))
if __name__ == "__main__":
app.run(debug=True, port=5000)
app.run(debug=os.environ.get("FLASK_DEBUG") == "1", port=5000)