From dc29a359e7f87ccb3158ec9ecada4369ddf57e7f Mon Sep 17 00:00:00 2001 From: Amir Khan Date: Wed, 16 Jul 2025 16:59:28 +0200 Subject: [PATCH 1/2] feat(user-management): add invitation --- src/user_management/operations.rs | 12 ++ .../operations/accept_invitation.rs | 132 ++++++++++++++ .../operations/get_invitation.rs | 131 ++++++++++++++ .../operations/get_invitation_by_token.rs | 131 ++++++++++++++ .../operations/list_invitations.rs | 167 ++++++++++++++++++ .../operations/revoke_invitation.rs | 132 ++++++++++++++ .../operations/send_invitation.rs | 166 +++++++++++++++++ src/user_management/types.rs | 2 + src/user_management/types/invitation.rs | 60 +++++++ 9 files changed, 933 insertions(+) create mode 100644 src/user_management/operations/accept_invitation.rs create mode 100644 src/user_management/operations/get_invitation.rs create mode 100644 src/user_management/operations/get_invitation_by_token.rs create mode 100644 src/user_management/operations/list_invitations.rs create mode 100644 src/user_management/operations/revoke_invitation.rs create mode 100644 src/user_management/operations/send_invitation.rs create mode 100644 src/user_management/types/invitation.rs diff --git a/src/user_management/operations.rs b/src/user_management/operations.rs index 4e02ef2..fd21bcf 100644 --- a/src/user_management/operations.rs +++ b/src/user_management/operations.rs @@ -23,6 +23,12 @@ mod list_auth_factors; mod list_users; mod reset_password; mod update_user; +mod send_invitation; +mod get_invitation; +mod get_invitation_by_token; +mod accept_invitation; +mod revoke_invitation; +mod list_invitations; pub use authenticate_with_code::*; pub use authenticate_with_email_verification::*; @@ -49,3 +55,9 @@ pub use list_auth_factors::*; pub use list_users::*; pub use reset_password::*; pub use update_user::*; +pub use send_invitation::*; +pub use get_invitation::*; +pub use get_invitation_by_token::*; +pub use accept_invitation::*; +pub use revoke_invitation::*; +pub use list_invitations::*; diff --git a/src/user_management/operations/accept_invitation.rs b/src/user_management/operations/accept_invitation.rs new file mode 100644 index 0000000..2f9e053 --- /dev/null +++ b/src/user_management/operations/accept_invitation.rs @@ -0,0 +1,132 @@ +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 std::collections::HashSet; + /// + /// # use workos_sdk::WorkOsResult; + /// # use workos_sdk::user_management::*; + /// use workos_sdk::{ApiKey, WorkOs}; /// + /// # + /// use workos_sdk::organizations::OrganizationId; + /// + /// 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, + id: &InvitationId, + ) -> WorkOsResult; +} + +#[async_trait] +impl AcceptInvitation for UserManagement<'_> { + async fn accept_invitation( + &self, + id: &InvitationId, + ) -> WorkOsResult { + let url = self + .workos + .base_url() + .join(&format!("/user_management/invitations/{id}/accept"))?; + + let invitation = self + .workos + .client() + .post(url) + .bearer_auth(self.workos.key()) + .send() + .await? + .handle_unauthorized_or_generic_error()? + .json::() + .await?; + + Ok(invitation) + } +} + +#[cfg(test)] +mod test { + use serde_json::json; + use tokio; + + 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": "pending", + "accepted_at": "2021-07-01T19: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", + "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.email, String::from("marcelina.davis@example.com")); + assert!(invitation.accepted_at.is_some()); + } +} diff --git a/src/user_management/operations/get_invitation.rs b/src/user_management/operations/get_invitation.rs new file mode 100644 index 0000000..ac1ba34 --- /dev/null +++ b/src/user_management/operations/get_invitation.rs @@ -0,0 +1,131 @@ +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 std::collections::HashSet; + /// + /// # use workos_sdk::WorkOsResult; + /// # use workos_sdk::user_management::*; + /// use workos_sdk::{ApiKey, WorkOs}; /// + /// # + /// use workos_sdk::organizations::OrganizationId; + /// + /// 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 invitation = self + .workos + .client() + .get(url) + .bearer_auth(self.workos.key()) + .send() + .await? + .handle_unauthorized_or_generic_error()? + .json::() + .await?; + + Ok(invitation) + } +} + +#[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_123456789", + ) + .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_invitation_by_token.rs b/src/user_management/operations/get_invitation_by_token.rs new file mode 100644 index 0000000..e91a318 --- /dev/null +++ b/src/user_management/operations/get_invitation_by_token.rs @@ -0,0 +1,131 @@ +use async_trait::async_trait; +use thiserror::Error; + +use crate::user_management::{Invitation, InvitationToken, UserManagement}; +use crate::{ResponseExt, WorkOsError, WorkOsResult}; + +/// An error returned from [`GetInvitationByToken`]. +#[derive(Debug, Error)] +pub enum GetInvitationByTokenError {} + +impl From for WorkOsError { + fn from(err: GetInvitationByTokenError) -> 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 GetInvitationByToken { + /// 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 std::collections::HashSet; + /// + /// # use workos_sdk::WorkOsResult; + /// # use workos_sdk::user_management::*; + /// use workos_sdk::{ApiKey, WorkOs}; + /// # + /// use workos_sdk::organizations::OrganizationId; + /// + /// async fn run() -> WorkOsResult<(), GetInvitationByTokenError> { + /// let workos = WorkOs::new(&ApiKey::from("sk_example_123456789")); + /// + /// let invitation = workos + /// .user_management() + /// .get_invitation_by_token(&InvitationToken::from("Z1uX3RbwcIl5fIGJJJCXXisdI")) + /// .await?; + /// # Ok(()) + /// # } + /// ``` + async fn get_invitation_by_token( + &self, + token: &InvitationToken, + ) -> WorkOsResult; +} + +#[async_trait] +impl GetInvitationByToken for UserManagement<'_> { + async fn get_invitation_by_token( + &self, + token: &InvitationToken, + ) -> WorkOsResult { + let url = self + .workos + .base_url() + .join(&format!("user_management/invitations/by_token/{token}"))?; + + let invitation = self + .workos + .client() + .get(url) + .bearer_auth(self.workos.key()) + .send() + .await? + .handle_unauthorized_or_generic_error()? + .json::() + .await?; + + Ok(invitation) + } +} + +#[cfg(test)] +mod test { + use serde_json::json; + use tokio; + + use crate::{ApiKey, WorkOs}; + use crate::user_management::InvitationId; + use super::*; + + #[tokio::test] + async fn it_calls_the_get_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() + .get_invitation_by_token(&InvitationToken::from("Z1uX3RbwcIl5fIGJJJCXXisdI")) + .await + .unwrap(); + + assert_eq!(invitation.id, InvitationId::from("invitation_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..9078804 --- /dev/null +++ b/src/user_management/operations/list_invitations.rs @@ -0,0 +1,167 @@ +use async_trait::async_trait; +use serde::Serialize; +use thiserror::Error; + +use crate::user_management::{Invitation, UserManagement}; +use crate::{PaginatedList, PaginationParams, ResponseExt, WorkOsError, WorkOsResult}; +use crate::organizations::OrganizationId; + +/// The parameters for [`ListInvitations`]. +#[derive(Debug, Serialize, Default)] +pub struct ListInvitationParams<'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>, + + /// The pagination parameters to use when listing users. + #[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 std::collections::HashSet; + /// + /// # 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(&ListInvitationParams { + /// email: "marcelina.davis@example.com", + /// organization_id: Some(&OrganizationId::from("org_01E4ZCR3C56J083X43JQXF3JK5")), + /// ..Default::default() + /// }) + /// .await?; + /// # Ok(()) + /// # } + /// ``` + async fn list_invitations( + &self, + params: &ListInvitationParams, + ) -> WorkOsResult, ListInvitationsError>; +} + +#[async_trait] +impl ListInvitations for UserManagement<'_> { + async fn list_invitations( + &self, + params: &ListInvitationParams, + ) -> WorkOsResult, ListInvitationsError> { + let url = self + .workos + .base_url() + .join("user_management/invitations")?; + + let invitations = self + .workos + .client() + .get(url) + .bearer_auth(self.workos.key()) + .query(¶ms) + .send() + .await? + .handle_unauthorized_or_generic_error()? + .json::>() + .await?; + + Ok(invitations) + } +} + +#[cfg(test)] +mod test { + use serde_json::json; + use tokio; + + use crate::{ApiKey, WorkOs}; + use crate::user_management::{InvitationId, UserId}; + 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/invitation_123456789", + ) + .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_invitations = workos + .user_management() + .list_invitations(&ListInvitationParams { + email: "marcelina.davis@example.com", + organization_id: Some(&OrganizationId::from("org_01E4ZCR3C56J083X43JQXF3JK5")), + ..Default::default() + }) + .await + .unwrap(); + + assert_eq!( + paginated_invitations.data.into_iter().next().map(|invitation| invitation.id), + Some(InvitationId::from("invitation_01E4ZCR3C56J083X43JQXF3JK5")) + ) + } +} diff --git a/src/user_management/operations/revoke_invitation.rs b/src/user_management/operations/revoke_invitation.rs new file mode 100644 index 0000000..4029e5e --- /dev/null +++ b/src/user_management/operations/revoke_invitation.rs @@ -0,0 +1,132 @@ +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: Revokes an existing invitation](https://workos.com/docs/reference/user-management/invitation/revoke +#[async_trait] +pub trait RevokeInvitation { + /// Revokes an existing invitation. + /// + /// [WorkOS Docs: Revokes an existing invitation](https://workos.com/docs/reference/user-management/invitation/revoke) + /// + /// # Examples + /// + /// ``` + /// use std::collections::HashSet; + /// + /// # use workos_sdk::WorkOsResult; + /// # use workos_sdk::user_management::*; + /// use workos_sdk::{ApiKey, WorkOs}; /// + /// # + /// use workos_sdk::organizations::OrganizationId; + /// + /// 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, + id: &InvitationId, + ) -> WorkOsResult; +} + +#[async_trait] +impl RevokeInvitation for UserManagement<'_> { + async fn revoke_invitation( + &self, + id: &InvitationId, + ) -> WorkOsResult { + let url = self + .workos + .base_url() + .join(&format!("/user_management/invitations/{id}/revoke"))?; + + let invitation = self + .workos + .client() + .post(url) + .bearer_auth(self.workos.key()) + .send() + .await? + .handle_unauthorized_or_generic_error()? + .json::() + .await?; + + Ok(invitation) + } +} + +#[cfg(test)] +mod test { + use serde_json::json; + use tokio; + + 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": "pending", + "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", + "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.email, String::from("marcelina.davis@example.com")); + 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..9e35536 --- /dev/null +++ b/src/user_management/operations/send_invitation.rs @@ -0,0 +1,166 @@ +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. + /// Must be between 1 and 30 days. Defaults to 7 days if not specified. + pub expires_in_days: Option<&'a usize>, + + /// 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 std::collections::HashSet; + /// + /// # use workos_sdk::WorkOsResult; + /// # use workos_sdk::user_management::*; + /// use workos_sdk::{ApiKey, WorkOs}; /// + /// # + /// use workos_sdk::organizations::OrganizationId; + /// + /// async fn run() -> WorkOsResult<(), SendInvitationError> { + /// let workos = WorkOs::new(&ApiKey::from("sk_example_123456789")); + /// + /// let invitation = workos + /// .user_management() + /// .send_invitation(&SendInvitationParams { + /// email: "marcelina.davis@example.com", + /// organization_id: Some(&OrganizationId::from("org_01E4ZCR3C56J083X43JQXF3JK5")), + /// expires_in_days: Some(&7), + /// inviter_user_id: Some(&UserId::from("user_01HYGBX8ZGD19949T3BM4FW1C3")), + /// role_slug: Some("member"), + /// }) + /// .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 invitation = self + .workos + .client() + .post(url) + .bearer_auth(self.workos.key()) + .json(¶ms) + .send() + .await? + .handle_unauthorized_or_generic_error()? + .json::() + .await?; + + Ok(invitation) + } +} + +#[cfg(test)] +mod test { + use serde_json::json; + use tokio; + + 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(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() + .send_invitation(&SendInvitationParams { + email: "marcelina.davis@example.com", + organization_id: Some(&OrganizationId::from("org_01E4ZCR3C56J083X43JQXF3JK5")), + expires_in_days: Some(&7), + inviter_user_id: Some(&UserId::from("user_01HYGBX8ZGD19949T3BM4FW1C3")), + role_slug: Some("member"), + }) + .await + .unwrap(); + + assert_eq!(invitation.email, String::from("marcelina.davis@example.com")) + } +} diff --git a/src/user_management/types.rs b/src/user_management/types.rs index a8d6c92..16e134c 100644 --- a/src/user_management/types.rs +++ b/src/user_management/types.rs @@ -12,6 +12,7 @@ mod provider; mod refresh_token; mod session_id; mod user; +mod invitation; pub use authenticate_error::*; pub use authenticate_methods::*; @@ -27,3 +28,4 @@ pub use provider::*; pub use refresh_token::*; pub use session_id::*; pub use user::*; +pub use invitation::*; diff --git a/src/user_management/types/invitation.rs b/src/user_management/types/invitation.rs new file mode 100644 index 0000000..663e704 --- /dev/null +++ b/src/user_management/types/invitation.rs @@ -0,0 +1,60 @@ +use derive_more::{Deref, Display, From}; +use serde::{Deserialize, Serialize}; +use url::Url; +use crate::{ Timestamp}; +use crate::organizations::OrganizationId; +use crate::user_management::UserId; + +/// The ID of a [`Invitation`]. +#[derive( + Clone, Debug, Deref, Display, From, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, +)] +#[from(forward)] +pub struct InvitationId(String); + +/// 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: User](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: String, + + /// The timestamp when the invitation was accepted. + pub accepted_at: Option, + + /// The timestamp when the invitation was revoked. + pub revoked_at: Option, + + /// The timestamp when the invitation expires. + pub expires_at: Timestamp, + + /// The token for the invitation. + pub token: String, + + /// The URL to accept the invitation. + pub accept_invitation_url: Url, + + /// The organization ID that the invitation is for. + pub organization_id: OrganizationId, + + /// The user ID of the user who invited the recipient. + pub inviter_user_id: UserId, + + /// When the invitation was created. + pub created_at: Timestamp, + + /// When the invitation was last updated. + pub updated_at: Timestamp +} From d15038580f4f7811fb48d7c9f8e532feeddd2f81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABlle=20Huisman?= Date: Fri, 22 Aug 2025 18:57:52 +0200 Subject: [PATCH 2/2] refactor: cleanup --- src/user_management/operations.rs | 24 +-- .../operations/accept_invitation.rs | 66 ++++--- .../operations/create_magic_auth.rs | 2 - .../operations/create_password_reset.rs | 2 - src/user_management/operations/create_user.rs | 2 - .../operations/enroll_auth_factor.rs | 2 - ...y_token.rs => find_invitation_by_token.rs} | 74 ++++---- .../operations/get_invitation.rs | 48 +++-- .../operations/get_magic_auth.rs | 4 +- .../operations/list_invitations.rs | 169 +++++++++++++++--- .../operations/reset_password.rs | 2 - .../operations/revoke_invitation.rs | 66 ++++--- .../operations/send_invitation.rs | 79 ++++---- src/user_management/types.rs | 4 +- src/user_management/types/invitation.rs | 48 +++-- 15 files changed, 352 insertions(+), 240 deletions(-) rename src/user_management/operations/{get_invitation_by_token.rs => find_invitation_by_token.rs} (52%) diff --git a/src/user_management/operations.rs b/src/user_management/operations.rs index fd21bcf..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,16 +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 update_user; -mod send_invitation; -mod get_invitation; -mod get_invitation_by_token; -mod accept_invitation; mod revoke_invitation; -mod list_invitations; +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::*; @@ -41,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::*; @@ -52,12 +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 update_user::*; -pub use send_invitation::*; -pub use get_invitation::*; -pub use get_invitation_by_token::*; -pub use accept_invitation::*; pub use revoke_invitation::*; -pub use list_invitations::*; +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 index 2f9e053..99506bf 100644 --- a/src/user_management/operations/accept_invitation.rs +++ b/src/user_management/operations/accept_invitation.rs @@ -14,25 +14,21 @@ impl From for WorkOsError { } } -/// [WorkOS Docs: Accept an invitation](https://workos.com/docs/reference/user-management/invitation/accept +/// [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. + /// 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 std::collections::HashSet; - /// /// # use workos_sdk::WorkOsResult; /// # use workos_sdk::user_management::*; - /// use workos_sdk::{ApiKey, WorkOs}; /// - /// # - /// use workos_sdk::organizations::OrganizationId; + /// use workos_sdk::{ApiKey, WorkOs}; /// - /// async fn run() -> WorkOsResult<(), AcceptInvitationError> { + /// # async fn run() -> WorkOsResult<(), AcceptInvitationError> { /// let workos = WorkOs::new(&ApiKey::from("sk_example_123456789")); /// /// let invitation = workos @@ -44,7 +40,7 @@ pub trait AcceptInvitation { /// ``` async fn accept_invitation( &self, - id: &InvitationId, + invitation_id: &InvitationId, ) -> WorkOsResult; } @@ -52,14 +48,12 @@ pub trait AcceptInvitation { impl AcceptInvitation for UserManagement<'_> { async fn accept_invitation( &self, - id: &InvitationId, + invitation_id: &InvitationId, ) -> WorkOsResult { - let url = self - .workos - .base_url() - .join(&format!("/user_management/invitations/{id}/accept"))?; - - let invitation = self + let url = self.workos.base_url().join(&format!( + "/user_management/invitations/{invitation_id}/accept" + ))?; + let user = self .workos .client() .post(url) @@ -70,7 +64,7 @@ impl AcceptInvitation for UserManagement<'_> { .json::() .await?; - Ok(invitation) + Ok(user) } } @@ -79,6 +73,7 @@ mod test { use serde_json::json; use tokio; + use crate::user_management::InvitationId; use crate::{ApiKey, WorkOs}; use super::*; @@ -93,27 +88,25 @@ mod test { .build(); server - .mock( - "POST", - "/user_management/invitations/invitation_01E4ZCR3C56J083X43JQXF3JK5/accept", - ) + .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": "pending", - "accepted_at": "2021-07-01T19: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", - "created_at": "2021-06-25T19:07:33.155Z", - "updated_at": "2021-06-25T19:07:33.155Z" + "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(), ) @@ -126,7 +119,10 @@ mod test { .await .unwrap(); - assert_eq!(invitation.email, String::from("marcelina.davis@example.com")); + 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/get_invitation_by_token.rs b/src/user_management/operations/find_invitation_by_token.rs similarity index 52% rename from src/user_management/operations/get_invitation_by_token.rs rename to src/user_management/operations/find_invitation_by_token.rs index e91a318..4813b18 100644 --- a/src/user_management/operations/get_invitation_by_token.rs +++ b/src/user_management/operations/find_invitation_by_token.rs @@ -4,19 +4,19 @@ use thiserror::Error; use crate::user_management::{Invitation, InvitationToken, UserManagement}; use crate::{ResponseExt, WorkOsError, WorkOsResult}; -/// An error returned from [`GetInvitationByToken`]. +/// An error returned from [`FindInvitationByToken`]. #[derive(Debug, Error)] -pub enum GetInvitationByTokenError {} +pub enum FindInvitationByTokenError {} -impl From for WorkOsError { - fn from(err: GetInvitationByTokenError) -> Self { +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 GetInvitationByToken { +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) @@ -24,42 +24,37 @@ pub trait GetInvitationByToken { /// # Examples /// /// ``` - /// use std::collections::HashSet; - /// /// # use workos_sdk::WorkOsResult; /// # use workos_sdk::user_management::*; /// use workos_sdk::{ApiKey, WorkOs}; - /// # - /// use workos_sdk::organizations::OrganizationId; /// - /// async fn run() -> WorkOsResult<(), GetInvitationByTokenError> { + /// # async fn run() -> WorkOsResult<(), FindInvitationByTokenError> { /// let workos = WorkOs::new(&ApiKey::from("sk_example_123456789")); /// /// let invitation = workos /// .user_management() - /// .get_invitation_by_token(&InvitationToken::from("Z1uX3RbwcIl5fIGJJJCXXisdI")) + /// .find_invitation_by_token(&InvitationToken::from("Z1uX3RbwcIl5fIGJJJCXXisdI")) /// .await?; /// # Ok(()) /// # } /// ``` - async fn get_invitation_by_token( + async fn find_invitation_by_token( &self, token: &InvitationToken, - ) -> WorkOsResult; + ) -> WorkOsResult; } #[async_trait] -impl GetInvitationByToken for UserManagement<'_> { - async fn get_invitation_by_token( +impl FindInvitationByToken for UserManagement<'_> { + async fn find_invitation_by_token( &self, token: &InvitationToken, - ) -> WorkOsResult { + ) -> WorkOsResult { let url = self .workos .base_url() - .join(&format!("user_management/invitations/by_token/{token}"))?; - - let invitation = self + .join(&format!("/user_management/invitations/by_token/{token}"))?; + let organization = self .workos .client() .get(url) @@ -70,7 +65,7 @@ impl GetInvitationByToken for UserManagement<'_> { .json::() .await?; - Ok(invitation) + Ok(organization) } } @@ -79,12 +74,12 @@ mod test { use serde_json::json; use tokio; - use crate::{ApiKey, WorkOs}; - use crate::user_management::InvitationId; + use crate::{ApiKey, WorkOs, user_management::InvitationId}; + use super::*; #[tokio::test] - async fn it_calls_the_get_invitation_by_token_endpoint() { + 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")) @@ -101,19 +96,19 @@ mod test { .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" + "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(), ) @@ -122,10 +117,13 @@ mod test { let invitation = workos .user_management() - .get_invitation_by_token(&InvitationToken::from("Z1uX3RbwcIl5fIGJJJCXXisdI")) + .find_invitation_by_token(&InvitationToken::from("Z1uX3RbwcIl5fIGJJJCXXisdI")) .await .unwrap(); - assert_eq!(invitation.id, InvitationId::from("invitation_01E4ZCR3C56J083X43JQXF3JK5")); + 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 index ac1ba34..2411075 100644 --- a/src/user_management/operations/get_invitation.rs +++ b/src/user_management/operations/get_invitation.rs @@ -24,15 +24,11 @@ pub trait GetInvitation { /// # Examples /// /// ``` - /// use std::collections::HashSet; - /// /// # use workos_sdk::WorkOsResult; /// # use workos_sdk::user_management::*; - /// use workos_sdk::{ApiKey, WorkOs}; /// - /// # - /// use workos_sdk::organizations::OrganizationId; + /// use workos_sdk::{ApiKey, WorkOs}; /// - /// async fn run() -> WorkOsResult<(), GetInvitationError> { + /// # async fn run() -> WorkOsResult<(), GetInvitationError> { /// let workos = WorkOs::new(&ApiKey::from("sk_example_123456789")); /// /// let invitation = workos @@ -57,9 +53,8 @@ impl GetInvitation for UserManagement<'_> { let url = self .workos .base_url() - .join(&format!("user_management/invitations/{id}"))?; - - let invitation = self + .join(&format!("/user_management/invitations/{id}"))?; + let organization = self .workos .client() .get(url) @@ -70,7 +65,7 @@ impl GetInvitation for UserManagement<'_> { .json::() .await?; - Ok(invitation) + Ok(organization) } } @@ -95,25 +90,25 @@ mod test { server .mock( "GET", - "/user_management/invitations/invitation_123456789", + "/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" + "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(), ) @@ -126,6 +121,9 @@ mod test { .await .unwrap(); - assert_eq!(invitation.id, InvitationId::from("invitation_01E4ZCR3C56J083X43JQXF3JK5")); + 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 index 9078804..69fc25b 100644 --- a/src/user_management/operations/list_invitations.rs +++ b/src/user_management/operations/list_invitations.rs @@ -2,20 +2,20 @@ use async_trait::async_trait; use serde::Serialize; use thiserror::Error; -use crate::user_management::{Invitation, UserManagement}; -use crate::{PaginatedList, PaginationParams, ResponseExt, WorkOsError, WorkOsResult}; use crate::organizations::OrganizationId; +use crate::user_management::{Invitation, UserManagement}; +use crate::{PaginatedList, PaginationParams, ResponseExt, WorkOsError, WorkOsResult}; -/// The parameters for [`ListInvitations`]. +/// The parameters for the [`ListInvitations`] function. #[derive(Debug, Serialize, Default)] -pub struct ListInvitationParams<'a> { +pub struct ListInvitationsParams<'a> { /// The email address of the recipient. - pub email: &'a str, + 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 users. + /// The pagination parameters to use when listing invitations. #[serde(flatten)] pub pagination: PaginationParams<'a>, } @@ -40,21 +40,18 @@ pub trait ListInvitations { /// # Examples /// /// ``` - /// use std::collections::HashSet; - /// /// # use workos_sdk::WorkOsResult; /// # use workos_sdk::user_management::*; /// use workos_sdk::{ApiKey, WorkOs}; - /// # /// use workos_sdk::organizations::OrganizationId; /// - /// async fn run() -> WorkOsResult<(), ListInvitationsError> { + /// # async fn run() -> WorkOsResult<(), ListInvitationsError> { /// let workos = WorkOs::new(&ApiKey::from("sk_example_123456789")); /// /// let invitations = workos /// .user_management() - /// .list_invitations(&ListInvitationParams { - /// email: "marcelina.davis@example.com", + /// .list_invitations(&ListInvitationsParams { + /// email: Some("marcelina.davis@example.com"), /// organization_id: Some(&OrganizationId::from("org_01E4ZCR3C56J083X43JQXF3JK5")), /// ..Default::default() /// }) @@ -64,7 +61,7 @@ pub trait ListInvitations { /// ``` async fn list_invitations( &self, - params: &ListInvitationParams, + params: &ListInvitationsParams, ) -> WorkOsResult, ListInvitationsError>; } @@ -72,19 +69,19 @@ pub trait ListInvitations { impl ListInvitations for UserManagement<'_> { async fn list_invitations( &self, - params: &ListInvitationParams, + params: &ListInvitationsParams, ) -> WorkOsResult, ListInvitationsError> { let url = self .workos .base_url() - .join("user_management/invitations")?; + .join("/user_management/invitations")?; let invitations = self .workos .client() .get(url) - .bearer_auth(self.workos.key()) .query(¶ms) + .bearer_auth(self.workos.key()) .send() .await? .handle_unauthorized_or_generic_error()? @@ -97,11 +94,13 @@ impl ListInvitations for UserManagement<'_> { #[cfg(test)] mod test { + use mockito::Matcher; use serde_json::json; use tokio; + use crate::user_management::InvitationId; use crate::{ApiKey, WorkOs}; - use crate::user_management::{InvitationId, UserId}; + use super::*; #[tokio::test] @@ -114,10 +113,133 @@ mod test { .build(); server - .mock( - "GET", - "/user_management/invitations/invitation_123456789", + .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( @@ -149,10 +271,9 @@ mod test { .create_async() .await; - let paginated_invitations = workos + let paginated_list = workos .user_management() - .list_invitations(&ListInvitationParams { - email: "marcelina.davis@example.com", + .list_invitations(&ListInvitationsParams { organization_id: Some(&OrganizationId::from("org_01E4ZCR3C56J083X43JQXF3JK5")), ..Default::default() }) @@ -160,7 +281,7 @@ mod test { .unwrap(); assert_eq!( - paginated_invitations.data.into_iter().next().map(|invitation| invitation.id), + 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 index 4029e5e..c01e14d 100644 --- a/src/user_management/operations/revoke_invitation.rs +++ b/src/user_management/operations/revoke_invitation.rs @@ -14,25 +14,21 @@ impl From for WorkOsError { } } -/// [WorkOS Docs: Revokes an existing invitation](https://workos.com/docs/reference/user-management/invitation/revoke +/// [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: Revokes an existing invitation](https://workos.com/docs/reference/user-management/invitation/revoke) + /// [WorkOS Docs: Revoke an invitation](https://workos.com/docs/reference/user-management/invitation/revoke) /// /// # Examples /// /// ``` - /// use std::collections::HashSet; - /// /// # use workos_sdk::WorkOsResult; /// # use workos_sdk::user_management::*; - /// use workos_sdk::{ApiKey, WorkOs}; /// - /// # - /// use workos_sdk::organizations::OrganizationId; + /// use workos_sdk::{ApiKey, WorkOs}; /// - /// async fn run() -> WorkOsResult<(), RevokeInvitationError> { + /// # async fn run() -> WorkOsResult<(), RevokeInvitationError> { /// let workos = WorkOs::new(&ApiKey::from("sk_example_123456789")); /// /// let invitation = workos @@ -44,7 +40,7 @@ pub trait RevokeInvitation { /// ``` async fn revoke_invitation( &self, - id: &InvitationId, + invitation_id: &InvitationId, ) -> WorkOsResult; } @@ -52,14 +48,12 @@ pub trait RevokeInvitation { impl RevokeInvitation for UserManagement<'_> { async fn revoke_invitation( &self, - id: &InvitationId, + invitation_id: &InvitationId, ) -> WorkOsResult { - let url = self - .workos - .base_url() - .join(&format!("/user_management/invitations/{id}/revoke"))?; - - let invitation = self + let url = self.workos.base_url().join(&format!( + "/user_management/invitations/{invitation_id}/revoke" + ))?; + let user = self .workos .client() .post(url) @@ -70,7 +64,7 @@ impl RevokeInvitation for UserManagement<'_> { .json::() .await?; - Ok(invitation) + Ok(user) } } @@ -79,6 +73,7 @@ mod test { use serde_json::json; use tokio; + use crate::user_management::InvitationId; use crate::{ApiKey, WorkOs}; use super::*; @@ -93,27 +88,25 @@ mod test { .build(); server - .mock( - "POST", - "/user_management/invitations/invitation_01E4ZCR3C56J083X43JQXF3JK5/revoke", - ) + .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": "pending", - "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", - "created_at": "2021-06-25T19:07:33.155Z", - "updated_at": "2021-06-25T19:07:33.155Z" + "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(), ) @@ -126,7 +119,10 @@ mod test { .await .unwrap(); - assert_eq!(invitation.email, String::from("marcelina.davis@example.com")); + 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 index 9e35536..94ee57a 100644 --- a/src/user_management/operations/send_invitation.rs +++ b/src/user_management/operations/send_invitation.rs @@ -16,10 +16,11 @@ pub struct SendInvitationParams<'a> { pub organization_id: Option<&'a OrganizationId>, /// How many days the invitations will be valid for. - /// Must be between 1 and 30 days. Defaults to 7 days if not specified. - pub expires_in_days: Option<&'a usize>, + pub expires_in_days: Option, - /// The ID of the user who invites the recipient. The invitation email will mention the name of this user. + /// 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. @@ -46,25 +47,21 @@ pub trait SendInvitation { /// # Examples /// /// ``` - /// use std::collections::HashSet; - /// /// # use workos_sdk::WorkOsResult; /// # use workos_sdk::user_management::*; - /// use workos_sdk::{ApiKey, WorkOs}; /// - /// # - /// use workos_sdk::organizations::OrganizationId; + /// use workos_sdk::{ApiKey, WorkOs}; /// - /// async fn run() -> WorkOsResult<(), SendInvitationError> { + /// # async fn run() -> WorkOsResult<(), SendInvitationError> { /// let workos = WorkOs::new(&ApiKey::from("sk_example_123456789")); /// /// let invitation = workos /// .user_management() /// .send_invitation(&SendInvitationParams { - /// email: "marcelina.davis@example.com", - /// organization_id: Some(&OrganizationId::from("org_01E4ZCR3C56J083X43JQXF3JK5")), - /// expires_in_days: Some(&7), - /// inviter_user_id: Some(&UserId::from("user_01HYGBX8ZGD19949T3BM4FW1C3")), - /// role_slug: Some("member"), + /// email: "marcelina@example.com", + /// organization_id: None, + /// expires_in_days: None, + /// inviter_user_id: None, + /// role_slug: None, /// }) /// .await?; /// # Ok(()) @@ -86,8 +83,7 @@ impl SendInvitation for UserManagement<'_> { .workos .base_url() .join("/user_management/invitations")?; - - let invitation = self + let user = self .workos .client() .post(url) @@ -99,7 +95,7 @@ impl SendInvitation for UserManagement<'_> { .json::() .await?; - Ok(invitation) + Ok(user) } } @@ -108,6 +104,7 @@ mod test { use serde_json::json; use tokio; + use crate::user_management::InvitationId; use crate::{ApiKey, WorkOs}; use super::*; @@ -122,27 +119,24 @@ mod test { .build(); server - .mock( - "POST", - "/user_management/invitations", - ) + .mock("POST", "/user_management/invitations") .match_header("Authorization", "Bearer sk_example_123456789") - .with_status(200) + .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" + "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(), ) @@ -152,15 +146,18 @@ mod test { let invitation = workos .user_management() .send_invitation(&SendInvitationParams { - email: "marcelina.davis@example.com", - organization_id: Some(&OrganizationId::from("org_01E4ZCR3C56J083X43JQXF3JK5")), - expires_in_days: Some(&7), - inviter_user_id: Some(&UserId::from("user_01HYGBX8ZGD19949T3BM4FW1C3")), - role_slug: Some("member"), + email: "marcelina@example.com", + organization_id: None, + expires_in_days: None, + inviter_user_id: None, + role_slug: None, }) .await .unwrap(); - assert_eq!(invitation.email, String::from("marcelina.davis@example.com")) + assert_eq!( + invitation.id, + InvitationId::from("invitation_01E4ZCR3C56J083X43JQXF3JK5") + ) } } diff --git a/src/user_management/types.rs b/src/user_management/types.rs index 16e134c..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; @@ -12,7 +13,6 @@ mod provider; mod refresh_token; mod session_id; mod user; -mod invitation; pub use authenticate_error::*; pub use authenticate_methods::*; @@ -20,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::*; @@ -28,4 +29,3 @@ pub use provider::*; pub use refresh_token::*; pub use session_id::*; pub use user::*; -pub use invitation::*; diff --git a/src/user_management/types/invitation.rs b/src/user_management/types/invitation.rs index 663e704..94a5d35 100644 --- a/src/user_management/types/invitation.rs +++ b/src/user_management/types/invitation.rs @@ -1,17 +1,35 @@ use derive_more::{Deref, Display, From}; use serde::{Deserialize, Serialize}; use url::Url; -use crate::{ Timestamp}; + use crate::organizations::OrganizationId; use crate::user_management::UserId; +use crate::{KnownOrUnknown, Timestamp, Timestamps}; -/// The ID of a [`Invitation`]. +/// 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, @@ -19,7 +37,7 @@ pub struct InvitationId(String); #[from(forward)] pub struct InvitationToken(String); -/// [WorkOS Docs: User](https://workos.com/docs/reference/user-management/invitation) +/// [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. @@ -29,32 +47,30 @@ pub struct Invitation { pub email: String, /// The state of the invitation. - pub state: String, + pub state: KnownOrUnknown, - /// The timestamp when the invitation was accepted. + /// The timestamp indicating when the invitation was accepted. pub accepted_at: Option, - /// The timestamp when the invitation was revoked. + /// The timestamp indicating when the invitation was revoked. pub revoked_at: Option, - /// The timestamp when the invitation expires. + /// The timestamp indicating when the invitation expires. pub expires_at: Timestamp, /// The token for the invitation. - pub token: String, + pub token: InvitationToken, - /// The URL to accept the invitation. + /// The URL used to accept the invitation. pub accept_invitation_url: Url, - /// The organization ID that the invitation is for. + /// The ID of the organization that the recipient will join. pub organization_id: OrganizationId, - /// The user ID of the user who invited the recipient. + /// The ID of the user who invited the recipient. pub inviter_user_id: UserId, - /// When the invitation was created. - pub created_at: Timestamp, - - /// When the invitation was last updated. - pub updated_at: Timestamp + /// The timestamps for the invitation. + #[serde(flatten)] + pub timestamps: Timestamps, }