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>
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>
- 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>
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>
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>
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>
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>
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>
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>
Replace single delayed updateSize() with requestAnimationFrame loop
that continuously resizes FullCalendar throughout the 300ms CSS
transition, eliminating the visual desync between panel and calendar.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add animate-fade-in page transitions to all pages
- Persist sidebar collapsed state in localStorage
- Add two-click logout confirmation using useConfirmAction
- Restructure Todos header: replace <select> with pill filters, move search right
- Move Reminders search right-aligned with spacer
- Add event search dropdown + Create Event button to Calendar toolbar
- Add search input to Projects header with name/description filtering
- Fix CategoryFilterBar search focus ring clipping with ring-inset
- Create EventDetailPanel: read-only event view with copyable fields,
recurrence display, edit/delete actions, location name resolution
- Refactor CalendarPage to 55/45 split-panel layout matching People/Locations
- Add mobile overlay panel for calendar event details
- Add navigation state handler for CalendarPage (date/view from dashboard)
- Add navigation state handler for ProjectsPage (status filter from dashboard)
- Make all dashboard widgets navigable: stat cards → pages, week timeline
days → calendar day view, upcoming items → source pages, countdown items
→ calendar, today's events/todos/reminders → respective pages
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>
Swap LockProvider to outer wrapper so AlertsProvider can read isLocked.
When locked, dismiss all visible reminder toasts and skip firing new ones.
Toasts re-fire normally on unlock via the firedRef.clear() reset.
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>
- Remove instant invalid:ring/border from Input component (was showing
red outline on empty required fields before any interaction)
- Add CSS rule: form[data-submitted] input:invalid shows red border
- Add global submit listener in main.tsx that sets data-submitted on forms
- Add required prop to Labels missing asterisks: PersonForm (First Name),
LocationForm (Location Name), CalendarForm (Name), LockScreen
(Username, Password, Confirm Password)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Integrations card now has inline icon+title (no description), future-proofed
for additional integrations as sub-sections
- ntfy is its own sub-section with Bell icon + "ntfy Push Notifications" header
- Connection settings (URL, topic, token) collapse into a "Configure ntfy
connection" disclosure after saving, keeping the UI tidy
- Notification types and test/save buttons remain visible when enabled
- First-time setup shows connection fields expanded; they collapse on save
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Left column: Profile, Appearance, Calendar, Dashboard, Weather (prefs & display)
Right column: Security, Authentication, Integrations (security & services)
Also inlines the "Lock after [input] minutes" onto a single row.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The onChange was using parseInt(...) || 5 which snapped empty input back
to 5 immediately. Now allows empty string as intermediate state; value
is clamped to 1-60 on blur/Enter.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The swatches were added to SettingsPage but useTheme only had 5 presets,
so selecting the new colors saved to DB but never applied CSS variables.
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>
Adds Authentication card (password change + TOTP 5-state setup flow) and
Integrations card (ntfy master toggle, connection config, per-type toggles,
test button) to SettingsPage right column in correct order.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>