routes.rs
⎇
Raw
1use std::path::Path;
2
3use api::{
4 ChangePassword, Credentials, Device, DeviceToken, Login, MAX_BATCH, MAX_PRECISION_M,
5 MAX_TRACK_SECS, Me, NewDevice, NewShare, NewUser, Person, PersonDevice, Point, ResetPassword,
6 SetRetention, SetRole, SetTwoFactor, SetupStatus, Share, ShareSettings, Shares, Trail,
7 Uploaded,
8};
9use axum::extract::{Path as UrlPath, Query, State};
10use axum::http::{HeaderMap, Uri, header};
11use axum::response::{IntoResponse, Response};
12use axum::routing::{delete, get, post, put};
13use axum::{Json, Router};
14use rusqlite::{Connection, OptionalExtension, Row, params};
15use serde::Deserialize;
16use tower_http::services::ServeDir;
17
18use crate::auth::{self, Admin, ClientIp, User};
19use crate::passkeys;
20use crate::{AppState, Error, now};
21use crate::{device, guest};
22
23type Result<T> = std::result::Result<T, Error>;
24
25pub fn router(state: AppState, web_dir: &Path) -> Router {
26 Router::new()
27 .route("/api/setup", get(setup_status).post(setup))
28 .route("/api/login", post(login))
29 .route("/api/logout", post(logout))
30 .route("/api/passkey/login", post(passkeys::login_begin))
31 .route("/api/passkey/login/finish", post(passkeys::login_finish))
32 .route("/api/me", get(me))
33 .route(
34 "/api/me/password",
35 post(change_password).delete(delete_password),
36 )
37 .route("/api/me/two-factor", put(set_two_factor))
38 .route("/api/me/retention", put(set_retention))
39 .route("/api/passkeys", get(passkeys::list))
40 .route("/api/passkeys/register", post(passkeys::register_begin))
41 .route(
42 "/api/passkeys/register/finish",
43 post(passkeys::register_finish),
44 )
45 .route("/api/passkeys/{id}", delete(passkeys::delete))
46 .route("/api/people", get(people))
47 .route("/api/people/{id}/track", get(track))
48 .route("/api/devices", get(list_devices).post(create_device))
49 .route("/api/devices/pair/begin", post(device::pair_begin))
50 .route("/api/devices/pair", post(device::pair_finish))
51 .route("/api/device", get(device::me))
52 .route("/api/device/track", get(device::track))
53 .route("/api/devices/{id}", delete(delete_device))
54 .route("/api/points", post(upload))
55 .route("/api/shares", get(list_shares).post(create_share))
56 .route("/api/shares/{id}", delete(delete_share))
57 .route("/api/usernames", get(usernames))
58 .route("/api/users", get(list_users).post(create_user))
59 .route("/api/users/{id}", delete(delete_user))
60 .route("/api/users/{id}/role", put(set_role))
61 .route("/api/users/{id}/password", post(reset_user_password))
62 .route("/api/links", get(guest::list).post(guest::create))
63 .route("/api/links/{id}", delete(guest::delete))
64 .route("/api/guest", post(guest::view))
65 .route("/api/guest/track", post(guest::track))
66 .route("/api/guest/unlock", post(guest::unlock))
67 .route("/healthz", get(healthz))
68 .fallback_service(ServeDir::new(web_dir))
69 .with_state(state)
70}
71
72async fn healthz(State(s): State<AppState>) -> Result<&'static str> {
73 s.db().query_row("SELECT 1", [], |_| Ok(()))?;
74 Ok("ok")
75}
76
77fn no_users(db: &Connection) -> rusqlite::Result<bool> {
78 db.query_row("SELECT NOT EXISTS (SELECT 1 FROM users)", [], |r| r.get(0))
79}
80
81async fn setup_status(State(s): State<AppState>) -> Result<Json<SetupStatus>> {
82 Ok(Json(SetupStatus {
83 needed: no_users(&s.db())?,
84 }))
85}
86
87/// Creates the first account, an admin. Only works while no user exists.
88async fn setup(State(s): State<AppState>, Json(b): Json<Credentials>) -> Result<Response> {
89 let username = crate::check_username(&b.username)?.to_owned();
90 auth::check_new_password(&b.password).map_err(|m| Error::BadRequest(m.into()))?;
91 let already = || Error::Conflict("the server is already set up".into());
92 // Checked before hashing, so a request to a set-up server costs no Argon2 work.
93 if !no_users(&s.db())? {
94 return Err(already());
95 }
96 let hash = auth::hash_password_async(b.password).await?;
97 let id = {
98 let db = s.db();
99 // Checked again under the same lock as the insert, so two setups cannot both win.
100 if !no_users(&db)? {
101 return Err(already());
102 }
103 crate::insert_user(&db, &username, &hash, true)?
104 };
105 passkeys::sign_in(&s, id)
106}
107
108async fn login(
109 State(s): State<AppState>,
110 ClientIp(ip): ClientIp,
111 uri: Uri,
112 headers: HeaderMap,
113 Json(b): Json<Login>,
114) -> Result<Response> {
115 let ok = auth::check_password(&s, ip, b.username.trim(), &b.password).await?;
116 if ok.two_factor {
117 return passkeys::second_factor(&s, &uri, &headers, ok.id);
118 }
119 passkeys::sign_in(&s, ok.id)
120}
121
122async fn logout(State(s): State<AppState>, user: User) -> Result<impl IntoResponse> {
123 s.db().execute(
124 "DELETE FROM sessions WHERE token_hash = ?1",
125 [user.session_hash],
126 )?;
127 Ok(([(header::SET_COOKIE, auth::clear_session(&s))], Json(())))
128}
129
130async fn me(State(s): State<AppState>, user: User) -> Result<Json<Me>> {
131 let (has_password, two_factor, retention_days) = s.db().query_row(
132 "SELECT pw_hash IS NOT NULL, two_factor, retention_days FROM users WHERE id = ?1",
133 [user.id],
134 |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)),
135 )?;
136 Ok(Json(Me {
137 id: user.id,
138 username: user.username,
139 is_admin: user.is_admin,
140 has_password,
141 two_factor,
142 retention_days,
143 max_retention_days: (s.max_retention_days > 0).then_some(s.max_retention_days),
144 public_url: s
145 .public_url
146 .as_ref()
147 .map(|u| u.as_str().trim_end_matches('/').to_owned()),
148 }))
149}
150
151/// Sets or changes the password. Changing an existing one needs the old one.
152async fn change_password(
153 State(s): State<AppState>,
154 ClientIp(ip): ClientIp,
155 user: User,
156 Json(b): Json<ChangePassword>,
157) -> Result<Json<()>> {
158 auth::check_new_password(&b.new).map_err(|m| Error::BadRequest(m.into()))?;
159 let has_password: bool = s.db().query_row(
160 "SELECT pw_hash IS NOT NULL FROM users WHERE id = ?1",
161 [user.id],
162 |r| r.get(0),
163 )?;
164 if has_password {
165 // 400, not 401: the session is still valid, only the old password is wrong.
166 auth::check_password(&s, ip, &user.username, b.old.as_deref().unwrap_or_default())
167 .await
168 .map_err(|e| match e {
169 Error::Unauthorized => Error::BadRequest("wrong current password".into()),
170 e => e,
171 })?;
172 }
173 let hash = auth::hash_password_async(b.new).await?;
174 s.db().execute(
175 "UPDATE users SET pw_hash = ?1 WHERE id = ?2",
176 params![hash, user.id],
177 )?;
178 auth::end_other_sessions(&s, &user)?;
179 Ok(Json(()))
180}
181
182/// Leaves the account on passkeys alone.
183async fn delete_password(State(s): State<AppState>, user: User) -> Result<Json<()>> {
184 let db = s.db();
185 if passkeys::count(&db, user.id)? == 0 {
186 return Err(Error::BadRequest(
187 "add a passkey before removing your password".into(),
188 ));
189 }
190 let two_factor: bool = db.query_row(
191 "SELECT two_factor FROM users WHERE id = ?1",
192 [user.id],
193 |r| r.get(0),
194 )?;
195 if two_factor {
196 return Err(Error::BadRequest(
197 "turn off two-factor sign-in before removing your password".into(),
198 ));
199 }
200 db.execute("UPDATE users SET pw_hash = NULL WHERE id = ?1", [user.id])?;
201 drop(db);
202 auth::end_other_sessions(&s, &user)?;
203 Ok(Json(()))
204}
205
206async fn set_two_factor(
207 State(s): State<AppState>,
208 user: User,
209 Json(b): Json<SetTwoFactor>,
210) -> Result<Json<()>> {
211 let db = s.db();
212 if b.enabled {
213 let has_password: bool = db.query_row(
214 "SELECT pw_hash IS NOT NULL FROM users WHERE id = ?1",
215 [user.id],
216 |r| r.get(0),
217 )?;
218 if !has_password {
219 return Err(Error::BadRequest(
220 "set a password before turning on two-factor sign-in".into(),
221 ));
222 }
223 if passkeys::count(&db, user.id)? == 0 {
224 return Err(Error::BadRequest(
225 "add a passkey before turning on two-factor sign-in".into(),
226 ));
227 }
228 }
229 db.execute(
230 "UPDATE users SET two_factor = ?1 WHERE id = ?2",
231 params![b.enabled, user.id],
232 )?;
233 drop(db);
234 auth::end_other_sessions(&s, &user)?;
235 Ok(Json(()))
236}
237
238/// Users can only keep their points for less time than the server allows, never longer.
239async fn set_retention(
240 State(s): State<AppState>,
241 user: User,
242 Json(b): Json<SetRetention>,
243) -> Result<Json<()>> {
244 if let Some(days) = b.days {
245 let max = s.max_retention_days;
246 if days < 1 || (max > 0 && days > max) {
247 let range = if max > 0 {
248 format!("1 to {max}")
249 } else {
250 "at least 1".into()
251 };
252 return Err(Error::BadRequest(format!("retention must be {range} days")));
253 }
254 }
255 let db = s.db();
256 db.execute(
257 "UPDATE users SET retention_days = ?1 WHERE id = ?2",
258 params![b.days, user.id],
259 )?;
260 crate::purge_points(&db, user.id, b.days, s.max_retention_days)?;
261 Ok(Json(()))
262}
263
264const POINT_COLS: &str = "ts, lat, lon, acc, alt, speed, bearing, battery";
265
266/// Reads the POINT_COLS columns, starting at column `i`.
267fn point_at(r: &Row, i: usize) -> rusqlite::Result<Point> {
268 Ok(Point {
269 ts: r.get(i)?,
270 lat: r.get(i + 1)?,
271 lon: r.get(i + 2)?,
272 acc: r.get(i + 3)?,
273 alt: r.get(i + 4)?,
274 speed: r.get(i + 5)?,
275 bearing: r.get(i + 6)?,
276 battery: r.get(i + 7)?,
277 })
278}
279
280/// Snaps a point to a grid of about `m` metres and drops the fields that would reveal more.
281fn coarsen(p: &mut Point, m: u32) {
282 if m == 0 {
283 return;
284 }
285 // A jump to the next cell shows when the owner crossed the cell edge, and where that edge is.
286 // Rounding the time to m seconds keeps that crossing about m metres vague at walking speed.
287 p.ts -= p.ts.rem_euclid(i64::from(m));
288 let step = f64::from(m) / 111_320.0;
289 p.lat = ((p.lat / step).round() * step).clamp(-90.0, 90.0);
290 // A degree of longitude shrinks toward the poles. Using the snapped latitude keeps one grid per row.
291 let lon_step = step / p.lat.to_radians().cos().max(0.01);
292 p.lon = ((p.lon / lon_step).round() * lon_step).clamp(-180.0, 180.0);
293 p.acc = Some(p.acc.unwrap_or(0.0).max(m as f32));
294 p.alt = None;
295 p.speed = None;
296 p.bearing = None;
297}
298
299/// What a viewer may see of one owner.
300pub struct Access {
301 pub owner: i64,
302 pub username: String,
303 /// None for the viewer's own account.
304 pub share: Option<i64>,
305 pub all_devices: bool,
306 pub trail: Trail,
307 pub precision_m: u32,
308}
309
310/// Columns of `shares s` that `access_at` reads, after the owner id and username.
311pub const ACCESS_COLS: &str = "s.id, s.all_devices, s.trail, s.trail_since, s.precision_m";
312
313/// Reads an owner id, a username and ACCESS_COLS.
314pub fn access_at(r: &Row) -> rusqlite::Result<Access> {
315 Ok(Access {
316 owner: r.get(0)?,
317 username: r.get(1)?,
318 share: r.get(2)?,
319 all_devices: r.get(3)?,
320 trail: trail_at(r, 4)?,
321 precision_m: r.get(6)?,
322 })
323}
324
325/// Limits `devices d` to the ones an Access allows. Binds ?2 = share, ?3 = all_devices.
326const DEVICE_ALLOWED: &str =
327 "(?3 OR d.id IN (SELECT device_id FROM share_devices WHERE share_id = ?2))";
328
329/// The viewer first, then everyone with an active share to the viewer.
330fn accesses(db: &Connection, viewer: i64) -> rusqlite::Result<Vec<Access>> {
331 let mut list: Vec<Access> = db
332 .prepare_cached(&format!(
333 "SELECT id, username, NULL, 1, 1, NULL, 0 FROM users WHERE id = ?1
334 UNION ALL
335 SELECT u.id, u.username, {ACCESS_COLS}
336 FROM shares s JOIN users u ON u.id = s.owner_id
337 WHERE s.viewer_id = ?1 AND (s.expires_at IS NULL OR s.expires_at > ?2)"
338 ))?
339 .query_map(params![viewer, now()], access_at)?
340 .collect::<rusqlite::Result<_>>()?;
341 list.sort_by_key(|a| (a.owner != viewer, a.username.to_lowercase()));
342 Ok(list)
343}
344
345/// The owner's allowed devices with their newest point.
346pub fn person_for(db: &Connection, a: Access) -> rusqlite::Result<Person> {
347 let devices = db
348 .prepare_cached(&format!(
349 "SELECT d.id, d.name, {POINT_COLS} FROM devices d
350 JOIN points p ON p.device_id = d.id AND p.ts = (SELECT MAX(ts) FROM points WHERE device_id = d.id)
351 WHERE d.user_id = ?1 AND {DEVICE_ALLOWED}
352 ORDER BY p.ts DESC"
353 ))?
354 .query_map(params![a.owner, a.share, a.all_devices], |r| {
355 let mut last = point_at(r, 2)?;
356 coarsen(&mut last, a.precision_m);
357 Ok(PersonDevice {
358 id: r.get(0)?,
359 name: r.get(1)?,
360 last,
361 })
362 })?
363 .collect::<rusqlite::Result<_>>()?;
364 Ok(Person {
365 id: a.owner,
366 username: a.username,
367 devices,
368 trail: a.trail,
369 precision_m: a.precision_m,
370 })
371}
372
373fn people_for(db: &Connection, viewer: i64) -> rusqlite::Result<Vec<Person>> {
374 accesses(db, viewer)?
375 .into_iter()
376 .map(|a| person_for(db, a))
377 .collect()
378}
379
380async fn people(State(s): State<AppState>, user: User) -> Result<Json<Vec<Person>>> {
381 Ok(Json(people_for(&s.db(), user.id)?))
382}
383
384#[derive(Deserialize)]
385struct TrackQuery {
386 from: i64,
387 to: i64,
388 device: i64,
389}
390
391const MAX_TRACK_POINTS: i64 = 50_000;
392
393async fn track(
394 State(s): State<AppState>,
395 user: User,
396 UrlPath(id): UrlPath<i64>,
397 Query(q): Query<TrackQuery>,
398) -> Result<Json<Vec<Point>>> {
399 let db = s.db();
400 let a = accesses(&db, user.id)?
401 .into_iter()
402 .find(|a| a.owner == id)
403 .ok_or(Error::NotFound)?;
404 Ok(Json(track_points(&db, &a, q.device, q.from, q.to)?))
405}
406
407/// One device's points in a time range, as far as the access allows.
408pub fn track_points(
409 db: &Connection,
410 a: &Access,
411 device: i64,
412 from: i64,
413 to: i64,
414) -> Result<Vec<Point>> {
415 if to < from || to - from > MAX_TRACK_SECS {
416 return Err(Error::BadRequest("range must be 0 to 31 days".into()));
417 }
418 let from = match a.trail {
419 Trail::None => return Err(Error::Forbidden),
420 Trail::Since(since) => from.max(since),
421 Trail::All => from,
422 };
423 let allowed: bool = db.query_row(
424 &format!(
425 "SELECT EXISTS (SELECT 1 FROM devices d WHERE d.id = ?4 AND d.user_id = ?1 AND {DEVICE_ALLOWED})"
426 ),
427 params![a.owner, a.share, a.all_devices, device],
428 |r| r.get(0),
429 )?;
430 if !allowed {
431 return Err(Error::NotFound);
432 }
433 // ponytail: past the limit the oldest points go. Thin the trail evenly if long ranges need all of it.
434 let mut points: Vec<Point> = db
435 .prepare_cached(&format!(
436 "SELECT * FROM (SELECT {POINT_COLS} FROM points WHERE device_id = ?1 AND ts BETWEEN ?2 AND ?3
437 ORDER BY ts DESC LIMIT {MAX_TRACK_POINTS}) ORDER BY ts"
438 ))?
439 .query_map(params![device, from, to], |r| {
440 let mut p = point_at(r, 0)?;
441 coarsen(&mut p, a.precision_m);
442 Ok(p)
443 })?
444 .collect::<rusqlite::Result<_>>()?;
445 points.dedup_by(|b, a| (a.ts, a.lat, a.lon) == (b.ts, b.lat, b.lon));
446 Ok(points)
447}
448
449async fn list_devices(State(s): State<AppState>, user: User) -> Result<Json<Vec<Device>>> {
450 let devices = s
451 .db()
452 .prepare_cached(
453 "SELECT id, name, token_hash IS NULL, created_at, last_seen_at FROM devices
454 WHERE user_id = ?1 ORDER BY created_at",
455 )?
456 .query_map([user.id], |r| {
457 Ok(Device {
458 id: r.get(0)?,
459 name: r.get(1)?,
460 web: r.get(2)?,
461 created_at: r.get(3)?,
462 last_seen_at: r.get(4)?,
463 })
464 })?
465 .collect::<rusqlite::Result<_>>()?;
466 Ok(Json(devices))
467}
468
469pub fn check_device_name(name: &str) -> Result<&str> {
470 let name = name.trim();
471 if name.is_empty() || name.chars().count() > 100 {
472 return Err(Error::BadRequest(
473 "device name must have 1 to 100 characters".into(),
474 ));
475 }
476 Ok(name)
477}
478
479pub fn insert_device(db: &Connection, user_id: i64, name: &str) -> Result<DeviceToken> {
480 let name = check_device_name(name)?;
481 let (token, hash) = auth::new_secret();
482 db.execute(
483 "INSERT INTO devices (user_id, name, token_hash, created_at) VALUES (?1, ?2, ?3, ?4)",
484 params![user_id, name, hash, now()],
485 )?;
486 Ok(DeviceToken { token })
487}
488
489async fn create_device(
490 State(s): State<AppState>,
491 user: User,
492 Json(b): Json<NewDevice>,
493) -> Result<Json<DeviceToken>> {
494 Ok(Json(insert_device(&s.db(), user.id, &b.name)?))
495}
496
497async fn delete_device(
498 State(s): State<AppState>,
499 user: User,
500 UrlPath(id): UrlPath<i64>,
501) -> Result<Json<()>> {
502 let n = s.db().execute(
503 "DELETE FROM devices WHERE id = ?1 AND user_id = ?2",
504 [id, user.id],
505 )?;
506 if n == 0 {
507 return Err(Error::NotFound);
508 }
509 Ok(Json(()))
510}
511
512/// Clock skew we accept from a device, so a wrong clock cannot write far into the future.
513const MAX_FUTURE_SECS: i64 = 86400;
514
515fn check_point(p: &Point, now: i64) -> std::result::Result<(), String> {
516 if !(-90.0..=90.0).contains(&p.lat) || !(-180.0..=180.0).contains(&p.lon) {
517 return Err(format!("point {}: coordinates out of range", p.ts));
518 }
519 if p.ts <= 0 || p.ts > now + MAX_FUTURE_SECS {
520 return Err(format!("point {}: timestamp out of range", p.ts));
521 }
522 if p.battery.is_some_and(|b| b > 100) {
523 return Err(format!("point {}: battery above 100", p.ts));
524 }
525 Ok(())
526}
527
528async fn upload(
529 State(s): State<AppState>,
530 uploader: auth::Uploader,
531 Json(points): Json<Vec<Point>>,
532) -> Result<Json<Uploaded>> {
533 if points.len() > MAX_BATCH {
534 return Err(Error::BadRequest(format!(
535 "at most {MAX_BATCH} points per request"
536 )));
537 }
538 let now = now();
539 let total = points.len();
540 let points: Vec<Point> = points
541 .into_iter()
542 .filter(|p| check_point(p, now).is_ok())
543 .collect();
544
545 let mut db = s.db();
546 let tx = db.transaction()?;
547 let mut stored = 0;
548 {
549 let mut insert = tx.prepare_cached(&format!(
550 "INSERT OR IGNORE INTO points (device_id, {POINT_COLS}) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)"
551 ))?;
552 for p in &points {
553 stored += insert.execute(params![
554 uploader.device_id,
555 p.ts,
556 p.lat,
557 p.lon,
558 p.acc,
559 p.alt,
560 p.speed,
561 p.bearing,
562 p.battery
563 ])?;
564 }
565 }
566 tx.execute(
567 "UPDATE devices SET last_seen_at = ?1 WHERE id = ?2",
568 [now, uploader.device_id],
569 )?;
570 tx.commit()?;
571 Ok(Json(Uploaded {
572 stored,
573 skipped: total - points.len(),
574 }))
575}
576
577fn trail_at(r: &Row, i: usize) -> rusqlite::Result<Trail> {
578 Ok(match (r.get::<_, bool>(i)?, r.get(i + 1)?) {
579 (false, _) => Trail::None,
580 (true, Some(since)) => Trail::Since(since),
581 (true, None) => Trail::All,
582 })
583}
584
585/// The `trail` and `trail_since` columns.
586fn trail_columns(t: Trail) -> (bool, Option<i64>) {
587 match t {
588 Trail::None => (false, None),
589 Trail::Since(since) => (true, Some(since)),
590 Trail::All => (true, None),
591 }
592}
593
594/// Reads ACCESS_COLS from column `i` on.
595pub fn settings_at(db: &Connection, r: &Row, i: usize) -> rusqlite::Result<ShareSettings> {
596 let id: i64 = r.get(i)?;
597 let devices = match r.get::<_, bool>(i + 1)? {
598 true => None,
599 false => Some(
600 db.prepare_cached("SELECT device_id FROM share_devices WHERE share_id = ?1")?
601 .query_map([id], |r| r.get(0))?
602 .collect::<rusqlite::Result<_>>()?,
603 ),
604 };
605 Ok(ShareSettings {
606 devices,
607 trail: trail_at(r, i + 2)?,
608 precision_m: r.get(i + 4)?,
609 })
610}
611
612pub fn check_settings(set: &ShareSettings, expires_at: Option<i64>) -> Result<()> {
613 if expires_at.is_some_and(|t| t <= now()) {
614 return Err(Error::BadRequest("expiry must be in the future".into()));
615 }
616 if set.precision_m > MAX_PRECISION_M {
617 return Err(Error::BadRequest(format!(
618 "precision must be at most {MAX_PRECISION_M} metres"
619 )));
620 }
621 if set.devices.as_ref().is_some_and(Vec::is_empty) {
622 return Err(Error::BadRequest("select at least one device".into()));
623 }
624 Ok(())
625}
626
627/// Writes the settings columns and the device selection of a share or link.
628pub fn save_settings(db: &Connection, id: i64, owner: i64, set: &ShareSettings) -> Result<()> {
629 let (trail, since) = trail_columns(set.trail);
630 db.execute(
631 "UPDATE shares SET all_devices = ?2, trail = ?3, trail_since = ?4, precision_m = ?5 WHERE id = ?1",
632 params![id, set.devices.is_none(), trail, since, set.precision_m],
633 )?;
634 db.execute("DELETE FROM share_devices WHERE share_id = ?1", [id])?;
635 for device in set.devices.iter().flatten() {
636 let added = db.execute(
637 "INSERT OR IGNORE INTO share_devices SELECT ?1, id FROM devices WHERE id = ?2 AND user_id = ?3",
638 [id, *device, owner],
639 )?;
640 if added == 0 {
641 return Err(Error::BadRequest("no such device".into()));
642 }
643 }
644 Ok(())
645}
646
647async fn list_shares(State(s): State<AppState>, user: User) -> Result<Json<Shares>> {
648 let db = s.db();
649 let query = |other: &str, me: &str| -> rusqlite::Result<Vec<Share>> {
650 db.prepare_cached(&format!(
651 "SELECT u.username, s.expires_at, s.created_at, {ACCESS_COLS}
652 FROM shares s JOIN users u ON u.id = s.{other} WHERE s.{me} = ?1 ORDER BY u.username"
653 ))?
654 .query_map([user.id], |r| {
655 Ok(Share {
656 id: r.get(3)?,
657 username: r.get(0)?,
658 expires_at: r.get(1)?,
659 created_at: r.get(2)?,
660 settings: settings_at(&db, r, 3)?,
661 })
662 })?
663 .collect()
664 };
665 Ok(Json(Shares {
666 outgoing: query("viewer_id", "owner_id")?,
667 incoming: query("owner_id", "viewer_id")?,
668 }))
669}
670
671async fn create_share(
672 State(s): State<AppState>,
673 user: User,
674 Json(b): Json<NewShare>,
675) -> Result<Json<Share>> {
676 check_settings(&b.settings, b.expires_at)?;
677 let mut db = s.db();
678 let (viewer_id, username): (i64, String) = db
679 .query_row(
680 "SELECT id, username FROM users WHERE username = ?1",
681 [b.viewer.trim()],
682 |r| Ok((r.get(0)?, r.get(1)?)),
683 )
684 .optional()?
685 .ok_or_else(|| Error::BadRequest("no such user".into()))?;
686 if viewer_id == user.id {
687 return Err(Error::BadRequest("you cannot share with yourself".into()));
688 }
689 let tx = db.transaction()?;
690 let (id, created_at) = tx.query_row(
691 "INSERT INTO shares (owner_id, viewer_id, expires_at, created_at) VALUES (?1, ?2, ?3, ?4)
692 ON CONFLICT (owner_id, viewer_id) DO UPDATE SET expires_at = excluded.expires_at
693 RETURNING id, created_at",
694 params![user.id, viewer_id, b.expires_at, now()],
695 |r| Ok((r.get(0)?, r.get(1)?)),
696 )?;
697 save_settings(&tx, id, user.id, &b.settings)?;
698 tx.commit()?;
699 Ok(Json(Share {
700 id,
701 username,
702 expires_at: b.expires_at,
703 created_at,
704 settings: b.settings,
705 }))
706}
707
708/// Either side can end a share. Guest links have their own endpoint.
709async fn delete_share(
710 State(s): State<AppState>,
711 user: User,
712 UrlPath(id): UrlPath<i64>,
713) -> Result<Json<()>> {
714 let n = s.db().execute(
715 "DELETE FROM shares WHERE id = ?1 AND viewer_id IS NOT NULL AND (owner_id = ?2 OR viewer_id = ?2)",
716 [id, user.id],
717 )?;
718 if n == 0 {
719 return Err(Error::NotFound);
720 }
721 Ok(Json(()))
722}
723
724/// Everyone else's username, for picking whom to share with.
725async fn usernames(State(s): State<AppState>, user: User) -> Result<Json<Vec<String>>> {
726 let names = s
727 .db()
728 .prepare_cached("SELECT username FROM users WHERE id <> ?1 ORDER BY username")?
729 .query_map([user.id], |r| r.get(0))?
730 .collect::<rusqlite::Result<_>>()?;
731 Ok(Json(names))
732}
733
734async fn list_users(State(s): State<AppState>, _: Admin) -> Result<Json<Vec<api::User>>> {
735 let users = s
736 .db()
737 .prepare_cached("SELECT id, username, is_admin, created_at FROM users ORDER BY username")?
738 .query_map([], |r| {
739 Ok(api::User {
740 id: r.get(0)?,
741 username: r.get(1)?,
742 is_admin: r.get(2)?,
743 created_at: r.get(3)?,
744 })
745 })?
746 .collect::<rusqlite::Result<_>>()?;
747 Ok(Json(users))
748}
749
750async fn create_user(
751 State(s): State<AppState>,
752 _: Admin,
753 Json(b): Json<NewUser>,
754) -> Result<Json<api::User>> {
755 let username = crate::check_username(&b.username)?.to_owned();
756 auth::check_new_password(&b.password).map_err(|m| Error::BadRequest(m.into()))?;
757 let hash = auth::hash_password_async(b.password).await?;
758 let db = s.db();
759 let id = crate::insert_user(&db, &username, &hash, b.is_admin)?;
760 Ok(Json(api::User {
761 id,
762 username,
763 is_admin: b.is_admin,
764 created_at: now(),
765 }))
766}
767
768/// Admins cannot change their own role, so at least one admin always remains.
769async fn set_role(
770 State(s): State<AppState>,
771 Admin(admin): Admin,
772 UrlPath(id): UrlPath<i64>,
773 Json(b): Json<SetRole>,
774) -> Result<Json<()>> {
775 if id == admin.id {
776 return Err(Error::BadRequest("you cannot change your own role".into()));
777 }
778 if s.db().execute(
779 "UPDATE users SET is_admin = ?1 WHERE id = ?2",
780 params![b.is_admin, id],
781 )? == 0
782 {
783 return Err(Error::NotFound);
784 }
785 Ok(Json(()))
786}
787
788async fn delete_user(
789 State(s): State<AppState>,
790 Admin(admin): Admin,
791 UrlPath(id): UrlPath<i64>,
792) -> Result<Json<()>> {
793 if id == admin.id {
794 return Err(Error::BadRequest(
795 "you cannot delete your own account".into(),
796 ));
797 }
798 if s.db().execute("DELETE FROM users WHERE id = ?1", [id])? == 0 {
799 return Err(Error::NotFound);
800 }
801 Ok(Json(()))
802}
803
804/// The recovery path for a user who lost their password or passkey.
805async fn reset_user_password(
806 State(s): State<AppState>,
807 _: Admin,
808 UrlPath(id): UrlPath<i64>,
809 Json(b): Json<ResetPassword>,
810) -> Result<Json<()>> {
811 auth::check_new_password(&b.password).map_err(|m| Error::BadRequest(m.into()))?;
812 let hash = auth::hash_password_async(b.password).await?;
813 let db = s.db();
814 if db
815 .query_row("SELECT 1 FROM users WHERE id = ?1", [id], |_| Ok(()))
816 .optional()?
817 .is_none()
818 {
819 return Err(Error::NotFound);
820 }
821 crate::reset_password(&db, id, &hash)?;
822 Ok(Json(()))
823}
824
825#[cfg(test)]
826mod tests {
827 use super::*;
828
829 fn pt(ts: i64, lat: f64, lon: f64) -> Point {
830 Point {
831 ts,
832 lat,
833 lon,
834 acc: None,
835 alt: None,
836 speed: None,
837 bearing: None,
838 battery: None,
839 }
840 }
841
842 #[test]
843 fn point_validation() {
844 let now = 1_800_000_000;
845 assert!(check_point(&pt(now, 48.1, 11.5), now).is_ok());
846 assert!(check_point(&pt(now, 91.0, 0.0), now).is_err());
847 assert!(check_point(&pt(now, 0.0, -180.1), now).is_err());
848 assert!(check_point(&pt(now + 2 * MAX_FUTURE_SECS, 0.0, 0.0), now).is_err());
849 assert!(
850 check_point(
851 &Point {
852 battery: Some(101),
853 ..pt(now, 0.0, 0.0)
854 },
855 now
856 )
857 .is_err()
858 );
859 }
860
861 #[test]
862 fn coarse_points_stay_near_and_hide_motion() {
863 let exact = Point {
864 acc: Some(5.0),
865 speed: Some(3.0),
866 ..pt(1_800_000_999, 48.137_15, 11.575_49)
867 };
868 let mut p = exact.clone();
869 coarsen(&mut p, 0);
870 assert_eq!(p, exact);
871 coarsen(&mut p, 1000);
872 let (dy, dx) = (
873 (p.lat - exact.lat) * 111_320.0,
874 (p.lon - exact.lon) * 111_320.0 * exact.lat.to_radians().cos(),
875 );
876 assert!(
877 dy.abs() <= 500.0 && dx.abs() <= 510.0,
878 "moved {dy} m, {dx} m"
879 );
880 assert_eq!((p.acc, p.speed, p.ts), (Some(1000.0), None, 1_800_000_000));
881 let mut near = pt(1, exact.lat + 0.000_01, exact.lon + 0.000_01);
882 coarsen(&mut near, 1000);
883 assert_eq!((near.lat, near.lon), (p.lat, p.lon));
884 }
885
886 #[test]
887 fn people_shows_only_shared_devices() {
888 let db = crate::test_db();
889 db.execute_batch(
890 "INSERT INTO users (id, username, webauthn_id, created_at) VALUES (1, 'a', '1', 0), (2, 'b', '2', 0);
891 INSERT INTO devices (id, user_id, name, token_hash, created_at) VALUES (10, 2, 'phone', x'01', 0), (11, 2, 'car', x'02', 0);
892 INSERT INTO points (device_id, ts, lat, lon) VALUES (10, 100, 1, 1), (11, 200, 2, 2);
893 INSERT INTO shares (id, owner_id, viewer_id, created_at, all_devices) VALUES (5, 2, 1, 0, 1);",
894 )
895 .unwrap();
896 let devices = |db: &Connection| -> Vec<String> {
897 people_for(db, 1).unwrap()[1]
898 .devices
899 .iter()
900 .map(|d| d.name.clone())
901 .collect()
902 };
903 assert_eq!(devices(&db), ["car", "phone"]);
904 db.execute_batch(
905 "UPDATE shares SET all_devices = 0; INSERT INTO share_devices VALUES (5, 10);",
906 )
907 .unwrap();
908 assert_eq!(devices(&db), ["phone"]);
909 }
910
911 #[test]
912 fn people_respects_share_expiry() {
913 let db = crate::test_db();
914 db.execute_batch(
915 "INSERT INTO users (id, username, webauthn_id, created_at) VALUES (1, 'a', '1', 0), (2, 'b', '2', 0), (3, 'c', '3', 0);
916 INSERT INTO shares (owner_id, viewer_id, expires_at, created_at) VALUES (2, 1, NULL, 0), (3, 1, 1, 0);",
917 )
918 .unwrap();
919 let names: Vec<_> = people_for(&db, 1)
920 .unwrap()
921 .into_iter()
922 .map(|p| p.username)
923 .collect();
924 assert_eq!(names, ["a", "b"]);
925 }
926}
927