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