From d0fb3471299004893c5f045092d427ff8676dab7 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 12 Oct 2026 17:57:29 +0100 Subject: [PATCH] feat: add passkeys for version 0.17.0 --- Cargo.toml | 2 +- README.md | 2 +- docs/examples/account/create-passkey-token.md | 20 ++ docs/examples/account/create-passkey.md | 23 ++ docs/examples/account/delete-passkey.md | 20 ++ docs/examples/account/get-passkey.md | 22 ++ docs/examples/account/list-passkeys.md | 23 ++ docs/examples/account/update-passkey-token.md | 23 ++ .../account/update-passkey-verification.md | 23 ++ docs/examples/account/update-passkey.md | 23 ++ .../examples/project/update-passkey-policy.md | 22 ++ docs/examples/users/delete-passkey.md | 21 ++ docs/examples/users/get-passkey.md | 23 ++ docs/examples/users/list-passkeys.md | 24 ++ docs/examples/users/update-passkey.md | 24 ++ src/client.rs | 4 +- src/lib.rs | 4 +- src/models/mod.rs | 12 + src/models/passkey.rs | 77 ++++++ src/models/passkey_challenge.rs | 79 ++++++ src/models/passkey_list.rs | 49 ++++ src/models/token_secret.rs | 88 +++++++ src/services/account.rs | 231 ++++++++++++++++++ src/services/project.rs | 23 ++ src/services/users.rs | 164 +++++++++++++ 25 files changed, 1020 insertions(+), 6 deletions(-) create mode 100644 docs/examples/account/create-passkey-token.md create mode 100644 docs/examples/account/create-passkey.md create mode 100644 docs/examples/account/delete-passkey.md create mode 100644 docs/examples/account/get-passkey.md create mode 100644 docs/examples/account/list-passkeys.md create mode 100644 docs/examples/account/update-passkey-token.md create mode 100644 docs/examples/account/update-passkey-verification.md create mode 100644 docs/examples/account/update-passkey.md create mode 100644 docs/examples/project/update-passkey-policy.md create mode 100644 docs/examples/users/delete-passkey.md create mode 100644 docs/examples/users/get-passkey.md create mode 100644 docs/examples/users/list-passkeys.md create mode 100644 docs/examples/users/update-passkey.md create mode 100644 src/models/passkey.rs create mode 100644 src/models/passkey_challenge.rs create mode 100644 src/models/passkey_list.rs create mode 100644 src/models/token_secret.rs diff --git a/Cargo.toml b/Cargo.toml index 596c294..10d0783 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "appwrite" -version = "0.16.0" +version = "0.17.0" edition = "2021" rust-version = "1.83" authors = ["appwrite"] diff --git a/README.md b/README.md index 8c04e98..859c159 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Add this to your `Cargo.toml`: ```toml [dependencies] -appwrite = "0.16.0" +appwrite = "0.17.0" tokio = { version = "1.48", features = ["full"] } ``` diff --git a/docs/examples/account/create-passkey-token.md b/docs/examples/account/create-passkey-token.md new file mode 100644 index 0000000..d57670a --- /dev/null +++ b/docs/examples/account/create-passkey-token.md @@ -0,0 +1,20 @@ +```rust +use appwrite::Client; +use appwrite::services::Account; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new(); + client.set_endpoint("https://.cloud.appwrite.io/v1"); // Your API Endpoint + client.set_project(""); // Your project ID + client.set_session(""); // The user session to authenticate with + + let account = Account::new(&client); + + let result = account.create_passkey_token().await?; + + let _ = result; + + Ok(()) +} +``` diff --git a/docs/examples/account/create-passkey.md b/docs/examples/account/create-passkey.md new file mode 100644 index 0000000..4e4bf95 --- /dev/null +++ b/docs/examples/account/create-passkey.md @@ -0,0 +1,23 @@ +```rust +use appwrite::Client; +use appwrite::services::Account; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new(); + client.set_endpoint("https://.cloud.appwrite.io/v1"); // Your API Endpoint + client.set_project(""); // Your project ID + client.set_session(""); // The user session to authenticate with + + let account = Account::new(&client); + + let result = account.create_passkey( + Some(""), // optional + Some("") // optional + ).await?; + + let _ = result; + + Ok(()) +} +``` diff --git a/docs/examples/account/delete-passkey.md b/docs/examples/account/delete-passkey.md new file mode 100644 index 0000000..c2b3af8 --- /dev/null +++ b/docs/examples/account/delete-passkey.md @@ -0,0 +1,20 @@ +```rust +use appwrite::Client; +use appwrite::services::Account; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new(); + client.set_endpoint("https://.cloud.appwrite.io/v1"); // Your API Endpoint + client.set_project(""); // Your project ID + client.set_session(""); // The user session to authenticate with + + let account = Account::new(&client); + + account.delete_passkey( + "" + ).await?; + + Ok(()) +} +``` diff --git a/docs/examples/account/get-passkey.md b/docs/examples/account/get-passkey.md new file mode 100644 index 0000000..f6ff2ad --- /dev/null +++ b/docs/examples/account/get-passkey.md @@ -0,0 +1,22 @@ +```rust +use appwrite::Client; +use appwrite::services::Account; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new(); + client.set_endpoint("https://.cloud.appwrite.io/v1"); // Your API Endpoint + client.set_project(""); // Your project ID + client.set_session(""); // The user session to authenticate with + + let account = Account::new(&client); + + let result = account.get_passkey( + "" + ).await?; + + let _ = result; + + Ok(()) +} +``` diff --git a/docs/examples/account/list-passkeys.md b/docs/examples/account/list-passkeys.md new file mode 100644 index 0000000..f44cf68 --- /dev/null +++ b/docs/examples/account/list-passkeys.md @@ -0,0 +1,23 @@ +```rust +use appwrite::Client; +use appwrite::services::Account; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new(); + client.set_endpoint("https://.cloud.appwrite.io/v1"); // Your API Endpoint + client.set_project(""); // Your project ID + client.set_session(""); // The user session to authenticate with + + let account = Account::new(&client); + + let result = account.list_passkeys( + Some(vec![]), // optional + Some(false) // optional + ).await?; + + let _ = result; + + Ok(()) +} +``` diff --git a/docs/examples/account/update-passkey-token.md b/docs/examples/account/update-passkey-token.md new file mode 100644 index 0000000..d959824 --- /dev/null +++ b/docs/examples/account/update-passkey-token.md @@ -0,0 +1,23 @@ +```rust +use appwrite::Client; +use appwrite::services::Account; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new(); + client.set_endpoint("https://.cloud.appwrite.io/v1"); // Your API Endpoint + client.set_project(""); // Your project ID + client.set_session(""); // The user session to authenticate with + + let account = Account::new(&client); + + let result = account.update_passkey_token( + "", + serde_json::json!({}) + ).await?; + + let _ = result; + + Ok(()) +} +``` diff --git a/docs/examples/account/update-passkey-verification.md b/docs/examples/account/update-passkey-verification.md new file mode 100644 index 0000000..f795f36 --- /dev/null +++ b/docs/examples/account/update-passkey-verification.md @@ -0,0 +1,23 @@ +```rust +use appwrite::Client; +use appwrite::services::Account; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new(); + client.set_endpoint("https://.cloud.appwrite.io/v1"); // Your API Endpoint + client.set_project(""); // Your project ID + client.set_session(""); // The user session to authenticate with + + let account = Account::new(&client); + + let result = account.update_passkey_verification( + "", + serde_json::json!({}) + ).await?; + + let _ = result; + + Ok(()) +} +``` diff --git a/docs/examples/account/update-passkey.md b/docs/examples/account/update-passkey.md new file mode 100644 index 0000000..043b64a --- /dev/null +++ b/docs/examples/account/update-passkey.md @@ -0,0 +1,23 @@ +```rust +use appwrite::Client; +use appwrite::services::Account; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new(); + client.set_endpoint("https://.cloud.appwrite.io/v1"); // Your API Endpoint + client.set_project(""); // Your project ID + client.set_session(""); // The user session to authenticate with + + let account = Account::new(&client); + + let result = account.update_passkey( + "", + "" + ).await?; + + let _ = result; + + Ok(()) +} +``` diff --git a/docs/examples/project/update-passkey-policy.md b/docs/examples/project/update-passkey-policy.md new file mode 100644 index 0000000..71ad764 --- /dev/null +++ b/docs/examples/project/update-passkey-policy.md @@ -0,0 +1,22 @@ +```rust +use appwrite::Client; +use appwrite::services::Project; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new(); + client.set_endpoint("https://.cloud.appwrite.io/v1"); // Your API Endpoint + client.set_project(""); // Your project ID + client.set_key(""); // Your secret API key + + let project = Project::new(&client); + + let result = project.update_passkey_policy( + "" + ).await?; + + let _ = result; + + Ok(()) +} +``` diff --git a/docs/examples/users/delete-passkey.md b/docs/examples/users/delete-passkey.md new file mode 100644 index 0000000..79a3456 --- /dev/null +++ b/docs/examples/users/delete-passkey.md @@ -0,0 +1,21 @@ +```rust +use appwrite::Client; +use appwrite::services::Users; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new(); + client.set_endpoint("https://.cloud.appwrite.io/v1"); // Your API Endpoint + client.set_project(""); // Your project ID + client.set_key(""); // Your secret API key + + let users = Users::new(&client); + + users.delete_passkey( + "", + "" + ).await?; + + Ok(()) +} +``` diff --git a/docs/examples/users/get-passkey.md b/docs/examples/users/get-passkey.md new file mode 100644 index 0000000..c6751c6 --- /dev/null +++ b/docs/examples/users/get-passkey.md @@ -0,0 +1,23 @@ +```rust +use appwrite::Client; +use appwrite::services::Users; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new(); + client.set_endpoint("https://.cloud.appwrite.io/v1"); // Your API Endpoint + client.set_project(""); // Your project ID + client.set_key(""); // Your secret API key + + let users = Users::new(&client); + + let result = users.get_passkey( + "", + "" + ).await?; + + let _ = result; + + Ok(()) +} +``` diff --git a/docs/examples/users/list-passkeys.md b/docs/examples/users/list-passkeys.md new file mode 100644 index 0000000..a9acdc7 --- /dev/null +++ b/docs/examples/users/list-passkeys.md @@ -0,0 +1,24 @@ +```rust +use appwrite::Client; +use appwrite::services::Users; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new(); + client.set_endpoint("https://.cloud.appwrite.io/v1"); // Your API Endpoint + client.set_project(""); // Your project ID + client.set_key(""); // Your secret API key + + let users = Users::new(&client); + + let result = users.list_passkeys( + "", + Some(vec![]), // optional + Some(false) // optional + ).await?; + + let _ = result; + + Ok(()) +} +``` diff --git a/docs/examples/users/update-passkey.md b/docs/examples/users/update-passkey.md new file mode 100644 index 0000000..a7705cc --- /dev/null +++ b/docs/examples/users/update-passkey.md @@ -0,0 +1,24 @@ +```rust +use appwrite::Client; +use appwrite::services::Users; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = Client::new(); + client.set_endpoint("https://.cloud.appwrite.io/v1"); // Your API Endpoint + client.set_project(""); // Your project ID + client.set_key(""); // Your secret API key + + let users = Users::new(&client); + + let result = users.update_passkey( + "", + "", + "" + ).await?; + + let _ = result; + + Ok(()) +} +``` diff --git a/src/client.rs b/src/client.rs index ed7d143..b3fabe6 100644 --- a/src/client.rs +++ b/src/client.rs @@ -95,7 +95,7 @@ impl Client { headers.insert( "user-agent", format!( - "AppwriteRustSDK/0.16.0 ({}; {})", + "AppwriteRustSDK/0.17.0 ({}; {})", std::env::consts::OS, std::env::consts::ARCH ) @@ -105,7 +105,7 @@ impl Client { headers.insert("x-sdk-name", "Rust".parse().unwrap()); headers.insert("x-sdk-platform", "server".parse().unwrap()); headers.insert("x-sdk-language", "rust".parse().unwrap()); - headers.insert("x-sdk-version", "0.16.0".parse().unwrap()); + headers.insert("x-sdk-version", "0.17.0".parse().unwrap()); let config = Config { endpoint: "https://cloud.appwrite.io/v1".to_string(), diff --git a/src/lib.rs b/src/lib.rs index 4c60469..cd01ed8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ //! //! ```toml //! [dependencies] -//! appwrite = "0.16.0" +//! appwrite = "0.17.0" //! ``` //! //! ## Usage @@ -51,7 +51,7 @@ pub use input_file::InputFile; pub type Result = std::result::Result; /// SDK version -pub const VERSION: &str = "0.16.0"; +pub const VERSION: &str = "0.17.0"; /// SDK name pub const SDK_NAME: &str = "Rust"; diff --git a/src/models/mod.rs b/src/models/mod.rs index e5c99bf..4e6ae90 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -418,6 +418,14 @@ pub mod mfa_challenge; pub use mfa_challenge::MfaChallenge; pub mod mfa_challenge_secret; pub use mfa_challenge_secret::MfaChallengeSecret; +pub mod token_secret; +pub use token_secret::TokenSecret; +pub mod passkey; +pub use passkey::Passkey; +pub mod passkey_challenge; +pub use passkey_challenge::PasskeyChallenge; +pub mod passkey_list; +pub use passkey_list::PasskeyList; pub mod mfa_recovery_codes; pub use mfa_recovery_codes::MfaRecoveryCodes; pub mod mfa_type; @@ -817,6 +825,10 @@ impl Model for ProxyRule {} impl Model for EmailTemplate {} impl Model for MfaChallenge {} impl Model for MfaChallengeSecret {} +impl Model for TokenSecret {} +impl Model for Passkey {} +impl Model for PasskeyChallenge {} +impl Model for PasskeyList {} impl Model for MfaRecoveryCodes {} impl Model for MfaType {} impl Model for MfaFactors {} diff --git a/src/models/passkey.rs b/src/models/passkey.rs new file mode 100644 index 0000000..0685af4 --- /dev/null +++ b/src/models/passkey.rs @@ -0,0 +1,77 @@ +//! Passkey model for Appwrite SDK + +use serde::{Deserialize, Serialize}; + +/// Passkey +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(test, derive(Default))] +pub struct Passkey { + /// Passkey ID. + #[serde(rename = "$id")] + pub id: String, + /// Passkey creation date in ISO 8601 format. + #[serde(rename = "$createdAt")] + pub created_at: String, + /// Passkey update date in ISO 8601 format. + #[serde(rename = "$updatedAt")] + pub updated_at: String, + /// Passkey name. + #[serde(rename = "name")] + pub name: String, + /// Most recent sign-in with this passkey in ISO 8601 format. Empty until the + /// passkey is used to sign in. + #[serde(rename = "accessedAt")] + pub accessed_at: String, +} + +impl Passkey { + /// Get id + pub fn id(&self) -> &String { + &self.id + } + + /// Get created_at + pub fn created_at(&self) -> &String { + &self.created_at + } + + /// Get updated_at + pub fn updated_at(&self) -> &String { + &self.updated_at + } + + /// Get name + pub fn name(&self) -> &String { + &self.name + } + + /// Get accessed_at + pub fn accessed_at(&self) -> &String { + &self.accessed_at + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_passkey_creation() { + let _model = ::default(); + let _ = _model.id(); + let _ = _model.created_at(); + let _ = _model.updated_at(); + let _ = _model.name(); + let _ = _model.accessed_at(); + } + + #[test] + fn test_passkey_serialization() { + let model = ::default(); + let json = serde_json::to_string(&model); + assert!(json.is_ok()); + + let deserialized: Result = serde_json::from_str(&json.unwrap()); + assert!(deserialized.is_ok()); + } +} diff --git a/src/models/passkey_challenge.rs b/src/models/passkey_challenge.rs new file mode 100644 index 0000000..8ecada5 --- /dev/null +++ b/src/models/passkey_challenge.rs @@ -0,0 +1,79 @@ +//! PasskeyChallenge model for Appwrite SDK + +use serde::{Deserialize, Serialize}; + +/// Passkey Challenge +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(test, derive(Default))] +pub struct PasskeyChallenge { + /// Challenge ID. When signing in, pass it back with the credential to receive + /// a token. + #[serde(rename = "$id")] + pub id: String, + /// Challenge creation date in ISO 8601 format. + #[serde(rename = "$createdAt")] + pub created_at: String, + /// ID of the passkey being registered. Empty for sign-in challenges. + #[serde(rename = "passkeyId")] + pub passkey_id: String, + /// Challenge expiration date in ISO 8601 format. + #[serde(rename = "expire")] + pub expire: String, + /// WebAuthn options in JSON form. Pass them to + /// `PublicKeyCredential.parseCreationOptionsFromJSON()` when registering, or + /// `PublicKeyCredential.parseRequestOptionsFromJSON()` when signing in. + #[serde(rename = "publicKey")] + pub public_key: serde_json::Value, +} + +impl PasskeyChallenge { + /// Get id + pub fn id(&self) -> &String { + &self.id + } + + /// Get created_at + pub fn created_at(&self) -> &String { + &self.created_at + } + + /// Get passkey_id + pub fn passkey_id(&self) -> &String { + &self.passkey_id + } + + /// Get expire + pub fn expire(&self) -> &String { + &self.expire + } + + /// Get public_key + pub fn public_key(&self) -> &serde_json::Value { + &self.public_key + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_passkey_challenge_creation() { + let _model = ::default(); + let _ = _model.id(); + let _ = _model.created_at(); + let _ = _model.passkey_id(); + let _ = _model.expire(); + let _ = _model.public_key(); + } + + #[test] + fn test_passkey_challenge_serialization() { + let model = ::default(); + let json = serde_json::to_string(&model); + assert!(json.is_ok()); + + let deserialized: Result = serde_json::from_str(&json.unwrap()); + assert!(deserialized.is_ok()); + } +} diff --git a/src/models/passkey_list.rs b/src/models/passkey_list.rs new file mode 100644 index 0000000..8b9e05e --- /dev/null +++ b/src/models/passkey_list.rs @@ -0,0 +1,49 @@ +//! PasskeyList model for Appwrite SDK + +use serde::{Deserialize, Serialize}; + +/// Passkeys List +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(test, derive(Default))] +pub struct PasskeyList { + /// Total number of passkeys that matched your query. + #[serde(rename = "total")] + pub total: i64, + /// List of passkeys. + #[serde(rename = "passkeys")] + pub passkeys: Vec, +} + +impl PasskeyList { + /// Get total + pub fn total(&self) -> &i64 { + &self.total + } + + /// Get passkeys + pub fn passkeys(&self) -> &Vec { + &self.passkeys + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_passkey_list_creation() { + let _model = ::default(); + let _ = _model.total(); + let _ = _model.passkeys(); + } + + #[test] + fn test_passkey_list_serialization() { + let model = ::default(); + let json = serde_json::to_string(&model); + assert!(json.is_ok()); + + let deserialized: Result = serde_json::from_str(&json.unwrap()); + assert!(deserialized.is_ok()); + } +} diff --git a/src/models/token_secret.rs b/src/models/token_secret.rs new file mode 100644 index 0000000..442416e --- /dev/null +++ b/src/models/token_secret.rs @@ -0,0 +1,88 @@ +//! TokenSecret model for Appwrite SDK + +use serde::{Deserialize, Serialize}; + +/// Token Secret +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(test, derive(Default))] +pub struct TokenSecret { + /// Token ID. + #[serde(rename = "$id")] + pub id: String, + /// Token creation date in ISO 8601 format. + #[serde(rename = "$createdAt")] + pub created_at: String, + /// User ID. + #[serde(rename = "userId")] + pub user_id: String, + /// Token secret key. Exchange it for a session with the create session + /// endpoint. + #[serde(rename = "secret")] + pub secret: String, + /// Token expiration date in ISO 8601 format. + #[serde(rename = "expire")] + pub expire: String, + /// Security phrase of a token. Empty if security phrase was not requested when + /// creating a token. It includes randomly generated phrase which is also sent + /// in the external resource such as email. + #[serde(rename = "phrase")] + pub phrase: String, +} + +impl TokenSecret { + /// Get id + pub fn id(&self) -> &String { + &self.id + } + + /// Get created_at + pub fn created_at(&self) -> &String { + &self.created_at + } + + /// Get user_id + pub fn user_id(&self) -> &String { + &self.user_id + } + + /// Get secret + pub fn secret(&self) -> &String { + &self.secret + } + + /// Get expire + pub fn expire(&self) -> &String { + &self.expire + } + + /// Get phrase + pub fn phrase(&self) -> &String { + &self.phrase + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_token_secret_creation() { + let _model = ::default(); + let _ = _model.id(); + let _ = _model.created_at(); + let _ = _model.user_id(); + let _ = _model.secret(); + let _ = _model.expire(); + let _ = _model.phrase(); + } + + #[test] + fn test_token_secret_serialization() { + let model = ::default(); + let json = serde_json::to_string(&model); + assert!(json.is_ok()); + + let deserialized: Result = serde_json::from_str(&json.unwrap()); + assert!(deserialized.is_ok()); + } +} diff --git a/src/services/account.rs b/src/services/account.rs index c3f5004..effe950 100644 --- a/src/services/account.rs +++ b/src/services/account.rs @@ -517,6 +517,187 @@ impl Account { .await } + /// Get the list of verified passkeys registered by the currently logged in + /// user. + pub async fn list_passkeys( + &self, + queries: Option>, + total: Option, + ) -> crate::error::Result { + let mut params = HashMap::new(); + if let Some(value) = queries { + params.insert( + "queries".to_string(), + json!(value.into_iter().map(|s| s.into()).collect::>()), + ); + } + if let Some(value) = total { + params.insert("total".to_string(), json!(value)); + } + let mut api_headers = HashMap::new(); + api_headers.insert("accept".to_string(), "application/json".to_string()); + + let path = "/account/passkeys".to_string(); + + self.client + .call(Method::GET, &path, Some(api_headers), Some(params)) + .await + } + + /// Start registering a passkey for the currently logged in user. The session + /// must have signed in or completed an MFA challenge within the last 10 + /// minutes. Pass the returned `publicKey` options to + /// `navigator.credentials.create()`, then complete the registration with + /// [Update passkey + /// verification](/docs/references/cloud/client-web/account#updatePasskeyVerification). + /// The passkey stays pending until verified, and the challenge expires after 5 + /// minutes. + pub async fn create_passkey( + &self, + passkey_id: Option<&str>, + name: Option<&str>, + ) -> crate::error::Result { + let mut params = HashMap::new(); + if let Some(value) = passkey_id { + params.insert("passkeyId".to_string(), json!(value)); + } + if let Some(value) = name { + params.insert("name".to_string(), json!(value)); + } + let mut api_headers = HashMap::new(); + api_headers.insert("content-type".to_string(), "application/json".to_string()); + api_headers.insert("accept".to_string(), "application/json".to_string()); + + let path = "/account/passkeys".to_string(); + + self.client + .call(Method::POST, &path, Some(api_headers), Some(params)) + .await + } + + /// Get a verified passkey of the currently logged in user by its unique ID. + pub async fn get_passkey( + &self, + passkey_id: impl Into, + ) -> crate::error::Result { + let params = HashMap::new(); + let mut api_headers = HashMap::new(); + api_headers.insert("accept".to_string(), "application/json".to_string()); + + let passkey_id = passkey_id.into(); + if passkey_id.is_empty() { + return Err(crate::error::AppwriteError::new( + 0, + "Missing required parameter: \"passkeyId\"", + None, + String::new(), + )); + } + + let path = "/account/passkeys/{passkeyId}" + .to_string() + .replace("{passkeyId}", &passkey_id); + + self.client + .call(Method::GET, &path, Some(api_headers), Some(params)) + .await + } + + /// Rename a passkey of the currently logged in user. + pub async fn update_passkey( + &self, + passkey_id: impl Into, + name: impl Into, + ) -> crate::error::Result { + let mut params = HashMap::new(); + params.insert("name".to_string(), json!(name.into())); + let mut api_headers = HashMap::new(); + api_headers.insert("content-type".to_string(), "application/json".to_string()); + api_headers.insert("accept".to_string(), "application/json".to_string()); + + let passkey_id = passkey_id.into(); + if passkey_id.is_empty() { + return Err(crate::error::AppwriteError::new( + 0, + "Missing required parameter: \"passkeyId\"", + None, + String::new(), + )); + } + + let path = "/account/passkeys/{passkeyId}" + .to_string() + .replace("{passkeyId}", &passkey_id); + + self.client + .call(Method::PATCH, &path, Some(api_headers), Some(params)) + .await + } + + /// Delete a passkey from the currently logged in user. The passkey can no + /// longer be used to sign in, although it may remain stored on the user's + /// device. The session must have signed in or completed an MFA challenge + /// within the last 10 minutes. + pub async fn delete_passkey(&self, passkey_id: impl Into) -> crate::error::Result<()> { + let params = HashMap::new(); + let mut api_headers = HashMap::new(); + api_headers.insert("content-type".to_string(), "application/json".to_string()); + api_headers.insert("accept".to_string(), "application/json".to_string()); + + let passkey_id = passkey_id.into(); + if passkey_id.is_empty() { + return Err(crate::error::AppwriteError::new( + 0, + "Missing required parameter: \"passkeyId\"", + None, + String::new(), + )); + } + + let path = "/account/passkeys/{passkeyId}" + .to_string() + .replace("{passkeyId}", &passkey_id); + + self.client + .call(Method::DELETE, &path, Some(api_headers), Some(params)) + .await + } + + /// Complete a passkey registration started with [Create + /// passkey](/docs/references/cloud/client-web/account#createPasskey). Pass the + /// JSON form of the credential returned by `navigator.credentials.create()`, + /// for example `credential.toJSON()`. Each registration can be verified once; + /// a failed attempt requires starting it again. + pub async fn update_passkey_verification( + &self, + passkey_id: impl Into, + credential: serde_json::Value, + ) -> crate::error::Result { + let mut params = HashMap::new(); + params.insert("credential".to_string(), json!(credential)); + let mut api_headers = HashMap::new(); + api_headers.insert("content-type".to_string(), "application/json".to_string()); + api_headers.insert("accept".to_string(), "application/json".to_string()); + + let passkey_id = passkey_id.into(); + if passkey_id.is_empty() { + return Err(crate::error::AppwriteError::new( + 0, + "Missing required parameter: \"passkeyId\"", + None, + String::new(), + )); + } + + let path = "/account/passkeys/{passkeyId}/verification" + .to_string() + .replace("{passkeyId}", &passkey_id); + + self.client + .call(Method::PUT, &path, Some(api_headers), Some(params)) + .await + } + /// Update currently logged in user password. For validation, user is required /// to pass in the new password, and the old password. For users created with /// OAuth, Team Invites and Magic URL, oldPassword is optional. @@ -996,6 +1177,56 @@ impl Account { .await } + /// Start signing in with a passkey. No email or user ID is needed: pass the + /// returned `publicKey` options to `navigator.credentials.get()` and the user + /// picks one of their passkeys. Then call [Update passkey + /// token](/docs/references/cloud/client-web/account#updatePasskeyToken) with + /// the credential to receive a token, and exchange it with [Create + /// session](/docs/references/cloud/client-web/account#createSession). The + /// challenge expires after 5 minutes. + pub async fn create_passkey_token( + &self, + ) -> crate::error::Result { + let params = HashMap::new(); + let mut api_headers = HashMap::new(); + api_headers.insert("content-type".to_string(), "application/json".to_string()); + api_headers.insert("accept".to_string(), "application/json".to_string()); + + let path = "/account/tokens/passkey".to_string(); + + self.client + .call(Method::POST, &path, Some(api_headers), Some(params)) + .await + } + + /// Complete a passkey sign-in started with [Create passkey + /// token](/docs/references/cloud/client-web/account#createPasskeyToken). Pass + /// the challenge ID and the JSON form of the credential returned by + /// `navigator.credentials.get()`, for example `credential.toJSON()`. The + /// returned token includes its secret and expires after 1 minute: exchange it + /// for a session with [Create + /// session](/docs/references/cloud/client-web/account#createSession). Passkeys + /// require user verification, so the session also satisfies MFA. Each + /// challenge can only be used once. + pub async fn update_passkey_token( + &self, + challenge_id: impl Into, + credential: serde_json::Value, + ) -> crate::error::Result { + let mut params = HashMap::new(); + params.insert("challengeId".to_string(), json!(challenge_id.into())); + params.insert("credential".to_string(), json!(credential)); + let mut api_headers = HashMap::new(); + api_headers.insert("content-type".to_string(), "application/json".to_string()); + api_headers.insert("accept".to_string(), "application/json".to_string()); + + let path = "/account/tokens/passkey".to_string(); + + self.client + .call(Method::PUT, &path, Some(api_headers), Some(params)) + .await + } + /// Sends the user an SMS with a secret key for creating a session. If the /// provided user ID has not be registered, a new user will be created. Use the /// returned user ID and secret and submit a request to the [POST diff --git a/src/services/project.rs b/src/services/project.rs index e679d34..fc2c163 100644 --- a/src/services/project.rs +++ b/src/services/project.rs @@ -2309,6 +2309,29 @@ impl Project { .await } + /// Set the relying party ID passkeys are bound to: the domain of your + /// application. Passkeys work on the project's web platforms on that domain or + /// its subdomains, and on the domain itself for Apple apps. A localhost web + /// platform enables passkeys for local development without a relying party ID. + /// The relying party ID cannot change while users have passkeys, and any + /// change invalidates ceremonies in progress. + pub async fn update_passkey_policy( + &self, + rp_id: impl Into, + ) -> crate::error::Result { + let mut params = HashMap::new(); + params.insert("rpId".to_string(), json!(rp_id.into())); + let mut api_headers = HashMap::new(); + api_headers.insert("content-type".to_string(), "application/json".to_string()); + api_headers.insert("accept".to_string(), "application/json".to_string()); + + let path = "/project/policies/passkey".to_string(); + + self.client + .call(Method::PATCH, &path, Some(api_headers), Some(params)) + .await + } + /// Updating this policy allows you to control if new passwords are checked /// against most common passwords dictionary. When enabled, and user changes /// their password, password must not be contained in the dictionary. diff --git a/src/services/users.rs b/src/services/users.rs index f381a1e..7cd8e38 100644 --- a/src/services/users.rs +++ b/src/services/users.rs @@ -741,6 +741,170 @@ impl Users { .await } + /// Get the list of verified passkeys registered by a user. + pub async fn list_passkeys( + &self, + user_id: impl Into, + queries: Option>, + total: Option, + ) -> crate::error::Result { + let mut params = HashMap::new(); + if let Some(value) = queries { + params.insert( + "queries".to_string(), + json!(value.into_iter().map(|s| s.into()).collect::>()), + ); + } + if let Some(value) = total { + params.insert("total".to_string(), json!(value)); + } + let mut api_headers = HashMap::new(); + api_headers.insert("accept".to_string(), "application/json".to_string()); + + let user_id = user_id.into(); + if user_id.is_empty() { + return Err(crate::error::AppwriteError::new( + 0, + "Missing required parameter: \"userId\"", + None, + String::new(), + )); + } + + let path = "/users/{userId}/passkeys" + .to_string() + .replace("{userId}", &user_id); + + self.client + .call(Method::GET, &path, Some(api_headers), Some(params)) + .await + } + + /// Get a verified passkey of a user by its unique ID. + pub async fn get_passkey( + &self, + user_id: impl Into, + passkey_id: impl Into, + ) -> crate::error::Result { + let params = HashMap::new(); + let mut api_headers = HashMap::new(); + api_headers.insert("accept".to_string(), "application/json".to_string()); + + let user_id = user_id.into(); + if user_id.is_empty() { + return Err(crate::error::AppwriteError::new( + 0, + "Missing required parameter: \"userId\"", + None, + String::new(), + )); + } + + let passkey_id = passkey_id.into(); + if passkey_id.is_empty() { + return Err(crate::error::AppwriteError::new( + 0, + "Missing required parameter: \"passkeyId\"", + None, + String::new(), + )); + } + + let path = "/users/{userId}/passkeys/{passkeyId}" + .to_string() + .replace("{userId}", &user_id) + .replace("{passkeyId}", &passkey_id); + + self.client + .call(Method::GET, &path, Some(api_headers), Some(params)) + .await + } + + /// Rename a passkey of a user. + pub async fn update_passkey( + &self, + user_id: impl Into, + passkey_id: impl Into, + name: impl Into, + ) -> crate::error::Result { + let mut params = HashMap::new(); + params.insert("name".to_string(), json!(name.into())); + let mut api_headers = HashMap::new(); + api_headers.insert("content-type".to_string(), "application/json".to_string()); + api_headers.insert("accept".to_string(), "application/json".to_string()); + + let user_id = user_id.into(); + if user_id.is_empty() { + return Err(crate::error::AppwriteError::new( + 0, + "Missing required parameter: \"userId\"", + None, + String::new(), + )); + } + + let passkey_id = passkey_id.into(); + if passkey_id.is_empty() { + return Err(crate::error::AppwriteError::new( + 0, + "Missing required parameter: \"passkeyId\"", + None, + String::new(), + )); + } + + let path = "/users/{userId}/passkeys/{passkeyId}" + .to_string() + .replace("{userId}", &user_id) + .replace("{passkeyId}", &passkey_id); + + self.client + .call(Method::PATCH, &path, Some(api_headers), Some(params)) + .await + } + + /// Delete a passkey from a user, for example when a device is lost. The + /// passkey can no longer be used to sign in. + pub async fn delete_passkey( + &self, + user_id: impl Into, + passkey_id: impl Into, + ) -> crate::error::Result<()> { + let params = HashMap::new(); + let mut api_headers = HashMap::new(); + api_headers.insert("content-type".to_string(), "application/json".to_string()); + api_headers.insert("accept".to_string(), "application/json".to_string()); + + let user_id = user_id.into(); + if user_id.is_empty() { + return Err(crate::error::AppwriteError::new( + 0, + "Missing required parameter: \"userId\"", + None, + String::new(), + )); + } + + let passkey_id = passkey_id.into(); + if passkey_id.is_empty() { + return Err(crate::error::AppwriteError::new( + 0, + "Missing required parameter: \"passkeyId\"", + None, + String::new(), + )); + } + + let path = "/users/{userId}/passkeys/{passkeyId}" + .to_string() + .replace("{userId}", &user_id) + .replace("{passkeyId}", &passkey_id); + + self.client + .call(Method::DELETE, &path, Some(api_headers), Some(params)) + .await + } + /// Update the user password by its unique ID. pub async fn update_password( &self,