From 58b9aad357932dc7b131d6a04ae25da089c13fab Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Mon, 12 Oct 2026 17:56:48 +0100 Subject: [PATCH] feat: add passkeys for version 30.3.0 --- CHANGELOG.md | 8 + docs/account.md | 103 ++++++ docs/examples/account/create-passkey-token.md | 15 + docs/examples/account/create-passkey.md | 18 + docs/examples/account/delete-passkey.md | 17 + docs/examples/account/get-passkey.md | 17 + docs/examples/account/list-passkeys.md | 18 + docs/examples/account/update-passkey-token.md | 18 + .../account/update-passkey-verification.md | 18 + docs/examples/account/update-passkey.md | 18 + .../examples/project/update-passkey-policy.md | 17 + docs/examples/users/delete-passkey.md | 18 + docs/examples/users/get-passkey.md | 18 + docs/examples/users/list-passkeys.md | 19 + docs/examples/users/update-passkey.md | 19 + docs/project.md | 13 + docs/users.md | 58 +++ src/Appwrite/Client.php | 4 +- src/Appwrite/Models/Passkey.php | 77 ++++ src/Appwrite/Models/PasskeyChallenge.php | 77 ++++ src/Appwrite/Models/PasskeyList.php | 61 ++++ src/Appwrite/Models/TokenSecret.php | 84 +++++ src/Appwrite/Services/Account.php | 333 ++++++++++++++++++ src/Appwrite/Services/Project.php | 40 +++ src/Appwrite/Services/Users.php | 173 +++++++++ tests/Appwrite/Services/AccountTest.php | 181 ++++++++++ tests/Appwrite/Services/ProjectTest.php | 83 +++++ tests/Appwrite/Services/UsersTest.php | 95 +++++ 28 files changed, 1618 insertions(+), 2 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/Appwrite/Models/Passkey.php create mode 100644 src/Appwrite/Models/PasskeyChallenge.php create mode 100644 src/Appwrite/Models/PasskeyList.php create mode 100644 src/Appwrite/Models/TokenSecret.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c18a87e..343d88fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## 30.3.0 + +* Added: `account` passkey methods `listPasskeys`, `createPasskey`, `getPasskey`, `updatePasskey`, `deletePasskey` and `updatePasskeyVerification` +* Added: `account.createPasskeyToken` and `account.updatePasskeyToken` for passkey sign-in +* Added: `users.listPasskeys`, `users.getPasskey`, `users.updatePasskey` and `users.deletePasskey` +* Added: `project.updatePasskeyPolicy` to configure passkey sign-in +* Added: `Passkey`, `PasskeyList`, `PasskeyChallenge` and `TokenSecret` models + ## 30.2.2 * Fixed: a response model fills a missing field from its spec default instead of throwing `Missing required field`, so the SDK reads responses from servers that predate the field, such as `File.folder` and `File.key` from Appwrite 1.x diff --git a/docs/account.md b/docs/account.md index 1cd83a06..70ae607b 100644 --- a/docs/account.md +++ b/docs/account.md @@ -380,6 +380,88 @@ PATCH https://cloud.appwrite.io/v1/account/name | name | string | User name. Max length: 128 chars. | | +```http request +GET https://cloud.appwrite.io/v1/account/passkeys +``` + +** Get the list of verified passkeys registered by the currently logged in user. ** + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| queries | array | Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter and order on the following attributes: $id, $createdAt, $updatedAt, name, accessedAt | [] | +| total | boolean | When set to false, the total count returned will be 0 and will not be calculated. | 1 | + + +```http request +POST https://cloud.appwrite.io/v1/account/passkeys +``` + +** 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. ** + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| passkeyId | string | Passkey ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars. | unique() | +| name | string | Passkey name, shown when listing passkeys. Max length: 128 chars. | | + + +```http request +GET https://cloud.appwrite.io/v1/account/passkeys/{passkeyId} +``` + +** Get a verified passkey of the currently logged in user by its unique ID. ** + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| passkeyId | string | **Required** Passkey ID. | | + + +```http request +PATCH https://cloud.appwrite.io/v1/account/passkeys/{passkeyId} +``` + +** Rename a passkey of the currently logged in user. ** + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| passkeyId | string | **Required** Passkey ID. | | +| name | string | Passkey name. Max length: 128 chars. | | + + +```http request +DELETE https://cloud.appwrite.io/v1/account/passkeys/{passkeyId} +``` + +** 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. ** + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| passkeyId | string | **Required** Passkey ID. | | + + +```http request +PUT https://cloud.appwrite.io/v1/account/passkeys/{passkeyId}/verification +``` + +** 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. ** + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| passkeyId | string | **Required** Passkey ID. | | +| credential | object | Registration credential returned by the authenticator, in the JSON form produced by `PublicKeyCredential.toJSON()`. | {} | + + ```http request PATCH https://cloud.appwrite.io/v1/account/password ``` @@ -642,6 +724,27 @@ A user is limited to 10 active sessions at a time by default. [Learn more about | state | string | An opaque value your app generates and keeps, for example in a cookie. It is returned unchanged as the `state` query parameter on the success and failure URLs, so your app can check that the sign-in it receives is one it started. Printable ASCII only (RFC 6749 Appendix A.5). Max length: 256 chars. | | +```http request +POST https://cloud.appwrite.io/v1/account/tokens/passkey +``` + +** 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. ** + + +```http request +PUT https://cloud.appwrite.io/v1/account/tokens/passkey +``` + +** 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. ** + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| challengeId | string | Challenge ID returned by createPasskeyToken. | | +| credential | object | Authentication credential returned by the authenticator, in the JSON form produced by `PublicKeyCredential.toJSON()`. | {} | + + ```http request POST https://cloud.appwrite.io/v1/account/tokens/phone ``` diff --git a/docs/examples/account/create-passkey-token.md b/docs/examples/account/create-passkey-token.md new file mode 100644 index 00000000..d1b4ade6 --- /dev/null +++ b/docs/examples/account/create-passkey-token.md @@ -0,0 +1,15 @@ +```php +setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + ->setProject('') // Your project ID + ->setSession(''); // The user session to authenticate with + +$account = new Account($client); + +$result = $account->createPasskeyToken(); +``` diff --git a/docs/examples/account/create-passkey.md b/docs/examples/account/create-passkey.md new file mode 100644 index 00000000..ee648afa --- /dev/null +++ b/docs/examples/account/create-passkey.md @@ -0,0 +1,18 @@ +```php +setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + ->setProject('') // Your project ID + ->setSession(''); // The user session to authenticate with + +$account = new Account($client); + +$result = $account->createPasskey( + passkeyId: '', // optional + name: '' // optional +); +``` diff --git a/docs/examples/account/delete-passkey.md b/docs/examples/account/delete-passkey.md new file mode 100644 index 00000000..ee72432b --- /dev/null +++ b/docs/examples/account/delete-passkey.md @@ -0,0 +1,17 @@ +```php +setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + ->setProject('') // Your project ID + ->setSession(''); // The user session to authenticate with + +$account = new Account($client); + +$result = $account->deletePasskey( + passkeyId: '' +); +``` diff --git a/docs/examples/account/get-passkey.md b/docs/examples/account/get-passkey.md new file mode 100644 index 00000000..b5bd8a91 --- /dev/null +++ b/docs/examples/account/get-passkey.md @@ -0,0 +1,17 @@ +```php +setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + ->setProject('') // Your project ID + ->setSession(''); // The user session to authenticate with + +$account = new Account($client); + +$result = $account->getPasskey( + passkeyId: '' +); +``` diff --git a/docs/examples/account/list-passkeys.md b/docs/examples/account/list-passkeys.md new file mode 100644 index 00000000..f7e8f974 --- /dev/null +++ b/docs/examples/account/list-passkeys.md @@ -0,0 +1,18 @@ +```php +setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + ->setProject('') // Your project ID + ->setSession(''); // The user session to authenticate with + +$account = new Account($client); + +$result = $account->listPasskeys( + queries: [], // optional + total: false // optional +); +``` diff --git a/docs/examples/account/update-passkey-token.md b/docs/examples/account/update-passkey-token.md new file mode 100644 index 00000000..d335946b --- /dev/null +++ b/docs/examples/account/update-passkey-token.md @@ -0,0 +1,18 @@ +```php +setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + ->setProject('') // Your project ID + ->setSession(''); // The user session to authenticate with + +$account = new Account($client); + +$result = $account->updatePasskeyToken( + challengeId: '', + credential: [] +); +``` diff --git a/docs/examples/account/update-passkey-verification.md b/docs/examples/account/update-passkey-verification.md new file mode 100644 index 00000000..2e386b42 --- /dev/null +++ b/docs/examples/account/update-passkey-verification.md @@ -0,0 +1,18 @@ +```php +setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + ->setProject('') // Your project ID + ->setSession(''); // The user session to authenticate with + +$account = new Account($client); + +$result = $account->updatePasskeyVerification( + passkeyId: '', + credential: [] +); +``` diff --git a/docs/examples/account/update-passkey.md b/docs/examples/account/update-passkey.md new file mode 100644 index 00000000..59b97d4b --- /dev/null +++ b/docs/examples/account/update-passkey.md @@ -0,0 +1,18 @@ +```php +setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + ->setProject('') // Your project ID + ->setSession(''); // The user session to authenticate with + +$account = new Account($client); + +$result = $account->updatePasskey( + passkeyId: '', + name: '' +); +``` diff --git a/docs/examples/project/update-passkey-policy.md b/docs/examples/project/update-passkey-policy.md new file mode 100644 index 00000000..5bbfee52 --- /dev/null +++ b/docs/examples/project/update-passkey-policy.md @@ -0,0 +1,17 @@ +```php +setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + ->setProject('') // Your project ID + ->setKey(''); // Your secret API key + +$project = new Project($client); + +$result = $project->updatePasskeyPolicy( + rpId: '' +); +``` diff --git a/docs/examples/users/delete-passkey.md b/docs/examples/users/delete-passkey.md new file mode 100644 index 00000000..b7a138fa --- /dev/null +++ b/docs/examples/users/delete-passkey.md @@ -0,0 +1,18 @@ +```php +setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + ->setProject('') // Your project ID + ->setKey(''); // Your secret API key + +$users = new Users($client); + +$result = $users->deletePasskey( + userId: '', + passkeyId: '' +); +``` diff --git a/docs/examples/users/get-passkey.md b/docs/examples/users/get-passkey.md new file mode 100644 index 00000000..d5da6729 --- /dev/null +++ b/docs/examples/users/get-passkey.md @@ -0,0 +1,18 @@ +```php +setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + ->setProject('') // Your project ID + ->setKey(''); // Your secret API key + +$users = new Users($client); + +$result = $users->getPasskey( + userId: '', + passkeyId: '' +); +``` diff --git a/docs/examples/users/list-passkeys.md b/docs/examples/users/list-passkeys.md new file mode 100644 index 00000000..6fb79493 --- /dev/null +++ b/docs/examples/users/list-passkeys.md @@ -0,0 +1,19 @@ +```php +setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + ->setProject('') // Your project ID + ->setKey(''); // Your secret API key + +$users = new Users($client); + +$result = $users->listPasskeys( + userId: '', + queries: [], // optional + total: false // optional +); +``` diff --git a/docs/examples/users/update-passkey.md b/docs/examples/users/update-passkey.md new file mode 100644 index 00000000..1f7f6ff8 --- /dev/null +++ b/docs/examples/users/update-passkey.md @@ -0,0 +1,19 @@ +```php +setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + ->setProject('') // Your project ID + ->setKey(''); // Your secret API key + +$users = new Users($client); + +$result = $users->updatePasskey( + userId: '', + passkeyId: '', + name: '' +); +``` diff --git a/docs/project.md b/docs/project.md index 85e7a379..1472dbbe 100644 --- a/docs/project.md +++ b/docs/project.md @@ -1235,6 +1235,19 @@ PATCH https://cloud.appwrite.io/v1/project/policies/mfa-factors | custom | boolean | Set to true to allow the custom factor to complete an MFA challenge, or false to disable it. | | +```http request +PATCH https://cloud.appwrite.io/v1/project/policies/passkey +``` + +** 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. ** + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| rpId | string | Relying party ID: the domain of your application, such as `example.com`. Use `localhost` for local development, or an empty string to unset it. | | + + ```http request PATCH https://cloud.appwrite.io/v1/project/policies/password-dictionary ``` diff --git a/docs/users.md b/docs/users.md index 11a02a9a..17e7c54d 100644 --- a/docs/users.md +++ b/docs/users.md @@ -487,6 +487,64 @@ PATCH https://cloud.appwrite.io/v1/users/{userId}/name | name | string | User name. Max length: 128 chars. | | +```http request +GET https://cloud.appwrite.io/v1/users/{userId}/passkeys +``` + +** Get the list of verified passkeys registered by a user. ** + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| userId | string | **Required** User ID. | | +| queries | array | Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of 100 queries are allowed, each 4096 characters long. You may filter and order on the following attributes: $id, $createdAt, $updatedAt, name, accessedAt | [] | +| total | boolean | When set to false, the total count returned will be 0 and will not be calculated. | 1 | + + +```http request +GET https://cloud.appwrite.io/v1/users/{userId}/passkeys/{passkeyId} +``` + +** Get a verified passkey of a user by its unique ID. ** + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| userId | string | **Required** User ID. | | +| passkeyId | string | **Required** Passkey ID. | | + + +```http request +PATCH https://cloud.appwrite.io/v1/users/{userId}/passkeys/{passkeyId} +``` + +** Rename a passkey of a user. ** + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| userId | string | **Required** User ID. | | +| passkeyId | string | **Required** Passkey ID. | | +| name | string | Passkey name. Max length: 128 chars. | | + + +```http request +DELETE https://cloud.appwrite.io/v1/users/{userId}/passkeys/{passkeyId} +``` + +** Delete a passkey from a user, for example when a device is lost. The passkey can no longer be used to sign in. ** + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| userId | string | **Required** User ID. | | +| passkeyId | string | **Required** Passkey ID. | | + + ```http request PATCH https://cloud.appwrite.io/v1/users/{userId}/password ``` diff --git a/src/Appwrite/Client.php b/src/Appwrite/Client.php index 4719250f..cc275d06 100644 --- a/src/Appwrite/Client.php +++ b/src/Appwrite/Client.php @@ -43,11 +43,11 @@ class Client * */ protected array $headers = [ - 'user-agent' => 'AppwritePHPSDK/30.2.2 ()', + 'user-agent' => 'AppwritePHPSDK/30.3.0 ()', 'x-sdk-name' => 'PHP', 'x-sdk-platform' => 'server', 'x-sdk-language' => 'php', - 'x-sdk-version' => '30.2.2', + 'x-sdk-version' => '30.3.0', ]; /** diff --git a/src/Appwrite/Models/Passkey.php b/src/Appwrite/Models/Passkey.php new file mode 100644 index 00000000..19e27794 --- /dev/null +++ b/src/Appwrite/Models/Passkey.php @@ -0,0 +1,77 @@ + $data + */ + public static function from(array $data): static + { + if (!array_key_exists('$id', $data)) { + throw new \InvalidArgumentException('Missing required field "$id" for ' . static::class . '.'); + } + if (!array_key_exists('$createdAt', $data)) { + throw new \InvalidArgumentException('Missing required field "$createdAt" for ' . static::class . '.'); + } + if (!array_key_exists('$updatedAt', $data)) { + throw new \InvalidArgumentException('Missing required field "$updatedAt" for ' . static::class . '.'); + } + if (!array_key_exists('name', $data)) { + throw new \InvalidArgumentException('Missing required field "name" for ' . static::class . '.'); + } + if (!array_key_exists('accessedAt', $data)) { + throw new \InvalidArgumentException('Missing required field "accessedAt" for ' . static::class . '.'); + } + + return new static( + id: $data['$id'], + createdAt: $data['$createdAt'], + updatedAt: $data['$updatedAt'], + name: $data['name'], + accessedAt: $data['accessedAt'] + ); + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + '$id' => static::serializeValue($this->id), + '$createdAt' => static::serializeValue($this->createdAt), + '$updatedAt' => static::serializeValue($this->updatedAt), + 'name' => static::serializeValue($this->name), + 'accessedAt' => static::serializeValue($this->accessedAt) + ]; + } +} diff --git a/src/Appwrite/Models/PasskeyChallenge.php b/src/Appwrite/Models/PasskeyChallenge.php new file mode 100644 index 00000000..bb415366 --- /dev/null +++ b/src/Appwrite/Models/PasskeyChallenge.php @@ -0,0 +1,77 @@ + $data + */ + public static function from(array $data): static + { + if (!array_key_exists('$id', $data)) { + throw new \InvalidArgumentException('Missing required field "$id" for ' . static::class . '.'); + } + if (!array_key_exists('$createdAt', $data)) { + throw new \InvalidArgumentException('Missing required field "$createdAt" for ' . static::class . '.'); + } + if (!array_key_exists('passkeyId', $data)) { + throw new \InvalidArgumentException('Missing required field "passkeyId" for ' . static::class . '.'); + } + if (!array_key_exists('expire', $data)) { + throw new \InvalidArgumentException('Missing required field "expire" for ' . static::class . '.'); + } + if (!array_key_exists('publicKey', $data)) { + throw new \InvalidArgumentException('Missing required field "publicKey" for ' . static::class . '.'); + } + + return new static( + id: $data['$id'], + createdAt: $data['$createdAt'], + passkeyId: $data['passkeyId'], + expire: $data['expire'], + publicKey: $data['publicKey'] + ); + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + '$id' => static::serializeValue($this->id), + '$createdAt' => static::serializeValue($this->createdAt), + 'passkeyId' => static::serializeValue($this->passkeyId), + 'expire' => static::serializeValue($this->expire), + 'publicKey' => static::serializeValue($this->publicKey) + ]; + } +} diff --git a/src/Appwrite/Models/PasskeyList.php b/src/Appwrite/Models/PasskeyList.php new file mode 100644 index 00000000..f9506e55 --- /dev/null +++ b/src/Appwrite/Models/PasskeyList.php @@ -0,0 +1,61 @@ + $passkeys list of passkeys. + */ + public function __construct( + public int $total, + public array $passkeys + ) { + } + + /** + * @param array $data + */ + public static function from(array $data): static + { + if (!array_key_exists('total', $data)) { + throw new \InvalidArgumentException('Missing required field "total" for ' . static::class . '.'); + } + if (!array_key_exists('passkeys', $data)) { + throw new \InvalidArgumentException('Missing required field "passkeys" for ' . static::class . '.'); + } + + return new static( + total: $data['total'], + passkeys: is_array($data['passkeys']) + ? array_map( + static fn (mixed $item): mixed => static::hydrateTypedValue(Passkey::class, $item), + $data['passkeys'] + ) + : $data['passkeys'] + ); + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'total' => static::serializeValue($this->total), + 'passkeys' => static::serializeValue($this->passkeys) + ]; + } +} diff --git a/src/Appwrite/Models/TokenSecret.php b/src/Appwrite/Models/TokenSecret.php new file mode 100644 index 00000000..deedef6b --- /dev/null +++ b/src/Appwrite/Models/TokenSecret.php @@ -0,0 +1,84 @@ + $data + */ + public static function from(array $data): static + { + if (!array_key_exists('$id', $data)) { + throw new \InvalidArgumentException('Missing required field "$id" for ' . static::class . '.'); + } + if (!array_key_exists('$createdAt', $data)) { + throw new \InvalidArgumentException('Missing required field "$createdAt" for ' . static::class . '.'); + } + if (!array_key_exists('userId', $data)) { + throw new \InvalidArgumentException('Missing required field "userId" for ' . static::class . '.'); + } + if (!array_key_exists('secret', $data)) { + throw new \InvalidArgumentException('Missing required field "secret" for ' . static::class . '.'); + } + if (!array_key_exists('expire', $data)) { + throw new \InvalidArgumentException('Missing required field "expire" for ' . static::class . '.'); + } + if (!array_key_exists('phrase', $data)) { + throw new \InvalidArgumentException('Missing required field "phrase" for ' . static::class . '.'); + } + + return new static( + id: $data['$id'], + createdAt: $data['$createdAt'], + userId: $data['userId'], + secret: $data['secret'], + expire: $data['expire'], + phrase: $data['phrase'] + ); + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + '$id' => static::serializeValue($this->id), + '$createdAt' => static::serializeValue($this->createdAt), + 'userId' => static::serializeValue($this->userId), + 'secret' => static::serializeValue($this->secret), + 'expire' => static::serializeValue($this->expire), + 'phrase' => static::serializeValue($this->phrase) + ]; + } +} diff --git a/src/Appwrite/Services/Account.php b/src/Appwrite/Services/Account.php index be2bd027..e3fcb1dc 100644 --- a/src/Appwrite/Services/Account.php +++ b/src/Appwrite/Services/Account.php @@ -861,6 +861,255 @@ public function updateName(string $name): \Appwrite\Models\User return \Appwrite\Models\User::from($response); } + /** + * Get the list of verified passkeys registered by the currently logged in + * user. + * + * @throws AppwriteException + */ + public function listPasskeys(?array $queries = null, ?bool $total = null): \Appwrite\Models\PasskeyList + { + $apiPath = str_replace( + [], + [], + '/account/passkeys' + ); + + $apiParams = []; + + if (!is_null($queries)) { + $apiParams['queries'] = $queries; + } + + if (!is_null($total)) { + $apiParams['total'] = $total; + } + + $apiHeaders = []; + $apiHeaders['X-Appwrite-Project'] = $this->client->getConfig('project'); + $apiHeaders['accept'] = 'application/json'; + + $response = $this->client->call( + Client::METHOD_GET, + $apiPath, + $apiHeaders, + $apiParams + ); + + if (!is_array($response)) { + throw new \UnexpectedValueException('Expected array response when hydrating a response model.'); + } + + return \Appwrite\Models\PasskeyList::from($response); + } + + /** + * 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. + * + * @throws AppwriteException + */ + public function createPasskey(?string $passkeyId = null, ?string $name = null): \Appwrite\Models\PasskeyChallenge + { + $apiPath = str_replace( + [], + [], + '/account/passkeys' + ); + + $apiParams = []; + + if (!is_null($passkeyId)) { + $apiParams['passkeyId'] = $passkeyId; + } + + if (!is_null($name)) { + $apiParams['name'] = $name; + } + + $apiHeaders = []; + $apiHeaders['X-Appwrite-Project'] = $this->client->getConfig('project'); + $apiHeaders['content-type'] = 'application/json'; + $apiHeaders['accept'] = 'application/json'; + + $response = $this->client->call( + Client::METHOD_POST, + $apiPath, + $apiHeaders, + $apiParams + ); + + if (!is_array($response)) { + throw new \UnexpectedValueException('Expected array response when hydrating a response model.'); + } + + return \Appwrite\Models\PasskeyChallenge::from($response); + } + + /** + * Get a verified passkey of the currently logged in user by its unique ID. + * + * @throws AppwriteException + */ + public function getPasskey(string $passkeyId): \Appwrite\Models\Passkey + { + if ($passkeyId === '') { + throw new AppwriteException('Missing required parameter: "passkeyId"'); + } + + $apiPath = str_replace( + ['{passkeyId}'], + [$passkeyId], + '/account/passkeys/{passkeyId}' + ); + + $apiParams = []; + $apiParams['passkeyId'] = $passkeyId; + + $apiHeaders = []; + $apiHeaders['X-Appwrite-Project'] = $this->client->getConfig('project'); + $apiHeaders['accept'] = 'application/json'; + + $response = $this->client->call( + Client::METHOD_GET, + $apiPath, + $apiHeaders, + $apiParams + ); + + if (!is_array($response)) { + throw new \UnexpectedValueException('Expected array response when hydrating a response model.'); + } + + return \Appwrite\Models\Passkey::from($response); + } + + /** + * Rename a passkey of the currently logged in user. + * + * @throws AppwriteException + */ + public function updatePasskey(string $passkeyId, string $name): \Appwrite\Models\Passkey + { + if ($passkeyId === '') { + throw new AppwriteException('Missing required parameter: "passkeyId"'); + } + + $apiPath = str_replace( + ['{passkeyId}'], + [$passkeyId], + '/account/passkeys/{passkeyId}' + ); + + $apiParams = []; + $apiParams['passkeyId'] = $passkeyId; + $apiParams['name'] = $name; + + $apiHeaders = []; + $apiHeaders['X-Appwrite-Project'] = $this->client->getConfig('project'); + $apiHeaders['content-type'] = 'application/json'; + $apiHeaders['accept'] = 'application/json'; + + $response = $this->client->call( + Client::METHOD_PATCH, + $apiPath, + $apiHeaders, + $apiParams + ); + + if (!is_array($response)) { + throw new \UnexpectedValueException('Expected array response when hydrating a response model.'); + } + + return \Appwrite\Models\Passkey::from($response); + } + + /** + * 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. + * + * @throws AppwriteException + */ + public function deletePasskey(string $passkeyId): string + { + if ($passkeyId === '') { + throw new AppwriteException('Missing required parameter: "passkeyId"'); + } + + $apiPath = str_replace( + ['{passkeyId}'], + [$passkeyId], + '/account/passkeys/{passkeyId}' + ); + + $apiParams = []; + $apiParams['passkeyId'] = $passkeyId; + + $apiHeaders = []; + $apiHeaders['X-Appwrite-Project'] = $this->client->getConfig('project'); + $apiHeaders['content-type'] = 'application/json'; + $apiHeaders['accept'] = 'application/json'; + + return $this->client->call( + Client::METHOD_DELETE, + $apiPath, + $apiHeaders, + $apiParams + ); + } + + /** + * 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. + * + * @throws AppwriteException + */ + public function updatePasskeyVerification(string $passkeyId, array $credential): \Appwrite\Models\Passkey + { + if ($passkeyId === '') { + throw new AppwriteException('Missing required parameter: "passkeyId"'); + } + + $apiPath = str_replace( + ['{passkeyId}'], + [$passkeyId], + '/account/passkeys/{passkeyId}/verification' + ); + + $apiParams = []; + $apiParams['passkeyId'] = $passkeyId; + $apiParams['credential'] = $credential; + + $apiHeaders = []; + $apiHeaders['X-Appwrite-Project'] = $this->client->getConfig('project'); + $apiHeaders['content-type'] = 'application/json'; + $apiHeaders['accept'] = 'application/json'; + + $response = $this->client->call( + Client::METHOD_PUT, + $apiPath, + $apiHeaders, + $apiParams + ); + + if (!is_array($response)) { + throw new \UnexpectedValueException('Expected array response when hydrating a response model.'); + } + + return \Appwrite\Models\Passkey::from($response); + } + /** * 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 @@ -1670,6 +1919,90 @@ public function createOAuth2Token(OAuthProvider $provider, ?string $success = nu ); } + /** + * 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. + * + * @throws AppwriteException + */ + public function createPasskeyToken(): \Appwrite\Models\PasskeyChallenge + { + $apiPath = str_replace( + [], + [], + '/account/tokens/passkey' + ); + + $apiParams = []; + + $apiHeaders = []; + $apiHeaders['X-Appwrite-Project'] = $this->client->getConfig('project'); + $apiHeaders['content-type'] = 'application/json'; + $apiHeaders['accept'] = 'application/json'; + + $response = $this->client->call( + Client::METHOD_POST, + $apiPath, + $apiHeaders, + $apiParams + ); + + if (!is_array($response)) { + throw new \UnexpectedValueException('Expected array response when hydrating a response model.'); + } + + return \Appwrite\Models\PasskeyChallenge::from($response); + } + + /** + * 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. + * + * @throws AppwriteException + */ + public function updatePasskeyToken(string $challengeId, array $credential): \Appwrite\Models\TokenSecret + { + $apiPath = str_replace( + [], + [], + '/account/tokens/passkey' + ); + + $apiParams = []; + $apiParams['challengeId'] = $challengeId; + $apiParams['credential'] = $credential; + + $apiHeaders = []; + $apiHeaders['X-Appwrite-Project'] = $this->client->getConfig('project'); + $apiHeaders['content-type'] = 'application/json'; + $apiHeaders['accept'] = 'application/json'; + + $response = $this->client->call( + Client::METHOD_PUT, + $apiPath, + $apiHeaders, + $apiParams + ); + + if (!is_array($response)) { + throw new \UnexpectedValueException('Expected array response when hydrating a response model.'); + } + + return \Appwrite\Models\TokenSecret::from($response); + } + /** * 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 diff --git a/src/Appwrite/Services/Project.php b/src/Appwrite/Services/Project.php index 18292185..367af399 100644 --- a/src/Appwrite/Services/Project.php +++ b/src/Appwrite/Services/Project.php @@ -3361,6 +3361,46 @@ public function updateMFAFactorsPolicy(?bool $totp = null, ?bool $email = null, return \Appwrite\Models\Project::from($response); } + /** + * 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. + * + * @throws AppwriteException + */ + public function updatePasskeyPolicy(string $rpId): \Appwrite\Models\Project + { + $apiPath = str_replace( + [], + [], + '/project/policies/passkey' + ); + + $apiParams = []; + $apiParams['rpId'] = $rpId; + + $apiHeaders = []; + $apiHeaders['X-Appwrite-Project'] = $this->client->getConfig('project'); + $apiHeaders['content-type'] = 'application/json'; + $apiHeaders['accept'] = 'application/json'; + + $response = $this->client->call( + Client::METHOD_PATCH, + $apiPath, + $apiHeaders, + $apiParams + ); + + if (!is_array($response)) { + throw new \UnexpectedValueException('Expected array response when hydrating a response model.'); + } + + return \Appwrite\Models\Project::from($response); + } + /** * Updating this policy allows you to control if new passwords are checked * against most common passwords dictionary. When enabled, and user changes diff --git a/src/Appwrite/Services/Users.php b/src/Appwrite/Services/Users.php index 33754118..b0333df3 100644 --- a/src/Appwrite/Services/Users.php +++ b/src/Appwrite/Services/Users.php @@ -1099,6 +1099,179 @@ public function updateName(string $userId, string $name): \Appwrite\Models\User return \Appwrite\Models\User::from($response); } + /** + * Get the list of verified passkeys registered by a user. + * + * @throws AppwriteException + */ + public function listPasskeys(string $userId, ?array $queries = null, ?bool $total = null): \Appwrite\Models\PasskeyList + { + if ($userId === '') { + throw new AppwriteException('Missing required parameter: "userId"'); + } + + $apiPath = str_replace( + ['{userId}'], + [$userId], + '/users/{userId}/passkeys' + ); + + $apiParams = []; + $apiParams['userId'] = $userId; + + if (!is_null($queries)) { + $apiParams['queries'] = $queries; + } + + if (!is_null($total)) { + $apiParams['total'] = $total; + } + + $apiHeaders = []; + $apiHeaders['X-Appwrite-Project'] = $this->client->getConfig('project'); + $apiHeaders['accept'] = 'application/json'; + + $response = $this->client->call( + Client::METHOD_GET, + $apiPath, + $apiHeaders, + $apiParams + ); + + if (!is_array($response)) { + throw new \UnexpectedValueException('Expected array response when hydrating a response model.'); + } + + return \Appwrite\Models\PasskeyList::from($response); + } + + /** + * Get a verified passkey of a user by its unique ID. + * + * @throws AppwriteException + */ + public function getPasskey(string $userId, string $passkeyId): \Appwrite\Models\Passkey + { + if ($userId === '') { + throw new AppwriteException('Missing required parameter: "userId"'); + } + + if ($passkeyId === '') { + throw new AppwriteException('Missing required parameter: "passkeyId"'); + } + + $apiPath = str_replace( + ['{userId}', '{passkeyId}'], + [$userId, $passkeyId], + '/users/{userId}/passkeys/{passkeyId}' + ); + + $apiParams = []; + $apiParams['userId'] = $userId; + $apiParams['passkeyId'] = $passkeyId; + + $apiHeaders = []; + $apiHeaders['X-Appwrite-Project'] = $this->client->getConfig('project'); + $apiHeaders['accept'] = 'application/json'; + + $response = $this->client->call( + Client::METHOD_GET, + $apiPath, + $apiHeaders, + $apiParams + ); + + if (!is_array($response)) { + throw new \UnexpectedValueException('Expected array response when hydrating a response model.'); + } + + return \Appwrite\Models\Passkey::from($response); + } + + /** + * Rename a passkey of a user. + * + * @throws AppwriteException + */ + public function updatePasskey(string $userId, string $passkeyId, string $name): \Appwrite\Models\Passkey + { + if ($userId === '') { + throw new AppwriteException('Missing required parameter: "userId"'); + } + + if ($passkeyId === '') { + throw new AppwriteException('Missing required parameter: "passkeyId"'); + } + + $apiPath = str_replace( + ['{userId}', '{passkeyId}'], + [$userId, $passkeyId], + '/users/{userId}/passkeys/{passkeyId}' + ); + + $apiParams = []; + $apiParams['userId'] = $userId; + $apiParams['passkeyId'] = $passkeyId; + $apiParams['name'] = $name; + + $apiHeaders = []; + $apiHeaders['X-Appwrite-Project'] = $this->client->getConfig('project'); + $apiHeaders['content-type'] = 'application/json'; + $apiHeaders['accept'] = 'application/json'; + + $response = $this->client->call( + Client::METHOD_PATCH, + $apiPath, + $apiHeaders, + $apiParams + ); + + if (!is_array($response)) { + throw new \UnexpectedValueException('Expected array response when hydrating a response model.'); + } + + return \Appwrite\Models\Passkey::from($response); + } + + /** + * Delete a passkey from a user, for example when a device is lost. The + * passkey can no longer be used to sign in. + * + * @throws AppwriteException + */ + public function deletePasskey(string $userId, string $passkeyId): string + { + if ($userId === '') { + throw new AppwriteException('Missing required parameter: "userId"'); + } + + if ($passkeyId === '') { + throw new AppwriteException('Missing required parameter: "passkeyId"'); + } + + $apiPath = str_replace( + ['{userId}', '{passkeyId}'], + [$userId, $passkeyId], + '/users/{userId}/passkeys/{passkeyId}' + ); + + $apiParams = []; + $apiParams['userId'] = $userId; + $apiParams['passkeyId'] = $passkeyId; + + $apiHeaders = []; + $apiHeaders['X-Appwrite-Project'] = $this->client->getConfig('project'); + $apiHeaders['content-type'] = 'application/json'; + $apiHeaders['accept'] = 'application/json'; + + return $this->client->call( + Client::METHOD_DELETE, + $apiPath, + $apiHeaders, + $apiParams + ); + } + /** * Update the user password by its unique ID. * diff --git a/tests/Appwrite/Services/AccountTest.php b/tests/Appwrite/Services/AccountTest.php index 1e4565d9..33b3f25a 100644 --- a/tests/Appwrite/Services/AccountTest.php +++ b/tests/Appwrite/Services/AccountTest.php @@ -723,6 +723,141 @@ public function testMethodUpdateName(): void $this->assertInstanceOf(\Appwrite\Models\User::class, $response); } + public function testMethodListPasskeys(): void + { + $data = [ + "total" => 5, + "passkeys" => [ + [ + "\$id" => "5e5ea5c16897e", + "\$createdAt" => "2020-10-15T06:38:00.000+00:00", + "\$updatedAt" => "2020-10-15T06:38:00.000+00:00", + "name" => "My laptop", + "accessedAt" => "2020-10-15T06:38:00.000+00:00" + ] + ] + ]; + + $this->client + ->allows()->call(Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any()) + ->andReturn($data); + $this->client + ->allows()->getConfig(Mockery::any()) + ->andReturn(''); + + $response = $this->account->listPasskeys(); + + $this->assertInstanceOf(\Appwrite\Models\PasskeyList::class, $response); + } + public function testMethodCreatePasskey(): void + { + $data = [ + "\$id" => "bb8ea5c16897e", + "\$createdAt" => "2020-10-15T06:38:00.000+00:00", + "passkeyId" => "5e5ea5c16897e", + "expire" => "2020-10-15T06:38:00.000+00:00", + "publicKey" => [] + ]; + + $this->client + ->allows()->call(Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any()) + ->andReturn($data); + $this->client + ->allows()->getConfig(Mockery::any()) + ->andReturn(''); + + $response = $this->account->createPasskey(); + + $this->assertInstanceOf(\Appwrite\Models\PasskeyChallenge::class, $response); + } + public function testMethodGetPasskey(): void + { + $data = [ + "\$id" => "5e5ea5c16897e", + "\$createdAt" => "2020-10-15T06:38:00.000+00:00", + "\$updatedAt" => "2020-10-15T06:38:00.000+00:00", + "name" => "My laptop", + "accessedAt" => "2020-10-15T06:38:00.000+00:00" + ]; + + $this->client + ->allows()->call(Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any()) + ->andReturn($data); + $this->client + ->allows()->getConfig(Mockery::any()) + ->andReturn(''); + + $response = $this->account->getPasskey( + "" + ); + + $this->assertInstanceOf(\Appwrite\Models\Passkey::class, $response); + } + public function testMethodUpdatePasskey(): void + { + $data = [ + "\$id" => "5e5ea5c16897e", + "\$createdAt" => "2020-10-15T06:38:00.000+00:00", + "\$updatedAt" => "2020-10-15T06:38:00.000+00:00", + "name" => "My laptop", + "accessedAt" => "2020-10-15T06:38:00.000+00:00" + ]; + + $this->client + ->allows()->call(Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any()) + ->andReturn($data); + $this->client + ->allows()->getConfig(Mockery::any()) + ->andReturn(''); + + $response = $this->account->updatePasskey( + "", + "" + ); + + $this->assertInstanceOf(\Appwrite\Models\Passkey::class, $response); + } + public function testMethodDeletePasskey(): void + { + $data = ''; + + $this->client + ->allows()->call(Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any()) + ->andReturn($data); + $this->client + ->allows()->getConfig(Mockery::any()) + ->andReturn(''); + + $response = $this->account->deletePasskey( + "" + ); + + $this->assertSame($data, $response); + } + public function testMethodUpdatePasskeyVerification(): void + { + $data = [ + "\$id" => "5e5ea5c16897e", + "\$createdAt" => "2020-10-15T06:38:00.000+00:00", + "\$updatedAt" => "2020-10-15T06:38:00.000+00:00", + "name" => "My laptop", + "accessedAt" => "2020-10-15T06:38:00.000+00:00" + ]; + + $this->client + ->allows()->call(Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any()) + ->andReturn($data); + $this->client + ->allows()->getConfig(Mockery::any()) + ->andReturn(''); + + $response = $this->account->updatePasskeyVerification( + "", + [] + ); + + $this->assertInstanceOf(\Appwrite\Models\Passkey::class, $response); + } public function testMethodUpdatePassword(): void { $data = [ @@ -1448,6 +1583,52 @@ public function testMethodCreateOAuth2Token(): void $this->assertSame($data, $response); } + public function testMethodCreatePasskeyToken(): void + { + $data = [ + "\$id" => "bb8ea5c16897e", + "\$createdAt" => "2020-10-15T06:38:00.000+00:00", + "passkeyId" => "5e5ea5c16897e", + "expire" => "2020-10-15T06:38:00.000+00:00", + "publicKey" => [] + ]; + + $this->client + ->allows()->call(Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any()) + ->andReturn($data); + $this->client + ->allows()->getConfig(Mockery::any()) + ->andReturn(''); + + $response = $this->account->createPasskeyToken(); + + $this->assertInstanceOf(\Appwrite\Models\PasskeyChallenge::class, $response); + } + public function testMethodUpdatePasskeyToken(): void + { + $data = [ + "\$id" => "bb8ea5c16897e", + "\$createdAt" => "2020-10-15T06:38:00.000+00:00", + "userId" => "5e5ea5c168bb8", + "secret" => "[SECRET]", + "expire" => "2020-10-15T06:38:00.000+00:00", + "phrase" => "Golden Fox" + ]; + + $this->client + ->allows()->call(Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any()) + ->andReturn($data); + $this->client + ->allows()->getConfig(Mockery::any()) + ->andReturn(''); + + $response = $this->account->updatePasskeyToken( + "", + [] + ); + + $this->assertInstanceOf(\Appwrite\Models\TokenSecret::class, $response); + } public function testMethodCreatePhoneToken(): void { $data = [ diff --git a/tests/Appwrite/Services/ProjectTest.php b/tests/Appwrite/Services/ProjectTest.php index 0d0b6cb3..80ad0824 100644 --- a/tests/Appwrite/Services/ProjectTest.php +++ b/tests/Appwrite/Services/ProjectTest.php @@ -2434,6 +2434,89 @@ public function testMethodUpdateMFAFactorsPolicy(): void $this->assertInstanceOf(\Appwrite\Models\Project::class, $response); } + public function testMethodUpdatePasskeyPolicy(): void + { + $data = [ + "\$id" => "5e5ea5c16897e", + "\$createdAt" => "2020-10-15T06:38:00.000+00:00", + "\$updatedAt" => "2020-10-15T06:38:00.000+00:00", + "name" => "New Project", + "teamId" => "1592981250", + "region" => "fra", + "devKeys" => [ + [ + "\$id" => "5e5ea5c16897e", + "\$createdAt" => "2020-10-15T06:38:00.000+00:00", + "\$updatedAt" => "2020-10-15T06:38:00.000+00:00", + "name" => "Dev API Key", + "expire" => "2020-10-15T06:38:00.000+00:00", + "secret" => "919c2d18fb5d4...a2ae413da83346ad2", + "accessedAt" => "2020-10-15T06:38:00.000+00:00", + "sdks" => [] + ] + ], + "smtpEnabled" => true, + "smtpSenderName" => "John Appwrite", + "smtpSenderEmail" => "john@appwrite.io", + "smtpReplyToName" => "Support Team", + "smtpReplyToEmail" => "support@appwrite.io", + "smtpHost" => "mail.appwrite.io", + "smtpPort" => 25, + "smtpUsername" => "emailuser", + "smtpPassword" => "smtp-password", + "smtpSecure" => "tls", + "pingCount" => 1, + "pingedAt" => "2020-10-15T06:38:00.000+00:00", + "labels" => [], + "status" => "active", + "onboarding" => [], + "authMethods" => [ + [ + "\$id" => "email-password", + "enabled" => true + ] + ], + "services" => [ + [ + "\$id" => "account", + "enabled" => true + ] + ], + "protocols" => [ + [ + "\$id" => "rest", + "enabled" => true + ] + ], + "blocks" => [ + [ + "\$createdAt" => "2020-10-15T06:38:00.000+00:00", + "resourceType" => "project", + "resourceId" => "5e5ea5c16897e", + "mode" => "readOnly", + "projectName" => "My Project", + "region" => "fra", + "organizationName" => "Acme Inc.", + "organizationId" => "5e5ea5c16897e", + "billingPlan" => "pro" + ] + ], + "consoleAccessedAt" => "2020-10-15T06:38:00.000+00:00" + ]; + + $this->client + ->allows()->call(Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any()) + ->andReturn($data); + $this->client + ->allows()->getConfig(Mockery::any()) + ->andReturn(''); + + $response = $this->project->updatePasskeyPolicy( + "" + ); + + $this->assertInstanceOf(\Appwrite\Models\Project::class, $response); + } public function testMethodUpdatePasswordDictionaryPolicy(): void { $data = [ diff --git a/tests/Appwrite/Services/UsersTest.php b/tests/Appwrite/Services/UsersTest.php index ce365d4f..4179bae7 100644 --- a/tests/Appwrite/Services/UsersTest.php +++ b/tests/Appwrite/Services/UsersTest.php @@ -1016,6 +1016,101 @@ public function testMethodUpdateName(): void $this->assertInstanceOf(\Appwrite\Models\User::class, $response); } + public function testMethodListPasskeys(): void + { + $data = [ + "total" => 5, + "passkeys" => [ + [ + "\$id" => "5e5ea5c16897e", + "\$createdAt" => "2020-10-15T06:38:00.000+00:00", + "\$updatedAt" => "2020-10-15T06:38:00.000+00:00", + "name" => "My laptop", + "accessedAt" => "2020-10-15T06:38:00.000+00:00" + ] + ] + ]; + + $this->client + ->allows()->call(Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any()) + ->andReturn($data); + $this->client + ->allows()->getConfig(Mockery::any()) + ->andReturn(''); + + $response = $this->users->listPasskeys( + "" + ); + + $this->assertInstanceOf(\Appwrite\Models\PasskeyList::class, $response); + } + public function testMethodGetPasskey(): void + { + $data = [ + "\$id" => "5e5ea5c16897e", + "\$createdAt" => "2020-10-15T06:38:00.000+00:00", + "\$updatedAt" => "2020-10-15T06:38:00.000+00:00", + "name" => "My laptop", + "accessedAt" => "2020-10-15T06:38:00.000+00:00" + ]; + + $this->client + ->allows()->call(Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any()) + ->andReturn($data); + $this->client + ->allows()->getConfig(Mockery::any()) + ->andReturn(''); + + $response = $this->users->getPasskey( + "", + "" + ); + + $this->assertInstanceOf(\Appwrite\Models\Passkey::class, $response); + } + public function testMethodUpdatePasskey(): void + { + $data = [ + "\$id" => "5e5ea5c16897e", + "\$createdAt" => "2020-10-15T06:38:00.000+00:00", + "\$updatedAt" => "2020-10-15T06:38:00.000+00:00", + "name" => "My laptop", + "accessedAt" => "2020-10-15T06:38:00.000+00:00" + ]; + + $this->client + ->allows()->call(Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any()) + ->andReturn($data); + $this->client + ->allows()->getConfig(Mockery::any()) + ->andReturn(''); + + $response = $this->users->updatePasskey( + "", + "", + "" + ); + + $this->assertInstanceOf(\Appwrite\Models\Passkey::class, $response); + } + public function testMethodDeletePasskey(): void + { + $data = ''; + + $this->client + ->allows()->call(Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any()) + ->andReturn($data); + $this->client + ->allows()->getConfig(Mockery::any()) + ->andReturn(''); + + $response = $this->users->deletePasskey( + "", + "" + ); + + $this->assertSame($data, $response); + } public function testMethodUpdatePassword(): void { $data = [