diff --git a/README.md b/README.md index 0419373..6eea89b 100644 --- a/README.md +++ b/README.md @@ -59,22 +59,6 @@ Find more details about Passkey Flex on our [Passkey Flex Documentation](https:/ ## API Reference -### Retrieve App Info - -To retrieve information about the app, use the `get_app` method. - -```rust -use passage_flex::PassageFlex; - -let passage_flex = PassageFlex::new( - std::env::var("PASSAGE_APP_ID").unwrap(), - std::env::var("PASSAGE_API_KEY").unwrap(), -); - -let app_info = passage_flex.get_app().await.unwrap(); -println!("{}", app_info.auth_origin); -``` - ### Create a Registration Transaction To create a transaction to start a user passkey registration, use the `create_register_transaction` method. @@ -92,6 +76,7 @@ let passkey_display_name = "the label for the user's passkey that they will see when logging in".to_string(); let transaction = passage_flex + .auth .create_register_transaction(external_id, passkey_display_name) .await .unwrap(); @@ -112,6 +97,7 @@ let passage_flex = PassageFlex::new( let external_id = "a unique immutable string that represents your user".to_string(); let transaction = passage_flex + .auth .create_authenticate_transaction(external_id) .await .unwrap(); @@ -132,7 +118,7 @@ let passage_flex = PassageFlex::new( let nonce = "a unique single-use value received from the client after a passkey ceremony".to_string(); -match passage_flex.verify_nonce(nonce).await { +match passage_flex.auth.verify_nonce(nonce).await { Ok(external_id) => { // use external_id to do things like generate and send your own auth token } @@ -144,7 +130,7 @@ match passage_flex.verify_nonce(nonce).await { ## Retrieve User Info -To retrieve information about a user by their external ID -- which is the unique, immutable ID you supply to associate the Passage user with your user -- use the `get_user` method. +To retrieve information about a user by their external ID -- which is the unique, immutable ID you supply to associate the Passage user with your user -- use the `get` method. ```rust use passage_flex::PassageFlex; @@ -158,13 +144,13 @@ let passage_flex = PassageFlex::new( let external_id = your_user.id; // get user info -let user_info = passage_flex.get_user(external_id).await.unwrap(); -println!("{:?}", user_info.webauthn_devices); +let passage_user = passage_flex.user.get(external_id).await.unwrap(); +println!("{:?}", passage_user.webauthn_devices); ``` ## Retrieve a user's passkey devices -To retrieve information about a user's passkey devices, use the `get_devices` method. +To retrieve information about a user's passkey devices, use the `list_devices` method. ```rust use passage_flex::PassageFlex; @@ -177,8 +163,8 @@ let passage_flex = PassageFlex::new( // this is the same value used when creating a transaction let external_id = your_user.id; -// get devices -let passkey_devices = passage_flex.get_devices(external_id).await.unwrap(); +// list devices +let passkey_devices = passage_flex.user.list_devices(external_id).await.unwrap(); for device in passkey_devices { println!("{}", device.usage_count); } @@ -201,8 +187,8 @@ let passage_flex = PassageFlex::new( let external_id = your_user.id; let last_year = Utc::now().naive_utc().date() - Duration::days(365); -// get devices -let passkey_devices = passage_flex.get_devices(external_id.clone()).await.unwrap(); +// list devices +let passkey_devices = passage_flex.user.list_devices(external_id.clone()).await.unwrap(); for device in passkey_devices { // revoke old devices that haven't been used in the last year @@ -211,6 +197,7 @@ for device in passkey_devices { if last_login_at_parsed < last_year { if let Err(err) = passage_flex + .user .revoke_device(external_id.clone(), device.id) .await { diff --git a/examples/poem-openapi/src/main.rs b/examples/poem-openapi/src/main.rs index 4816a11..e169489 100644 --- a/examples/poem-openapi/src/main.rs +++ b/examples/poem-openapi/src/main.rs @@ -119,6 +119,7 @@ impl Api { let transaction_id = self .state .passage_client + .auth .create_register_transaction(user_id.clone(), req.0.email.clone()) .await .map_err(|e| { @@ -155,6 +156,7 @@ impl Api { let transaction_id = self .state .passage_client + .auth .create_authenticate_transaction(user_id) .await .map_err(|e| match e { @@ -180,7 +182,7 @@ impl Api { #[oai(path = "/auth/verify", method = "post")] async fn verify_nonce(&self, req: Json) -> poem::Result> { // Verify the nonce using the Passage SDK - match self.state.passage_client.verify_nonce(req.0.nonce).await { + match self.state.passage_client.auth.verify_nonce(req.0.nonce).await { Ok(user_id) => { // Find the user in the database and set the auth token let mut users = self.state.users.lock().await; @@ -225,6 +227,7 @@ impl Api { let transaction_id = self .state .passage_client + .auth .create_register_transaction(user.id.clone(), user.email.clone()) .await .map_err(|e| { @@ -261,7 +264,8 @@ impl Api { let passkeys = self .state .passage_client - .get_devices(user.id.clone()) + .user + .list_devices(user.id.clone()) .await .map(|devices| { devices @@ -306,6 +310,7 @@ impl Api { // Revoke the passkey device using the Passage SDK self.state .passage_client + .user .revoke_device(user.id.clone(), req.0.passkey_id.clone()) .await .map_err(|e| { diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..fb7af0c --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,141 @@ +use crate::openapi::apis::configuration::Configuration; +use crate::openapi::apis::{authenticate_api, transactions_api}; +use crate::Error; + +pub struct Auth { + pub(crate) configuration: Configuration, +} + +impl Auth { + /// Creates a new instance of the `Auth` struct. + pub fn new(configuration: Configuration) -> Self { + Self { configuration } + } + + /// Creates a transaction to start a user's registration process. + /// + /// # Arguments + /// + /// * `external_id` - A unique, immutable string that represents the user. + /// * `passkey_display_name` - The label for the user's passkey that they will see when logging in. + /// + /// # Returns + /// + /// A `Result` containing the transaction ID as a string or an `Error`. + /// + /// # Examples + /// + /// ```ignore + /// use passage_flex::PassageFlex; + /// + /// let passage_flex = PassageFlex::new( + /// std::env::var("PASSAGE_APP_ID").unwrap(), + /// std::env::var("PASSAGE_API_KEY").unwrap(), + /// ); + /// + /// let transaction = passage_flex + /// .auth + /// .create_register_transaction( + /// "00000000-0000-0000-0000-000000000001".to_string(), + /// "user@example.com".to_string(), + /// ) + /// .await + /// .unwrap(); + /// ``` + pub async fn create_register_transaction( + &self, + external_id: String, + passkey_display_name: String, + ) -> Result { + transactions_api::create_register_transaction( + &self.configuration, + crate::openapi::models::CreateTransactionRegisterRequest { + external_id, + passkey_display_name, + }, + ) + .await + .map(|response| response.transaction_id) + .map_err(Into::into) + } + + /// Creates a transaction to start a user's authentication process. + /// + /// # Arguments + /// + /// * `external_id` - A unique, immutable string that represents the user. + /// + /// # Returns + /// + /// A `Result` containing the transaction ID as a string or an `Error`. + /// + /// # Examples + /// + /// ```ignore + /// use passage_flex::PassageFlex; + /// + /// let passage_flex = PassageFlex::new( + /// std::env::var("PASSAGE_APP_ID").unwrap(), + /// std::env::var("PASSAGE_API_KEY").unwrap(), + /// ); + /// + /// let transaction = passage_flex + /// .auth + /// .create_authenticate_transaction( + /// "00000000-0000-0000-0000-000000000001".to_string(), + /// ) + /// .await + /// .unwrap(); + /// ``` + pub async fn create_authenticate_transaction( + &self, + external_id: String, + ) -> Result { + transactions_api::create_authenticate_transaction( + &self.configuration, + crate::openapi::models::CreateTransactionAuthenticateRequest { external_id }, + ) + .await + .map(|response| response.transaction_id) + .map_err(Into::into) + } + + /// Verifies the nonce received from a WebAuthn registration or authentication ceremony. + /// + /// # Arguments + /// + /// * `nonce` - The nonce string to be verified. + /// + /// # Returns + /// + /// A `Result` containing the external ID as a string or an `Error`. + /// + /// # Examples + /// + /// ```ignore + /// use passage_flex::PassageFlex; + /// + /// let passage_flex = PassageFlex::new( + /// std::env::var("PASSAGE_APP_ID").unwrap(), + /// std::env::var("PASSAGE_API_KEY").unwrap(), + /// ); + /// + /// match passage_flex.auth.verify_nonce("01234567890123456789".to_string()).await { + /// Ok(external_id) => { + /// // use external_id to do things like generate and send your own auth token + /// } + /// Err(err) => { + /// // nonce was invalid or unable to be verified + /// } + /// } + /// ``` + pub async fn verify_nonce(&self, nonce: String) -> Result { + authenticate_api::authenticate_verify_nonce( + &self.configuration, + crate::openapi::models::Nonce { nonce }, + ) + .await + .map(|response| response.external_id) + .map_err(Into::into) + } +} diff --git a/src/error.rs b/src/error.rs index 64866ab..f04339e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,4 @@ -use crate::openapi::apis::{ - apps_api, authenticate_api, transactions_api, user_devices_api, users_api, -}; +use crate::openapi::apis::{authenticate_api, transactions_api, user_devices_api, users_api}; use crate::Error; // This function converts an openapi error into a crate error @@ -16,17 +14,6 @@ fn convert_error(error: crate::openapi::apis::Error, map_fn: fn(Src) - } } -impl From> for Error { - fn from(e: crate::openapi::apis::Error) -> Self { - convert_error(e, |e| match e { - apps_api::GetAppError::Status401(model) => model.into(), - apps_api::GetAppError::Status404(model) => model.into(), - apps_api::GetAppError::Status500(model) => model.into(), - apps_api::GetAppError::UnknownValue(v) => Error::Other(v.to_string()), - }) - } -} - impl From> for Error { fn from( e: crate::openapi::apis::Error, diff --git a/src/lib.rs b/src/lib.rs index 7607bc8..a30a611 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,9 +10,6 @@ //! std::env::var("PASSAGE_APP_ID").unwrap(), //! std::env::var("PASSAGE_API_KEY").unwrap(), //! ); -//! -//! let app_info = passage_flex.get_app().await.unwrap(); -//! println!("{}", app_info.auth_origin); //! ``` use std::fmt; @@ -62,5 +59,7 @@ pub mod models; #[rustfmt::skip] pub mod openapi; +pub mod auth; pub mod passage_flex; +pub mod user; pub use passage_flex::PassageFlex; diff --git a/src/models/app_info.rs b/src/models/app_info.rs deleted file mode 100644 index f215a04..0000000 --- a/src/models/app_info.rs +++ /dev/null @@ -1,11 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct AppInfo { - #[serde(rename = "auth_origin")] - pub auth_origin: String, - #[serde(rename = "id")] - pub id: String, - #[serde(rename = "name")] - pub name: String, -} diff --git a/src/models/mod.rs b/src/models/mod.rs index 7fe6f06..427462d 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,5 +1,3 @@ -mod app_info; -mod user_info; +mod passage_user; -pub use app_info::AppInfo; -pub use user_info::UserInfo; +pub use passage_user::PassageUser; diff --git a/src/models/user_info.rs b/src/models/passage_user.rs similarity index 97% rename from src/models/user_info.rs rename to src/models/passage_user.rs index e15cb96..fe67482 100644 --- a/src/models/user_info.rs +++ b/src/models/passage_user.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct UserInfo { +pub struct PassageUser { #[serde(rename = "created_at")] pub created_at: String, /// The external ID of the user. Only set if the user was created in a Flex app. diff --git a/src/passage_flex.rs b/src/passage_flex.rs index c9d6065..4efbbdb 100644 --- a/src/passage_flex.rs +++ b/src/passage_flex.rs @@ -1,13 +1,11 @@ -use crate::models::{AppInfo, UserInfo}; +use crate::auth::Auth; use crate::openapi::apis::configuration::Configuration; -use crate::openapi::apis::{ - apps_api, authenticate_api, transactions_api, user_devices_api, users_api, -}; -use crate::Error; +use crate::user::User; pub struct PassageFlex { app_id: String, - configuration: Configuration, + pub auth: Auth, + pub user: User, } const SERVER_URL: &str = "https://api.passage.id"; @@ -31,10 +29,6 @@ impl PassageFlex { /// ); /// ``` pub fn new(app_id: String, api_key: String) -> Self { - let mut configuration = Configuration::new(); - // Use the api_key as the bearer access token - configuration.bearer_access_token = Some(api_key); - // Set the Passage-Version header to the version of the crate let mut headers = reqwest::header::HeaderMap::with_capacity(1); headers.insert( "Passage-Version", @@ -43,15 +37,18 @@ impl PassageFlex { env!("CARGO_PKG_VERSION") )), ); + + let mut configuration = Configuration::new(); + configuration.bearer_access_token = Some(api_key); configuration.client = reqwest::Client::builder() .default_headers(headers) .build() .expect("Failed to create reqwest client for Passage"); - let mut client = Self { - app_id, - configuration, - }; + let auth = Auth::new(configuration.clone()); + let user = User::new(configuration.clone()); + + let mut client = Self { app_id, auth, user }; // Set the default server URL client.set_server_url(SERVER_URL.to_string()); @@ -59,357 +56,8 @@ impl PassageFlex { } fn set_server_url(&mut self, server_url: String) { - // Use the app_id and server_url to set the base_path - self.configuration.base_path = format!("{}/v1/apps/{}", server_url, self.app_id); - } - - /// Retrieves information about the application. - /// - /// # Returns - /// - /// A `Result` containing the `AppInfo` struct or an `Error`. - /// - /// # Examples - /// - /// ```ignore - /// use passage_flex::PassageFlex; - /// - /// let passage_flex = PassageFlex::new( - /// std::env::var("PASSAGE_APP_ID").unwrap(), - /// std::env::var("PASSAGE_API_KEY").unwrap(), - /// ); - /// - /// let app_info = passage_flex.get_app().await.unwrap(); - /// println!("{}", app_info.auth_origin); - /// ``` - pub async fn get_app(&self) -> Result, Error> { - apps_api::get_app(&self.configuration) - .await - .map(|response| { - Box::new(AppInfo { - auth_origin: response.app.auth_origin, - id: response.app.id, - name: response.app.name, - }) - }) - .map_err(Into::into) - } - - /// Creates a transaction to start a user's registration process. - /// - /// # Arguments - /// - /// * `external_id` - A unique, immutable string that represents the user. - /// * `passkey_display_name` - The label for the user's passkey that they will see when logging in. - /// - /// # Returns - /// - /// A `Result` containing the transaction ID as a string or an `Error`. - /// - /// # Examples - /// - /// ```ignore - /// use passage_flex::PassageFlex; - /// - /// let passage_flex = PassageFlex::new( - /// std::env::var("PASSAGE_APP_ID").unwrap(), - /// std::env::var("PASSAGE_API_KEY").unwrap(), - /// ); - /// - /// let transaction = passage_flex - /// .create_register_transaction( - /// "00000000-0000-0000-0000-000000000001".to_string(), - /// "user@example.com".to_string(), - /// ) - /// .await - /// .unwrap(); - /// ``` - pub async fn create_register_transaction( - &self, - external_id: String, - passkey_display_name: String, - ) -> Result { - transactions_api::create_register_transaction( - &self.configuration, - crate::openapi::models::CreateTransactionRegisterRequest { - external_id, - passkey_display_name, - }, - ) - .await - .map(|response| response.transaction_id) - .map_err(Into::into) - } - - /// Creates a transaction to start a user's authentication process. - /// - /// # Arguments - /// - /// * `external_id` - A unique, immutable string that represents the user. - /// - /// # Returns - /// - /// A `Result` containing the transaction ID as a string or an `Error`. - /// - /// # Examples - /// - /// ```ignore - /// use passage_flex::PassageFlex; - /// - /// let passage_flex = PassageFlex::new( - /// std::env::var("PASSAGE_APP_ID").unwrap(), - /// std::env::var("PASSAGE_API_KEY").unwrap(), - /// ); - /// - /// let transaction = passage_flex - /// .create_authenticate_transaction( - /// "00000000-0000-0000-0000-000000000001".to_string(), - /// ) - /// .await - /// .unwrap(); - /// ``` - pub async fn create_authenticate_transaction( - &self, - external_id: String, - ) -> Result { - transactions_api::create_authenticate_transaction( - &self.configuration, - crate::openapi::models::CreateTransactionAuthenticateRequest { external_id }, - ) - .await - .map(|response| response.transaction_id) - .map_err(Into::into) - } - - /// Verifies the nonce received from a WebAuthn registration or authentication ceremony. - /// - /// # Arguments - /// - /// * `nonce` - The nonce string to be verified. - /// - /// # Returns - /// - /// A `Result` containing the external ID as a string or an `Error`. - /// - /// # Examples - /// - /// ```ignore - /// use passage_flex::PassageFlex; - /// - /// let passage_flex = PassageFlex::new( - /// std::env::var("PASSAGE_APP_ID").unwrap(), - /// std::env::var("PASSAGE_API_KEY").unwrap(), - /// ); - /// - /// match passage_flex.verify_nonce("01234567890123456789".to_string()).await { - /// Ok(external_id) => { - /// // use external_id to do things like generate and send your own auth token - /// } - /// Err(err) => { - /// // nonce was invalid or unable to be verified - /// } - /// } - /// ``` - pub async fn verify_nonce(&self, nonce: String) -> Result { - authenticate_api::authenticate_verify_nonce( - &self.configuration, - crate::openapi::models::Nonce { nonce }, - ) - .await - .map(|response| response.external_id) - .map_err(Into::into) - } - - /// Get a user's ID in Passage by their external ID - async fn get_id(&self, external_id: String) -> Result { - let users = users_api::list_paginated_users( - &self.configuration, - Some(1), - Some(1), - None, - None, - Some(&external_id), - None, - None, - None, - None, - None, - None, - ) - .await - .map(|response| response.users) - .map_err(Into::into); - - match users { - Ok(mut users) => match users.len() { - 0 => Err(Error::UserNotFound), - 1 => { - let user = users.remove(0); - Ok(user.id) - } - _ => Err(Error::Other("Multiple users found".to_string())), - }, - Err(e) => Err(e), - } - } - - /// Retrieves information about a user by their external ID. - /// - /// # Arguments - /// - /// * `external_id` - The unique, immutable ID that represents the user. - /// - /// # Returns - /// - /// A `Result` containing the `UserInfo` struct or an `Error`. - /// - /// # Examples - /// - /// ```ignore - /// use passage_flex::PassageFlex; - /// - /// let passage_flex = PassageFlex::new( - /// std::env::var("PASSAGE_APP_ID").unwrap(), - /// std::env::var("PASSAGE_API_KEY").unwrap(), - /// ); - /// - /// let external_id = "00000000-0000-0000-0000-000000000001"; - /// let user_info = passage_flex.get_user(external_id.to_string()).await.unwrap(); - /// println!("{:?}", user_info.id); - /// ``` - pub async fn get_user(&self, external_id: String) -> Result, Error> { - let user_id = self.get_id(external_id).await?; - self.get_user_by_id(user_id).await - } - - /// Retrieves information about a user's passkey devices. - /// - /// # Arguments - /// - /// * `external_id` - The unique, immutable ID that represents the user. - /// - /// # Returns - /// - /// A `Result` containing a vector of `WebAuthnDevices` or an `Error`. - /// - /// # Examples - /// - /// ```ignore - /// use passage_flex::PassageFlex; - /// - /// let passage_flex = PassageFlex::new( - /// std::env::var("PASSAGE_APP_ID").unwrap(), - /// std::env::var("PASSAGE_API_KEY").unwrap(), - /// ); - /// - /// let external_id = "00000000-0000-0000-0000-000000000001"; - /// let passkey_devices = passage_flex.get_devices(external_id.to_string()).await.unwrap(); - /// for device in passkey_devices { - /// println!("{}", device.usage_count); - /// } - /// ``` - pub async fn get_devices( - &self, - external_id: String, - ) -> Result, Error> { - let user_id = self.get_id(external_id).await?; - user_devices_api::list_user_devices(&self.configuration, &user_id) - .await - .map(|response| response.devices) - .map_err(Into::into) - } - - /// Revokes a user's passkey device. - /// - /// # Arguments - /// - /// * `external_id` - The unique, immutable ID that represents the user. - /// * `device_id` - The ID of the device to be revoked. - /// - /// # Returns - /// - /// A `Result` containing `()` or an `Error`. - /// - /// # Examples - /// - /// ```ignore - /// use passage_flex::PassageFlex; - /// use chrono::{Duration, NaiveDate, Utc}; - /// - /// let passage_flex = PassageFlex::new( - /// std::env::var("PASSAGE_APP_ID").unwrap(), - /// std::env::var("PASSAGE_API_KEY").unwrap(), - /// ); - /// - /// let external_id = "00000000-0000-0000-0000-000000000001"; - /// let last_year = Utc::now().naive_utc().date() - Duration::days(365); - /// - /// let passkey_devices = passage_flex.get_devices(external_id.to_string()).await.unwrap(); - /// - /// for device in passkey_devices { - /// let last_login_at_parsed = - /// NaiveDate::parse_from_str(&device.last_login_at, "%Y-%m-%dT%H:%M:%S%z").unwrap(); - /// - /// if last_login_at_parsed < last_year { - /// if let Err(err) = passage_flex - /// .revoke_device(external_id.clone(), device.id) - /// .await - /// { - /// // device couldn't be revoked - /// } - /// } - /// } - /// ``` - pub async fn revoke_device(&self, external_id: String, device_id: String) -> Result<(), Error> { - let user_id = self.get_id(external_id).await?; - user_devices_api::delete_user_devices(&self.configuration, &user_id, &device_id) - .await - .map_err(Into::into) - } - - /// Retrieves information about a user by their user ID in Passage. - /// - /// # Arguments - /// - /// * `user_id` - The ID of the user in Passage. - /// - /// # Returns - /// - /// A `Result` containing the `UserInfo` struct or an `Error`. - /// - /// # Examples - /// - /// ```ignore - /// use passage_flex::PassageFlex; - /// - /// let passage_flex = PassageFlex::new( - /// std::env::var("PASSAGE_APP_ID").unwrap(), - /// std::env::var("PASSAGE_API_KEY").unwrap(), - /// ); - /// - /// let user_id = "user_id_string"; - /// let user_info = passage_flex.get_user_by_id(user_id.to_string()).await.unwrap(); - /// println!("{:?}", user_info.external_id); - /// ``` - pub async fn get_user_by_id(&self, user_id: String) -> Result, Error> { - users_api::get_user(&self.configuration, &user_id) - .await - .map(|response| { - Box::new(UserInfo { - created_at: response.user.created_at, - external_id: response.user.external_id, - id: response.user.id, - last_login_at: response.user.last_login_at, - login_count: response.user.login_count, - status: response.user.status, - updated_at: response.user.updated_at, - user_metadata: response.user.user_metadata, - webauthn: response.user.webauthn, - webauthn_devices: response.user.webauthn_devices, - webauthn_types: response.user.webauthn_types, - }) - }) - .map_err(Into::into) + self.user.configuration.base_path = format!("{}/v1/apps/{}", server_url, self.app_id); + self.auth.configuration.base_path = format!("{}/v1/apps/{}", server_url, self.app_id); } } diff --git a/src/passage_flex/test.rs b/src/passage_flex/test.rs index 62c9da4..358b7cd 100644 --- a/src/passage_flex/test.rs +++ b/src/passage_flex/test.rs @@ -1,3 +1,5 @@ +use crate::Error; + use super::*; use mockito::{Matcher, Mock, Server, ServerGuard}; @@ -222,6 +224,7 @@ async fn test_create_register_transaction() { .await; let transaction_id = passage_flex + .auth .create_register_transaction("test".to_string(), "test".to_string()) .await .unwrap(); @@ -244,6 +247,7 @@ async fn test_create_authenticate_transaction() { .await; let transaction_id = passage_flex + .auth .create_authenticate_transaction("test".to_string()) .await .unwrap(); @@ -260,7 +264,7 @@ async fn test_verify_nonce() { .with_body(r#"{"error": "Could not verify nonce: nonce is invalid, expired, or cannot be found","code": "invalid_nonce"}"#) .create_async().await; - let invalid_result = passage_flex.verify_nonce("invalid".to_string()).await; + let invalid_result = passage_flex.auth.verify_nonce("invalid".to_string()).await; assert!(invalid_result.is_err()); assert!(matches!(invalid_result, Err(Error::InvalidNonce))); m1.assert_async().await; @@ -277,6 +281,7 @@ async fn test_verify_nonce() { .await; let external_id = passage_flex + .auth .verify_nonce("valid".to_string()) .await .unwrap(); @@ -289,7 +294,7 @@ async fn test_get_user() { let (app_id, passage_flex, mut server) = setup_passage_flex().await; let m1 = setup_empty_list_paginated_users_mock(&app_id, &mut server).await; - let invalid_result = passage_flex.get_user("invalid".to_string()).await; + let invalid_result = passage_flex.user.get("invalid".to_string()).await; m1.assert_async().await; assert!(invalid_result.is_err()); assert!(matches!(invalid_result, Err(Error::UserNotFound))); @@ -297,10 +302,10 @@ async fn test_get_user() { let m2 = setup_valid_list_paginated_users_mock(&app_id, &mut server).await; let m3 = setup_valid_get_user_mock(&app_id, &mut server).await; - let user_info = passage_flex.get_user("valid".to_string()).await.unwrap(); + let passage_user = passage_flex.user.get("valid".to_string()).await.unwrap(); m2.assert_async().await; m3.assert_async().await; - assert_eq!(user_info.external_id, "valid"); + assert_eq!(passage_user.external_id, "valid"); } #[tokio::test] @@ -308,14 +313,18 @@ async fn test_get_devices() { let (app_id, passage_flex, mut server) = setup_passage_flex().await; let m1 = setup_empty_list_paginated_users_mock(&app_id, &mut server).await; - let invalid_result = passage_flex.get_devices("invalid".to_string()).await; + let invalid_result = passage_flex.user.list_devices("invalid".to_string()).await; m1.assert_async().await; assert!(invalid_result.is_err()); assert!(matches!(invalid_result, Err(Error::UserNotFound))); let m2 = setup_valid_list_paginated_users_mock(&app_id, &mut server).await; let m3 = setup_valid_get_devices_mock(&app_id, &mut server).await; - let devices = passage_flex.get_devices("valid".to_string()).await.unwrap(); + let devices = passage_flex + .user + .list_devices("valid".to_string()) + .await + .unwrap(); m2.assert_async().await; m3.assert_async().await; assert_eq!(devices.len(), 1); @@ -328,6 +337,7 @@ async fn test_revoke_device() { let m1 = setup_empty_list_paginated_users_mock(&app_id, &mut server).await; let invalid_result = passage_flex + .user .revoke_device("invalid".to_string(), "invalid".to_string()) .await; m1.assert_async().await; @@ -337,106 +347,10 @@ async fn test_revoke_device() { let m2 = setup_valid_list_paginated_users_mock(&app_id, &mut server).await; let m3 = setup_valid_revoke_device_mock(&app_id, &mut server).await; let result = passage_flex + .user .revoke_device("valid".to_string(), "test_device_id".to_string()) .await; m2.assert_async().await; m3.assert_async().await; assert!(result.is_ok()); } - -#[tokio::test] -async fn test_get_app() { - let mut server = Server::new_async().await; - let api_key = "test_api_key"; - - let app_id = "invalid"; - let mut invalid_passage = PassageFlex::new(app_id.to_string(), api_key.to_string()); - invalid_passage.set_server_url(server.url()); - let m1 = server - .mock("GET", "/v1/apps/invalid/") - .with_status(401) - .with_body(r#"{"error": "Invalid access token","code": "invalid_access_token"}"#) - .create_async() - .await; - let invalid_result = invalid_passage.get_app().await; - m1.assert_async().await; - assert!(invalid_result.is_err()); - assert!(matches!(invalid_result, Err(Error::InvalidAccessToken))); - - let app_id = "test_app_id"; - let mut passage = PassageFlex::new(app_id.to_string(), api_key.to_string()); - passage.set_server_url(server.url()); - let m2 = server - .mock("GET", "/v1/apps/test_app_id/") - .with_status(200) - .with_body( - r#"{ - "app": { - "additional_auth_origins": [], - "allowed_callback_urls": [], - "allowed_identifier": "external", - "allowed_logout_urls": [], - "application_login_uri": "", - "auth_fallback_method": "none", - "auth_fallback_method_ttl": 300, - "auth_methods": { - "magic_link": { - "enabled": false, - "ttl": 300, - "ttl_display_unit": "s" - }, - "otp": { - "enabled": false, - "ttl": 300, - "ttl_display_unit": "s" - }, - "passkeys": { - "enabled": true - } - }, - "auth_origin": "https://auth.test.com", - "auto_theme_enabled": true, - "created_at": "2021-01-01T00:00:00Z", - "default_language": "en", - "element_customization": {}, - "element_customization_dark": {}, - "hosted": false, - "hosted_subdomain": "test", - "hosted_theme": "auto", - "id": "test_app_id", - "id_token_lifetime": 600, - "layouts": { - "profile": [], - "registration": [] - }, - "login_url": "/", - "name": "Test App", - "passage_branding": true, - "profile_management": false, - "public_signup": false, - "redirect_url": "/", - "refresh_absolute_lifetime": 2592000, - "refresh_enabled": false, - "refresh_inactivity_lifetime": 432000, - "require_email_verification": false, - "require_identifier_verification": false, - "required_identifier": "external", - "role": "", - "rsa_public_key": "", - "secret": null, - "session_timeout_length": 5, - "technologies": [], - "type": "flex", - "user_metadata_schema": [] - } - }"#, - ) - .create_async() - .await; - - let app_info = passage.get_app().await.unwrap(); - m2.assert_async().await; - assert_eq!(app_info.id, "test_app_id"); - assert_eq!(app_info.name, "Test App"); - assert_eq!(app_info.auth_origin, "https://auth.test.com"); -} diff --git a/src/user.rs b/src/user.rs new file mode 100644 index 0000000..404fa8d --- /dev/null +++ b/src/user.rs @@ -0,0 +1,180 @@ +use crate::models::PassageUser; +use crate::openapi::apis::configuration::Configuration; +use crate::openapi::apis::{user_devices_api, users_api}; +use crate::Error; + +pub struct User { + pub(crate) configuration: Configuration, +} + +impl User { + /// Creates a new instance of the `User` struct. + pub fn new(configuration: Configuration) -> Self { + Self { configuration } + } + + /// Get a user's ID in Passage by their external ID + async fn get_id(&self, external_id: String) -> Result { + let users = users_api::list_paginated_users( + &self.configuration, + Some(1), + Some(1), + None, + None, + Some(&external_id), + None, + None, + None, + None, + None, + None, + ) + .await + .map(|response| response.users) + .map_err(Into::into); + + match users { + Ok(mut users) => match users.len() { + 0 => Err(Error::UserNotFound), + 1 => { + let user = users.remove(0); + Ok(user.id) + } + _ => Err(Error::Other("Multiple users found".to_string())), + }, + Err(e) => Err(e), + } + } + + /// Retrieves information about a user by their external ID. + /// + /// # Arguments + /// + /// * `external_id` - The unique, immutable ID that represents the user. + /// + /// # Returns + /// + /// A `Result` containing the `UserInfo` struct or an `Error`. + /// + /// # Examples + /// + /// ```ignore + /// use passage_flex::PassageFlex; + /// + /// let passage_flex = PassageFlex::new( + /// std::env::var("PASSAGE_APP_ID").unwrap(), + /// std::env::var("PASSAGE_API_KEY").unwrap(), + /// ); + /// + /// let external_id = "00000000-0000-0000-0000-000000000001"; + /// let passage_user = passage_flex.user.get(external_id.to_string()).await.unwrap(); + /// println!("{:?}", passage_user.id); + /// ``` + pub async fn get(&self, external_id: String) -> Result, Error> { + let user_id = self.get_id(external_id).await?; + users_api::get_user(&self.configuration, &user_id) + .await + .map(|response| { + Box::new(PassageUser { + created_at: response.user.created_at, + external_id: response.user.external_id, + id: response.user.id, + last_login_at: response.user.last_login_at, + login_count: response.user.login_count, + status: response.user.status, + updated_at: response.user.updated_at, + user_metadata: response.user.user_metadata, + webauthn: response.user.webauthn, + webauthn_devices: response.user.webauthn_devices, + webauthn_types: response.user.webauthn_types, + }) + }) + .map_err(Into::into) + } + + /// Retrieves information about a user's passkey devices. + /// + /// # Arguments + /// + /// * `external_id` - The unique, immutable ID that represents the user. + /// + /// # Returns + /// + /// A `Result` containing a vector of `WebAuthnDevices` or an `Error`. + /// + /// # Examples + /// + /// ```ignore + /// use passage_flex::PassageFlex; + /// + /// let passage_flex = PassageFlex::new( + /// std::env::var("PASSAGE_APP_ID").unwrap(), + /// std::env::var("PASSAGE_API_KEY").unwrap(), + /// ); + /// + /// let external_id = "00000000-0000-0000-0000-000000000001"; + /// let passkey_devices = passage_flex.user.list_devices(external_id.to_string()).await.unwrap(); + /// for device in passkey_devices { + /// println!("{}", device.usage_count); + /// } + /// ``` + pub async fn list_devices( + &self, + external_id: String, + ) -> Result, Error> { + let user_id = self.get_id(external_id).await?; + user_devices_api::list_user_devices(&self.configuration, &user_id) + .await + .map(|response| response.devices) + .map_err(Into::into) + } + + /// Revokes a user's passkey device. + /// + /// # Arguments + /// + /// * `external_id` - The unique, immutable ID that represents the user. + /// * `device_id` - The ID of the device to be revoked. + /// + /// # Returns + /// + /// A `Result` containing `()` or an `Error`. + /// + /// # Examples + /// + /// ```ignore + /// use passage_flex::PassageFlex; + /// use chrono::{Duration, NaiveDate, Utc}; + /// + /// let passage_flex = PassageFlex::new( + /// std::env::var("PASSAGE_APP_ID").unwrap(), + /// std::env::var("PASSAGE_API_KEY").unwrap(), + /// ); + /// + /// let external_id = "00000000-0000-0000-0000-000000000001"; + /// let last_year = Utc::now().naive_utc().date() - Duration::days(365); + /// + /// let passkey_devices = passage_flex.user.list_devices(external_id.to_string()).await.unwrap(); + /// + /// for device in passkey_devices { + /// let last_login_at_parsed = + /// NaiveDate::parse_from_str(&device.last_login_at, "%Y-%m-%dT%H:%M:%S%z").unwrap(); + /// + /// if last_login_at_parsed < last_year { + /// if let Err(err) = passage_flex + /// .user + /// .revoke_device(external_id.clone(), device.id) + /// .await + /// { + /// // device couldn't be revoked + /// } + /// } + /// } + /// ``` + pub async fn revoke_device(&self, external_id: String, device_id: String) -> Result<(), Error> { + let user_id = self.get_id(external_id).await?; + user_devices_api::delete_user_devices(&self.configuration, &user_id, &device_id) + .await + .map_err(Into::into) + } +}