NEW-1: add_header in location /api block suppressed server-level security
headers (HSTS, CSP, X-Frame-Options, etc). Duplicate all security headers
into the /api block explicitly per nginx inheritance rules.
NEW-2: Add 0.0.0.0/8 to _BLOCKED_NETWORKS — on Linux 0.0.0.0 connects
to localhost, bypassing the existing loopback check.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Nginx already forwards X-Forwarded-For and X-Real-IP, but the backend
read request.client.host directly — always returning 172.18.0.x. Added
get_client_ip() helper to audit service; updated all 13 call sites.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reduce session expiry from 30 days to 7 days of inactivity while
preserving a 30-day absolute token lifetime for itsdangerous:
- SESSION_MAX_AGE_DAYS=7: sliding window for DB expires_at + cookie
- SESSION_TOKEN_HARD_CEILING_DAYS=30: itsdangerous max_age (prevents
rejecting renewed tokens whose creation timestamp exceeds 7 days)
- get_current_user: silently extends expires_at and re-issues cookie
when >1 day has elapsed since last renewal
- Active users never notice; 7 days of inactivity forces re-login;
30-day absolute ceiling forces re-login regardless of activity
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- W-01: Update README.md security section to reflect removed in-memory
rate limiter and add /setup to nginx rate-limited endpoint list
- W-02: Replace misleading ALLOW_LAN_NTFY reference with actionable
guidance to edit _BLOCKED_NETWORKS directly
- S-04: Add comment explaining burst=3 on /api/auth/setup vs burst=5
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove ineffective in-memory IP rate limiter from auth.py (F-01):
nginx limit_req_zone handles real-IP throttling, DB lockout is the per-user guard
- Block RFC 1918 + IPv6 ULA ranges in ntfy SSRF guard (F-02):
prevents requests to Docker-internal services via user-controlled ntfy URL
- Rate-limit /api/auth/setup at nginx with burst=3 (F-06)
- Document production deployment checklist in .env.example (F-03/F-04/F-05)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove RFC 1918 blocks from _BLOCKED_NETWORKS — only block loopback
and link-local. Self-hosted ntfy servers are typically on the same LAN.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add ntfy columns to Settings model (server_url, topic, auth_token, enabled, per-type toggles, lead times)
- Create NtfySent dedup model to prevent duplicate notifications
- Create ntfy service with SSRF validation and async httpx send
- Create ntfy_templates service with per-type payload builders
- Create APScheduler background dispatch job (60s interval, events/reminders/todos/projects)
- Register scheduler in main.py lifespan with max_instances=1
- Update SettingsUpdate with ntfy validators (URL scheme, topic regex, lead time ranges)
- Update SettingsResponse with ntfy fields; ntfy_has_token computed, token never exposed
- Add POST /api/settings/ntfy/test endpoint
- Update GET/PUT settings to use explicit _to_settings_response() helper
- Add Alembic migration 022 for ntfy settings columns + ntfy_sent table
- Add httpx==0.27.2 and apscheduler==3.10.4 to requirements.txt
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- W1: Add key prop to FullCalendar so firstDay change triggers remount
- W2: Revert firstDayOfWeek toggle state on API failure
- S1: Extract _rule_int helper in recurrence service to reduce duplication
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The parent template is hidden from the calendar listing, but the
recurrence service was only generating children starting from the
second occurrence. Now generates a child for the parent's own start
date so the first occurrence is always visible.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
model_dump() includes None values for optional RecurrenceRule fields.
When serialized to JSON, these become explicit nulls (e.g. "weekday": null).
The recurrence service then does int(None) which raises TypeError.
Fix: strip None values when serializing rule to JSON, and add defensive
None handling in recurrence service for all rule.get() calls.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>