Security audit, bug fixes, auto-save, and writing assistant improvements

Backend security & correctness:
- Add rate_limit.py: sliding-window rate limiter (asyncio) applied to login,
  register, forgot/reset password endpoints (10/60s or 5/300s per IP)
- app.py: add security headers in after_request (X-Frame-Options, CSP,
  X-Content-Type-Options, Referrer-Policy) using setdefault to preserve SSE headers
- auth.py: refactor duplicate login_required/admin_required into shared _check_auth()
- config.py: add TRUST_PROXY_HEADERS for proxy-aware client IP resolution
- routes/auth.py: rate limiting, _client_ip() helper, cleaned-up reset_password route
- routes/chat.py, notes.py, tasks.py: int() DoS fix on last_event_id; limit capped
  at 500; date.fromisoformat() wrapped in try/except → 400 on invalid dates
- services/auth.py: fix Setting.user_id update bug (filter on NULL not user.id);
  reset_password_with_token returns int|None (user_id) instead of bool
- services/backup.py: add _security_notice to full backup JSON export
- services/assist.py: system prompt explicitly preserves markdown list structure
  and nested indented sub-items

Infrastructure:
- docker-compose.yml: add healthcheck on app service (/api/health, 10s interval)
- .dockerignore: prevent secrets/node_modules/__pycache__/.env.* leaking into build

Frontend bug fixes:
- TaskCard.vue, TaskViewerView.vue: fix isOverdue() timezone bug (ISO string compare)
- useAssist.ts: accept() now resets state to idle when document changed since proposal
- stores/chat.ts: fix memory leak in _pollUntilLoaded() (try/catch around fetchStatus)
- TiptapEditor.vue: selection offset uses closest-match strategy (not first-match)
- utils/markdown.ts: explicit DOMPurify config with FORCE_BODY; remove as const
  (DOMPurify expects mutable string[])

New features:
- Auto-save (5-minute interval) in NoteEditorView and TaskEditorView — only when
  editing an existing dirty record; silent on error, shows "Auto-saved" toast
- sectionParser.ts: top-level bullet/numbered list items are now individual sections
  in the AI Assist panel (previously treated as one undifferentiated block)
- editor-shared.css: extracted ~500 lines of CSS duplicated between both editors;
  includes .inline-assist-btn at global scope (required for teleported elements)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 20:08:44 -05:00
parent 7b92d13863
commit 667ccd70cd
24 changed files with 865 additions and 1193 deletions
+16 -6
View File
@@ -45,7 +45,7 @@ async def list_notes_route():
tag = request.args.getlist("tag")
sort = request.args.get("sort", "updated_at")
order = request.args.get("order", "desc")
limit = request.args.get("limit", 50, type=int)
limit = min(request.args.get("limit", 50, type=int), 500)
offset = request.args.get("offset", 0, type=int)
# Default to non-task notes only; ?is_task=true for tasks, ?all=true for everything
@@ -74,7 +74,10 @@ async def create_note_route():
priority = data.get("priority")
due_date = None
if data.get("due_date"):
due_date = date.fromisoformat(data["due_date"])
try:
due_date = date.fromisoformat(data["due_date"])
except ValueError:
return jsonify({"error": "Invalid date format. Use YYYY-MM-DD."}), 400
note = await create_note(
uid,
@@ -178,9 +181,13 @@ async def update_note_route(note_id: int):
fields[key] = data[key]
if "due_date" in data:
fields["due_date"] = (
date.fromisoformat(data["due_date"]) if data["due_date"] else None
)
if data["due_date"]:
try:
fields["due_date"] = date.fromisoformat(data["due_date"])
except ValueError:
return jsonify({"error": "Invalid date format. Use YYYY-MM-DD."}), 400
else:
fields["due_date"] = None
if "body" in fields:
fields["tags"] = extract_tags(fields["body"])
@@ -272,7 +279,10 @@ async def assist_stream_route():
return jsonify({"error": "No active assist generation"}), 404
last_id_str = request.headers.get("Last-Event-ID") or request.args.get("last_event_id")
last_id = int(last_id_str) if last_id_str is not None else -1
try:
last_id = int(last_id_str) if last_id_str is not None else -1
except (ValueError, TypeError):
last_id = -1
async def stream():
cursor = last_id