# UMBRA A self-hosted personal life administration app with a dark-themed UI. Manage your todos, calendar events, projects, reminders, contacts, and locations from a single dashboard. ## Features - **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 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 (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 ### Prerequisites - [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) ### Setup 1. **Clone the repository** ```bash git clone https://your-gitea-instance/youruser/umbra.git cd umbra ``` 2. **Configure environment variables** ```bash cp .env.example .env ``` Edit `.env` and set secure values (see [Production Hardening](#production-hardening) below for generation commands): ```env POSTGRES_USER=umbra POSTGRES_PASSWORD=your-secure-password POSTGRES_DB=umbra DATABASE_URL=postgresql+asyncpg://umbra:your-secure-password@db:5432/umbra SECRET_KEY=your-random-secret-key OPENWEATHERMAP_API_KEY=your-openweathermap-api-key ``` > **Weather widget**: The dashboard weather widget requires a free [OpenWeatherMap](https://openweathermap.org/api) API key. Set `OPENWEATHERMAP_API_KEY` in `.env`, then configure your city in Settings. 3. **Build and run** ```bash docker-compose up --build ``` 4. **Open the app** Navigate to `http://localhost` in your browser. On first launch you'll be prompted to create a username and password. ## Architecture ``` +-----------+ | Browser | +-----+-----+ | port 80 (HTTP) | +-------+-------+ | Nginx | | (frontend) | | non-root:8080 | +---+-------+---+ | | static | | /api/* files | | (rate-limited auth) v v +---+-------+---+ | FastAPI | | (backend) | | non-root | +-------+-------+ | +-------+-------+ | PostgreSQL | | (db) | | port 5432 | +---------------+ ``` - **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) on `/api/auth/login` (burst=5), `/verify-password` (burst=5), `/change-password` (burst=5), `/totp-verify` (burst=5), `/setup` (burst=3) - **DB-backed account lockout** — 10 failed attempts triggers 30-minute lock per account - **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/*` | 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 | API documentation is available at `/api/docs` (Swagger UI) when `ENVIRONMENT=development`. ## Development ### Rebuild a single service ```bash docker-compose up --build backend # Backend only docker-compose up --build frontend # Frontend only ``` ### View logs ```bash docker-compose logs -f # All services docker-compose logs -f backend # Backend only ``` ### Reset database ```bash docker-compose down -v && docker-compose up --build ``` ### Stop all services ```bash docker-compose down ``` ## Project Structure ``` umbra/ ├── docker-compose.yaml ├── .env / .env.example ├── backend/ │ ├── Dockerfile │ ├── requirements.txt │ ├── alembic.ini │ ├── alembic/versions/ # 25 migrations (001–025) │ └── app/ │ ├── 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, date-utils.ts, utils.ts ├── hooks/ # useAuth, useSettings, useTheme, useCalendars, useConfirmAction, useCategoryOrder, useTableVisibility ├── types/ # TypeScript interfaces └── components/ ├── 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 This project is for personal use. Feel free to fork and adapt for your own needs.