use anyhow::{anyhow, Result}; use serde::{Deserialize, Serialize}; /// Format for error response payloads for Google APIs. /// /// This error format is standardized across 'v1' Google APIs; its used for both /// POST /v1/accounts/lookup and POST /v1/token requests. /// /// This format is documented at https://firebase.google.com/docs/reference/rest/auth#section-error-format /// as well as https://cloud.google.com/apis/design/errors#error_mapping. In the Google Cloud /// documentation, refer to the 'HTTP mapping' for the schema (which is canonically defined in /// protobuf). /// /// The nested 'errors' field mentioned in the documentation is explicitly omitted since it is /// deprecated and largely redundant with the top level error `code` and `message`. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FirebaseError { pub code: i32, pub message: String, } impl std::error::Error for FirebaseError {} impl std::fmt::Display for FirebaseError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "Firebase request failed with status {} and message: {}", self.code, self.message ) } } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct ProviderUserInfo { display_name: Option, email: Option, provider_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AccountInfo { pub local_id: String, pub photo_url: Option, pub screen_name: Option, display_name: Option, email: Option, #[serde(default)] provider_user_info: Vec, } impl AccountInfo { /// Construct a partial `AccountInfo` given user profile information. pub fn from_profile( firebase_uid: String, photo_url: Option, display_name: Option, email: Option, ) -> Self { Self { local_id: firebase_uid, photo_url, screen_name: display_name.clone(), display_name, email, provider_user_info: vec![], } } pub fn display_name(&self) -> Option<&str> { self.display_name.as_deref().or_else(|| { self.provider_user_info .iter() .find_map(|user_info| user_info.display_name.as_deref()) }) } pub fn email(&self) -> Result<&str> { self.email .as_deref() .or_else(|| { self.provider_user_info .iter() .find_map(|user_info| user_info.email.as_deref()) }) .ok_or_else(|| anyhow!("Email address missing from user information")) } pub fn has_sso_link(&self) -> bool { self.provider_user_info .iter() .any(|user_info| user_info.provider_id.as_deref() == Some("oidc.workos")) } } /// Format for successful response payload for POST /v1/accounts/lookup request. /// /// Reference: https://cloud.google.com/identity-platform/docs/use-rest-api#section-get-account-info. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GetAccountInfoResponsePayload { users: Vec, } impl GetAccountInfoResponsePayload { pub fn user_account_info(self) -> Result { self.users.into_iter().next().ok_or_else(|| { anyhow!("field `users` was unexpectedly empty in GetAccountInfoResponse") }) } } /// Top-level response format for POST /v1/accounts/lookup request (part of the identitytoolkit GCP /// API). /// /// Reference: https://cloud.google.com/identity-platform/docs/use-rest-api#section-get-account-info. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum GetAccountInfoResponse { Success(GetAccountInfoResponsePayload), Error { error: FirebaseError }, } /// The possible response values from fetching an access token from a refresh token /// See https://firebase.google.com/docs/reference/rest/auth #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum FetchAccessTokenResponse { // Note we need to support both camel and snake case because the two different // endpoints for converting refresh / custom tokens to access tokens use different conventions. Success { #[serde(alias = "expiresIn")] expires_in: String, #[serde(alias = "idToken")] id_token: String, #[serde(alias = "refreshToken")] refresh_token: String, }, Error { error: FirebaseError, }, }