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