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