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