Microservice to bring 2FA to self hosted PDSes
1use serde::de::DeserializeOwned;
2use serde::Serialize;
3use std::fmt;
4
5#[derive(Debug)]
6pub enum AdminProxyError {
7 RequestFailed(String),
8 PdsError {
9 status: u16,
10 error: String,
11 message: String,
12 },
13 ParseError(String),
14}
15
16impl fmt::Display for AdminProxyError {
17 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18 match self {
19 AdminProxyError::RequestFailed(msg) => write!(f, "Request failed: {msg}"),
20 AdminProxyError::PdsError {
21 status,
22 error,
23 message,
24 } => write!(f, "PDS error ({status}): {error} - {message}"),
25 AdminProxyError::ParseError(msg) => write!(f, "Parse error: {msg}"),
26 }
27 }
28}
29
30/// Make an authenticated GET request to a PDS XRPC endpoint.
31pub async fn admin_xrpc_get<R: DeserializeOwned>(
32 pds_base_url: &str,
33 admin_password: &str,
34 endpoint: &str,
35 query_params: &[(&str, &str)],
36) -> Result<R, AdminProxyError> {
37 let url = format!(
38 "{}/xrpc/{}",
39 pds_base_url.trim_end_matches('/'),
40 endpoint
41 );
42 let client = reqwest::Client::new();
43 let resp = client
44 .get(&url)
45 .query(query_params)
46 .basic_auth("admin", Some(admin_password))
47 .send()
48 .await
49 .map_err(|e| AdminProxyError::RequestFailed(e.to_string()))?;
50
51 if !resp.status().is_success() {
52 let status = resp.status().as_u16();
53 let body = resp.text().await.unwrap_or_default();
54 if let Ok(err_json) = serde_json::from_str::<serde_json::Value>(&body) {
55 return Err(AdminProxyError::PdsError {
56 status,
57 error: err_json["error"]
58 .as_str()
59 .unwrap_or("Unknown")
60 .to_string(),
61 message: err_json["message"]
62 .as_str()
63 .unwrap_or(&body)
64 .to_string(),
65 });
66 }
67 return Err(AdminProxyError::PdsError {
68 status,
69 error: "Unknown".to_string(),
70 message: body,
71 });
72 }
73
74 resp.json::<R>()
75 .await
76 .map_err(|e| AdminProxyError::ParseError(e.to_string()))
77}
78
79/// Make an authenticated POST request to a PDS XRPC endpoint.
80pub async fn admin_xrpc_post<T: Serialize, R: DeserializeOwned>(
81 pds_base_url: &str,
82 admin_password: &str,
83 endpoint: &str,
84 body: &T,
85) -> Result<R, AdminProxyError> {
86 let url = format!(
87 "{}/xrpc/{}",
88 pds_base_url.trim_end_matches('/'),
89 endpoint
90 );
91 let client = reqwest::Client::new();
92 let resp = client
93 .post(&url)
94 .json(body)
95 .basic_auth("admin", Some(admin_password))
96 .send()
97 .await
98 .map_err(|e| AdminProxyError::RequestFailed(e.to_string()))?;
99
100 if !resp.status().is_success() {
101 let status = resp.status().as_u16();
102 let body = resp.text().await.unwrap_or_default();
103 if let Ok(err_json) = serde_json::from_str::<serde_json::Value>(&body) {
104 return Err(AdminProxyError::PdsError {
105 status,
106 error: err_json["error"]
107 .as_str()
108 .unwrap_or("Unknown")
109 .to_string(),
110 message: err_json["message"]
111 .as_str()
112 .unwrap_or(&body)
113 .to_string(),
114 });
115 }
116 return Err(AdminProxyError::PdsError {
117 status,
118 error: "Unknown".to_string(),
119 message: body,
120 });
121 }
122
123 resp.json::<R>()
124 .await
125 .map_err(|e| AdminProxyError::ParseError(e.to_string()))
126}
127
128/// Make an authenticated POST request that returns no meaningful body (just success/failure).
129pub async fn admin_xrpc_post_no_response<T: Serialize>(
130 pds_base_url: &str,
131 admin_password: &str,
132 endpoint: &str,
133 body: &T,
134) -> Result<(), AdminProxyError> {
135 let url = format!(
136 "{}/xrpc/{}",
137 pds_base_url.trim_end_matches('/'),
138 endpoint
139 );
140 let client = reqwest::Client::new();
141 let resp = client
142 .post(&url)
143 .json(body)
144 .basic_auth("admin", Some(admin_password))
145 .send()
146 .await
147 .map_err(|e| AdminProxyError::RequestFailed(e.to_string()))?;
148
149 if !resp.status().is_success() {
150 let status = resp.status().as_u16();
151 let body = resp.text().await.unwrap_or_default();
152 if let Ok(err_json) = serde_json::from_str::<serde_json::Value>(&body) {
153 return Err(AdminProxyError::PdsError {
154 status,
155 error: err_json["error"]
156 .as_str()
157 .unwrap_or("Unknown")
158 .to_string(),
159 message: err_json["message"]
160 .as_str()
161 .unwrap_or(&body)
162 .to_string(),
163 });
164 }
165 return Err(AdminProxyError::PdsError {
166 status,
167 error: "Unknown".to_string(),
168 message: body,
169 });
170 }
171
172 Ok(())
173}
174
175/// Make an unauthenticated GET request that returns text (e.g., _health).
176pub async fn get_text(
177 pds_base_url: &str,
178 endpoint: &str,
179) -> Result<String, AdminProxyError> {
180 let url = format!(
181 "{}/{}",
182 pds_base_url.trim_end_matches('/'),
183 endpoint
184 );
185 let client = reqwest::Client::new();
186 let resp = client
187 .get(&url)
188 .send()
189 .await
190 .map_err(|e| AdminProxyError::RequestFailed(e.to_string()))?;
191
192 if !resp.status().is_success() {
193 let status = resp.status().as_u16();
194 let body = resp.text().await.unwrap_or_default();
195 return Err(AdminProxyError::PdsError {
196 status,
197 error: "Unknown".to_string(),
198 message: body,
199 });
200 }
201
202 resp.text()
203 .await
204 .map_err(|e| AdminProxyError::ParseError(e.to_string()))
205}
206
207/// Make an unauthenticated POST request to an arbitrary base URL (e.g., relay).
208pub async fn public_xrpc_post<T: Serialize>(
209 base_url: &str,
210 endpoint: &str,
211 body: &T,
212) -> Result<(), AdminProxyError> {
213 let url = format!(
214 "{}/xrpc/{}",
215 base_url.trim_end_matches('/'),
216 endpoint
217 );
218 let client = reqwest::Client::new();
219 let resp = client
220 .post(&url)
221 .json(body)
222 .send()
223 .await
224 .map_err(|e| AdminProxyError::RequestFailed(e.to_string()))?;
225
226 if !resp.status().is_success() {
227 let status = resp.status().as_u16();
228 let body = resp.text().await.unwrap_or_default();
229 if let Ok(err_json) = serde_json::from_str::<serde_json::Value>(&body) {
230 return Err(AdminProxyError::PdsError {
231 status,
232 error: err_json["error"]
233 .as_str()
234 .unwrap_or("Unknown")
235 .to_string(),
236 message: err_json["message"]
237 .as_str()
238 .unwrap_or(&body)
239 .to_string(),
240 });
241 }
242 return Err(AdminProxyError::PdsError {
243 status,
244 error: "Unknown".to_string(),
245 message: body,
246 });
247 }
248
249 Ok(())
250}
251
252/// Make an unauthenticated GET request with JSON response parsing.
253pub async fn public_xrpc_get<R: DeserializeOwned>(
254 pds_base_url: &str,
255 endpoint: &str,
256 query_params: &[(&str, &str)],
257) -> Result<R, AdminProxyError> {
258 let url = format!(
259 "{}/xrpc/{}",
260 pds_base_url.trim_end_matches('/'),
261 endpoint
262 );
263 let client = reqwest::Client::new();
264 let resp = client
265 .get(&url)
266 .query(query_params)
267 .send()
268 .await
269 .map_err(|e| AdminProxyError::RequestFailed(e.to_string()))?;
270
271 if !resp.status().is_success() {
272 let status = resp.status().as_u16();
273 let body = resp.text().await.unwrap_or_default();
274 if let Ok(err_json) = serde_json::from_str::<serde_json::Value>(&body) {
275 return Err(AdminProxyError::PdsError {
276 status,
277 error: err_json["error"]
278 .as_str()
279 .unwrap_or("Unknown")
280 .to_string(),
281 message: err_json["message"]
282 .as_str()
283 .unwrap_or(&body)
284 .to_string(),
285 });
286 }
287 return Err(AdminProxyError::PdsError {
288 status,
289 error: "Unknown".to_string(),
290 message: body,
291 });
292 }
293
294 resp.json::<R>()
295 .await
296 .map_err(|e| AdminProxyError::ParseError(e.to_string()))
297}