Apply QA suggestions and update all documentation
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>
This commit is contained in:
parent
22d8d5414f
commit
ad102c24ed
@ -7,6 +7,12 @@ POSTGRES_DB=umbra
|
|||||||
DATABASE_URL=postgresql+asyncpg://umbra:changeme_in_production@db:5432/umbra
|
DATABASE_URL=postgresql+asyncpg://umbra:changeme_in_production@db:5432/umbra
|
||||||
SECRET_KEY=change-this-to-a-random-secret-key-in-production
|
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)
|
# Timezone (applied to backend + db containers via env_file)
|
||||||
TZ=Australia/Perth
|
TZ=Australia/Perth
|
||||||
|
|
||||||
|
|||||||
171
README.md
171
README.md
@ -4,30 +4,29 @@ A self-hosted personal life administration app with a dark-themed UI. Manage you
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Dashboard** - At-a-glance overview with today's events, upcoming todos, active reminders, and project stats
|
- **Dashboard** - Contextual greeting, week timeline, stat cards, upcoming events, weather widget, day briefing
|
||||||
- **Todos** - Task management with priorities, due dates, and completion tracking
|
- **Todos** - Task management with priorities, due dates, recurrence, and grouped sections (overdue/today/upcoming)
|
||||||
- **Calendar** - Full interactive calendar (month/week/day views) with drag-and-drop event rescheduling
|
- **Calendar** - Multi-calendar system with month/week/day views, recurring events, drag-and-drop, event templates
|
||||||
- **Projects** - Project boards with nested task lists, status tracking, and progress indicators
|
- **Projects** - Project boards with kanban view, nested tasks/subtasks, comments, progress tracking
|
||||||
- **Reminders** - Time-based reminders with dismiss functionality
|
- **Reminders** - Time-based reminders with snooze, dismiss, recurrence, and real-time alert notifications (dashboard banner + toasts)
|
||||||
- **People** - Contact directory with relationship tracking and task assignment
|
- **People** - Contact directory with avatar initials, favourites, birthday tracking, category filtering
|
||||||
- **Locations** - Location management with categories
|
- **Locations** - Location management with OSM search integration, category filtering, frequent locations
|
||||||
- **Weather** - Dashboard weather widget with temperature, conditions, and rain warnings
|
- **Weather** - Dashboard weather widget with temperature, conditions, and contextual rain warnings
|
||||||
- **Settings** - Customizable accent color, upcoming days range, weather city, and PIN management
|
- **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)
|
||||||
## Screenshots
|
|
||||||
|
|
||||||
*Coming soon*
|
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
| Layer | Technology |
|
| Layer | Technology |
|
||||||
|--------------|------------|
|
|--------------|------------|
|
||||||
| Frontend | React 18, TypeScript, Vite, Tailwind CSS |
|
| Frontend | React 18, TypeScript, Vite 6, Tailwind CSS 3 |
|
||||||
| UI Components | Custom shadcn/ui-style components, FullCalendar, Lucide icons |
|
| 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 |
|
| State | TanStack Query v5, React Router v6 |
|
||||||
| Backend | FastAPI, Python 3.12, Pydantic v2 |
|
| Backend | FastAPI, Python 3.12, Pydantic v2 |
|
||||||
| Database | PostgreSQL 16, SQLAlchemy 2.0 (async), Alembic |
|
| Database | PostgreSQL 16, SQLAlchemy 2.0 (async), Alembic (25 migrations) |
|
||||||
| Auth | PIN-based with bcrypt + signed cookies |
|
| 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 |
|
| Deployment | Docker Compose (3 services), Nginx reverse proxy |
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
@ -48,7 +47,7 @@ A self-hosted personal life administration app with a dark-themed UI. Manage you
|
|||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
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
|
```env
|
||||||
POSTGRES_USER=umbra
|
POSTGRES_USER=umbra
|
||||||
POSTGRES_PASSWORD=your-secure-password
|
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**
|
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
|
## Architecture
|
||||||
|
|
||||||
@ -81,15 +80,16 @@ A self-hosted personal life administration app with a dark-themed UI. Manage you
|
|||||||
+-------+-------+
|
+-------+-------+
|
||||||
| Nginx |
|
| Nginx |
|
||||||
| (frontend) |
|
| (frontend) |
|
||||||
|
| non-root:8080 |
|
||||||
+---+-------+---+
|
+---+-------+---+
|
||||||
| |
|
| |
|
||||||
static | | /api/*
|
static | | /api/*
|
||||||
files | |
|
files | | (rate-limited auth)
|
||||||
v v
|
v v
|
||||||
+---+-------+---+
|
+---+-------+---+
|
||||||
| FastAPI |
|
| FastAPI |
|
||||||
| (backend) |
|
| (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.
|
- **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, then serves the FastAPI application.
|
- **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.
|
- **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
|
## API Overview
|
||||||
|
|
||||||
All endpoints require authentication (signed session cookie) except auth routes and the health check.
|
All endpoints require authentication (signed session cookie) except auth routes and the health check.
|
||||||
|
|
||||||
| Endpoint | Description |
|
| Endpoint | Description |
|
||||||
|--------------------|-------------|
|
|-----------------------|-------------|
|
||||||
| `GET /health` | Health check |
|
| `GET /health` | Health check |
|
||||||
| `/api/auth/*` | PIN setup, login, logout, status |
|
| `/api/auth/*` | Login, logout, setup, status, password change, TOTP MFA |
|
||||||
| `/api/todos/*` | Todos CRUD + toggle completion |
|
| `/api/todos/*` | Todos CRUD + toggle completion |
|
||||||
| `/api/events/*` | Calendar events CRUD |
|
| `/api/events/*` | Calendar events CRUD (incl. recurring) |
|
||||||
| `/api/reminders/*` | Reminders CRUD + dismiss |
|
| `/api/event-templates/*` | Event templates CRUD |
|
||||||
| `/api/projects/*` | Projects + nested tasks CRUD |
|
| `/api/calendars/*` | User calendars CRUD + visibility |
|
||||||
| `/api/people/*` | People CRUD |
|
| `/api/reminders/*` | Reminders CRUD + dismiss + snooze + due alerts |
|
||||||
| `/api/locations/*` | Locations CRUD |
|
| `/api/projects/*` | Projects + nested tasks + comments CRUD |
|
||||||
| `/api/settings/*` | App settings + PIN change |
|
| `/api/people/*` | People CRUD |
|
||||||
| `/api/dashboard` | Dashboard aggregation |
|
| `/api/locations/*` | Locations CRUD + search |
|
||||||
| `/api/upcoming` | Unified upcoming items feed |
|
| `/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
|
## Development
|
||||||
|
|
||||||
@ -160,37 +208,40 @@ umbra/
|
|||||||
├── backend/
|
├── backend/
|
||||||
│ ├── Dockerfile
|
│ ├── Dockerfile
|
||||||
│ ├── requirements.txt
|
│ ├── requirements.txt
|
||||||
│ ├── start.sh
|
|
||||||
│ ├── alembic.ini
|
│ ├── alembic.ini
|
||||||
│ ├── alembic/ # Database migrations
|
│ ├── alembic/versions/ # 25 migrations (001–025)
|
||||||
│ └── app/
|
│ └── app/
|
||||||
│ ├── main.py # FastAPI app entry point
|
│ ├── main.py # FastAPI app, router registration, health endpoint
|
||||||
│ ├── config.py # Environment settings
|
│ ├── config.py # Pydantic BaseSettings (DATABASE_URL, SECRET_KEY, CORS, etc.)
|
||||||
│ ├── database.py # Async SQLAlchemy setup
|
│ ├── database.py # Async SQLAlchemy engine + session factory
|
||||||
│ ├── models/ # SQLAlchemy ORM models
|
│ ├── models/ # 17 SQLAlchemy ORM models
|
||||||
│ ├── schemas/ # Pydantic request/response schemas
|
│ ├── schemas/ # Pydantic v2 request/response schemas
|
||||||
│ └── routers/ # API route handlers
|
│ ├── routers/ # 14 API route handlers
|
||||||
|
│ ├── services/ # Auth (Argon2id), recurrence, TOTP, ntfy
|
||||||
|
│ └── jobs/ # APScheduler notification dispatch
|
||||||
└── frontend/
|
└── frontend/
|
||||||
├── Dockerfile
|
├── Dockerfile
|
||||||
├── nginx.conf
|
├── nginx.conf
|
||||||
|
├── proxy-params.conf # Shared proxy settings (DRY include)
|
||||||
├── package.json
|
├── package.json
|
||||||
└── src/
|
└── src/
|
||||||
├── App.tsx # Routes and auth guard
|
├── App.tsx # Routes and auth guard
|
||||||
├── lib/api.ts # Axios client
|
├── lib/ # api.ts, date-utils.ts, utils.ts
|
||||||
├── hooks/ # Auth, settings, theme hooks
|
├── hooks/ # useAuth, useSettings, useTheme, useCalendars, useConfirmAction, useCategoryOrder, useTableVisibility
|
||||||
├── types/ # TypeScript interfaces
|
├── types/ # TypeScript interfaces
|
||||||
└── components/
|
└── components/
|
||||||
├── ui/ # Base UI components
|
├── ui/ # 18 base components (Button, Dialog, Sheet, Card, Input, Select, Switch, etc.)
|
||||||
├── layout/ # App shell and sidebar
|
├── shared/ # EntityTable, EntityDetailPanel, CategoryFilterBar, CategoryAutocomplete, CopyableField
|
||||||
├── auth/ # PIN login screen
|
├── layout/ # AppLayout, Sidebar, LockOverlay
|
||||||
├── dashboard/ # Dashboard and widgets
|
├── auth/ # LockScreen, AmbientBackground
|
||||||
├── calendar/ # Calendar and event form
|
├── dashboard/ # DashboardPage + 8 widgets
|
||||||
├── todos/ # Todo management
|
├── calendar/ # CalendarPage, CalendarSidebar, CalendarForm, EventForm, TemplateForm
|
||||||
├── reminders/ # Reminder management
|
├── todos/ # TodosPage, TodoList, TodoItem, TodoForm
|
||||||
├── projects/ # Project boards and tasks
|
├── reminders/ # RemindersPage, ReminderList, ReminderItem, ReminderForm, SnoozeDropdown, AlertBanner
|
||||||
├── people/ # Contact directory
|
├── projects/ # ProjectsPage, ProjectCard, ProjectDetail, ProjectForm, KanbanBoard, TaskRow, TaskForm, TaskDetailPanel
|
||||||
├── locations/ # Location management
|
├── people/ # PeoplePage, PersonForm
|
||||||
└── settings/ # App settings
|
├── locations/ # LocationsPage, LocationForm
|
||||||
|
└── settings/ # SettingsPage, NtfySettingsSection, TotpSetupSection
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
@ -51,7 +51,7 @@ app.add_middleware(
|
|||||||
allow_origins=[o.strip() for o in settings.CORS_ORIGINS.split(",") if o.strip()],
|
allow_origins=[o.strip() for o in settings.CORS_ORIGINS.split(",") if o.strip()],
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
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
|
# Include routers with /api prefix
|
||||||
|
|||||||
@ -23,6 +23,7 @@ COPY --from=build /app/dist /usr/share/nginx/html
|
|||||||
|
|
||||||
# Copy nginx configuration
|
# Copy nginx configuration
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY proxy-params.conf /etc/nginx/proxy-params.conf
|
||||||
|
|
||||||
# Expose port 8080 (unprivileged)
|
# Expose port 8080 (unprivileged)
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|||||||
@ -21,53 +21,29 @@ server {
|
|||||||
return 404;
|
return 404;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Rate-limited auth endpoints
|
# Rate-limited auth endpoints (keep in sync with proxy-params.conf)
|
||||||
location /api/auth/login {
|
location /api/auth/login {
|
||||||
limit_req zone=auth_limit burst=5 nodelay;
|
limit_req zone=auth_limit burst=5 nodelay;
|
||||||
limit_req_status 429;
|
limit_req_status 429;
|
||||||
|
include /etc/nginx/proxy-params.conf;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
location /api/auth/verify-password {
|
location /api/auth/verify-password {
|
||||||
limit_req zone=auth_limit burst=5 nodelay;
|
limit_req zone=auth_limit burst=5 nodelay;
|
||||||
limit_req_status 429;
|
limit_req_status 429;
|
||||||
|
include /etc/nginx/proxy-params.conf;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
location /api/auth/totp-verify {
|
location /api/auth/totp-verify {
|
||||||
limit_req zone=auth_limit burst=5 nodelay;
|
limit_req zone=auth_limit burst=5 nodelay;
|
||||||
limit_req_status 429;
|
limit_req_status 429;
|
||||||
|
include /etc/nginx/proxy-params.conf;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
location /api/auth/change-password {
|
location /api/auth/change-password {
|
||||||
limit_req zone=auth_limit burst=5 nodelay;
|
limit_req zone=auth_limit burst=5 nodelay;
|
||||||
limit_req_status 429;
|
limit_req_status 429;
|
||||||
|
include /etc/nginx/proxy-params.conf;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# API proxy
|
# API proxy
|
||||||
|
|||||||
6
frontend/proxy-params.conf
Normal file
6
frontend/proxy-params.conf
Normal file
@ -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;
|
||||||
Loading…
x
Reference in New Issue
Block a user