Self-hosted, federated location sharing app and server that prioritizes user privacy and security
end-to-end-encryption
location-sharing
privacy
self-hosted
federated
1use axum::{
2 http::StatusCode,
3 response::{IntoResponse, Response},
4};
5use ed25519_dalek::VerifyingKey;
6use serde::{Deserialize, Serialize};
7use std::{
8 collections::{HashMap, HashSet},
9 sync::Arc,
10};
11use tokio::sync::Mutex;
12
13// doing everything in memory for now
14#[derive(Clone)]
15pub struct AppState {
16 pub users: Arc<Mutex<Vec<User>>>,
17 pub signup_keys: Arc<Mutex<HashSet<String>>>,
18 pub friend_requests: Arc<Mutex<HashSet<Link>>>,
19 pub links: Arc<Mutex<HashSet<Link>>>,
20 pub positions: Arc<Mutex<HashMap<Link, RingBuffer>>>,
21 pub admin_id: Arc<Mutex<Option<String>>>,
22 pub ring_buffer_cap: usize,
23}
24
25#[derive(Debug, Deserialize, Clone)]
26pub struct AuthData {
27 pub user_id: String,
28 pub signature: String,
29}
30
31pub struct RingBuffer {
32 pub ring: Box<[Option<EncryptedPing>]>,
33 pub idx: usize,
34}
35
36#[derive(Clone, Serialize)]
37pub struct EncryptedPing(pub String);
38
39// represents a ring buffer for a directed friend connection (ex.: user1 sending to user2, which in that case it's only user1's positions)
40impl RingBuffer {
41 pub fn new(capacity: usize) -> Self {
42 return Self {
43 ring: vec![None; capacity].into_boxed_slice(),
44 idx: 0,
45 };
46 }
47
48 pub fn add(&mut self, p: EncryptedPing) {
49 self.idx = (self.idx + 1) % self.ring.len();
50 self.ring[self.idx] = Some(p);
51 }
52
53 /// Returns a `Vec<String>` of all the `encrypted_ping` values in the ring buffer,
54 /// starting from `self.idx` and iterating **backwards**, wrapping around to the end,
55 /// skipping `None` entries.
56 ///
57 /// # Notes
58 /// - The first element in the returned vector corresponds to the current index (`self.idx`).
59 /// - `None` entries are ignored.
60 pub fn flatten(&self) -> Vec<EncryptedPing> {
61 let len = self.ring.len();
62
63 let mut result = Vec::with_capacity(len);
64
65 for i in 0..len {
66 let temp = (self.idx + len - i) % len;
67 let position = &self.ring[temp];
68 match position {
69 Some(p) => result.push(p.clone()),
70 None => continue,
71 }
72 }
73
74 return result;
75 }
76}
77
78#[derive(serde::Deserialize)]
79pub struct CreateUserRequest {
80 pub signup_key: String,
81 pub pub_key_b64: String, // base64-encoded public key
82}
83
84#[derive(Debug, PartialEq, Eq)]
85pub struct User {
86 pub id: String,
87 pub pub_key: VerifyingKey,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Hash)]
91pub struct Link(String, String);
92
93impl Link {
94 pub fn new(a: String, b: String) -> Self {
95 if a < b { Link(a, b) } else { Link(b, a) } // normalize order
96 }
97}
98
99#[derive(Serialize)]
100pub struct CreateAccountResponse {
101 pub user_id: String,
102 pub is_admin: bool,
103}
104
105#[derive(Deserialize)]
106pub struct PingPayload {
107 pub receiver_id: String,
108 pub encrypted_ping: String,
109}
110
111pub struct PlainBool(pub bool);
112
113impl IntoResponse for PlainBool {
114 fn into_response(self) -> Response {
115 self.0.to_string().into_response()
116 }
117}
118
119#[derive(Serialize)]
120pub struct EncryptedPingVec(pub Vec<EncryptedPing>);
121
122impl IntoResponse for EncryptedPingVec {
123 fn into_response(self) -> Response {
124 axum::Json(self.0).into_response()
125 }
126}
127
128#[derive(Debug)]
129pub struct SrvErr {
130 pub msg: String,
131 pub cause: Option<String>,
132}
133
134impl IntoResponse for SrvErr {
135 fn into_response(self) -> Response {
136 // Log once here (this runs only for real errors)
137 match &self.cause {
138 Some(c) => eprintln!("[ERR] {} | cause: {}", self.msg, c),
139 None => eprintln!("[ERR] {}", self.msg),
140 }
141
142 let body = if cfg!(debug_assertions) {
143 match &self.cause {
144 Some(c) => format!("{} | cause: {}", self.msg, c),
145 None => self.msg.clone(),
146 }
147 } else {
148 self.msg.clone()
149 };
150
151 (StatusCode::INTERNAL_SERVER_ERROR, body).into_response()
152 }
153}
154
155/// Central policy: what gets logged, what gets returned.
156pub fn mk_srv_err(msg: impl Into<String>, cause: Option<String>) -> SrvErr {
157 SrvErr {
158 msg: msg.into(),
159 cause,
160 }
161}
162
163#[macro_export]
164macro_rules! SrvErr {
165 ($msg:expr) => {
166 $crate::mk_srv_err($msg, None)
167 };
168 ($msg:expr, $err:expr) => {
169 $crate::mk_srv_err($msg, Some(format!("{:?}", $err)))
170 };
171}
172
173#[macro_export]
174macro_rules! ReqBail {
175 ($msg:expr) => {{
176 return Err($crate::SrvErr!($msg));
177 }};
178 ($msg:expr, $err:expr) => {{
179 return Err($crate::SrvErr!($msg, $err));
180 }};
181}