Toast accept button captured a stale `respond` reference from the
Sonner closure. Use respondRef pattern so clicks always dispatch
through the current mutation. Backend respond endpoint now catches
unhandled exceptions and returns proper JSON with detail field
instead of plain-text 500s.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- C-01: Wrap accept flow flush/commit in IntegrityError handling (409)
- C-02: Use separate remote_timestamps dict instead of pop() on shared profile
- W-01: Add birthday sync in Link conversion path (existing person → umbral)
- W-02: Add None guard on max(updated_at) comparison in get_person
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Override updated_at on PersonResponse with max(Person, User, Settings)
so the panel reflects when the connected user last changed their profile.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Inject umbral_name into shared_fields for umbral contacts (always visible)
- Show @umbralname subtitle in detail panel header
- Add preferred_name to panel fields with synced label for umbral contacts
- Add Link button on standard contacts to tie to umbral user via connection request
- Migration 046: person_id FK on connection_requests with index
- Validate person_id ownership on send, re-validate + convert on accept
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bug 1: _to_settings_response() was missing share_first_name and
share_last_name — the response always returned false (Pydantic default),
causing the frontend to sync toggles back to off after save.
Bug 2: Table column renderers read from stale Person record fields.
Added sf() helper that overlays shared_fields for umbral contacts,
applied to name, phone, email, role, and birthday columns. The table
now shows live shared profile data matching the detail panel.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Delete person now severs the bidirectional connection when the person
is an umbral contact — removes both UserConnection rows and detaches
the counterpart's Person record. Fixes "Already connected" error
when trying to reconnect after deleting an umbral contact.
New PUT /people/{id}/unlink endpoint converts an umbral contact to a
standard contact (detaches linked fields) while also severing the
bidirectional connection, keeping the Person in the contact list.
Frontend: EntityDetailPanel gains extraActions prop. PeoplePage renders
an "Unlink" button in the panel footer for umbral contacts. Delete
mutation now also invalidates connections query.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
W-08: Add CHECK constraint on notifications.type (migration 044) with
defensive pre-check and matching __table_args__ on model.
W-05: Auto-detach umbral contact before Person delete — nulls out
connection's person_id so the connection survives deletion.
W-01: Add PUT /requests/{id}/cancel endpoint with atomic UPDATE,
silent notification cleanup, and audit logging. Frontend: direction-aware
ConnectionRequestCard, cancel mutation, pending requests section on
PeoplePage with incoming/outgoing subsections.
W-06: Convert useNotifications to context provider pattern — single
subscription shared via NotificationProvider in AppLayout. Adds
refreshNotifications convenience function.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- C-01: Wrap connection request flush in IntegrityError handler for
TOCTOU race on partial unique index
- W-02: Extract ntfy config into plain dict before commit to avoid
DetachedInstanceError in background tasks
- W-04: Add integer range validation (1–2147483647) on notification IDs
- W-07: Add typed response models for respond_to_request endpoint
- W-09: Document resolved_at requirement for future cancel endpoint
- S-02: Use Literal type for ConnectionRequestResponse.status
- S-04: Check ntfy master switch in extract_ntfy_config
- S-05: Move date import to module level in connection service
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rewrite NotificationToaster with max-ID watermark for reliable
new-notification detection and faster unread count polling (15s)
- Block connection search and requests when sender has
accept_connections disabled (backend + frontend gate)
- Remove duplicate sender_settings fetch in send_connection_request
- Show actionable error messages in toast respond failures
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Notification fixes:
- Add NotificationToaster component with real-time toast notifications
for new incoming notifications (30s polling, 15s stale time)
- Connection request toasts show inline Accept/Reject buttons
- Add inline Accept/Reject buttons to connection_request notifications
in NotificationsPage (prevents bricked requests after navigation)
- Don't mark connection_request as read or navigate away when pending
- Auto-refetch notification list when unread count increases
Admin panel fixes:
- Add error state UI to UserDetailSection and ConfigPage (previously
silently returned null/empty on API errors)
- Fix get_user response missing must_change_password and locked_until
- Fix create_user response missing preferred_name and date_of_birth
- Add defensive limit(1) on settings query to prevent MultipleResultsFound
- Guard _target_username_col JSONB cast with CASE to prevent crash on
non-JSON audit detail values
- Add connection audit action types to ConfigPage filter dropdown
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add umbral_name to ProfileUpdate schema with regex validation
- Add uniqueness check in PUT /auth/profile handler
- Replace disabled input with editable save-on-blur field in Social card
- Client-side validation (3-50 chars, alphanumeric/hyphens/underscores)
- Inline error display for validation failures and taken names
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Admin create, first-user setup, and registration endpoints were
missing umbral_name assignment, causing NOT NULL constraint failures
when creating new users after migration 039.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Critical fixes:
- C-01: Add receiver_umbral_name/receiver_preferred_name to frontend ConnectionRequest type
- C-02: Flush connection request before notification to populate source_id
- C-03: Add umbral_name to ProfileResponse/UserProfile, use in Settings Social card
- C-04: Remove dead code in sharing-overrides endpoint, merge instead of replace
Warning fixes:
- W-01/W-02: Batch-fetch settings in incoming/outgoing/list connection endpoints (N+1 fix)
- W-04: Add _purge_resolved_requests job for rejected/cancelled requests (30-day retention)
- W-10: Add e.stopPropagation() to notification mark-read and delete buttons
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements the full User Connections & Notification Centre feature:
Phase 1 - Database: migrations 039-043 adding umbral_name to users,
profile/social fields to settings, notifications table, connection
request/user_connection tables, and linked_user_id to people.
Phase 2 - Notifications: backend CRUD router + service + 90-day purge,
frontend NotificationsPage with All/Unread filter, bell icon in sidebar
with unread badge polling every 60s.
Phase 3 - Settings: profile fields (phone, mobile, address, company,
job_title), social card with accept_connections toggle and per-field
sharing defaults, umbral name display with CopyableField.
Phase 4 - Connections: timing-safe user search, send/accept/reject flow
with atomic status updates, bidirectional UserConnection + Person records,
in-app + ntfy notifications, per-receiver pending cap, nginx rate limiting.
Phase 5 - People integration: batch-loaded shared profiles (N+1 prevention),
Ghost icon for umbral contacts, Umbral filter pill, split Add Person button,
shared field indicators (synced labels + Lock icons), disabled form inputs
for synced fields on umbral contacts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
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>
- 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>
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>
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>
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>
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>
- 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>
- 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>
- 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>