routes.rs
⎇
Raw
1use std::path::Path;
2
3use api::{
4 ChangePassword, Credentials, Device, DeviceToken, Login, MAX_BATCH, MAX_TRACK_SECS, Me,
5 NewDevice, NewShare, NewUser, PRECISIONS_M, Person, PersonDevice, Point, ResetPassword,
6 SetRetention, SetRole, SetTwoFactor, SetupStatus, Share, ShareSettings, Shares, Uploaded,
7};
8use axum::extract::{Path as UrlPath, Query, State};
9use axum::http::{HeaderMap, HeaderValue, Uri, header};
10use axum::response::{IntoResponse, Response};
11use axum::routing::{delete, get, post, put};
12use axum::{Json, Router};
13use base64::Engine;
14use rusqlite::{Connection, OptionalExtension, Row, ToSql, params};
15use serde::Deserialize;
16use sha2::{Digest, Sha256};
17use tower_http::services::ServeDir;
18
19use crate::auth::{self, Admin, ClientIp, User};
20use crate::passkeys::{self, Pending};
21use crate::{AppState, Error, now};
22use crate::{device, guest};
23
24type Result<T> = std::result::Result<T, Error>;
25
26pub fn router(state: AppState, web_dir: &Path) -> Router {
27 let csp = HeaderValue::from_str(&content_security_policy(web_dir)).expect("CSP is ASCII");
28 Router::new()
29 .route("/api/setup", get(setup_status).post(setup))
30 .route("/api/login", post(login))
31 .route("/api/logout", post(logout))
32 .route("/api/passkey/login", post(passkeys::login_begin))
33 .route("/api/passkey/login/finish", post(passkeys::login_finish))
34 .route("/api/me", get(me))
35 .route(
36 "/api/me/password",
37 post(change_password).delete(delete_password),
38 )
39 .route("/api/me/two-factor", put(set_two_factor))
40 .route("/api/me/retention", put(set_retention))
41 .route("/api/passkeys", get(passkeys::list))
42 .route("/api/passkeys/register", post(passkeys::register_begin))
43 .route(
44 "/api/passkeys/register/finish",
45 post(passkeys::register_finish),
46 )
47 .route("/api/passkeys/{id}", delete(passkeys::delete))
48 .route("/api/people", get(people))
49 .route("/api/people/{id}/track", get(track))
50 .route("/api/devices", get(list_devices).post(create_device))
51 .route("/api/devices/pair/begin", post(device::pair_begin))
52 .route("/api/devices/pair", post(device::pair_finish))
53 .route("/api/device", get(device::me))
54 .route("/api/device/track", get(device::track))
55 .route("/api/devices/{id}", delete(delete_device))
56 .route("/api/points", post(upload))
57 .route("/api/shares", get(list_shares).post(create_share))
58 .route("/api/shares/{id}", delete(delete_share))
59 .route("/api/usernames", get(usernames))
60 .route("/api/users", get(list_users).post(create_user))
61 .route("/api/users/{id}", delete(delete_user))
62 .route("/api/users/{id}/role", put(set_role))
63 .route("/api/users/{id}/password", post(reset_user_password))
64 .route("/api/links", get(guest::list).post(guest::create))
65 .route("/api/links/{id}", delete(guest::delete))
66 .route("/api/guest", post(guest::view))
67 .route("/api/guest/track", post(guest::track))
68 .route("/api/guest/unlock", post(guest::unlock))
69 .route("/healthz", get(healthz))
70 .fallback_service(ServeDir::new(web_dir))
71 .layer(axum::middleware::map_response(move |mut res: Response| {
72 let csp = csp.clone();
73 async move {
74 let h = res.headers_mut();
75 h.insert(header::CONTENT_SECURITY_POLICY, csp);
76 h.insert(header::X_FRAME_OPTIONS, HeaderValue::from_static("DENY"));
77 h.insert(
78 header::X_CONTENT_TYPE_OPTIONS,
79 HeaderValue::from_static("nosniff"),
80 );
81 // Not no-referrer: the OpenStreetMap tile servers require a Referer.
82 h.insert(
83 header::REFERRER_POLICY,
84 HeaderValue::from_static("strict-origin-when-cross-origin"),
85 );
86 res
87 }
88 }))
89 .with_state(state)
90}
91
92/// Allows the inline scripts of the built index.html by hash. Trunk names them anew in each build.
93fn content_security_policy(web_dir: &Path) -> String {
94 let html = std::fs::read_to_string(web_dir.join("index.html")).unwrap_or_default();
95 let hashes: String = html
96 .split("<script")
97 .skip(1)
98 .filter_map(|s| {
99 let (tag, rest) = s.split_once('>')?;
100 let body = rest.split_once("</script>")?.0;
101 let hash = base64::engine::general_purpose::STANDARD.encode(Sha256::digest(body));
102 (!tag.contains("src=")).then(|| format!(" 'sha256-{hash}'"))
103 })
104 .collect();
105 // Keep the tile hosts in sync with web/src/map.rs.
106 format!(
107 "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'{hashes}; style-src 'self' 'unsafe-inline'; \
108 img-src 'self' data: https://tile.openstreetmap.org https://*.tile.openstreetmap.fr \
109 https://*.tile-cyclosm.openstreetmap.fr https://*.tile.opentopomap.org https://server.arcgisonline.com; \
110 connect-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'"
111 )
112}
113
114async fn healthz(State(s): State<AppState>) -> Result<&'static str> {
115 s.db().query_row("SELECT 1", [], |_| Ok(()))?;
116 Ok("ok")
117}
118
119async fn setup_status(State(s): State<AppState>) -> Result<Json<SetupStatus>> {
120 Ok(Json(SetupStatus {
121 needed: crate::no_users(&s.db())?,
122 }))
123}
124
125/// Creates the first account, an admin. Only works while no user exists.
126async fn setup(State(s): State<AppState>, Json(b): Json<Credentials>) -> Result<Response> {
127 let username = crate::check_username(&b.username)?.to_owned();
128 auth::check_new_password(&b.password).map_err(|m| Error::BadRequest(m.into()))?;
129 let already = || Error::Conflict("the server is already set up".into());
130 // Checked before hashing, so a request to a set-up server costs no Argon2 work.
131 if !crate::no_users(&s.db())? {
132 return Err(already());
133 }
134 let hash = auth::hash_password_async(b.password).await?;
135 let id = {
136 let db = s.db();
137 // Checked again under the same lock as the insert, so two setups cannot both win.
138 if !crate::no_users(&db)? {
139 return Err(already());
140 }
141 crate::insert_user(&db, &username, &hash, true)?
142 };
143 passkeys::sign_in(&s, id)
144}
145
146async fn login(
147 State(s): State<AppState>,
148 ClientIp(ip): ClientIp,
149 uri: Uri,
150 headers: HeaderMap,
151 Json(b): Json<Login>,
152) -> Result<Response> {
153 match &b.state_id {
154 // The passkey already passed. This is the password step of a two-factor sign-in.
155 Some(state_id) => {
156 let Some(Pending::NeedsPassword { user_id }) = s.ceremonies.take(state_id) else {
157 return Err(passkeys::expired());
158 };
159 let username: String =
160 s.db()
161 .query_row("SELECT username FROM users WHERE id = ?1", [user_id], |r| {
162 r.get(0)
163 })?;
164 let ok = auth::check_password(&s, ip, &username, &b.password).await?;
165 passkeys::sign_in(&s, ok.id)
166 }
167 None => {
168 let ok = auth::check_password(&s, ip, b.username.trim(), &b.password).await?;
169 if ok.two_factor {
170 return passkeys::second_factor(&s, &uri, &headers, ok.id);
171 }
172 passkeys::sign_in(&s, ok.id)
173 }
174 }
175}
176
177async fn logout(State(s): State<AppState>, user: User) -> Result<impl IntoResponse> {
178 s.db().execute(
179 "DELETE FROM sessions WHERE token_hash = ?1",
180 [user.session_hash],
181 )?;
182 Ok(([(header::SET_COOKIE, auth::clear_session(&s))], Json(())))
183}
184
185async fn me(State(s): State<AppState>, user: User) -> Result<Json<Me>> {
186 let (has_password, two_factor, retention_days) = s.db().query_row(
187 "SELECT pw_hash IS NOT NULL, two_factor, retention_days FROM users WHERE id = ?1",
188 [user.id],
189 |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)),
190 )?;
191 Ok(Json(Me {
192 id: user.id,
193 username: user.username,
194 is_admin: user.is_admin,
195 has_password,
196 two_factor,
197 retention_days,
198 max_retention_days: (s.max_retention_days > 0).then_some(s.max_retention_days),
199 public_url: s
200 .public_url
201 .as_ref()
202 .map(|u| u.as_str().trim_end_matches('/').to_owned()),
203 }))
204}
205
206/// Sets or changes the password. Changing an existing one needs the old one.
207async fn change_password(
208 State(s): State<AppState>,
209 ClientIp(ip): ClientIp,
210 user: User,
211 Json(b): Json<ChangePassword>,
212) -> Result<Json<()>> {
213 auth::check_new_password(&b.new).map_err(|m| Error::BadRequest(m.into()))?;
214 let has_password: bool = s.db().query_row(
215 "SELECT pw_hash IS NOT NULL FROM users WHERE id = ?1",
216 [user.id],
217 |r| r.get(0),
218 )?;
219 if has_password {
220 // 400, not 401: the session is still valid, only the old password is wrong.
221 auth::check_password(&s, ip, &user.username, b.old.as_deref().unwrap_or_default())
222 .await
223 .map_err(|e| match e {
224 Error::Unauthorized => Error::BadRequest("wrong current password".into()),
225 e => e,
226 })?;
227 } else {
228 user.check_recent()?;
229 }
230 let hash = auth::hash_password_async(b.new).await?;
231 s.db().execute(
232 "UPDATE users SET pw_hash = ?1 WHERE id = ?2",
233 params![hash, user.id],
234 )?;
235 auth::end_other_sessions(&s, &user)?;
236 Ok(Json(()))
237}
238
239/// Leaves the account on passkeys alone.
240async fn delete_password(State(s): State<AppState>, user: User) -> Result<Json<()>> {
241 user.check_recent()?;
242 let db = s.db();
243 if passkeys::count(&db, user.id)? == 0 {
244 return Err(Error::BadRequest(
245 "add a passkey before removing your password".into(),
246 ));
247 }
248 let two_factor: bool = db.query_row(
249 "SELECT two_factor FROM users WHERE id = ?1",
250 [user.id],
251 |r| r.get(0),
252 )?;
253 if two_factor {
254 return Err(Error::BadRequest(
255 "turn off two-factor sign-in before removing your password".into(),
256 ));
257 }
258 db.execute("UPDATE users SET pw_hash = NULL WHERE id = ?1", [user.id])?;
259 drop(db);
260 auth::end_other_sessions(&s, &user)?;
261 Ok(Json(()))
262}
263
264async fn set_two_factor(
265 State(s): State<AppState>,
266 user: User,
267 Json(b): Json<SetTwoFactor>,
268) -> Result<Json<()>> {
269 user.check_recent()?;
270 let db = s.db();
271 if b.enabled {
272 let has_password: bool = db.query_row(
273 "SELECT pw_hash IS NOT NULL FROM users WHERE id = ?1",
274 [user.id],
275 |r| r.get(0),
276 )?;
277 if !has_password {
278 return Err(Error::BadRequest(
279 "set a password before turning on two-factor sign-in".into(),
280 ));
281 }
282 if passkeys::count(&db, user.id)? == 0 {
283 return Err(Error::BadRequest(
284 "add a passkey before turning on two-factor sign-in".into(),
285 ));
286 }
287 }
288 db.execute(
289 "UPDATE users SET two_factor = ?1 WHERE id = ?2",
290 params![b.enabled, user.id],
291 )?;
292 drop(db);
293 auth::end_other_sessions(&s, &user)?;
294 Ok(Json(()))
295}
296
297/// Users can only keep their points for less time than the server allows, never longer.
298async fn set_retention(
299 State(s): State<AppState>,
300 user: User,
301 Json(b): Json<SetRetention>,
302) -> Result<Json<()>> {
303 if let Some(days) = b.days {
304 let max = s.max_retention_days;
305 if days < 1 || (max > 0 && days > max) {
306 let range = if max > 0 {
307 format!("1 to {max}")
308 } else {
309 "at least 1".into()
310 };
311 return Err(Error::BadRequest(format!("retention must be {range} days")));
312 }
313 }
314 let db = s.db();
315 db.execute(
316 "UPDATE users SET retention_days = ?1 WHERE id = ?2",
317 params![b.days, user.id],
318 )?;
319 crate::purge_points(&db, user.id, b.days, s.max_retention_days)?;
320 Ok(Json(()))
321}
322
323const POINT_COLS: &str = "ts, lat, lon, acc, alt, speed, bearing, battery";
324
325/// POINT_COLS, with the stored cell of `m` metres in place of the exact position.
326fn point_cols(m: u32) -> String {
327 match m {
328 0 => POINT_COLS.into(),
329 m => format!("ts, lat_{m}, lon_{m}, acc, alt, speed, bearing, battery"),
330 }
331}
332
333/// The cell columns of all PRECISIONS_M, in order.
334pub fn cell_cols() -> String {
335 PRECISIONS_M.map(|m| format!("lat_{m}, lon_{m}")).join(", ")
336}
337
338pub type Cells = [(f64, f64); PRECISIONS_M.len()];
339
340/// The cells for a new point of a device, given the cells of its previous point.
341pub fn cells(prev: Option<&Cells>, lat: f64, lon: f64) -> Cells {
342 std::array::from_fn(|i| cell(prev.map(|c| c[i]), lat, lon, PRECISIONS_M[i]))
343}
344
345/// The centre of a grid cell of about `m` metres. Keeps the previous cell until the point is a quarter cell
346/// past its edge. Otherwise GPS noise near an edge flips between two cells and shows where the edge is.
347fn cell(prev: Option<(f64, f64)>, lat: f64, lon: f64, m: u32) -> (f64, f64) {
348 let step = f64::from(m) / 111_320.0;
349 // A degree of longitude shrinks toward the poles. Using the snapped latitude keeps one grid per row.
350 let lon_step = |lat: f64| step / lat.to_radians().cos().max(0.01);
351 if let Some((clat, clon)) = prev
352 && (lat - clat).abs() <= 0.75 * step
353 && (lon - clon).abs() <= 0.75 * lon_step(clat)
354 {
355 return (clat, clon);
356 }
357 let clat = ((lat / step).round() * step).clamp(-90.0, 90.0);
358 let clon = ((lon / lon_step(clat)).round() * lon_step(clat)).clamp(-180.0, 180.0);
359 (clat, clon)
360}
361
362/// Reads the POINT_COLS columns, starting at column `i`.
363fn point_at(r: &Row, i: usize) -> rusqlite::Result<Point> {
364 Ok(Point {
365 ts: r.get(i)?,
366 lat: r.get(i + 1)?,
367 lon: r.get(i + 2)?,
368 acc: r.get(i + 3)?,
369 alt: r.get(i + 4)?,
370 speed: r.get(i + 5)?,
371 bearing: r.get(i + 6)?,
372 battery: r.get(i + 7)?,
373 })
374}
375
376/// Drops what would reveal more than a point read with `point_cols(m)` should. The position is already its cell.
377fn coarsen(p: &mut Point, m: u32) {
378 if m == 0 {
379 return;
380 }
381 // A jump to the next cell shows when the owner crossed the cell edge, and where that edge is.
382 // Rounding the time to m seconds keeps that crossing about m metres vague at walking speed.
383 p.ts -= p.ts.rem_euclid(i64::from(m));
384 p.acc = Some(p.acc.unwrap_or(0.0).max(m as f32));
385 p.alt = None;
386 p.speed = None;
387 p.bearing = None;
388 // The battery drains steadily, so it would date a point within its rounded time.
389 p.battery = None;
390}
391
392/// What a viewer may see of one owner.
393pub struct Access {
394 pub owner: i64,
395 pub username: String,
396 /// None for the viewer's own account.
397 pub share: Option<i64>,
398 pub all_devices: bool,
399 pub trail_since: Option<i64>,
400 pub precision_m: u32,
401}
402
403/// Columns of `shares s` that `access_at` reads, after the owner id and username.
404pub const ACCESS_COLS: &str = "s.id, s.all_devices, s.trail_since, s.precision_m";
405
406/// Reads an owner id, a username and ACCESS_COLS.
407pub fn access_at(r: &Row) -> rusqlite::Result<Access> {
408 Ok(Access {
409 owner: r.get(0)?,
410 username: r.get(1)?,
411 share: r.get(2)?,
412 all_devices: r.get(3)?,
413 trail_since: r.get(4)?,
414 precision_m: r.get(5)?,
415 })
416}
417
418/// Limits `devices d` to the ones an Access allows. Binds ?2 = share, ?3 = all_devices.
419const DEVICE_ALLOWED: &str =
420 "(?3 OR d.id IN (SELECT device_id FROM share_devices WHERE share_id = ?2))";
421
422/// The viewer first, then everyone with an active share to the viewer.
423fn accesses(db: &Connection, viewer: i64) -> rusqlite::Result<Vec<Access>> {
424 let mut list: Vec<Access> = db
425 .prepare_cached(&format!(
426 "SELECT id, username, NULL, 1, 0, 0 FROM users WHERE id = ?1
427 UNION ALL
428 SELECT u.id, u.username, {ACCESS_COLS}
429 FROM shares s JOIN users u ON u.id = s.owner_id
430 WHERE s.viewer_id = ?1 AND (s.expires_at IS NULL OR s.expires_at > ?2)"
431 ))?
432 .query_map(params![viewer, now()], access_at)?
433 .collect::<rusqlite::Result<_>>()?;
434 list.sort_by_key(|a| (a.owner != viewer, a.username.to_lowercase()));
435 Ok(list)
436}
437
438/// The owner's allowed devices with their newest point.
439pub fn person_for(db: &Connection, a: Access) -> rusqlite::Result<Person> {
440 let mut devices: Vec<PersonDevice> = db
441 .prepare_cached(&format!(
442 "SELECT d.id, d.name, {} FROM devices d
443 JOIN points p ON p.device_id = d.id AND p.ts = (SELECT MAX(ts) FROM points WHERE device_id = d.id)
444 WHERE d.user_id = ?1 AND {DEVICE_ALLOWED}",
445 point_cols(a.precision_m)
446 ))?
447 .query_map(params![a.owner, a.share, a.all_devices], |r| {
448 let mut last = point_at(r, 2)?;
449 coarsen(&mut last, a.precision_m);
450 Ok(PersonDevice {
451 id: r.get(0)?,
452 name: r.get(1)?,
453 last,
454 })
455 })?
456 .collect::<rusqlite::Result<_>>()?;
457 // By rounded time, so the order does not tell which device sent last within the same rounded time.
458 devices.sort_by_key(|d| (std::cmp::Reverse(d.last.ts), d.id));
459 Ok(Person {
460 id: a.owner,
461 username: a.username,
462 devices,
463 trail_since: a.trail_since,
464 precision_m: a.precision_m,
465 })
466}
467
468fn people_for(db: &Connection, viewer: i64) -> rusqlite::Result<Vec<Person>> {
469 accesses(db, viewer)?
470 .into_iter()
471 .map(|a| person_for(db, a))
472 .collect()
473}
474
475async fn people(State(s): State<AppState>, user: User) -> Result<Json<Vec<Person>>> {
476 Ok(Json(people_for(&s.db(), user.id)?))
477}
478
479#[derive(Deserialize)]
480struct TrackQuery {
481 from: i64,
482 to: i64,
483 device: i64,
484}
485
486const MAX_TRACK_POINTS: i64 = 50_000;
487
488async fn track(
489 State(s): State<AppState>,
490 user: User,
491 UrlPath(id): UrlPath<i64>,
492 Query(q): Query<TrackQuery>,
493) -> Result<Json<Vec<Point>>> {
494 let db = s.db();
495 let a = accesses(&db, user.id)?
496 .into_iter()
497 .find(|a| a.owner == id)
498 .ok_or(Error::NotFound)?;
499 Ok(Json(track_points(&db, &a, q.device, q.from, q.to)?))
500}
501
502/// One device's points in a time range, as far as the access allows.
503pub fn track_points(
504 db: &Connection,
505 a: &Access,
506 device: i64,
507 from: i64,
508 to: i64,
509) -> Result<Vec<Point>> {
510 if to
511 .checked_sub(from)
512 .is_none_or(|d| !(0..=MAX_TRACK_SECS).contains(&d))
513 {
514 return Err(Error::BadRequest("range must be 0 to 31 days".into()));
515 }
516 let since = a.trail_since.ok_or(Error::Forbidden)?;
517 // Only whole m-second buckets. A bound inside one would split its points by raw time,
518 // and moving the bound would reveal the raw times that the rounding hides.
519 // Saturation only matters far from any stored time.
520 let m = i64::from(a.precision_m.max(1));
521 let floor = |t: i64| t.saturating_sub(t.rem_euclid(m));
522 let from = floor(from).max(floor(since.saturating_add(m - 1)));
523 let to = floor(to).saturating_add(m - 1);
524 let allowed: bool = db.query_row(
525 &format!(
526 "SELECT EXISTS (SELECT 1 FROM devices d WHERE d.id = ?4 AND d.user_id = ?1 AND {DEVICE_ALLOWED})"
527 ),
528 params![a.owner, a.share, a.all_devices, device],
529 |r| r.get(0),
530 )?;
531 if !allowed {
532 return Err(Error::NotFound);
533 }
534 // ponytail: past the limit the oldest points go. Thin the trail evenly if long ranges need all of it.
535 let mut points: Vec<Point> = db
536 .prepare_cached(&format!(
537 "SELECT * FROM (SELECT {} FROM points WHERE device_id = ?1 AND ts BETWEEN ?2 AND ?3
538 ORDER BY ts DESC LIMIT {MAX_TRACK_POINTS}) ORDER BY ts",
539 point_cols(a.precision_m)
540 ))?
541 .query_map(params![device, from, to], |r| {
542 let mut p = point_at(r, 0)?;
543 coarsen(&mut p, a.precision_m);
544 Ok(p)
545 })?
546 .collect::<rusqlite::Result<_>>()?;
547 // The limit can split the oldest bucket, which would show raw times again.
548 if points.len() as i64 == MAX_TRACK_POINTS && a.precision_m > 0 {
549 let oldest = points[0].ts;
550 points.retain(|p| p.ts != oldest);
551 }
552 points.dedup_by(|b, a| (a.ts, a.lat, a.lon) == (b.ts, b.lat, b.lon));
553 Ok(points)
554}
555
556async fn list_devices(State(s): State<AppState>, user: User) -> Result<Json<Vec<Device>>> {
557 let devices = s
558 .db()
559 .prepare_cached(
560 "SELECT id, name, token_hash IS NULL, created_at, last_seen_at FROM devices
561 WHERE user_id = ?1 ORDER BY created_at",
562 )?
563 .query_map([user.id], |r| {
564 Ok(Device {
565 id: r.get(0)?,
566 name: r.get(1)?,
567 web: r.get(2)?,
568 created_at: r.get(3)?,
569 last_seen_at: r.get(4)?,
570 })
571 })?
572 .collect::<rusqlite::Result<_>>()?;
573 Ok(Json(devices))
574}
575
576pub fn check_device_name(name: &str) -> Result<&str> {
577 let name = name.trim();
578 if name.is_empty() || name.chars().count() > 100 {
579 return Err(Error::BadRequest(
580 "device name must have 1 to 100 characters".into(),
581 ));
582 }
583 Ok(name)
584}
585
586pub fn insert_device(db: &Connection, user_id: i64, name: &str) -> Result<DeviceToken> {
587 let name = check_device_name(name)?;
588 let (token, hash) = auth::new_secret();
589 db.execute(
590 "INSERT INTO devices (user_id, name, token_hash, created_at) VALUES (?1, ?2, ?3, ?4)",
591 params![user_id, name, hash, now()],
592 )?;
593 Ok(DeviceToken { token })
594}
595
596async fn create_device(
597 State(s): State<AppState>,
598 user: User,
599 Json(b): Json<NewDevice>,
600) -> Result<Json<DeviceToken>> {
601 Ok(Json(insert_device(&s.db(), user.id, &b.name)?))
602}
603
604async fn delete_device(
605 State(s): State<AppState>,
606 user: User,
607 UrlPath(id): UrlPath<i64>,
608) -> Result<Json<()>> {
609 let n = s.db().execute(
610 "DELETE FROM devices WHERE id = ?1 AND user_id = ?2",
611 [id, user.id],
612 )?;
613 if n == 0 {
614 return Err(Error::NotFound);
615 }
616 Ok(Json(()))
617}
618
619/// Clock skew we accept from a device, so a wrong clock cannot write far into the future.
620const MAX_FUTURE_SECS: i64 = 86400;
621
622fn check_point(p: &Point, now: i64) -> std::result::Result<(), String> {
623 if !(-90.0..=90.0).contains(&p.lat) || !(-180.0..=180.0).contains(&p.lon) {
624 return Err(format!("point {}: coordinates out of range", p.ts));
625 }
626 if p.ts <= 0 || p.ts > now + MAX_FUTURE_SECS {
627 return Err(format!("point {}: timestamp out of range", p.ts));
628 }
629 if p.battery.is_some_and(|b| b > 100) {
630 return Err(format!("point {}: battery above 100", p.ts));
631 }
632 Ok(())
633}
634
635async fn upload(
636 State(s): State<AppState>,
637 uploader: auth::Uploader,
638 Json(points): Json<Vec<Point>>,
639) -> Result<Json<Uploaded>> {
640 if points.len() > MAX_BATCH {
641 return Err(Error::BadRequest(format!(
642 "at most {MAX_BATCH} points per request"
643 )));
644 }
645 let now = now();
646 let total = points.len();
647 let mut points: Vec<Point> = points
648 .into_iter()
649 .filter(|p| check_point(p, now).is_ok())
650 .collect();
651 // Each point's cells follow from the previous point's, so older points go first.
652 points.sort_by_key(|p| p.ts);
653
654 let mut db = s.db();
655 let tx = db.transaction()?;
656 let mut stored = 0;
657 {
658 let cols = cell_cols();
659 let mut insert = tx.prepare_cached(&format!(
660 "INSERT OR IGNORE INTO points (device_id, {POINT_COLS}, {cols}) VALUES ({})",
661 (1..=9 + 2 * PRECISIONS_M.len())
662 .map(|i| format!("?{i}"))
663 .collect::<Vec<_>>()
664 .join(", ")
665 ))?;
666 let mut prev = tx.prepare_cached(&format!(
667 "SELECT {cols} FROM points WHERE device_id = ?1 AND ts < ?2 ORDER BY ts DESC LIMIT 1"
668 ))?;
669 for p in &points {
670 let before: Option<Cells> = prev
671 .query_row(params![uploader.device_id, p.ts], |r| {
672 let mut c = [(0.0, 0.0); PRECISIONS_M.len()];
673 for (i, c) in c.iter_mut().enumerate() {
674 *c = (r.get(2 * i)?, r.get(2 * i + 1)?);
675 }
676 Ok(c)
677 })
678 .optional()?;
679 let cells = cells(before.as_ref(), p.lat, p.lon);
680 let mut values: Vec<&dyn ToSql> = vec![
681 &uploader.device_id,
682 &p.ts,
683 &p.lat,
684 &p.lon,
685 &p.acc,
686 &p.alt,
687 &p.speed,
688 &p.bearing,
689 &p.battery,
690 ];
691 values.extend(cells.iter().flat_map(|(a, b)| [a as &dyn ToSql, b]));
692 stored += insert.execute(values.as_slice())?;
693 }
694 }
695 tx.execute(
696 "UPDATE devices SET last_seen_at = ?1 WHERE id = ?2",
697 [now, uploader.device_id],
698 )?;
699 tx.commit()?;
700 Ok(Json(Uploaded {
701 stored,
702 skipped: total - points.len(),
703 }))
704}
705
706/// Reads ACCESS_COLS from column `i` on.
707pub fn settings_at(db: &Connection, r: &Row, i: usize) -> rusqlite::Result<ShareSettings> {
708 let id: i64 = r.get(i)?;
709 let devices = match r.get::<_, bool>(i + 1)? {
710 true => None,
711 false => Some(
712 db.prepare_cached("SELECT device_id FROM share_devices WHERE share_id = ?1")?
713 .query_map([id], |r| r.get(0))?
714 .collect::<rusqlite::Result<_>>()?,
715 ),
716 };
717 Ok(ShareSettings {
718 devices,
719 trail_since: r.get(i + 2)?,
720 precision_m: r.get(i + 3)?,
721 })
722}
723
724pub fn check_settings(set: &ShareSettings, expires_at: Option<i64>) -> Result<()> {
725 if expires_at.is_some_and(|t| t <= now()) {
726 return Err(Error::BadRequest("expiry must be in the future".into()));
727 }
728 if set.precision_m != 0 && !PRECISIONS_M.contains(&set.precision_m) {
729 return Err(Error::BadRequest(
730 "precision must be 0, 100, 1000, 10000 or 100000 metres".into(),
731 ));
732 }
733 if set.devices.as_ref().is_some_and(Vec::is_empty) {
734 return Err(Error::BadRequest("select at least one device".into()));
735 }
736 Ok(())
737}
738
739/// Writes the settings columns and the device selection of a share or link.
740pub fn save_settings(db: &Connection, id: i64, owner: i64, set: &ShareSettings) -> Result<()> {
741 db.execute(
742 "UPDATE shares SET all_devices = ?2, trail_since = ?3, precision_m = ?4 WHERE id = ?1",
743 params![id, set.devices.is_none(), set.trail_since, set.precision_m],
744 )?;
745 db.execute("DELETE FROM share_devices WHERE share_id = ?1", [id])?;
746 for device in set.devices.iter().flatten() {
747 let added = db.execute(
748 "INSERT OR IGNORE INTO share_devices SELECT ?1, id FROM devices WHERE id = ?2 AND user_id = ?3",
749 [id, *device, owner],
750 )?;
751 if added == 0 {
752 return Err(Error::BadRequest("no such device".into()));
753 }
754 }
755 Ok(())
756}
757
758async fn list_shares(State(s): State<AppState>, user: User) -> Result<Json<Shares>> {
759 let db = s.db();
760 let query = |other: &str, me: &str| -> rusqlite::Result<Vec<Share>> {
761 db.prepare_cached(&format!(
762 "SELECT u.username, s.expires_at, s.created_at, {ACCESS_COLS}
763 FROM shares s JOIN users u ON u.id = s.{other} WHERE s.{me} = ?1 ORDER BY u.username"
764 ))?
765 .query_map([user.id], |r| {
766 Ok(Share {
767 id: r.get(3)?,
768 username: r.get(0)?,
769 expires_at: r.get(1)?,
770 created_at: r.get(2)?,
771 settings: settings_at(&db, r, 3)?,
772 })
773 })?
774 .collect()
775 };
776 Ok(Json(Shares {
777 outgoing: query("viewer_id", "owner_id")?,
778 incoming: query("owner_id", "viewer_id")?,
779 }))
780}
781
782async fn create_share(
783 State(s): State<AppState>,
784 user: User,
785 Json(b): Json<NewShare>,
786) -> Result<Json<Share>> {
787 check_settings(&b.settings, b.expires_at)?;
788 let mut db = s.db();
789 let (viewer_id, username): (i64, String) = db
790 .query_row(
791 "SELECT id, username FROM users WHERE username = ?1",
792 [b.viewer.trim()],
793 |r| Ok((r.get(0)?, r.get(1)?)),
794 )
795 .optional()?
796 .ok_or_else(|| Error::BadRequest("no such user".into()))?;
797 if viewer_id == user.id {
798 return Err(Error::BadRequest("you cannot share with yourself".into()));
799 }
800 let tx = db.transaction()?;
801 let (id, created_at) = tx.query_row(
802 "INSERT INTO shares (owner_id, viewer_id, expires_at, created_at) VALUES (?1, ?2, ?3, ?4)
803 ON CONFLICT (owner_id, viewer_id) DO UPDATE SET expires_at = excluded.expires_at
804 RETURNING id, created_at",
805 params![user.id, viewer_id, b.expires_at, now()],
806 |r| Ok((r.get(0)?, r.get(1)?)),
807 )?;
808 save_settings(&tx, id, user.id, &b.settings)?;
809 tx.commit()?;
810 Ok(Json(Share {
811 id,
812 username,
813 expires_at: b.expires_at,
814 created_at,
815 settings: b.settings,
816 }))
817}
818
819/// Either side can end a share. Guest links have their own endpoint.
820async fn delete_share(
821 State(s): State<AppState>,
822 user: User,
823 UrlPath(id): UrlPath<i64>,
824) -> Result<Json<()>> {
825 let n = s.db().execute(
826 "DELETE FROM shares WHERE id = ?1 AND viewer_id IS NOT NULL AND (owner_id = ?2 OR viewer_id = ?2)",
827 [id, user.id],
828 )?;
829 if n == 0 {
830 return Err(Error::NotFound);
831 }
832 Ok(Json(()))
833}
834
835/// Everyone else's username, for picking whom to share with.
836async fn usernames(State(s): State<AppState>, user: User) -> Result<Json<Vec<String>>> {
837 let names = s
838 .db()
839 .prepare_cached("SELECT username FROM users WHERE id <> ?1 ORDER BY username")?
840 .query_map([user.id], |r| r.get(0))?
841 .collect::<rusqlite::Result<_>>()?;
842 Ok(Json(names))
843}
844
845async fn list_users(State(s): State<AppState>, _: Admin) -> Result<Json<Vec<api::User>>> {
846 let users = s
847 .db()
848 .prepare_cached("SELECT id, username, is_admin, created_at FROM users ORDER BY username")?
849 .query_map([], |r| {
850 Ok(api::User {
851 id: r.get(0)?,
852 username: r.get(1)?,
853 is_admin: r.get(2)?,
854 created_at: r.get(3)?,
855 })
856 })?
857 .collect::<rusqlite::Result<_>>()?;
858 Ok(Json(users))
859}
860
861async fn create_user(
862 State(s): State<AppState>,
863 Admin(admin): Admin,
864 Json(b): Json<NewUser>,
865) -> Result<Json<api::User>> {
866 admin.check_recent()?;
867 let username = crate::check_username(&b.username)?.to_owned();
868 auth::check_new_password(&b.password).map_err(|m| Error::BadRequest(m.into()))?;
869 let hash = auth::hash_password_async(b.password).await?;
870 let db = s.db();
871 let id = crate::insert_user(&db, &username, &hash, b.is_admin)?;
872 Ok(Json(api::User {
873 id,
874 username,
875 is_admin: b.is_admin,
876 created_at: now(),
877 }))
878}
879
880/// Admins cannot change their own role, so at least one admin always remains.
881/// The statements check that the caller is still an admin, so two admins cannot demote or delete each other at once.
882async fn set_role(
883 State(s): State<AppState>,
884 Admin(admin): Admin,
885 UrlPath(id): UrlPath<i64>,
886 Json(b): Json<SetRole>,
887) -> Result<Json<()>> {
888 if id == admin.id {
889 return Err(Error::BadRequest("you cannot change your own role".into()));
890 }
891 admin.check_recent()?;
892 if s.db().execute(
893 "UPDATE users SET is_admin = ?1 WHERE id = ?2 AND (SELECT is_admin FROM users WHERE id = ?3)",
894 params![b.is_admin, id, admin.id],
895 )? == 0
896 {
897 return Err(Error::NotFound);
898 }
899 Ok(Json(()))
900}
901
902async fn delete_user(
903 State(s): State<AppState>,
904 Admin(admin): Admin,
905 UrlPath(id): UrlPath<i64>,
906) -> Result<Json<()>> {
907 if id == admin.id {
908 return Err(Error::BadRequest(
909 "you cannot delete your own account".into(),
910 ));
911 }
912 if s.db().execute(
913 "DELETE FROM users WHERE id = ?1 AND (SELECT is_admin FROM users WHERE id = ?2)",
914 [id, admin.id],
915 )? == 0
916 {
917 return Err(Error::NotFound);
918 }
919 Ok(Json(()))
920}
921
922/// The recovery path for a user who lost their password or passkey.
923async fn reset_user_password(
924 State(s): State<AppState>,
925 Admin(admin): Admin,
926 UrlPath(id): UrlPath<i64>,
927 Json(b): Json<ResetPassword>,
928) -> Result<Json<()>> {
929 admin.check_recent()?;
930 auth::check_new_password(&b.password).map_err(|m| Error::BadRequest(m.into()))?;
931 let hash = auth::hash_password_async(b.password).await?;
932 let mut db = s.db();
933 if db
934 .query_row("SELECT 1 FROM users WHERE id = ?1", [id], |_| Ok(()))
935 .optional()?
936 .is_none()
937 {
938 return Err(Error::NotFound);
939 }
940 crate::reset_password(&mut db, id, &hash)?;
941 Ok(Json(()))
942}
943
944#[cfg(test)]
945mod tests {
946 use super::*;
947
948 #[test]
949 fn point_validation() {
950 let now = 1_800_000_000;
951 let p = |ts, lat, lon| Point {
952 ts,
953 lat,
954 lon,
955 ..Default::default()
956 };
957 assert!(check_point(&p(now, 48.1, 11.5), now).is_ok());
958 assert!(check_point(&p(now, 91.0, 0.0), now).is_err());
959 assert!(check_point(&p(now, 0.0, -180.1), now).is_err());
960 assert!(check_point(&p(now + 2 * MAX_FUTURE_SECS, 0.0, 0.0), now).is_err());
961 assert!(
962 check_point(
963 &Point {
964 battery: Some(101),
965 ..p(now, 0.0, 0.0)
966 },
967 now
968 )
969 .is_err()
970 );
971 }
972
973 #[test]
974 fn coarse_points_hide_motion() {
975 let exact = Point {
976 ts: 1_800_000_999,
977 lat: 48.137_15,
978 lon: 11.575_49,
979 acc: Some(5.0),
980 speed: Some(3.0),
981 battery: Some(80),
982 ..Default::default()
983 };
984 let mut p = exact.clone();
985 coarsen(&mut p, 0);
986 assert_eq!(p, exact);
987 coarsen(&mut p, 1000);
988 assert_eq!(
989 (p.acc, p.speed, p.battery, p.ts),
990 (Some(1000.0), None, None, 1_800_000_000)
991 );
992 }
993
994 #[test]
995 fn cells_stay_near_and_stick_through_noise() {
996 let (lat, lon) = (48.137_15, 11.575_49);
997 let (clat, clon) = cell(None, lat, lon, 1000);
998 let (dy, dx) = (
999 (clat - lat) * 111_320.0,
1000 (clon - lon) * 111_320.0 * lat.to_radians().cos(),
1001 );
1002 assert!(
1003 dy.abs() <= 500.0 && dx.abs() <= 510.0,
1004 "moved {dy} m, {dx} m"
1005 );
1006 // A point 0.1 m past the south edge, then noise of 10 m around the edge.
1007 let edge = clat - 500.0 / 111_320.0;
1008 let first = cell(None, edge - 0.1 / 111_320.0, clon, 1000);
1009 let mut c = first;
1010 for i in 0..20 {
1011 let noise = if i % 2 == 0 { 10.0 } else { -10.0 };
1012 c = cell(Some(c), edge + noise / 111_320.0, clon, 1000);
1013 assert_eq!(c, first);
1014 }
1015 // Clearly in the next cell.
1016 let moved = cell(Some(c), clat, clon, 1000);
1017 assert_eq!(moved, (clat, clon));
1018 }
1019
1020 fn coarse_db() -> Connection {
1021 let db = crate::test_db();
1022 db.execute_batch(
1023 "INSERT INTO users (id, username, webauthn_id, created_at) VALUES (1, 'a', '1', 0);
1024 INSERT INTO devices (id, user_id, name, token_hash, created_at) VALUES (1, 1, 'p', x'01', 0);
1025 INSERT INTO points (device_id, ts, lat, lon, lat_100, lon_100) VALUES
1026 (1, 1000, 0, 0, 0, 0), (1, 1042, 1, 1, 1, 1), (1, 1099, 2, 2, 2, 2), (1, 1100, 3, 3, 3, 3);",
1027 )
1028 .unwrap();
1029 db
1030 }
1031
1032 fn coarse(trail_since: i64) -> Access {
1033 Access {
1034 owner: 1,
1035 username: "a".into(),
1036 share: None,
1037 all_devices: true,
1038 trail_since: Some(trail_since),
1039 precision_m: 100,
1040 }
1041 }
1042
1043 #[test]
1044 fn coarse_tracks_cover_whole_buckets_only() {
1045 let db = coarse_db();
1046 let lats = |a: &Access, from, to| -> Vec<f64> {
1047 track_points(&db, a, 1, from, to)
1048 .unwrap()
1049 .iter()
1050 .map(|p| p.lat)
1051 .collect()
1052 };
1053 // Any bound inside a bucket gives the whole bucket, so it cannot split 1042 from 1099.
1054 for to in [1000, 1041, 1042, 1099] {
1055 assert_eq!(lats(&coarse(0), 1000, to), [0.0, 1.0, 2.0], "to {to}");
1056 }
1057 for from in [1001, 1042, 1043, 1099] {
1058 assert_eq!(lats(&coarse(0), from, 1099), [0.0, 1.0, 2.0], "from {from}");
1059 }
1060 // A trail start inside a bucket leaves out the whole bucket.
1061 assert_eq!(lats(&coarse(1042), 0, 2000), [3.0]);
1062 }
1063
1064 #[test]
1065 fn huge_ranges_are_refused() {
1066 let db = coarse_db();
1067 assert!(matches!(
1068 track_points(&db, &coarse(0), 1, i64::MIN, i64::MAX),
1069 Err(Error::BadRequest(_))
1070 ));
1071 assert!(track_points(&db, &coarse(i64::MIN), 1, i64::MAX - 10, i64::MAX).is_ok());
1072 }
1073
1074 async fn admin(s: &AppState, id: i64) -> Result<Json<()>> {
1075 let caller = Admin(User {
1076 id,
1077 username: String::new(),
1078 is_admin: true,
1079 session_hash: vec![],
1080 signed_in_at: now(),
1081 });
1082 set_role(
1083 State(s.clone()),
1084 caller,
1085 UrlPath(3 - id),
1086 Json(SetRole { is_admin: false }),
1087 )
1088 .await
1089 }
1090
1091 #[tokio::test]
1092 async fn two_admins_cannot_demote_each_other() {
1093 let s = crate::test_state();
1094 s.db()
1095 .execute_batch(
1096 "INSERT INTO users (id, username, webauthn_id, is_admin, created_at) VALUES (1, 'a', '1', 1, 0), (2, 'b', '2', 1, 0);",
1097 )
1098 .unwrap();
1099 assert!(admin(&s, 1).await.is_ok());
1100 // User 2's request passed the extractor before user 1 demoted them.
1101 assert!(admin(&s, 2).await.is_err());
1102 let admins: i64 = s
1103 .db()
1104 .query_row("SELECT COUNT(*) FROM users WHERE is_admin", [], |r| {
1105 r.get(0)
1106 })
1107 .unwrap();
1108 assert_eq!(admins, 1);
1109 }
1110
1111 #[test]
1112 fn people_shows_only_shared_devices() {
1113 let db = crate::test_db();
1114 db.execute_batch(
1115 "INSERT INTO users (id, username, webauthn_id, created_at) VALUES (1, 'a', '1', 0), (2, 'b', '2', 0);
1116 INSERT INTO devices (id, user_id, name, token_hash, created_at) VALUES (10, 2, 'phone', x'01', 0), (11, 2, 'car', x'02', 0);
1117 INSERT INTO points (device_id, ts, lat, lon) VALUES (10, 100, 1, 1), (11, 200, 2, 2);
1118 INSERT INTO shares (id, owner_id, viewer_id, created_at, all_devices) VALUES (5, 2, 1, 0, 1);",
1119 )
1120 .unwrap();
1121 let devices = |db: &Connection| -> Vec<String> {
1122 people_for(db, 1).unwrap()[1]
1123 .devices
1124 .iter()
1125 .map(|d| d.name.clone())
1126 .collect()
1127 };
1128 assert_eq!(devices(&db), ["car", "phone"]);
1129 db.execute_batch(
1130 "UPDATE shares SET all_devices = 0; INSERT INTO share_devices VALUES (5, 10);",
1131 )
1132 .unwrap();
1133 assert_eq!(devices(&db), ["phone"]);
1134 }
1135
1136 #[test]
1137 fn people_respects_share_expiry() {
1138 let db = crate::test_db();
1139 db.execute_batch(
1140 "INSERT INTO users (id, username, webauthn_id, created_at) VALUES (1, 'a', '1', 0), (2, 'b', '2', 0), (3, 'c', '3', 0);
1141 INSERT INTO shares (owner_id, viewer_id, expires_at, created_at) VALUES (2, 1, NULL, 0), (3, 1, 1, 0);",
1142 )
1143 .unwrap();
1144 let names: Vec<_> = people_for(&db, 1)
1145 .unwrap()
1146 .into_iter()
1147 .map(|p| p.username)
1148 .collect();
1149 assert_eq!(names, ["a", "b"]);
1150 }
1151}
1152