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