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>
PT-01: Add set_real_ip_from/real_ip_header/real_ip_recursive to restore
real client IP from X-Forwarded-For. Rate limiting now keys on actual
client IP instead of the Pangolin proxy IP.
PT-02: Add Strict-Transport-Security header (max-age 1 year) to both
the server block and static assets block.
PT-04: Replace bare 404 on dotfile requests with JSON response to
suppress nginx server identity disclosure in error pages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The .env.example value didn't match the sentinel checked in config.py,
so copying .env.example verbatim to production would bypass the fatal
safety exit. Aligned to use the same default string.
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>
The global axios 401 interceptor was firing window.location.href =
'/login' on every 401 response, including POST /auth/login with wrong
credentials. This caused a full page reload to /login, which remounted
the entire React tree and reset all LockScreen state (loginError,
username, password) before the user could see the error alert.
Fix: skip the redirect for /auth/* endpoints, which legitimately
return 401 for invalid credentials. The interceptor still redirects
to /login for expired sessions on protected API calls.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After a failed login, the browser's password manager fires onChange
events on the username/password inputs (clearing or resetting them).
The onChange handlers were calling setLoginError(null), which wiped
the error alert immediately after it appeared.
Fix: remove setLoginError(null) from input onChange handlers. The
error now clears at the start of the next submit attempt via the
existing setLoginError(null) in handleCredentialSubmit.
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>
The desktop detail panels are pre-mounted (always in DOM, hidden with w-0).
useState(isCreating) only captures the initial value on mount (false), so
when isCreating later becomes true via props, isEditing stays false. The
view-mode branch then runs with a null entity, crashing on property access.
Fix: use (isEditing || isCreating) for all conditionals that gate between
edit/create form and view mode, ensuring the form always renders when
isCreating is true regardless of isEditing state.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All three DetailPanel components initialized isEditing=false even
when isCreating=true. The useEffect that flips it to true runs AFTER
the first render, so the view-mode branch executes with todo=null,
crashing on null.priority. Initialize isEditing from isCreating.
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>
Submenu was positioned left-full (opening rightward) but the parent
dropdown is already at the right edge. Changed to right-full so it
opens leftward into available space.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The table wrapper's overflow-x-auto forced overflow-y to also clip,
hiding the 3-dot actions dropdown below the container boundary.
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>
Complete multi-user integration for UMBRA:
- Database: role enum, system_config, audit_log, user_id FKs on all tables
- Auth: RBAC with require_role(), registration, MFA enforcement
- Routing: All 12 routers scoped by user_id
- Admin API: 14 endpoints for IAM, config, audit, dashboard
- Admin Portal: IAM page, config page, dashboard with stats
- Registration flow: LockScreen with register/MFA-enforce/password-change
- Security: SEC-01 through SEC-15 mitigations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Creates 7 files: useAdmin hook with TanStack Query v5, AdminPortal
layout with horizontal tab nav, IAMPage with user table + stat cards
+ system settings, UserActionsMenu with two-click confirms, CreateUserDialog,
ConfigPage with paginated audit log + action filter, AdminDashboardPage
with stats + recent logins/actions tables.
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>
Add nginx map directive to prefer X-Forwarded-Proto header from
Traefik/Pangolin when present, falling back to $scheme for direct
internal HTTP access. Applied to both nginx.conf and proxy-params.conf.
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>
Add fullWidth field option to PanelField interface. Short fields
render in a grid grid-cols-2 layout; fullWidth fields (address, notes)
render below at full width. Add icons to People and Locations fields
for consistency with other detail panels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Match TaskDetailPanel gold standard: short fields use grid grid-cols-2
with icon+label headers, full-width fields (description) below the grid.
All grid slots render with "—" fallback to keep alignment consistent.
- Todo: Priority, Category, Due Date, Recurrence in grid
- Reminder: Status, Recurrence, Remind At, Snoozed Until in grid
- Calendar: Calendar, Starred, Start, End, Location, Recurrence in grid
- Task: Add "Updated X ago" footer (was missing)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Wrap CategoryFilterBar in flex-1 min-w-0 so search aligns right
- Add first_name, last_name, nickname to People search filter
- Add ring-inset to all header search inputs (People, Todos,
Locations, Reminders, Calendar) to prevent focus ring clipping
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove the collapsible search behavior that hid the input behind an
icon when ≥4 categories existed. Search should always be visible and
match the standard header pattern (w-52 input with icon).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CategoryFilterBar's internal flex-1 spacer needs room to push search
right. Wrapping in flex-1 min-w-0 matches the Locations/People pattern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>