From ad102c24edb0795744ca4a5fac40843aac3cbc8c Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Wed, 25 Feb 2026 20:36:12 +0800 Subject: [PATCH] Apply QA suggestions and update all documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .env.example | 6 ++ README.md | 171 ++++++++++++++++++++++++------------- backend/app/main.py | 2 +- frontend/Dockerfile | 1 + frontend/nginx.conf | 34 ++------ frontend/proxy-params.conf | 6 ++ 6 files changed, 130 insertions(+), 90 deletions(-) create mode 100644 frontend/proxy-params.conf diff --git a/.env.example b/.env.example index 310f615..ed961bb 100644 --- a/.env.example +++ b/.env.example @@ -7,6 +7,12 @@ POSTGRES_DB=umbra DATABASE_URL=postgresql+asyncpg://umbra:changeme_in_production@db:5432/umbra SECRET_KEY=change-this-to-a-random-secret-key-in-production +# Environment (development|production — controls Swagger/ReDoc visibility) +# ENVIRONMENT=development + +# CORS allowed origins (comma-separated, default: http://localhost:5173) +# CORS_ORIGINS=http://localhost:5173 + # Timezone (applied to backend + db containers via env_file) TZ=Australia/Perth diff --git a/README.md b/README.md index 91cf80e..cbcf9e8 100644 --- a/README.md +++ b/README.md @@ -4,30 +4,29 @@ A self-hosted personal life administration app with a dark-themed UI. Manage you ## Features -- **Dashboard** - At-a-glance overview with today's events, upcoming todos, active reminders, and project stats -- **Todos** - Task management with priorities, due dates, and completion tracking -- **Calendar** - Full interactive calendar (month/week/day views) with drag-and-drop event rescheduling -- **Projects** - Project boards with nested task lists, status tracking, and progress indicators -- **Reminders** - Time-based reminders with dismiss functionality -- **People** - Contact directory with relationship tracking and task assignment -- **Locations** - Location management with categories -- **Weather** - Dashboard weather widget with temperature, conditions, and rain warnings -- **Settings** - Customizable accent color, upcoming days range, weather city, and PIN management - -## Screenshots - -*Coming soon* +- **Dashboard** - Contextual greeting, week timeline, stat cards, upcoming events, weather widget, day briefing +- **Todos** - Task management with priorities, due dates, recurrence, and grouped sections (overdue/today/upcoming) +- **Calendar** - Multi-calendar system with month/week/day views, recurring events, drag-and-drop, event templates +- **Projects** - Project boards with kanban view, nested tasks/subtasks, comments, progress tracking +- **Reminders** - Time-based reminders with snooze, dismiss, recurrence, and real-time alert notifications (dashboard banner + toasts) +- **People** - Contact directory with avatar initials, favourites, birthday tracking, category filtering +- **Locations** - Location management with OSM search integration, category filtering, frequent locations +- **Weather** - Dashboard weather widget with temperature, conditions, and contextual rain warnings +- **Settings** - Accent color picker (5 presets), first day of week, weather city, ntfy push notifications, TOTP two-factor auth, auto-lock, password management +- **Notifications** - ntfy push notifications for reminders (configurable per-user) ## Tech Stack | Layer | Technology | |--------------|------------| -| Frontend | React 18, TypeScript, Vite, Tailwind CSS | -| UI Components | Custom shadcn/ui-style components, FullCalendar, Lucide icons | +| Frontend | React 18, TypeScript, Vite 6, Tailwind CSS 3 | +| UI Components | Custom shadcn/ui-style components, FullCalendar 6, Lucide icons, Sonner toasts | +| Fonts | Sora (headings), DM Sans (body) via Google Fonts | | State | TanStack Query v5, React Router v6 | | Backend | FastAPI, Python 3.12, Pydantic v2 | -| Database | PostgreSQL 16, SQLAlchemy 2.0 (async), Alembic | -| Auth | PIN-based with bcrypt + signed cookies | +| Database | PostgreSQL 16, SQLAlchemy 2.0 (async), Alembic (25 migrations) | +| Auth | Username/password with Argon2id hashing, DB-backed sessions (signed cookies), optional TOTP MFA | +| Scheduler | APScheduler (async) for ntfy notification dispatch | | Deployment | Docker Compose (3 services), Nginx reverse proxy | ## Quick Start @@ -48,7 +47,7 @@ A self-hosted personal life administration app with a dark-themed UI. Manage you ```bash cp .env.example .env ``` - Edit `.env` and set secure values: + Edit `.env` and set secure values (see [Production Hardening](#production-hardening) below for generation commands): ```env POSTGRES_USER=umbra POSTGRES_PASSWORD=your-secure-password @@ -67,7 +66,7 @@ A self-hosted personal life administration app with a dark-themed UI. Manage you 4. **Open the app** - Navigate to `http://localhost` in your browser. On first launch you'll be prompted to create a PIN. + Navigate to `http://localhost` in your browser. On first launch you'll be prompted to create a username and password. ## Architecture @@ -81,15 +80,16 @@ A self-hosted personal life administration app with a dark-themed UI. Manage you +-------+-------+ | Nginx | | (frontend) | + | non-root:8080 | +---+-------+---+ | | static | | /api/* - files | | + files | | (rate-limited auth) v v +---+-------+---+ | FastAPI | | (backend) | - | port 8000 | + | non-root | +-------+-------+ | +-------+-------+ @@ -99,29 +99,77 @@ A self-hosted personal life administration app with a dark-themed UI. Manage you +---------------+ ``` -- **Frontend** is built as static files and served by Nginx. Nginx also reverse-proxies API requests to the backend. -- **Backend** runs Alembic migrations on startup, then serves the FastAPI application. +- **Frontend** is built as static files and served by `nginxinc/nginx-unprivileged`. Nginx also reverse-proxies API requests to the backend with rate limiting on auth endpoints. +- **Backend** runs Alembic migrations on startup as a non-root user (`appuser`), then serves the FastAPI application with `--no-server-header`. - **Database** uses a named Docker volume (`postgres_data`) for persistence. +- **Backend port 8000 is not exposed externally** — only accessible via the internal Docker network. + +## Security + +### Hardened by default + +- **Non-root containers** — both backend (`appuser:1000`) and frontend (`nginx-unprivileged`) run as non-root +- **No external backend port** — port 8000 is internal-only; all traffic flows through nginx +- **Server version suppression** — `server_tokens off` (nginx) and `--no-server-header` (uvicorn) +- **Rate limiting** — nginx `limit_req_zone` (10 req/min, burst=5) on `/api/auth/login`, `/verify-password`, `/change-password`, `/totp-verify` +- **Application-level rate limiting** — in-memory IP-based rate limit (5 attempts / 5 min) + DB-backed account lockout (10 failures = 30-min lock) +- **Dotfile blocking** — `/.env`, `/.git/config`, etc. return 404 (`.well-known` preserved for ACME) +- **CSP headers** — Content-Security-Policy on all responses, scoped for Google Fonts +- **CORS** — configurable origins with explicit method/header allowlists +- **API docs disabled in production** — Swagger/ReDoc/OpenAPI only available when `ENVIRONMENT=development` +- **Argon2id password hashing** with transparent bcrypt migration on first login +- **DB-backed sessions** — revocable, with signed itsdangerous cookies +- **Optional TOTP MFA** — authenticator app support with backup codes + +### Production Hardening + +Before deploying to production, generate secure values for your `.env`: + +```bash +# Generate a secure SECRET_KEY (64-char hex string) +python3 -c "import secrets; print(secrets.token_hex(32))" +# or: openssl rand -hex 32 + +# Generate a secure database password +python3 -c "import secrets; print(secrets.token_urlsafe(24))" +# or: openssl rand -base64 24 + +# Set ENVIRONMENT to disable Swagger/ReDoc +ENVIRONMENT=production + +# Enable secure cookies (requires HTTPS) +COOKIE_SECURE=true +``` + +Additionally for production: +- Place behind a reverse proxy with TLS termination (e.g., Caddy, Traefik, or nginx with Let's Encrypt) +- Set `COOKIE_SECURE=true` to enforce HTTPS-only session cookies +- Set `ENVIRONMENT=production` to disable API documentation endpoints +- Set `CORS_ORIGINS` to your actual domain (e.g., `https://umbra.example.com`) +- Consider adding HSTS headers at the TLS-terminating proxy layer ## API Overview All endpoints require authentication (signed session cookie) except auth routes and the health check. -| Endpoint | Description | -|--------------------|-------------| -| `GET /health` | Health check | -| `/api/auth/*` | PIN setup, login, logout, status | -| `/api/todos/*` | Todos CRUD + toggle completion | -| `/api/events/*` | Calendar events CRUD | -| `/api/reminders/*` | Reminders CRUD + dismiss | -| `/api/projects/*` | Projects + nested tasks CRUD | -| `/api/people/*` | People CRUD | -| `/api/locations/*` | Locations CRUD | -| `/api/settings/*` | App settings + PIN change | -| `/api/dashboard` | Dashboard aggregation | -| `/api/upcoming` | Unified upcoming items feed | +| Endpoint | Description | +|-----------------------|-------------| +| `GET /health` | Health check | +| `/api/auth/*` | Login, logout, setup, status, password change, TOTP MFA | +| `/api/todos/*` | Todos CRUD + toggle completion | +| `/api/events/*` | Calendar events CRUD (incl. recurring) | +| `/api/event-templates/*` | Event templates CRUD | +| `/api/calendars/*` | User calendars CRUD + visibility | +| `/api/reminders/*` | Reminders CRUD + dismiss + snooze + due alerts | +| `/api/projects/*` | Projects + nested tasks + comments CRUD | +| `/api/people/*` | People CRUD | +| `/api/locations/*` | Locations CRUD + search | +| `/api/settings/*` | App settings + password change + ntfy config | +| `/api/dashboard` | Dashboard aggregation | +| `/api/upcoming` | Unified upcoming items feed | +| `/api/weather/*` | Weather data proxy | -Full API documentation is available at `http://localhost:8000/docs` (Swagger UI) when the backend is running. +API documentation is available at `/api/docs` (Swagger UI) when `ENVIRONMENT=development`. ## Development @@ -160,37 +208,40 @@ umbra/ ├── backend/ │ ├── Dockerfile │ ├── requirements.txt -│ ├── start.sh │ ├── alembic.ini -│ ├── alembic/ # Database migrations +│ ├── alembic/versions/ # 25 migrations (001–025) │ └── app/ -│ ├── main.py # FastAPI app entry point -│ ├── config.py # Environment settings -│ ├── database.py # Async SQLAlchemy setup -│ ├── models/ # SQLAlchemy ORM models -│ ├── schemas/ # Pydantic request/response schemas -│ └── routers/ # API route handlers +│ ├── main.py # FastAPI app, router registration, health endpoint +│ ├── config.py # Pydantic BaseSettings (DATABASE_URL, SECRET_KEY, CORS, etc.) +│ ├── database.py # Async SQLAlchemy engine + session factory +│ ├── models/ # 17 SQLAlchemy ORM models +│ ├── schemas/ # Pydantic v2 request/response schemas +│ ├── routers/ # 14 API route handlers +│ ├── services/ # Auth (Argon2id), recurrence, TOTP, ntfy +│ └── jobs/ # APScheduler notification dispatch └── frontend/ ├── Dockerfile ├── nginx.conf + ├── proxy-params.conf # Shared proxy settings (DRY include) ├── package.json └── src/ - ├── App.tsx # Routes and auth guard - ├── lib/api.ts # Axios client - ├── hooks/ # Auth, settings, theme hooks - ├── types/ # TypeScript interfaces + ├── App.tsx # Routes and auth guard + ├── lib/ # api.ts, date-utils.ts, utils.ts + ├── hooks/ # useAuth, useSettings, useTheme, useCalendars, useConfirmAction, useCategoryOrder, useTableVisibility + ├── types/ # TypeScript interfaces └── components/ - ├── ui/ # Base UI components - ├── layout/ # App shell and sidebar - ├── auth/ # PIN login screen - ├── dashboard/ # Dashboard and widgets - ├── calendar/ # Calendar and event form - ├── todos/ # Todo management - ├── reminders/ # Reminder management - ├── projects/ # Project boards and tasks - ├── people/ # Contact directory - ├── locations/ # Location management - └── settings/ # App settings + ├── ui/ # 18 base components (Button, Dialog, Sheet, Card, Input, Select, Switch, etc.) + ├── shared/ # EntityTable, EntityDetailPanel, CategoryFilterBar, CategoryAutocomplete, CopyableField + ├── layout/ # AppLayout, Sidebar, LockOverlay + ├── auth/ # LockScreen, AmbientBackground + ├── dashboard/ # DashboardPage + 8 widgets + ├── calendar/ # CalendarPage, CalendarSidebar, CalendarForm, EventForm, TemplateForm + ├── todos/ # TodosPage, TodoList, TodoItem, TodoForm + ├── reminders/ # RemindersPage, ReminderList, ReminderItem, ReminderForm, SnoozeDropdown, AlertBanner + ├── projects/ # ProjectsPage, ProjectCard, ProjectDetail, ProjectForm, KanbanBoard, TaskRow, TaskForm, TaskDetailPanel + ├── people/ # PeoplePage, PersonForm + ├── locations/ # LocationsPage, LocationForm + └── settings/ # SettingsPage, NtfySettingsSection, TotpSetupSection ``` ## License diff --git a/backend/app/main.py b/backend/app/main.py index a088282..c3a7e9a 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -51,7 +51,7 @@ app.add_middleware( allow_origins=[o.strip() for o in settings.CORS_ORIGINS.split(",") if o.strip()], allow_credentials=True, allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], - allow_headers=["Content-Type", "Authorization", "Cookie", "X-Requested-With"], + allow_headers=["Content-Type", "Authorization", "Cookie"], ) # Include routers with /api prefix diff --git a/frontend/Dockerfile b/frontend/Dockerfile index b77e595..ef60a12 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -23,6 +23,7 @@ COPY --from=build /app/dist /usr/share/nginx/html # Copy nginx configuration COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY proxy-params.conf /etc/nginx/proxy-params.conf # Expose port 8080 (unprivileged) EXPOSE 8080 diff --git a/frontend/nginx.conf b/frontend/nginx.conf index 9fe9c9e..0605125 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -21,53 +21,29 @@ server { return 404; } - # Rate-limited auth endpoints + # Rate-limited auth endpoints (keep in sync with proxy-params.conf) location /api/auth/login { limit_req zone=auth_limit burst=5 nodelay; limit_req_status 429; - - proxy_pass http://backend:8000; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + include /etc/nginx/proxy-params.conf; } location /api/auth/verify-password { limit_req zone=auth_limit burst=5 nodelay; limit_req_status 429; - - proxy_pass http://backend:8000; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + include /etc/nginx/proxy-params.conf; } location /api/auth/totp-verify { limit_req zone=auth_limit burst=5 nodelay; limit_req_status 429; - - proxy_pass http://backend:8000; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + include /etc/nginx/proxy-params.conf; } location /api/auth/change-password { limit_req zone=auth_limit burst=5 nodelay; limit_req_status 429; - - proxy_pass http://backend:8000; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + include /etc/nginx/proxy-params.conf; } # API proxy diff --git a/frontend/proxy-params.conf b/frontend/proxy-params.conf new file mode 100644 index 0000000..8d88715 --- /dev/null +++ b/frontend/proxy-params.conf @@ -0,0 +1,6 @@ +proxy_pass http://backend:8000; +proxy_http_version 1.1; +proxy_set_header Host $host; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $scheme;