diff --git a/Cargo.lock b/Cargo.lock index cb812e6..7342f4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1590,7 +1590,7 @@ dependencies = [ [[package]] name = "rs_maps" -version = "0.3.0" +version = "0.3.1" dependencies = [ "anyhow", "argon2", diff --git a/Cargo.toml b/Cargo.toml index d91ae4c..0217058 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ # Cargo.toml [package] name = "rs_maps" -version = "0.3.0" +version = "0.3.1" edition = "2021" [dependencies] diff --git a/README.md b/README.md index 3b6ced7..f27b31a 100644 --- a/README.md +++ b/README.md @@ -1,114 +1,132 @@ -# rs_maps — build and deploy +# rs_maps -Single Rust binary + one rootless Postgres container. See `DESIGN.md` for the reasoning. +A self-hosted replacement for the parts of Google Maps most people actually use: +saving places you care about, and planning walking routes. It runs as a single +Rust binary with one Postgres container behind it, and it's built for a handful +of trusted users rather than the public. -> **This code has not been compiled.** It was written without a Rust toolchain available, -> so treat the first `cargo build` as the real review. Expect to fix a handful of import -> paths and trait-bound details, not the structure. +It is not a Google Maps clone. There are no reviews, no photos, no live traffic +and no turn-by-turn navigation, and there is no plan to add them. -## 1. Refresh dependency versions +## What it does -`Cargo.toml` has plausible versions, but pin them properly: +**Places.** Click the map to drop a tagged marker with a name, kind, colour and +description. Markers are private by default; flip one to shared and everyone +else with an account sees it too. + +**Search.** Type a place or address and get up to eight matches with their type +and full address, so two branches of the same chain are distinguishable. Picking +one drops a pin and shows what was found. "More details" pulls opening hours, +phone, website and similar from OpenStreetMap where they exist — and says so +plainly where they don't. Any hit can be saved as a place in one click. + +**Routes.** Draw a walking route by clicking waypoints on the map. The line +snaps to real paths and tracks, updating as you drag points around, with the +routed distance and total ascent shown. Save it and the GPX lands in a shared +folder both users can see. Saved routes can be reopened and edited later. + +**GPX files.** Open a `.gpx` from your device to view it without uploading +anything, or save it to the shared folder if you want it on the server. Files in +the shared folder can be rendered on the map by anyone logged in. + +**Live location.** Toggle it on to see where you are while the map is open. It's +a dot on a map, not navigation guidance. + +## What it's built on + +- **Rust binary** (`axum`) serving the API and the whole frontend, which is + embedded into the executable at compile time. One file to deploy. +- **Postgres** in a rootless podman container, holding users, sessions and + markers. +- **A plain directory** for GPX files. No database involvement, no metadata. +- **Nothing else self-hosted.** Map tiles come from OpenStreetMap, search from + Nominatim, route snapping from brouter.de, and place details from Overpass — + called directly from the browser, or proxied through one endpoint. Hosting any + of them would mean gigabytes of data for a tool used a few times a week. + +## Requirements + +- A Linux server with systemd and podman +- nginx (or equivalent) terminating TLS — **not optional**: browser geolocation + refuses to run without HTTPS, and session cookies are `Secure` +- A Rust toolchain to build, or a release binary +- A domain + +## Quick start ```bash -cargo add axum -F multipart,macros -cargo add tokio -F full -cargo add sqlx --no-default-features -F runtime-tokio,tls-rustls,postgres,uuid,time,macros,migrate -cargo add tower-sessions -cargo add tower-sessions-sqlx-store -F postgres -cargo add lettre --no-default-features -F tokio1-rustls,smtp-transport,builder,pool -cargo add rust-embed -F debug-embed -cargo build +git clone && cd rs_maps +cargo build --release ``` -Two version-sensitive spots to check first if it doesn't compile: - -- **`tower-sessions` / `tower-sessions-sqlx-store` must be a matching pair.** The store crate - tracks the main crate's version and they break in lockstep. Check the store's own README for - which `tower-sessions` it expects. -- **axum 0.8 changed path params from `:id` to `{id}`.** This code uses `{id}`. If you end up on - 0.7 for some reason, they all need changing back. - -## 2. Database +Database and directories: ```bash -podman secret create mapserver-db-password - # type the password, then Ctrl-D -mkdir -p ~/.config/containers/systemd +mkdir -p ~/.config/containers/systemd ~/gpx cp deploy/postgres.container ~/.config/containers/systemd/ + +# Hex, not base64: / and + in a password break DATABASE_URL parsing. +openssl rand -hex 32 | tr -d '\n' | podman secret create rs_maps-db-password - + systemctl --user daemon-reload systemctl --user start postgres ``` -Migrations run automatically on startup — both `./migrations` and the session store's own. +Copy `.env.example` to `.env` and fill in at least these: -## 3. Config +| Key | Notes | +|---|---| +| `DATABASE_URL` | `postgres://rs_maps:PASSWORD@127.0.0.1:5432/rs_maps` | +| `REG_TOKEN` | `openssl rand -hex 32`. Anyone with this can register — it is the only access control | +| `PUBLIC_URL` | Origin only: `https://example.com`. No trailing slash, no subpath | +| `BASE_PATH` | `/maps` if served under a subpath, empty for the domain root | +| `GPX_DIR` | Absolute path to a directory the service can write to | +| `BIND_ADDR` | `127.0.0.1:8080` — loopback only, nginx is the only client | + +`PUBLIC_URL` must match the browser's `Origin` header exactly. A mismatch +(`www.` versus bare, or a stray trailing slash) means every save fails with a +403 while pages load normally — a confusing thing to debug. + +Run it: ```bash -mkdir -p ~/.config/mapserver ~/gpx -cp .env.example ~/.config/mapserver/.env -chmod 600 ~/.config/mapserver/.env -$EDITOR ~/.config/mapserver/.env +./target/release/rs_maps ``` -Must change: `DATABASE_URL`, `REG_TOKEN`, `PUBLIC_URL`, `BASE_PATH`, `GPX_DIR`. +Migrations apply on startup. Point nginx at it using `deploy/nginx-snippet.conf` +— note the `client_max_body_size 25m` on the upload path and the redirect for +the bare subpath. Then open the site, register with your `REG_TOKEN`, and you're +in. The first account isn't special: there are no roles and no admin UI. -Leave `SMTP_HOST` empty for now — reset links get logged instead of emailed, and the whole flow -is testable without a provider. +For the service unit, systemd hardening and the release/upgrade scripts, see +`deploy/`. `DESIGN.md` covers why the architecture is the way it is. -## 4. Run +## Passwords and email + +Email is optional and used only for password resets. Give a bad address and the +only thing you lose is your own ability to reset — nothing else depends on it. +With `SMTP_HOST` empty, reset links are written to the log rather than sent, +which is enough to test the flow without an email provider. + +Changing a password affects future logins only. Sessions already active on other +devices stay valid; clearing those means deleting the session rows directly. +That's a deliberate trade for a two-person deployment. + +## Being a good citizen + +Tiles, search, routing and place details all come from volunteer-run +infrastructure. The app is built to stay inside their usage policies: search +runs on submit rather than as you type, route previews are debounced, and place +details are fetched only when asked for. If you fork this and put it in front of +a lot of users, read those policies before scaling up. + +## Tests ```bash -cargo build --release -mkdir -p ~/.local/bin && cp target/release/mapserver ~/.local/bin/ -cp deploy/mapserver.service ~/.config/systemd/user/ -systemctl --user daemon-reload -systemctl --user enable --now mapserver -loginctl enable-linger "$USER" # or nothing starts at boot -journalctl --user -u mapserver -f +cargo test ``` -## 5. nginx - -Merge `deploy/nginx-snippet.conf` into your existing TLS server block, then -`nginx -t && systemctl reload nginx`. - -## 6. First account - -Visit `https://your-domain/maps/`, choose "Create an account", and enter the `REG_TOKEN` from -`.env`. - ---- - -## Testing the reset flow with real mail - -`mailpit` gives you a local SMTP server and a web inbox with no signup: - -``` -SMTP_HOST=127.0.0.1 -SMTP_PORT=1025 -SMTP_TLS=none -SMTP_USERNAME= -SMTP_PASSWORD= -``` - -When you move to a real provider: use an app-specific password (any account with 2FA will reject -the normal one over SMTP), check SMTP isn't disabled by default on the account, and make sure -`SMTP_FROM` is an address that account may send as. - -## Things worth checking by hand after the first deploy - -- `curl -i https://your-domain/maps/api/gpx/file/../../etc/passwd` → 400 or 404, never a file -- Live location works (needs HTTPS; it silently fails on plain HTTP) -- Session survives `systemctl --user restart mapserver` -- Upload a >2 MB GPX — catches both the axum and nginx body limits at once -- Upload the same filename twice — second one should come back as `name-1.gpx` -- Reboot the server, confirm both units come back (this is what `enable-linger` is for) - -## Unit tests - - cargo test - -No database needed — every test is pure: base-path normalisation, bbox parsing, -GPX filename safety, and the rate-limiter window. +No database needed — the tests cover base-path handling, bbox parsing, GPX +filename safety, waypoint embedding, coordinate ordering and the rate limiter.