A wrapper around reqwest to make working with Pocketbase a breeze
at develop 9.5 kB view raw
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 &params, 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 &params, 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 &params, 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 &params, 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}