diff --git a/src/user_management/operations.rs b/src/user_management/operations.rs index 4e02ef2..52d91fd 100644 --- a/src/user_management/operations.rs +++ b/src/user_management/operations.rs @@ -1,3 +1,4 @@ +mod accept_invitation; mod authenticate_with_code; mod authenticate_with_email_verification; mod authenticate_with_magic_auth; @@ -9,8 +10,10 @@ mod create_password_reset; mod create_user; mod delete_user; mod enroll_auth_factor; +mod find_invitation_by_token; mod get_authorization_url; mod get_email_verification; +mod get_invitation; mod get_jwks; mod get_jwks_url; mod get_logout_url; @@ -20,10 +23,14 @@ mod get_user; mod get_user_by_external_id; mod get_user_identities; mod list_auth_factors; +mod list_invitations; mod list_users; mod reset_password; +mod revoke_invitation; +mod send_invitation; mod update_user; +pub use accept_invitation::*; pub use authenticate_with_code::*; pub use authenticate_with_email_verification::*; pub use authenticate_with_magic_auth::*; @@ -35,8 +42,10 @@ pub use create_password_reset::*; pub use create_user::*; pub use delete_user::*; pub use enroll_auth_factor::*; +pub use find_invitation_by_token::*; pub use get_authorization_url::*; pub use get_email_verification::*; +pub use get_invitation::*; pub use get_jwks::*; pub use get_jwks_url::*; pub use get_logout_url::*; @@ -46,6 +55,9 @@ pub use get_user::*; pub use get_user_by_external_id::*; pub use get_user_identities::*; pub use list_auth_factors::*; +pub use list_invitations::*; pub use list_users::*; pub use reset_password::*; +pub use revoke_invitation::*; +pub use send_invitation::*; pub use update_user::*; diff --git a/src/user_management/operations/accept_invitation.rs b/src/user_management/operations/accept_invitation.rs new file mode 100644 index 0000000..99506bf --- /dev/null +++ b/src/user_management/operations/accept_invitation.rs @@ -0,0 +1,128 @@ +use async_trait::async_trait; +use thiserror::Error; + +use crate::user_management::{Invitation, InvitationId, UserManagement}; +use crate::{ResponseExt, WorkOsError, WorkOsResult}; + +/// An error returned from [`AcceptInvitation`]. +#[derive(Debug, Error)] +pub enum AcceptInvitationError {} + +impl From for WorkOsError { + fn from(err: AcceptInvitationError) -> Self { + Self::Operation(err) + } +} + +/// [WorkOS Docs: Accept an invitation](https://workos.com/docs/reference/user-management/invitation/accept) +#[async_trait] +pub trait AcceptInvitation { + /// Accepts an invitation and, if linked to an organization, activates the user's membership in that organization. + /// + /// [WorkOS Docs: Accept an invitation](https://workos.com/docs/reference/user-management/invitation/accept) + /// + /// # Examples + /// + /// ``` + /// # use workos_sdk::WorkOsResult; + /// # use workos_sdk::user_management::*; + /// use workos_sdk::{ApiKey, WorkOs}; + /// + /// # async fn run() -> WorkOsResult<(), AcceptInvitationError> { + /// let workos = WorkOs::new(&ApiKey::from("sk_example_123456789")); + /// + /// let invitation = workos + /// .user_management() + /// .accept_invitation(&InvitationId::from("invitation_01E4ZCR3C56J083X43JQXF3JK5")) + /// .await?; + /// # Ok(()) + /// # } + /// ``` + async fn accept_invitation( + &self, + invitation_id: &InvitationId, + ) -> WorkOsResult; +} + +#[async_trait] +impl AcceptInvitation for UserManagement<'_> { + async fn accept_invitation( + &self, + invitation_id: &InvitationId, + ) -> WorkOsResult { + let url = self.workos.base_url().join(&format!( + "/user_management/invitations/{invitation_id}/accept" + ))?; + let user = self + .workos + .client() + .post(url) + .bearer_auth(self.workos.key()) + .send() + .await? + .handle_unauthorized_or_generic_error()? + .json::() + .await?; + + Ok(user) + } +} + +#[cfg(test)] +mod test { + use serde_json::json; + use tokio; + + use crate::user_management::InvitationId; + use crate::{ApiKey, WorkOs}; + + use super::*; + + #[tokio::test] + async fn it_calls_the_accept_invitation_endpoint() { + let mut server = mockito::Server::new_async().await; + + let workos = WorkOs::builder(&ApiKey::from("sk_example_123456789")) + .base_url(&server.url()) + .unwrap() + .build(); + + server + .mock("POST", "/user_management/invitations/invitation_01E4ZCR3C56J083X43JQXF3JK5/accept") + .match_header("Authorization", "Bearer sk_example_123456789") + .with_status(200) + .with_body( + json!({ + "object": "invitation", + "id": "invitation_01E4ZCR3C56J083X43JQXF3JK5", + "email": "marcelina.davis@example.com", + "state": "accepted", + "accepted_at": "2021-06-27T19:07:33.155Z", + "revoked_at": null, + "expires_at": "2021-07-01T19:07:33.155Z", + "token": "Z1uX3RbwcIl5fIGJJJCXXisdI", + "accept_invitation_url": "https://your-app.com/invite?invitation_token=Z1uX3RbwcIl5fIGJJJCXXisdI", + "organization_id": "org_01E4ZCR3C56J083X43JQXF3JK5", + "inviter_user_id": "user_01HYGBX8ZGD19949T3BM4FW1C3", + "accepted_user_id": "user_01JBJDMMV04RSWPG30MQE8ADFV", + "created_at": "2021-06-25T19:07:33.155Z", + "updated_at": "2021-06-25T19:07:33.155Z" + }) + .to_string(), + ) + .create_async() + .await; + + let invitation = workos + .user_management() + .accept_invitation(&InvitationId::from("invitation_01E4ZCR3C56J083X43JQXF3JK5")) + .await + .unwrap(); + + assert_eq!( + invitation.id, + InvitationId::from("invitation_01E4ZCR3C56J083X43JQXF3JK5") + ); + assert!(invitation.accepted_at.is_some()); + } +} diff --git a/src/user_management/operations/create_magic_auth.rs b/src/user_management/operations/create_magic_auth.rs index 5cce238..e800fb7 100644 --- a/src/user_management/operations/create_magic_auth.rs +++ b/src/user_management/operations/create_magic_auth.rs @@ -35,8 +35,6 @@ pub trait CreateMagicAuth { /// # Examples /// /// ``` - /// use std::collections::HashSet; - /// /// # use workos_sdk::WorkOsResult; /// # use workos_sdk::user_management::*; /// use workos_sdk::{ApiKey, WorkOs}; diff --git a/src/user_management/operations/create_password_reset.rs b/src/user_management/operations/create_password_reset.rs index 82c9990..44d9942 100644 --- a/src/user_management/operations/create_password_reset.rs +++ b/src/user_management/operations/create_password_reset.rs @@ -73,8 +73,6 @@ pub trait CreatePasswordReset { /// # Examples /// /// ``` - /// use std::collections::HashSet; - /// /// # use workos_sdk::WorkOsResult; /// # use workos_sdk::user_management::*; /// use workos_sdk::{ApiKey, WorkOs}; diff --git a/src/user_management/operations/create_user.rs b/src/user_management/operations/create_user.rs index c2195c1..cb84669 100644 --- a/src/user_management/operations/create_user.rs +++ b/src/user_management/operations/create_user.rs @@ -51,8 +51,6 @@ pub trait CreateUser { /// # Examples /// /// ``` - /// use std::collections::HashSet; - /// /// # use workos_sdk::WorkOsResult; /// # use workos_sdk::user_management::*; /// use workos_sdk::{ApiKey, WorkOs}; diff --git a/src/user_management/operations/enroll_auth_factor.rs b/src/user_management/operations/enroll_auth_factor.rs index 1c5e9bb..2352bdd 100644 --- a/src/user_management/operations/enroll_auth_factor.rs +++ b/src/user_management/operations/enroll_auth_factor.rs @@ -107,8 +107,6 @@ pub trait EnrollAuthFactor { /// # Examples /// /// ``` - /// use std::collections::HashSet; - /// /// # use workos_sdk::WorkOsResult; /// # use workos_sdk::user_management::*; /// use workos_sdk::{ApiKey, WorkOs}; diff --git a/src/user_management/operations/find_invitation_by_token.rs b/src/user_management/operations/find_invitation_by_token.rs new file mode 100644 index 0000000..4813b18 --- /dev/null +++ b/src/user_management/operations/find_invitation_by_token.rs @@ -0,0 +1,129 @@ +use async_trait::async_trait; +use thiserror::Error; + +use crate::user_management::{Invitation, InvitationToken, UserManagement}; +use crate::{ResponseExt, WorkOsError, WorkOsResult}; + +/// An error returned from [`FindInvitationByToken`]. +#[derive(Debug, Error)] +pub enum FindInvitationByTokenError {} + +impl From for WorkOsError { + fn from(err: FindInvitationByTokenError) -> Self { + Self::Operation(err) + } +} + +/// [WorkOS Docs: Find an invitation by token](https://workos.com/docs/reference/user-management/invitation/find-by-token) +#[async_trait] +pub trait FindInvitationByToken { + /// Retrieve an existing invitation using the token. + /// + /// [WorkOS Docs: Find an invitation by token](https://workos.com/docs/reference/user-management/invitation/find-by-token) + /// + /// # Examples + /// + /// ``` + /// # use workos_sdk::WorkOsResult; + /// # use workos_sdk::user_management::*; + /// use workos_sdk::{ApiKey, WorkOs}; + /// + /// # async fn run() -> WorkOsResult<(), FindInvitationByTokenError> { + /// let workos = WorkOs::new(&ApiKey::from("sk_example_123456789")); + /// + /// let invitation = workos + /// .user_management() + /// .find_invitation_by_token(&InvitationToken::from("Z1uX3RbwcIl5fIGJJJCXXisdI")) + /// .await?; + /// # Ok(()) + /// # } + /// ``` + async fn find_invitation_by_token( + &self, + token: &InvitationToken, + ) -> WorkOsResult; +} + +#[async_trait] +impl FindInvitationByToken for UserManagement<'_> { + async fn find_invitation_by_token( + &self, + token: &InvitationToken, + ) -> WorkOsResult { + let url = self + .workos + .base_url() + .join(&format!("/user_management/invitations/by_token/{token}"))?; + let organization = self + .workos + .client() + .get(url) + .bearer_auth(self.workos.key()) + .send() + .await? + .handle_unauthorized_or_generic_error()? + .json::() + .await?; + + Ok(organization) + } +} + +#[cfg(test)] +mod test { + use serde_json::json; + use tokio; + + use crate::{ApiKey, WorkOs, user_management::InvitationId}; + + use super::*; + + #[tokio::test] + async fn it_calls_the_find_invitation_by_token_endpoint() { + let mut server = mockito::Server::new_async().await; + + let workos = WorkOs::builder(&ApiKey::from("sk_example_123456789")) + .base_url(&server.url()) + .unwrap() + .build(); + + server + .mock( + "GET", + "/user_management/invitations/by_token/Z1uX3RbwcIl5fIGJJJCXXisdI", + ) + .match_header("Authorization", "Bearer sk_example_123456789") + .with_status(200) + .with_body( + json!({ + "object": "invitation", + "id": "invitation_01E4ZCR3C56J083X43JQXF3JK5", + "email": "marcelina.davis@example.com", + "state": "pending", + "accepted_at": null, + "revoked_at": null, + "expires_at": "2021-07-01T19:07:33.155Z", + "token": "Z1uX3RbwcIl5fIGJJJCXXisdI", + "accept_invitation_url": "https://your-app.com/invite?invitation_token=Z1uX3RbwcIl5fIGJJJCXXisdI", + "organization_id": "org_01E4ZCR3C56J083X43JQXF3JK5", + "inviter_user_id": "user_01HYGBX8ZGD19949T3BM4FW1C3", + "created_at": "2021-06-25T19:07:33.155Z", + "updated_at": "2021-06-25T19:07:33.155Z" + }) + .to_string(), + ) + .create_async() + .await; + + let invitation = workos + .user_management() + .find_invitation_by_token(&InvitationToken::from("Z1uX3RbwcIl5fIGJJJCXXisdI")) + .await + .unwrap(); + + assert_eq!( + invitation.id, + InvitationId::from("invitation_01E4ZCR3C56J083X43JQXF3JK5") + ) + } +} diff --git a/src/user_management/operations/get_invitation.rs b/src/user_management/operations/get_invitation.rs new file mode 100644 index 0000000..2411075 --- /dev/null +++ b/src/user_management/operations/get_invitation.rs @@ -0,0 +1,129 @@ +use async_trait::async_trait; +use thiserror::Error; + +use crate::user_management::{Invitation, InvitationId, UserManagement}; +use crate::{ResponseExt, WorkOsError, WorkOsResult}; + +/// An error returned from [`GetInvitation`]. +#[derive(Debug, Error)] +pub enum GetInvitationError {} + +impl From for WorkOsError { + fn from(err: GetInvitationError) -> Self { + Self::Operation(err) + } +} + +/// [WorkOS Docs: Get an invitation](https://workos.com/docs/reference/user-management/invitation/get) +#[async_trait] +pub trait GetInvitation { + /// Get the details of an existing invitation. + /// + /// [WorkOS Docs: Get an invitation](https://workos.com/docs/reference/user-management/invitation/get) + /// + /// # Examples + /// + /// ``` + /// # use workos_sdk::WorkOsResult; + /// # use workos_sdk::user_management::*; + /// use workos_sdk::{ApiKey, WorkOs}; + /// + /// # async fn run() -> WorkOsResult<(), GetInvitationError> { + /// let workos = WorkOs::new(&ApiKey::from("sk_example_123456789")); + /// + /// let invitation = workos + /// .user_management() + /// .get_invitation(&InvitationId::from("invitation_01E4ZCR3C56J083X43JQXF3JK5")) + /// .await?; + /// # Ok(()) + /// # } + /// ``` + async fn get_invitation( + &self, + id: &InvitationId, + ) -> WorkOsResult; +} + +#[async_trait] +impl GetInvitation for UserManagement<'_> { + async fn get_invitation( + &self, + id: &InvitationId, + ) -> WorkOsResult { + let url = self + .workos + .base_url() + .join(&format!("/user_management/invitations/{id}"))?; + let organization = self + .workos + .client() + .get(url) + .bearer_auth(self.workos.key()) + .send() + .await? + .handle_unauthorized_or_generic_error()? + .json::() + .await?; + + Ok(organization) + } +} + +#[cfg(test)] +mod test { + use serde_json::json; + use tokio; + + use crate::{ApiKey, WorkOs}; + + use super::*; + + #[tokio::test] + async fn it_calls_the_get_invitation_endpoint() { + let mut server = mockito::Server::new_async().await; + + let workos = WorkOs::builder(&ApiKey::from("sk_example_123456789")) + .base_url(&server.url()) + .unwrap() + .build(); + + server + .mock( + "GET", + "/user_management/invitations/invitation_01E4ZCR3C56J083X43JQXF3JK5", + ) + .match_header("Authorization", "Bearer sk_example_123456789") + .with_status(200) + .with_body( + json!({ + "object": "invitation", + "id": "invitation_01E4ZCR3C56J083X43JQXF3JK5", + "email": "marcelina.davis@example.com", + "state": "pending", + "accepted_at": null, + "revoked_at": null, + "expires_at": "2021-07-01T19:07:33.155Z", + "token": "Z1uX3RbwcIl5fIGJJJCXXisdI", + "accept_invitation_url": "https://your-app.com/invite?invitation_token=Z1uX3RbwcIl5fIGJJJCXXisdI", + "organization_id": "org_01E4ZCR3C56J083X43JQXF3JK5", + "inviter_user_id": "user_01HYGBX8ZGD19949T3BM4FW1C3", + "created_at": "2021-06-25T19:07:33.155Z", + "updated_at": "2021-06-25T19:07:33.155Z" + }) + .to_string(), + ) + .create_async() + .await; + + let invitation = workos + .user_management() + .get_invitation(&InvitationId::from("invitation_01E4ZCR3C56J083X43JQXF3JK5")) + .await + .unwrap(); + + assert_eq!( + invitation.id, + InvitationId::from("invitation_01E4ZCR3C56J083X43JQXF3JK5") + ) + } +} diff --git a/src/user_management/operations/get_magic_auth.rs b/src/user_management/operations/get_magic_auth.rs index 1edca6c..206683a 100644 --- a/src/user_management/operations/get_magic_auth.rs +++ b/src/user_management/operations/get_magic_auth.rs @@ -103,14 +103,14 @@ mod test { .create_async() .await; - let organization = workos + let magic_auth = workos .user_management() .get_magic_auth(&MagicAuthId::from("magic_auth_01E4ZCR3C56J083X43JQXF3JK5")) .await .unwrap(); assert_eq!( - organization.id, + magic_auth.id, MagicAuthId::from("magic_auth_01E4ZCR3C56J083X43JQXF3JK5") ) } diff --git a/src/user_management/operations/list_invitations.rs b/src/user_management/operations/list_invitations.rs new file mode 100644 index 0000000..69fc25b --- /dev/null +++ b/src/user_management/operations/list_invitations.rs @@ -0,0 +1,288 @@ +use async_trait::async_trait; +use serde::Serialize; +use thiserror::Error; + +use crate::organizations::OrganizationId; +use crate::user_management::{Invitation, UserManagement}; +use crate::{PaginatedList, PaginationParams, ResponseExt, WorkOsError, WorkOsResult}; + +/// The parameters for the [`ListInvitations`] function. +#[derive(Debug, Serialize, Default)] +pub struct ListInvitationsParams<'a> { + /// The email address of the recipient. + pub email: Option<&'a str>, + + /// The ID of the organization that the recipient will join. + pub organization_id: Option<&'a OrganizationId>, + + /// The pagination parameters to use when listing invitations. + #[serde(flatten)] + pub pagination: PaginationParams<'a>, +} + +/// An error returned from [`ListInvitations`]. +#[derive(Debug, Error)] +pub enum ListInvitationsError {} + +impl From for WorkOsError { + fn from(err: ListInvitationsError) -> Self { + Self::Operation(err) + } +} + +/// [WorkOS Docs: List invitations](https://workos.com/docs/reference/user-management/invitation/list) +#[async_trait] +pub trait ListInvitations { + /// Get a list of all the invitations matching the criteria specified. + /// + /// [WorkOS Docs: List invitations](https://workos.com/docs/reference/user-management/invitation/list) + /// + /// # Examples + /// + /// ``` + /// # use workos_sdk::WorkOsResult; + /// # use workos_sdk::user_management::*; + /// use workos_sdk::{ApiKey, WorkOs}; + /// use workos_sdk::organizations::OrganizationId; + /// + /// # async fn run() -> WorkOsResult<(), ListInvitationsError> { + /// let workos = WorkOs::new(&ApiKey::from("sk_example_123456789")); + /// + /// let invitations = workos + /// .user_management() + /// .list_invitations(&ListInvitationsParams { + /// email: Some("marcelina.davis@example.com"), + /// organization_id: Some(&OrganizationId::from("org_01E4ZCR3C56J083X43JQXF3JK5")), + /// ..Default::default() + /// }) + /// .await?; + /// # Ok(()) + /// # } + /// ``` + async fn list_invitations( + &self, + params: &ListInvitationsParams, + ) -> WorkOsResult, ListInvitationsError>; +} + +#[async_trait] +impl ListInvitations for UserManagement<'_> { + async fn list_invitations( + &self, + params: &ListInvitationsParams, + ) -> WorkOsResult, ListInvitationsError> { + let url = self + .workos + .base_url() + .join("/user_management/invitations")?; + + let invitations = self + .workos + .client() + .get(url) + .query(¶ms) + .bearer_auth(self.workos.key()) + .send() + .await? + .handle_unauthorized_or_generic_error()? + .json::>() + .await?; + + Ok(invitations) + } +} + +#[cfg(test)] +mod test { + use mockito::Matcher; + use serde_json::json; + use tokio; + + use crate::user_management::InvitationId; + use crate::{ApiKey, WorkOs}; + + use super::*; + + #[tokio::test] + async fn it_calls_the_get_list_invitations_endpoint() { + let mut server = mockito::Server::new_async().await; + + let workos = WorkOs::builder(&ApiKey::from("sk_example_123456789")) + .base_url(&server.url()) + .unwrap() + .build(); + + server + .mock("GET", "/user_management/invitations") + .match_query(Matcher::UrlEncoded("order".to_string(), "desc".to_string())) + .match_header("Authorization", "Bearer sk_example_123456789") + .with_status(200) + .with_body( + json!({ + "data": [ + { + "object": "invitation", + "id": "invitation_01E4ZCR3C56J083X43JQXF3JK5", + "email": "marcelina.davis@example.com", + "state": "pending", + "accepted_at": null, + "revoked_at": null, + "expires_at": "2021-07-01T19:07:33.155Z", + "token": "Z1uX3RbwcIl5fIGJJJCXXisdI", + "accept_invitation_url": "https://your-app.com/invite?invitation_token=Z1uX3RbwcIl5fIGJJJCXXisdI", + "organization_id": "org_01E4ZCR3C56J083X43JQXF3JK5", + "inviter_user_id": "user_01HYGBX8ZGD19949T3BM4FW1C3", + "created_at": "2021-06-25T19:07:33.155Z", + "updated_at": "2021-06-25T19:07:33.155Z" + } + ], + "list_metadata": { + "before": "invitation_01E4ZCR3C56J083X43JQXF3JK5", + "after": "invitation_01EJBGJT2PC6638TN5Y380M40Z" + } + }) + .to_string(), + ) + .create_async() + .await; + + let paginated_list = workos + .user_management() + .list_invitations(&Default::default()) + .await + .unwrap(); + + assert_eq!( + paginated_list.metadata.after, + Some("invitation_01EJBGJT2PC6638TN5Y380M40Z".to_string()) + ) + } + + #[tokio::test] + async fn it_calls_the_list_invitations_endpoint_with_an_email() { + let mut server = mockito::Server::new_async().await; + + let workos = WorkOs::builder(&ApiKey::from("sk_example_123456789")) + .base_url(&server.url()) + .unwrap() + .build(); + + server + .mock("GET", "/user_management/invitations") + .match_query(Matcher::AllOf(vec![ + Matcher::UrlEncoded("order".to_string(), "desc".to_string()), + Matcher::UrlEncoded( + "email".to_string(), + "marcelina.davis@example.com".to_string(), + ), + ])) + .match_header("Authorization", "Bearer sk_example_123456789") + .with_status(200) + .with_body( + json!({ + "data": [ + { + "object": "invitation", + "id": "invitation_01E4ZCR3C56J083X43JQXF3JK5", + "email": "marcelina.davis@example.com", + "state": "pending", + "accepted_at": null, + "revoked_at": null, + "expires_at": "2021-07-01T19:07:33.155Z", + "token": "Z1uX3RbwcIl5fIGJJJCXXisdI", + "accept_invitation_url": "https://your-app.com/invite?invitation_token=Z1uX3RbwcIl5fIGJJJCXXisdI", + "organization_id": "org_01E4ZCR3C56J083X43JQXF3JK5", + "inviter_user_id": "user_01HYGBX8ZGD19949T3BM4FW1C3", + "created_at": "2021-06-25T19:07:33.155Z", + "updated_at": "2021-06-25T19:07:33.155Z" + } + ], + "list_metadata": { + "before": "invitation_01E4ZCR3C56J083X43JQXF3JK5", + "after": "invitation_01EJBGJT2PC6638TN5Y380M40Z" + } + }) + .to_string(), + ) + .create_async() + .await; + + let paginated_list = workos + .user_management() + .list_invitations(&ListInvitationsParams { + email: Some("marcelina.davis@example.com"), + ..Default::default() + }) + .await + .unwrap(); + + assert_eq!( + paginated_list.data.into_iter().next().map(|user| user.id), + Some(InvitationId::from("invitation_01E4ZCR3C56J083X43JQXF3JK5")) + ) + } + + #[tokio::test] + async fn it_calls_the_list_invitations_endpoint_with_an_organization_id() { + let mut server = mockito::Server::new_async().await; + + let workos = WorkOs::builder(&ApiKey::from("sk_example_123456789")) + .base_url(&server.url()) + .unwrap() + .build(); + + server + .mock("GET", "/user_management/invitations") + .match_query(Matcher::AllOf(vec![ + Matcher::UrlEncoded("order".to_string(), "desc".to_string()), + Matcher::UrlEncoded( + "organization_id".to_string(), + "org_01E4ZCR3C56J083X43JQXF3JK5".to_string(), + ), + ])) + .match_header("Authorization", "Bearer sk_example_123456789") + .with_status(200) + .with_body( + json!({ + "data": [ + { + "object": "invitation", + "id": "invitation_01E4ZCR3C56J083X43JQXF3JK5", + "email": "marcelina.davis@example.com", + "state": "pending", + "accepted_at": null, + "revoked_at": null, + "expires_at": "2021-07-01T19:07:33.155Z", + "token": "Z1uX3RbwcIl5fIGJJJCXXisdI", + "accept_invitation_url": "https://your-app.com/invite?invitation_token=Z1uX3RbwcIl5fIGJJJCXXisdI", + "organization_id": "org_01E4ZCR3C56J083X43JQXF3JK5", + "inviter_user_id": "user_01HYGBX8ZGD19949T3BM4FW1C3", + "created_at": "2021-06-25T19:07:33.155Z", + "updated_at": "2021-06-25T19:07:33.155Z" + } + ], + "list_metadata": { + "before": "invitation_01E4ZCR3C56J083X43JQXF3JK5", + "after": "invitation_01EJBGJT2PC6638TN5Y380M40Z" + } + }) + .to_string(), + ) + .create_async() + .await; + + let paginated_list = workos + .user_management() + .list_invitations(&ListInvitationsParams { + organization_id: Some(&OrganizationId::from("org_01E4ZCR3C56J083X43JQXF3JK5")), + ..Default::default() + }) + .await + .unwrap(); + + assert_eq!( + paginated_list.data.into_iter().next().map(|user| user.id), + Some(InvitationId::from("invitation_01E4ZCR3C56J083X43JQXF3JK5")) + ) + } +} diff --git a/src/user_management/operations/reset_password.rs b/src/user_management/operations/reset_password.rs index c932510..80006d6 100644 --- a/src/user_management/operations/reset_password.rs +++ b/src/user_management/operations/reset_password.rs @@ -111,8 +111,6 @@ pub trait ResetPassword { /// # Examples /// /// ``` - /// use std::collections::HashSet; - /// /// # use workos_sdk::WorkOsResult; /// # use workos_sdk::user_management::*; /// use workos_sdk::{ApiKey, WorkOs}; diff --git a/src/user_management/operations/revoke_invitation.rs b/src/user_management/operations/revoke_invitation.rs new file mode 100644 index 0000000..c01e14d --- /dev/null +++ b/src/user_management/operations/revoke_invitation.rs @@ -0,0 +1,128 @@ +use async_trait::async_trait; +use thiserror::Error; + +use crate::user_management::{Invitation, InvitationId, UserManagement}; +use crate::{ResponseExt, WorkOsError, WorkOsResult}; + +/// An error returned from [`RevokeInvitation`]. +#[derive(Debug, Error)] +pub enum RevokeInvitationError {} + +impl From for WorkOsError { + fn from(err: RevokeInvitationError) -> Self { + Self::Operation(err) + } +} + +/// [WorkOS Docs: Revoke an invitation](https://workos.com/docs/reference/user-management/invitation/revoke) +#[async_trait] +pub trait RevokeInvitation { + /// Revokes an existing invitation. + /// + /// [WorkOS Docs: Revoke an invitation](https://workos.com/docs/reference/user-management/invitation/revoke) + /// + /// # Examples + /// + /// ``` + /// # use workos_sdk::WorkOsResult; + /// # use workos_sdk::user_management::*; + /// use workos_sdk::{ApiKey, WorkOs}; + /// + /// # async fn run() -> WorkOsResult<(), RevokeInvitationError> { + /// let workos = WorkOs::new(&ApiKey::from("sk_example_123456789")); + /// + /// let invitation = workos + /// .user_management() + /// .revoke_invitation(&InvitationId::from("invitation_01E4ZCR3C56J083X43JQXF3JK5")) + /// .await?; + /// # Ok(()) + /// # } + /// ``` + async fn revoke_invitation( + &self, + invitation_id: &InvitationId, + ) -> WorkOsResult; +} + +#[async_trait] +impl RevokeInvitation for UserManagement<'_> { + async fn revoke_invitation( + &self, + invitation_id: &InvitationId, + ) -> WorkOsResult { + let url = self.workos.base_url().join(&format!( + "/user_management/invitations/{invitation_id}/revoke" + ))?; + let user = self + .workos + .client() + .post(url) + .bearer_auth(self.workos.key()) + .send() + .await? + .handle_unauthorized_or_generic_error()? + .json::() + .await?; + + Ok(user) + } +} + +#[cfg(test)] +mod test { + use serde_json::json; + use tokio; + + use crate::user_management::InvitationId; + use crate::{ApiKey, WorkOs}; + + use super::*; + + #[tokio::test] + async fn it_calls_the_revoke_invitation_endpoint() { + let mut server = mockito::Server::new_async().await; + + let workos = WorkOs::builder(&ApiKey::from("sk_example_123456789")) + .base_url(&server.url()) + .unwrap() + .build(); + + server + .mock("POST", "/user_management/invitations/invitation_01E4ZCR3C56J083X43JQXF3JK5/revoke") + .match_header("Authorization", "Bearer sk_example_123456789") + .with_status(200) + .with_body( + json!({ + "object": "invitation", + "id": "invitation_01E4ZCR3C56J083X43JQXF3JK5", + "email": "marcelina.davis@example.com", + "state": "revoked", + "accepted_at": null, + "revoked_at": "2021-07-01T19:07:33.155Z", + "expires_at": "2021-07-01T19:07:33.155Z", + "token": "Z1uX3RbwcIl5fIGJJJCXXisdI", + "accept_invitation_url": "https://your-app.com/invite?invitation_token=Z1uX3RbwcIl5fIGJJJCXXisdI", + "organization_id": "org_01E4ZCR3C56J083X43JQXF3JK5", + "inviter_user_id": "user_01HYGBX8ZGD19949T3BM4FW1C3", + "accepted_user_id": null, + "created_at": "2021-06-25T19:07:33.155Z", + "updated_at": "2021-06-25T19:07:33.155Z" + }) + .to_string(), + ) + .create_async() + .await; + + let invitation = workos + .user_management() + .revoke_invitation(&InvitationId::from("invitation_01E4ZCR3C56J083X43JQXF3JK5")) + .await + .unwrap(); + + assert_eq!( + invitation.id, + InvitationId::from("invitation_01E4ZCR3C56J083X43JQXF3JK5") + ); + assert!(invitation.revoked_at.is_some()); + } +} diff --git a/src/user_management/operations/send_invitation.rs b/src/user_management/operations/send_invitation.rs new file mode 100644 index 0000000..94ee57a --- /dev/null +++ b/src/user_management/operations/send_invitation.rs @@ -0,0 +1,163 @@ +use async_trait::async_trait; +use serde::Serialize; +use thiserror::Error; + +use crate::organizations::OrganizationId; +use crate::user_management::{Invitation, UserId, UserManagement}; +use crate::{ResponseExt, WorkOsError, WorkOsResult}; + +/// The parameters for [`SendInvitation`]. +#[derive(Debug, Serialize)] +pub struct SendInvitationParams<'a> { + /// The email address of the recipient. + pub email: &'a str, + + /// The ID of the organization that the recipient will join. + pub organization_id: Option<&'a OrganizationId>, + + /// How many days the invitations will be valid for. + pub expires_in_days: Option, + + /// The ID of the user who invites the recipient. + /// + /// The invitation email will mention the name of this user. + pub inviter_user_id: Option<&'a UserId>, + + /// The role that the recipient will receive when they join the organization in the invitation. + pub role_slug: Option<&'a str>, +} + +/// An error returned from [`SendInvitation`]. +#[derive(Debug, Error)] +pub enum SendInvitationError {} + +impl From for WorkOsError { + fn from(err: SendInvitationError) -> Self { + Self::Operation(err) + } +} + +/// [WorkOS Docs: Send an invitation](https://workos.com/docs/reference/user-management/invitation/send) +#[async_trait] +pub trait SendInvitation { + /// Sends an invitation email to the recipient. + /// + /// [WorkOS Docs: Send an invitation](https://workos.com/docs/reference/user-management/invitation/send) + /// + /// # Examples + /// + /// ``` + /// # use workos_sdk::WorkOsResult; + /// # use workos_sdk::user_management::*; + /// use workos_sdk::{ApiKey, WorkOs}; + /// + /// # async fn run() -> WorkOsResult<(), SendInvitationError> { + /// let workos = WorkOs::new(&ApiKey::from("sk_example_123456789")); + /// + /// let invitation = workos + /// .user_management() + /// .send_invitation(&SendInvitationParams { + /// email: "marcelina@example.com", + /// organization_id: None, + /// expires_in_days: None, + /// inviter_user_id: None, + /// role_slug: None, + /// }) + /// .await?; + /// # Ok(()) + /// # } + /// ``` + async fn send_invitation( + &self, + params: &SendInvitationParams<'_>, + ) -> WorkOsResult; +} + +#[async_trait] +impl SendInvitation for UserManagement<'_> { + async fn send_invitation( + &self, + params: &SendInvitationParams<'_>, + ) -> WorkOsResult { + let url = self + .workos + .base_url() + .join("/user_management/invitations")?; + let user = self + .workos + .client() + .post(url) + .bearer_auth(self.workos.key()) + .json(¶ms) + .send() + .await? + .handle_unauthorized_or_generic_error()? + .json::() + .await?; + + Ok(user) + } +} + +#[cfg(test)] +mod test { + use serde_json::json; + use tokio; + + use crate::user_management::InvitationId; + use crate::{ApiKey, WorkOs}; + + use super::*; + + #[tokio::test] + async fn it_calls_the_send_invitation_endpoint() { + let mut server = mockito::Server::new_async().await; + + let workos = WorkOs::builder(&ApiKey::from("sk_example_123456789")) + .base_url(&server.url()) + .unwrap() + .build(); + + server + .mock("POST", "/user_management/invitations") + .match_header("Authorization", "Bearer sk_example_123456789") + .with_status(201) + .with_body( + json!({ + "object": "invitation", + "id": "invitation_01E4ZCR3C56J083X43JQXF3JK5", + "email": "marcelina.davis@example.com", + "state": "pending", + "accepted_at": null, + "revoked_at": null, + "expires_at": "2021-07-01T19:07:33.155Z", + "token": "Z1uX3RbwcIl5fIGJJJCXXisdI", + "accept_invitation_url": "https://your-app.com/invite?invitation_token=Z1uX3RbwcIl5fIGJJJCXXisdI", + "organization_id": "org_01E4ZCR3C56J083X43JQXF3JK5", + "inviter_user_id": "user_01HYGBX8ZGD19949T3BM4FW1C3", + "created_at": "2021-06-25T19:07:33.155Z", + "updated_at": "2021-06-25T19:07:33.155Z" + }) + .to_string(), + ) + .create_async() + .await; + + let invitation = workos + .user_management() + .send_invitation(&SendInvitationParams { + email: "marcelina@example.com", + organization_id: None, + expires_in_days: None, + inviter_user_id: None, + role_slug: None, + }) + .await + .unwrap(); + + assert_eq!( + invitation.id, + InvitationId::from("invitation_01E4ZCR3C56J083X43JQXF3JK5") + ) + } +} diff --git a/src/user_management/types.rs b/src/user_management/types.rs index a8d6c92..323663b 100644 --- a/src/user_management/types.rs +++ b/src/user_management/types.rs @@ -4,6 +4,7 @@ mod authentication_response; mod email_verification; mod identity; mod impersonator; +mod invitation; mod magic_auth; mod password; mod password_reset; @@ -19,6 +20,7 @@ pub use authentication_response::*; pub use email_verification::*; pub use identity::*; pub use impersonator::*; +pub use invitation::*; pub use magic_auth::*; pub use password::*; pub use password_reset::*; diff --git a/src/user_management/types/invitation.rs b/src/user_management/types/invitation.rs new file mode 100644 index 0000000..94a5d35 --- /dev/null +++ b/src/user_management/types/invitation.rs @@ -0,0 +1,76 @@ +use derive_more::{Deref, Display, From}; +use serde::{Deserialize, Serialize}; +use url::Url; + +use crate::organizations::OrganizationId; +use crate::user_management::UserId; +use crate::{KnownOrUnknown, Timestamp, Timestamps}; + +/// The ID of an [`Invitation`]. +#[derive( + Clone, Debug, Deref, Display, From, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, +)] +#[from(forward)] +pub struct InvitationId(String); + +/// The state of an [`Invitation`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum InvitationState { + /// The invitation is pending. + Pending, + + /// The invitation is accepted. + Accepted, + + /// The invitation is expired. + Expired, + + /// The invitation is revoked. + Revoked, +} + +/// The token of an [`Invitation`]. +#[derive( + Clone, Debug, Deref, Display, From, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, +)] +#[from(forward)] +pub struct InvitationToken(String); + +/// [WorkOS Docs: Invitation](https://workos.com/docs/reference/user-management/invitation) +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Invitation { + /// The unique ID of the invitation. + pub id: InvitationId, + + /// The email address of the user. + pub email: String, + + /// The state of the invitation. + pub state: KnownOrUnknown, + + /// The timestamp indicating when the invitation was accepted. + pub accepted_at: Option, + + /// The timestamp indicating when the invitation was revoked. + pub revoked_at: Option, + + /// The timestamp indicating when the invitation expires. + pub expires_at: Timestamp, + + /// The token for the invitation. + pub token: InvitationToken, + + /// The URL used to accept the invitation. + pub accept_invitation_url: Url, + + /// The ID of the organization that the recipient will join. + pub organization_id: OrganizationId, + + /// The ID of the user who invited the recipient. + pub inviter_user_id: UserId, + + /// The timestamps for the invitation. + #[serde(flatten)] + pub timestamps: Timestamps, +}