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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user