use std::{ sync::{Arc, RwLock}, time::Duration, }; use base64::{Engine, prelude::BASE64_STANDARD_NO_PAD}; use reqwest::{Method, header::HeaderMap}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::{Value, json}; use crate::{Error, JsonObject, PocketBase, PocketBaseInner, record::RecordService}; #[derive(Debug, Clone)] pub struct AuthStore { pub token: String, pub record: RecordModel, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct AuthMethodList { pub mfa: AuthMethodMfa, pub oauth2: AuthMethodOAuth2, pub otp: AuthMethodOtp, pub password: AuthMethodPassword, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct AuthMethodMfa { #[serde( deserialize_with = "deserialize_duration", serialize_with = "serialize_duration" )] pub duration: Duration, pub enabled: bool, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct AuthMethodOAuth2 { pub enabled: bool, pub providers: Vec, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct AuthMethodOtp { #[serde( deserialize_with = "deserialize_duration", serialize_with = "serialize_duration" )] pub duration: Duration, pub enabled: bool, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct AuthMethodProvider { pub auth_url: String, pub code_challenge: String, pub code_challenge_method: String, pub code_verifier: String, pub display_name: String, pub name: String, pub pkce: Option, pub state: String, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct AuthMethodPassword { pub enabled: bool, pub identity_fields: Vec, } #[derive(Debug, Clone)] pub struct AuthRefreshBuilder<'a> { pub(crate) record_service: RecordService<'a>, pub(crate) expand_fields: Option<&'a str>, pub(crate) filter_fields: Option<&'a str>, } #[derive(Debug, Clone)] pub struct AuthWithOtpBuilder<'a> { pub(crate) record_service: RecordService<'a>, pub(crate) otp_id: &'a str, pub(crate) password: &'a str, pub(crate) expand_fields: Option<&'a str>, pub(crate) filter_fields: Option<&'a str>, } #[derive(Debug, Clone)] pub struct AuthWithPasswordBuilder<'a> { pub(crate) record_service: RecordService<'a>, pub(crate) identity: &'a str, pub(crate) password: &'a str, pub(crate) expand_fields: Option<&'a str>, pub(crate) filter_fields: Option<&'a str>, } #[derive(Debug, Clone)] pub struct ImpersonateBuilder<'a> { pub(crate) record_service: RecordService<'a>, pub(crate) id: &'a str, pub(crate) duration: Option, pub(crate) expand_fields: Option<&'a str>, pub(crate) filter_fields: Option<&'a str>, } #[derive(Debug, Deserialize, Clone)] pub struct RecordAuth { token: String, record: RecordModel, } #[derive(Debug, Deserialize, Clone)] #[serde(transparent)] pub struct RecordModel { pub(crate) data: JsonObject, } fn deserialize_duration<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { u64::deserialize(deserializer).map(Duration::from_secs) } #[cfg(target_arch = "wasm32")] fn time_since_unix_epoch() -> u64 { use web_time::{SystemTime, UNIX_EPOCH}; SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs() } #[cfg(not(target_arch = "wasm32"))] fn time_since_unix_epoch() -> u64 { use std::time::{SystemTime, UNIX_EPOCH}; SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs() } fn serialize_duration(dur: &Duration, serializer: S) -> Result where S: Serializer, { dur.as_secs().serialize(serializer) } impl AuthStore { pub fn is_valid(&self) -> bool { let parts: Vec<&str> = self.token.split(".").collect(); if parts.len() != 3 { return false; } let Ok(token_str) = String::from_utf8(BASE64_STANDARD_NO_PAD.decode(parts[1]).unwrap()) else { return false; }; let Ok(data) = serde_json::from_str::(&token_str) else { return false; }; match data.get("exp") { Some(Value::Number(exp)) => { if let Some(exp) = exp.as_u64() { exp > time_since_unix_epoch() } else { false } } _ => false, } } } impl<'a> AuthRefreshBuilder<'a> { pub fn expand_fields(mut self, field_expression: &'a str) -> Self { self.expand_fields = Some(field_expression); self } pub fn filter_fields(mut self, field_expression: &'a str) -> Self { self.filter_fields = Some(field_expression); self } pub async fn send(self) -> Result { let mut params = Vec::new(); if let Some(expand) = self.expand_fields { params.push(("expand", expand)); } if let Some(fields) = self.filter_fields { params.push(("fields", fields)); } let auth: RecordAuth = self .record_service .client .send( &format!( "/api/collections/{}/auth-refresh", self.record_service.collection ), Method::POST, HeaderMap::new(), ¶ms, None::<&()>, ) .await?; self.record_service.client.with_auth_store(AuthStore { token: auth.token.clone(), record: auth.record.clone(), }); debug_assert!({ let read = self.record_service.client.inner.read().unwrap(); read.auth_store.as_ref().unwrap().is_valid() }); Ok(auth) } } impl<'a> AuthWithOtpBuilder<'a> { pub fn expand_fields(mut self, field_expression: &'a str) -> Self { self.expand_fields = Some(field_expression); self } pub fn filter_fields(mut self, field_expression: &'a str) -> Self { self.filter_fields = Some(field_expression); self } pub async fn send(self) -> Result { let body = json!({"otpId": self.otp_id, "password": self.password}); let mut params = Vec::new(); if let Some(expand) = self.expand_fields { params.push(("expand", expand)); } if let Some(fields) = self.filter_fields { params.push(("fields", fields)); } let auth: RecordAuth = self .record_service .client .send( &format!( "/api/collections/{}/auth-with-otp", self.record_service.collection ), Method::POST, HeaderMap::new(), ¶ms, Some(&body), ) .await?; self.record_service.client.with_auth_store(AuthStore { token: auth.token.clone(), record: auth.record.clone(), }); debug_assert!({ let read = self.record_service.client.inner.read().unwrap(); read.auth_store.as_ref().unwrap().is_valid() }); Ok(auth) } } impl<'a> AuthWithPasswordBuilder<'a> { pub fn expand_fields(mut self, field_expression: &'a str) -> Self { self.expand_fields = Some(field_expression); self } pub fn filter_fields(mut self, field_expression: &'a str) -> Self { self.filter_fields = Some(field_expression); self } pub async fn send(self) -> Result { let body = json!({"identity": self.identity, "password": self.password}); let mut params = Vec::new(); if let Some(expand) = self.expand_fields { params.push(("expand", expand)); } if let Some(fields) = self.filter_fields { params.push(("fields", fields)); } let auth: RecordAuth = self .record_service .client .send( &format!( "/api/collections/{}/auth-with-password", self.record_service.collection ), Method::POST, HeaderMap::new(), ¶ms, Some(&body), ) .await?; self.record_service.client.with_auth_store(AuthStore { token: auth.token.clone(), record: auth.record.clone(), }); debug_assert!({ let read = self.record_service.client.inner.read().unwrap(); read.auth_store.as_ref().unwrap().is_valid() }); Ok(auth) } } impl<'a> ImpersonateBuilder<'a> { /// Set the duration that the generated auth token will be valid. /// /// If duration is not set, then the generated auth token will fallback to the /// default collection auth token duration. pub fn duration(mut self, duration: Duration) -> Self { self.duration = Some(duration); self } pub fn expand_fields(mut self, field_expression: &'a str) -> Self { self.expand_fields = Some(field_expression); self } pub fn filter_fields(mut self, field_expression: &'a str) -> Self { self.filter_fields = Some(field_expression); self } pub async fn send(self) -> Result<(PocketBase, RecordAuth), Error> { let mut params = Vec::new(); if let Some(expand) = self.expand_fields { params.push(("expand", expand)); } if let Some(fields) = self.filter_fields { params.push(("fields", fields)); } let auth: RecordAuth = self .record_service .client .send( &format!( "/api/collections/{}/impersonate/{}", self.record_service.collection, self.id ), Method::POST, HeaderMap::new(), ¶ms, if let Some(dur) = self.duration { Some(json!({ "duration": dur.as_secs() })) } else { None } .as_ref(), ) .await?; let client = { let inner = self.record_service.client.inner.read().unwrap(); PocketBase { inner: Arc::new(RwLock::new(PocketBaseInner { reqwest: inner.reqwest.clone(), auth_store: None, base_url: inner.base_url.clone(), lang: inner.lang.clone(), })), } }; client.with_auth_store(AuthStore { token: auth.token.clone(), record: auth.record.clone(), }); debug_assert!({ let read = client.inner.read().unwrap(); read.auth_store.as_ref().unwrap().is_valid() }); Ok((client, auth)) } } impl RecordModel { pub fn as_object(&self) -> &'_ JsonObject { &self.data } pub fn into_object(self) -> JsonObject { self.data } }