This commit is contained in:
TheFozid 2026-08-03 14:29:14 +01:00
parent b8ee4b4cc0
commit bd87587391
3 changed files with 106 additions and 88 deletions

2
Cargo.lock generated
View file

@ -1590,7 +1590,7 @@ dependencies = [
[[package]] [[package]]
name = "rs_maps" name = "rs_maps"
version = "0.3.0" version = "0.3.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",

View file

@ -1,7 +1,7 @@
# Cargo.toml # Cargo.toml
[package] [package]
name = "rs_maps" name = "rs_maps"
version = "0.3.0" version = "0.3.1"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

190
README.md
View file

@ -1,114 +1,132 @@
<!-- README.md --> <!-- README.md -->
# 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, It is not a Google Maps clone. There are no reviews, no photos, no live traffic
> so treat the first `cargo build` as the real review. Expect to fix a handful of import and no turn-by-turn navigation, and there is no plan to add them.
> paths and trait-bound details, not the structure.
## 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 ```bash
cargo add axum -F multipart,macros git clone <this repo> && cd rs_maps
cargo add tokio -F full cargo build --release
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
``` ```
Two version-sensitive spots to check first if it doesn't compile: Database and directories:
- **`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
```bash ```bash
podman secret create mapserver-db-password - # type the password, then Ctrl-D mkdir -p ~/.config/containers/systemd ~/gpx
mkdir -p ~/.config/containers/systemd
cp deploy/postgres.container ~/.config/containers/systemd/ 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 daemon-reload
systemctl --user start postgres 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 ```bash
mkdir -p ~/.config/mapserver ~/gpx ./target/release/rs_maps
cp .env.example ~/.config/mapserver/.env
chmod 600 ~/.config/mapserver/.env
$EDITOR ~/.config/mapserver/.env
``` ```
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 For the service unit, systemd hardening and the release/upgrade scripts, see
is testable without a provider. `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 ```bash
cargo build --release cargo test
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
``` ```
## 5. nginx No database needed — the tests cover base-path handling, bbox parsing, GPX
filename safety, waypoint embedding, coordinate ordering and the rate limiter.
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.