React/Vite frontend and Express/SQLite backend for tracking visited and planned cities on a map, with per-city notes, photos, links, and participant-based sharing between family members. Includes session auth with admin-managed user accounts, an OSM/Nominatim geocoding proxy, and a single-image Docker deployment. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
154 lines
6.2 KiB
Markdown
154 lines
6.2 KiB
Markdown
# CityTracker
|
|
|
|
A self-hostable webapp for tracking cities you've visited (or plan to visit) on a
|
|
map, with ratings, notes, photos, and links — built for sharing between family
|
|
members or a partner, each with their own login.
|
|
|
|
## Features
|
|
|
|
- Interactive world map (Leaflet + OpenStreetMap tiles, no API keys) with pins
|
|
colored by status (visited / planned) and a heart badge for favorites
|
|
- Add a city by searching its name (via OpenStreetMap Nominatim) or by clicking
|
|
directly on the map
|
|
- Per-city notes, a "liked" heart, one photo, a list of links, and the people
|
|
who were there (picked from your user list)
|
|
- Filter the map by visited/planned, favorites, year, or country
|
|
- Multiple user accounts; a city is visible/editable by everyone listed as a
|
|
participant on it
|
|
- Admin-managed user accounts (no public self-registration)
|
|
- Single Docker image, SQLite database, file-based photo storage — easy to back
|
|
up as one Docker volume
|
|
|
|
## Tech stack
|
|
|
|
- **Frontend:** React + TypeScript, Vite, React Router, TanStack Query, Leaflet
|
|
- **Backend:** Node.js + TypeScript, Express, better-sqlite3, argon2, multer
|
|
- **Database:** SQLite, single file, plain SQL migrations run automatically on
|
|
startup
|
|
- **Auth:** server-side sessions (httpOnly cookies), backed by a SQLite table so
|
|
logins survive container restarts
|
|
|
|
## Project structure
|
|
|
|
```
|
|
CityTracker/
|
|
├── shared/ # TypeScript types + zod validation schemas, used by both client and server
|
|
├── server/ # Express API (auth, cities, uploads, geocoding proxy)
|
|
└── client/ # React app (Vite)
|
|
```
|
|
|
|
## Running with Docker (recommended)
|
|
|
|
This is the intended way to run CityTracker — one image, one volume.
|
|
|
|
1. Copy the example environment file and fill in real values:
|
|
|
|
```sh
|
|
cp .env.example .env
|
|
```
|
|
|
|
At minimum, set:
|
|
- `SESSION_SECRET` — a long random string (generate one with
|
|
`node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`)
|
|
- `ADMIN_USERNAME` / `ADMIN_PASSWORD` — credentials for the first admin
|
|
account, created automatically the first time the server starts. If you
|
|
leave these unset, open the app once it's running and use the one-time
|
|
`/setup` screen instead.
|
|
|
|
2. Build and start the container:
|
|
|
|
```sh
|
|
docker compose up -d --build
|
|
```
|
|
|
|
3. Open `http://<your-host>:<HOST_PORT>` (default port `3000`) and log in with
|
|
the admin account you configured.
|
|
|
|
All application data — the SQLite database and uploaded photos — lives under
|
|
`/app/data` inside the container, on the `citytracker-data` named volume. Back
|
|
up that volume (e.g. `docker run --rm -v citytracker_citytracker-data:/data -v $(pwd):/backup alpine tar czf /backup/citytracker-backup.tar.gz /data`)
|
|
to back up everything.
|
|
|
|
### Behind Nginx Proxy Manager (or another reverse proxy)
|
|
|
|
The container just needs to be reachable and listening on `HOST_PORT`. Point a
|
|
proxy host at `docker.covenant.lan:<HOST_PORT>` (or wherever the container
|
|
runs), enable SSL as usual in your proxy. If your proxy terminates TLS and
|
|
forwards plain HTTP to the container, set `COOKIE_SECURE=true` in `.env` so
|
|
session cookies are still marked `Secure` correctly; leave it `false` if the
|
|
container itself is only ever reached over plain HTTP inside your LAN.
|
|
|
|
### First admin account
|
|
|
|
On first startup, if no admin user exists yet:
|
|
|
|
- If `ADMIN_USERNAME` and `ADMIN_PASSWORD` are set in `.env`, that account is
|
|
created automatically.
|
|
- Otherwise, visiting the app shows a one-time setup screen to create the
|
|
first admin account by hand.
|
|
|
|
After that, only an admin can create further user accounts, from the **Users**
|
|
page in the app.
|
|
|
|
## Adding more users
|
|
|
|
Log in as an admin, go to **Users**, and use the "Add a user" form. Admins can
|
|
also promote/demote other admins, deactivate/reactivate accounts, and reset
|
|
passwords from the same page. Deactivated accounts can no longer log in but
|
|
remain visible (grayed out) on cities they previously participated in, so
|
|
history isn't lost.
|
|
|
|
## Local development (without Docker)
|
|
|
|
Requires Node.js 20 (native modules `better-sqlite3` and `argon2` need a
|
|
recent Node build with prebuilt binaries available).
|
|
|
|
```sh
|
|
npm install
|
|
npm run build -w shared # shared types must be built once before starting the server or client
|
|
|
|
# in one terminal
|
|
PORT=3000 SESSION_SECRET=dev-secret ADMIN_USERNAME=admin ADMIN_PASSWORD=devpassword123 npm run dev:server
|
|
|
|
# in another terminal
|
|
npm run dev:client
|
|
```
|
|
|
|
The Vite dev server (`http://localhost:5173`) proxies `/api` and `/uploads`
|
|
requests to the backend on port 3000.
|
|
|
|
To produce a production build of everything (used by the Docker image):
|
|
|
|
```sh
|
|
npm run build
|
|
npm start
|
|
```
|
|
|
|
## Configuration reference
|
|
|
|
All configuration is via environment variables (see `.env.example`):
|
|
|
|
| Variable | Description | Default |
|
|
|---|---|---|
|
|
| `PORT` | Port the server listens on inside the container | `3000` |
|
|
| `HOST_PORT` | Port exposed on the Docker host (docker-compose only) | `3000` |
|
|
| `SESSION_SECRET` | Secret used to sign session cookies. **Required** in production. | — |
|
|
| `ADMIN_USERNAME` / `ADMIN_PASSWORD` | Credentials for the auto-created first admin account | — |
|
|
| `DB_PATH` | Path to the SQLite database file | `/app/data/citytracker.db` (Docker) |
|
|
| `UPLOAD_DIR` | Directory where uploaded photos are stored | `/app/data/uploads` (Docker) |
|
|
| `COOKIE_SECURE` | Mark session cookies `Secure` (only if TLS reaches the container or is terminated just in front of it) | `false` |
|
|
|
|
## Notes on scope / what's not included
|
|
|
|
- **Trips** (grouping cities into a named trip with its own date range) is
|
|
planned as a future addition. The database schema was deliberately kept
|
|
simple enough that adding `trips` / `trip_cities` tables later won't require
|
|
reworking existing data.
|
|
- The UI is English-only by design, to keep the app simple; phone/browser
|
|
auto-translate tools work fine over it if needed.
|
|
- Uploaded photos are served from `/uploads/<random-filename>` without a
|
|
per-request auth check — acceptable for a private LAN/home-use deployment
|
|
since filenames are non-guessable UUIDs, but worth knowing if you expose
|
|
this instance beyond your own network.
|
|
- There is no automated test suite; this is a personal/family-scale project
|
|
verified through manual end-to-end testing.
|