A wrapper around reqwest to make working with Pocketbase a breeze
1use std::{
2 sync::{Arc, RwLock},
3 time::Duration,
4};
5
6use base64::{Engine, prelude::BASE64_STANDARD_NO_PAD};
7use reqwest::{Method, header::HeaderMap};
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use serde_json::{Value, json};
10
11use crate::{Error, JsonObject, PocketBase, PocketBaseInner, record::RecordService};
12
13#[derive(Debug, Clone)]
14pub struct AuthStore {
15 pub token: String,
16 pub record: RecordModel,
17}
18
19#[derive(Clone, Debug, Deserialize, Serialize)]
20pub struct AuthMethodList {
21 pub mfa: AuthMethodMfa,
22 pub oauth2: AuthMethodOAuth2,
23 pub otp: AuthMethodOtp,
24 pub password: AuthMethodPassword,
25}
26
27#[derive(Clone, Debug, Deserialize, Serialize)]
28pub struct AuthMethodMfa {
29 #[serde(
30 deserialize_with = "deserialize_duration",
31 serialize_with = "serialize_duration"
32 )]
33 pub duration: Duration,
34 pub enabled: bool,
35}
36
37#[derive(Clone, Debug, Deserialize, Serialize)]
38pub struct AuthMethodOAuth2 {
39 pub enabled: bool,
40 pub providers: Vec<AuthMethodProvider>,
41}
42
43#[derive(Clone, Debug, Deserialize, Serialize)]
44pub struct AuthMethodOtp {
45 #[serde(
46 deserialize_with = "deserialize_duration",
47 serialize_with = "serialize_duration"
48 )]
49 pub duration: Duration,
50 pub enabled: bool,
51}
52
53#[derive(Clone, Debug, Deserialize, Serialize)]
54#[serde(rename_all = "camelCase")]
55pub struct AuthMethodProvider {
56 pub auth_url: String,
57 pub code_challenge: String,
58 pub code_challenge_method: String,
59 pub code_verifier: String,
60 pub display_name: String,
61 pub name: String,
62 pub pkce: Option<bool>,
63 pub state: String,
64}
65
66#[derive(Clone, Debug, Deserialize, Serialize)]
67#[serde(rename_all = "camelCase")]
68pub struct AuthMethodPassword {
69 pub enabled: bool,
70 pub identity_fields: Vec<String>,
71}
72
73#[derive(Debug, Clone)]
74pub struct AuthRefreshBuilder<'a> {
75 pub(crate) record_service: RecordService<'a>,
76 pub(crate) expand_fields: Option<&'a str>,
77 pub(crate) filter_fields: Option<&'a str>,
78}
79
80#[derive(Debug, Clone)]
81pub struct AuthWithOtpBuilder<'a> {
82 pub(crate) record_service: RecordService<'a>,
83 pub(crate) otp_id: &'a str,
84 pub(crate) password: &'a str,
85 pub(crate) expand_fields: Option<&'a str>,
86 pub(crate) filter_fields: Option<&'a str>,
87}
88
89#[derive(Debug, Clone)]
90pub struct AuthWithPasswordBuilder<'a> {
91 pub(crate) record_service: RecordService<'a>,
92 pub(crate) identity: &'a str,
93 pub(crate) password: &'a str,
94 pub(crate) expand_fields: Option<&'a str>,
95 pub(crate) filter_fields: Option<&'a str>,
96}
97
98#[derive(Debug, Clone)]
99pub struct ImpersonateBuilder<'a> {
100 pub(crate) record_service: RecordService<'a>,
101 pub(crate) id: &'a str,
102 pub(crate) duration: Option<Duration>,
103 pub(crate) expand_fields: Option<&'a str>,
104 pub(crate) filter_fields: Option<&'a str>,
105}
106
107#[derive(Debug, Deserialize, Clone)]
108pub struct RecordAuth {
109 token: String,
110 record: RecordModel,
111}
112
113#[derive(Debug, Deserialize, Clone)]
114#[serde(transparent)]
115pub struct RecordModel {
116 pub(crate) data: JsonObject,
117}
118
119fn deserialize_duration<'de, D>(deserializer: D) -> Result<Duration, D::Error>
120where
121 D: Deserializer<'de>,
122{
123 u64::deserialize(deserializer).map(Duration::from_secs)
124}
125
126#[cfg(target_arch = "wasm32")]
127fn time_since_unix_epoch() -> u64 {
128 use web_time::{SystemTime, UNIX_EPOCH};
129
130 SystemTime::now()
131 .duration_since(UNIX_EPOCH)
132 .unwrap()
133 .as_secs()
134}
135
136#[cfg(not(target_arch = "wasm32"))]
137fn time_since_unix_epoch() -> u64 {
138 use std::time::{SystemTime, UNIX_EPOCH};
139
140 SystemTime::now()
141 .duration_since(UNIX_EPOCH)
142 .unwrap()
143 .as_secs()
144}
145
146fn serialize_duration<S>(dur: &Duration, serializer: S) -> Result<S::Ok, S::Error>
147where
148 S: Serializer,
149{
150 dur.as_secs().serialize(serializer)
151}
152
153impl AuthStore {
154 pub fn is_valid(&self) -> bool {
155 let parts: Vec<&str> = self.token.split(".").collect();
156 if parts.len() != 3 {
157 return false;
158 }
159
160 let Ok(token_str) = String::from_utf8(BASE64_STANDARD_NO_PAD.decode(parts[1]).unwrap())
161 else {
162 return false;
163 };
164 let Ok(data) = serde_json::from_str::<JsonObject>(&token_str) else {
165 return false;
166 };
167
168 match data.get("exp") {
169 Some(Value::Number(exp)) => {
170 if let Some(exp) = exp.as_u64() {
171 exp > time_since_unix_epoch()
172 } else {
173 false
174 }
175 }
176 _ => false,
177 }
178 }
179}
180
181impl<'a> AuthRefreshBuilder<'a> {
182 pub fn expand_fields(mut self, field_expression: &'a str) -> Self {
183 self.expand_fields = Some(field_expression);
184 self
185 }
186
187 pub fn filter_fields(mut self, field_expression: &'a str) -> Self {
188 self.filter_fields = Some(field_expression);
189 self
190 }
191
192 pub async fn send(self) -> Result<RecordAuth, Error> {
193 let mut params = Vec::new();
194 if let Some(expand) = self.expand_fields {
195 params.push(("expand", expand));
196 }
197 if let Some(fields) = self.filter_fields {
198 params.push(("fields", fields));
199 }
200
201 let auth: RecordAuth = self
202 .record_service
203 .client
204 .send(
205 &format!(
206 "/api/collections/{}/auth-refresh",
207 self.record_service.collection
208 ),
209 Method::POST,
210 HeaderMap::new(),
211 ¶ms,
212 None::<&()>,
213 )
214 .await?;
215 self.record_service.client.with_auth_store(AuthStore {
216 token: auth.token.clone(),
217 record: auth.record.clone(),
218 });
219 debug_assert!({
220 let read = self.record_service.client.inner.read().unwrap();
221 read.auth_store.as_ref().unwrap().is_valid()
222 });
223 Ok(auth)
224 }
225}
226
227impl<'a> AuthWithOtpBuilder<'a> {
228 pub fn expand_fields(mut self, field_expression: &'a str) -> Self {
229 self.expand_fields = Some(field_expression);
230 self
231 }
232
233 pub fn filter_fields(mut self, field_expression: &'a str) -> Self {
234 self.filter_fields = Some(field_expression);
235 self
236 }
237
238 pub async fn send(self) -> Result<RecordAuth, Error> {
239 let body = json!({"otpId": self.otp_id, "password": self.password});
240 let mut params = Vec::new();
241 if let Some(expand) = self.expand_fields {
242 params.push(("expand", expand));
243 }
244 if let Some(fields) = self.filter_fields {
245 params.push(("fields", fields));
246 }
247
248 let auth: RecordAuth = self
249 .record_service
250 .client
251 .send(
252 &format!(
253 "/api/collections/{}/auth-with-otp",
254 self.record_service.collection
255 ),
256 Method::POST,
257 HeaderMap::new(),
258 ¶ms,
259 Some(&body),
260 )
261 .await?;
262 self.record_service.client.with_auth_store(AuthStore {
263 token: auth.token.clone(),
264 record: auth.record.clone(),
265 });
266 debug_assert!({
267 let read = self.record_service.client.inner.read().unwrap();
268 read.auth_store.as_ref().unwrap().is_valid()
269 });
270 Ok(auth)
271 }
272}
273
274impl<'a> AuthWithPasswordBuilder<'a> {
275 pub fn expand_fields(mut self, field_expression: &'a str) -> Self {
276 self.expand_fields = Some(field_expression);
277 self
278 }
279
280 pub fn filter_fields(mut self, field_expression: &'a str) -> Self {
281 self.filter_fields = Some(field_expression);
282 self
283 }
284
285 pub async fn send(self) -> Result<RecordAuth, Error> {
286 let body = json!({"identity": self.identity, "password": self.password});
287 let mut params = Vec::new();
288 if let Some(expand) = self.expand_fields {
289 params.push(("expand", expand));
290 }
291 if let Some(fields) = self.filter_fields {
292 params.push(("fields", fields));
293 }
294
295 let auth: RecordAuth = self
296 .record_service
297 .client
298 .send(
299 &format!(
300 "/api/collections/{}/auth-with-password",
301 self.record_service.collection
302 ),
303 Method::POST,
304 HeaderMap::new(),
305 ¶ms,
306 Some(&body),
307 )
308 .await?;
309 self.record_service.client.with_auth_store(AuthStore {
310 token: auth.token.clone(),
311 record: auth.record.clone(),
312 });
313 debug_assert!({
314 let read = self.record_service.client.inner.read().unwrap();
315 read.auth_store.as_ref().unwrap().is_valid()
316 });
317 Ok(auth)
318 }
319}
320
321impl<'a> ImpersonateBuilder<'a> {
322 /// Set the duration that the generated auth token will be valid.
323 ///
324 /// If duration is not set, then the generated auth token will fallback to the
325 /// default collection auth token duration.
326 pub fn duration(mut self, duration: Duration) -> Self {
327 self.duration = Some(duration);
328 self
329 }
330
331 pub fn expand_fields(mut self, field_expression: &'a str) -> Self {
332 self.expand_fields = Some(field_expression);
333 self
334 }
335
336 pub fn filter_fields(mut self, field_expression: &'a str) -> Self {
337 self.filter_fields = Some(field_expression);
338 self
339 }
340
341 pub async fn send(self) -> Result<(PocketBase, RecordAuth), Error> {
342 let mut params = Vec::new();
343 if let Some(expand) = self.expand_fields {
344 params.push(("expand", expand));
345 }
346 if let Some(fields) = self.filter_fields {
347 params.push(("fields", fields));
348 }
349
350 let auth: RecordAuth = self
351 .record_service
352 .client
353 .send(
354 &format!(
355 "/api/collections/{}/impersonate/{}",
356 self.record_service.collection, self.id
357 ),
358 Method::POST,
359 HeaderMap::new(),
360 ¶ms,
361 if let Some(dur) = self.duration {
362 Some(json!({ "duration": dur.as_secs() }))
363 } else {
364 None
365 }
366 .as_ref(),
367 )
368 .await?;
369
370 let client = {
371 let inner = self.record_service.client.inner.read().unwrap();
372 PocketBase {
373 inner: Arc::new(RwLock::new(PocketBaseInner {
374 reqwest: inner.reqwest.clone(),
375 auth_store: None,
376 base_url: inner.base_url.clone(),
377 lang: inner.lang.clone(),
378 })),
379 }
380 };
381
382 client.with_auth_store(AuthStore {
383 token: auth.token.clone(),
384 record: auth.record.clone(),
385 });
386 debug_assert!({
387 let read = client.inner.read().unwrap();
388 read.auth_store.as_ref().unwrap().is_valid()
389 });
390
391 Ok((client, auth))
392 }
393}
394
395impl RecordModel {
396 pub fn as_object(&self) -> &'_ JsonObject {
397 &self.data
398 }
399
400 pub fn into_object(self) -> JsonObject {
401 self.data
402 }
403}