L-01: Add Cache-Control: no-store to all /api/ responses via nginx
L-02: Validate ntfy_server_url against blocked networks at save time
I-03: Add Permissions-Policy header to restrict unused browser APIs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- W-01: Add date_of_birth validators to RegisterRequest and ProfileUpdate
(reject future dates and years before 1900)
- W-05: Replace .toISOString().slice() with local date formatting for
DatePicker max prop on registration form
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
UserDetailResponse was built from UserListItem (which excludes
date_of_birth), so the field always returned null. Explicitly
pass user.date_of_birth to the response constructor.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds date_of_birth to UserDetailResponse schema, AdminUserDetail
TypeScript type, and the User Information card in UserDetailSection.
Displays formatted date with age in parentheses (e.g. "3/02/2000 (26)").
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- S-01: Extract _EMAIL_REGEX, _validate_email_format, _validate_name_field
shared helpers in schemas/auth.py — used by RegisterRequest, ProfileUpdate,
and admin.CreateUserRequest (eliminates 3x duplicated regex)
- S-04: Migration 038 replaces plain unique constraint on email with a
partial unique index WHERE email IS NOT NULL
- Email is now required on registration (was optional)
- Date of birth is now required on registration, editable in settings
- User model gains date_of_birth (Date, nullable) column
- ProfileUpdate/ProfileResponse include date_of_birth
- Registration form adds required Email, Date of Birth fields
- Settings Profile card adds Date of Birth input (save-on-blur)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
C-01: Replace setattr loop with explicit field assignment in update_profile
C-02: Fix useEffect dependency to profileQuery.dataUpdatedAt for re-sync
W-01: Add audit log entry for profile updates
W-02: Use less misleading generic error for email uniqueness on registration
W-03: Early return on empty PUT body to avoid unnecessary commit
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Registration form now collects optional preferred_name and email fields.
Settings page Profile card expanded with first name, last name, and email
(editable via new GET/PUT /api/auth/profile endpoints). Email uniqueness
enforced on both registration and profile update. No migrations needed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
In production, CORS_ORIGINS now defaults to UMBRA_URL so deployers only
need to set the external URL once. In development it defaults to
http://localhost:5173 (Vite dev server). Explicit CORS_ORIGINS env var
is still respected as an override for multi-origin or custom setups.
This means a production .env only needs: ENVIRONMENT, SECRET_KEY,
UMBRA_URL, and DB credentials. COOKIE_SECURE and CORS_ORIGINS both
auto-derive.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Align with config.py check so the fatal safety exit triggers correctly
if this file is used verbatim in production.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Convert to two-stage build: builder stage installs gcc and compiles
Python C extensions, runtime stage copies only the installed packages.
Removes gcc and postgresql-client from the production image, reducing
attack surface. postgresql-client was unused (healthchecks use urllib).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PT-03: Make UMBRA_URL configurable via env var (default http://localhost).
Replaces hardcoded http://10.0.69.35 in notification dispatch job and
ntfy test endpoint. Add UMBRA_URL to .env.example.
PT-05: Add explicit path="/" to session cookie for clarity.
PT-06: Add concurrent session limit (MAX_SESSIONS_PER_USER, default 10).
When exceeded, oldest sessions are revoked. New login always succeeds.
PT-07: Escape LIKE metacharacters (%, _) in admin audit log action
filter to prevent wildcard abuse.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
FastAPI trailing-slash redirects (307) were using http:// instead of
https:// because uvicorn wasn't reading X-Forwarded-Proto from the
reverse proxy. When Pangolin (TLS-terminating proxy) received the
http:// redirect it returned 503, breaking all list endpoints
(/events, /calendars, /settings, /projects, /people, /locations).
Adding --proxy-headers makes uvicorn honour X-Forwarded-Proto so
redirects use the correct scheme. --forwarded-allow-ips '*' trusts
headers from any IP since nginx sits on the Docker bridge network.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
COOKIE_SECURE now defaults to None and auto-derives from ENVIRONMENT
(production → true, else false) via a Pydantic model_validator. Explicit
env var values are still respected as an override escape hatch. Adds a
startup log line showing the resolved value. Restructures .env.example
with clear sections and inline docs, removes redundant production
checklist block.
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>
- L-01: Setup endpoint used scalar_one_or_none() on unbounded User
query, throwing 500 MultipleResultsFound when >1 user exists.
Replaced with select(func.count()) for a reliable check.
- L-02: Change-password allowed reusing the same password, defeating
must_change_password enforcement. Added equality check before
accepting the new password.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- W-01: Move is_active check before hash upgrade so disabled accounts
don't get their password hash silently mutated on rejected login
- W-02: Narrow interceptor exclusion to specific auth endpoints instead
of blanket /auth/* prefix (future-proofs against new auth routes)
- W-03: Add null guard on optimistic setQueryData to handle undefined
cache gracefully instead of spreading undefined
- S-01: Clear loginError when switching from register back to login mode
- S-03: Add detail dict to auth.login_blocked_inactive audit event
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Backend: reject is_active=False users with HTTP 403 after password
verification but before session creation (prevents last_login_at
update, lockout reset, and MFA token issuance for disabled accounts)
- Frontend: optimistic setQueryData on successful login eliminates the
form flash between mutation success and auth query refetch
- LockScreen: replace lockoutMessage + toast.error with unified
loginError inline alert for 401/403/423 responses
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1. Audit log: COALESCE target_username with detail JSON fallback so
deleted users still show their username in the target column
(target_user_id is SET NULL by FK cascade, but detail JSON
preserves the username).
2. Create/get user: add exclude={"active_sessions"} to model_dump()
calls — UserListItem defaults active_sessions=0, so model_dump()
includes it, then the explicit active_sessions=N keyword argument
causes a duplicate keyword TypeError. DB commit already happened,
so the user exists but the response was a 500.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
S-03: Delete toast now shows the deleted username from the API response
S-04: Delete button hidden for the current admin's own row (backend
still guards with 403, but no reason to show a dead button)
Adds username to auth status response so the frontend can identify
the current user.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Migration 036 adds ondelete rules to 5 transitive FKs that would
otherwise block user deletion (calendar_events via calendars,
project_tasks via projects, todos via projects, etc.).
DELETE /api/admin/users/{user_id} with self-action guard, last-admin
guard, session revocation, and audit logging. Frontend gets a red
two-click confirm button in the IAM actions menu.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three functions (update_user_role, toggle_mfa_enforce, toggle_user_active)
had `request: Request` without a default after Path params with defaults,
causing Python SyntaxError on import.
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>
When a login targets a non-existent username, run Argon2id against a
pre-computed dummy hash so response time (~60ms) matches wrong-password
attempts. Also reorder the login flow to run verify_password_with_upgrade
BEFORE the lockout check, preventing timing side-channels that could
distinguish locked accounts from wrong passwords.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add max_length constraints to all string fields in request schemas,
matching DB column limits (title:255, description:5000, etc.)
- Add min_length=1 to required name/title fields
- Add ConfigDict(extra="forbid") to all request schemas to reject
unknown fields (prevents silent field injection)
- Add Path(ge=1, le=2147483647) to all integer path parameters across
all routers to prevent integer overflow → 500 errors
- Add max_length to TOTP inline schemas (code:6, mfa_token:256, etc.)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the admin-only verify_xhr dependency with a pure ASGI
CSRFHeaderMiddleware that validates X-Requested-With: XMLHttpRequest
on all POST/PUT/PATCH/DELETE requests globally. Pre-auth endpoints
(login, setup, register, totp-verify, enforce-setup/confirm) are
exempt. This closes the CSRF gap where non-admin routes accepted
requests without origin validation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move active_sessions field from UserDetailResponse into UserListItem
so GET /admin/users returns session counts. Uses a correlated subquery
to count non-revoked, non-expired sessions per user.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Added . to the username character whitelist regex. No security
reason to exclude it — dots are standard in usernames.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Migration 022 created a unique INDEX (ix_ntfy_sent_notification_key),
not a named unique CONSTRAINT. Migration 034 was trying to drop a
constraint name that only existed on upgraded DBs. Fixed to drop the
index instead, which works on both fresh and upgrade paths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
W-03: Unify split transactions — _create_db_session() now uses flush()
instead of commit(), callers own the final commit.
W-04: Time-bound dedup key fetch to 7-day purge window.
S-01: Type admin dashboard response with RecentLoginItem/RecentAuditItem.
S-02: Convert starred events index to partial index WHERE is_starred = true.
S-03: EventTemplate.created_at default changed to func.now() for consistency.
S-04: Add single-worker scaling note to weather cache.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
C-01: verifyTotp now sends backup_code field when in backup mode
C-02: Backup code input filter allows alphanumeric chars (not digits only)
W-01: Audit log ACTION_TYPES aligned with actual backend action strings
W-02: Added extra="forbid" to SetupRequest and LoginRequest schemas
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Migration 006 seeds default calendar rows. On a fresh install, no users
exist when migration 030 runs, so the backfill SELECT returns NULL and
SET NOT NULL fails. Now deletes orphan calendars before enforcing the
constraint — account setup will recreate defaults for new users.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove unused imports (UserCheck, Loader2, ShieldOff) and replace
non-existent SmartphoneOff icon with Smartphone in admin components.
Includes backend query fixes, performance indexes migration, and
admin page shared utilities extraction.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Critical fixes:
- C-01: Pass user_id to _mark_sent/_already_sent (ntfy crash)
- C-02: Align frontend HTTP methods with backend routes (PATCH->PUT,
DELETE->POST, fix reset-password/enforce-mfa/disable-mfa paths)
- C-03: Add X-Requested-With to CORS allow_headers
- C-04: Replace scalar_one_or_none with func.count for auth/status
Warning fixes:
- W-01: Batch audit log into same transaction in create_user, setup, register
- W-02: Extract users array from UserListResponse wrapper in useAdminUsers
- W-03: Update password hint from "8 chars" to "12 chars" in CreateUserDialog
- W-04: Remove password input from reset flow, show returned temp password
- W-06: Remove unused actor_alias variable in admin_dashboard
- W-07: Resolve usernames in dashboard audit entries via JOIN, remove
ip_address column from recent_logins (not tracked on User model)
Suggestions applied:
- S-01/S-06: Add extra="forbid" to all admin mutation schemas
- S-04: Add ondelete="SET NULL" to audit_log.actor_user_id FK
- S-05: Improve registration error message for better UX
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- backend: add POST /auth/totp/enforce-setup and /auth/totp/enforce-confirm
endpoints that operate on mfa_enforce_token (not session cookie), generate
TOTP secret/QR/backup codes, verify confirmation code, enable TOTP, clear
mfa_enforce_pending flag, and issue a full session cookie
- frontend: expand LockScreen to five modes — login, first-run setup,
open registration, TOTP challenge, MFA enforcement setup (QR -> verify ->
backup codes), and forced password change; all modes share AmbientBackground
and the existing card layout; registration visible only when
authStatus.registration_open is true
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>
Code changes (S-01, S-02, S-05):
- DRY nginx proxy blocks via shared proxy-params.conf include
- Add ENVIRONMENT and CORS_ORIGINS to .env.example
- Remove unused X-Requested-With from CORS allow_headers
Documentation updates:
- README.md: reflect auth upgrade, security hardening, production
deployment guide with secret generation commands, updated architecture
diagram, current project structure and feature list
- CLAUDE.md: codify established dev workflow (branch → implement →
test → QA → merge), update auth/infra/stack sections, add authority
links for progress.md and ntfy.md
- progress.md: add Phase 11 (auth upgrade) and Phase 12 (pentest
remediation), update file inventory, fix outstanding items
- ui_refresh.md: update current status line
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Clear IP failure counter on successful /change-password (W-01)
- Add nginx rate limiting for /api/auth/totp-verify (W-02)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove external backend port 8000 exposure (VULN-01)
- Conditionally disable Swagger/ReDoc/OpenAPI in non-dev (VULN-01)
- Suppress nginx and uvicorn server version headers (VULN-07)
- Fix CSP to allow Google Fonts (fonts.googleapis.com/gstatic) (VULN-08)
- Add nginx rate limiting on auth endpoints (10r/m burst=5) (VULN-09)
- Block dotfile access (/.env, /.git) while preserving .well-known (VULN-10)
- Make CORS origins configurable, tighten methods/headers (VULN-11)
- Run both containers as non-root users (VULN-12)
- Add IP rate limit + account lockout to /change-password
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- [C-1] Add rate limiting and account lockout to /verify-password endpoint
- [W-3] Add max length validator (128 chars) to VerifyPasswordRequest
- [W-1] Move activeMutations to ref in useLock to prevent timer thrashing
- [W-5] Add user_id field to frontend Settings interface
- [S-1] Export auth schemas from schemas registry
- [S-3] Add aria-label to LockOverlay password input
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The W9 fix added user_id to SettingsResponse but missed the manual
_to_settings_response() builder, causing Pydantic validation failure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- W1: Add ntfy_has_token property to Settings model for safe from_attributes usage
- W2: Eager-load event location and pass location_name to ntfy template builder
- W3: Add missing accent color swatches (red, pink, yellow) to match backend Literal
- W7: Cap IP rate-limit dict at 10k entries with stale-entry purge to prevent OOM
- W9: Include user_id in SettingsResponse for multi-user readiness
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- C3: Register User, UserSession, NtfySent, TOTPUsage, BackupCode in models/__init__.py
- C4: Enforce settings.user_id NOT NULL after backfill in migration 023, update model
- W4: Rename misleading current_user → current_settings in dashboard.py
- W5: Match NtfySettingsSection initial state defaults to backend (true/1/2)
- W8: Clear lockout banner on username/password input change in LockScreen
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>