From 6a2f36f8199f91f9e893e2ff571a4182c3f70a6e Mon Sep 17 00:00:00 2001 From: Tom Milewski Date: Thu, 10 Apr 2025 15:37:05 -0400 Subject: [PATCH 1/2] feat(backend): Adds user-centric functionality to Backend API client --- .changeset/thin-foxes-exist.md | 31 +++++++ packages/backend/src/api/endpoints/UserApi.ts | 90 ++++++++++++++++++- packages/backend/src/api/resources/JSON.ts | 3 + .../api/resources/OrganizationInvitation.ts | 10 ++- 4 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 .changeset/thin-foxes-exist.md diff --git a/.changeset/thin-foxes-exist.md b/.changeset/thin-foxes-exist.md new file mode 100644 index 00000000000..7d0e4fe5c37 --- /dev/null +++ b/.changeset/thin-foxes-exist.md @@ -0,0 +1,31 @@ +--- +'@clerk/backend': patch +--- + +Adds the following User-centric functionality to the Backend API client. + + +```ts + import { createClerkClient } from '@clerk/backend'; + + const clerkClient = createClerkClient(...); + + await clerkClient.users.getOrganizationInvitationList({ + userId: 'user_xxxxxx', + status: 'pending', + }); + await clerkClient.actorTokens.deleteUserPasskey({ + userId: 'user_xxxxxx', + passkeyIdentificationId: 'xxxxxxx', + }); + await clerkClient.actorTokens.deleteUserWeb3Wallet({ + userId: 'user_xxxxxx', + web3WalletIdentificationId: 'xxxxxxx', + }); + await clerkClient.actorTokens.deleteUserExternalAccount({ + userId: 'user_xxxxxx', + externalAccountId: 'xxxxxxx', + }); + await clerkClient.actorTokens.deleteUserBackupCodes('user_xxxxxx'); + await clerkClient.actorTokens.deleteUserTOTP('user_xxxxxx'); +``` \ No newline at end of file diff --git a/packages/backend/src/api/endpoints/UserApi.ts b/packages/backend/src/api/endpoints/UserApi.ts index 85bd03f0c14..fa9c65fe4fe 100644 --- a/packages/backend/src/api/endpoints/UserApi.ts +++ b/packages/backend/src/api/endpoints/UserApi.ts @@ -1,9 +1,15 @@ -import type { ClerkPaginationRequest, OAuthProvider } from '@clerk/types'; +import type { ClerkPaginationRequest, OAuthProvider, OrganizationInvitationStatus } from '@clerk/types'; import { runtime } from '../../runtime'; import { joinPaths } from '../../util/path'; import { deprecated } from '../../util/shared'; -import type { OauthAccessToken, OrganizationMembership, User } from '../resources'; +import type { + DeletedObject, + OauthAccessToken, + OrganizationInvitation, + OrganizationMembership, + User, +} from '../resources'; import type { PaginatedResourceResponse } from '../resources/Deserializer'; import { AbstractAPI } from './AbstractApi'; import type { WithSign } from './util-types'; @@ -110,6 +116,11 @@ type GetOrganizationMembershipListParams = ClerkPaginationRequest<{ userId: string; }>; +type GetOrganizationInvitationListParams = ClerkPaginationRequest<{ + userId: string; + status?: OrganizationInvitationStatus; +}>; + type VerifyPasswordParams = { userId: string; password: string; @@ -120,6 +131,25 @@ type VerifyTOTPParams = { code: string; }; +type DeleteUserPasskeyParams = { + userId: string; + passkeyIdentificationId: string; +}; + +type DeleteWeb3WalletParams = { + userId: string; + web3WalletIdentificationId: string; +}; + +type DeleteUserExternalAccountParams = { + userId: string; + externalAccountId: string; +}; + +type UserID = { + userId: string; +}; + export class UserAPI extends AbstractAPI { public async getUserList(params: UserListParams = {}) { const { limit, offset, orderBy, ...userCountParams } = params; @@ -232,7 +262,7 @@ export class UserAPI extends AbstractAPI { public async disableUserMFA(userId: string) { this.requireId(userId); - return this.request({ + return this.request({ method: 'DELETE', path: joinPaths(basePath, userId, 'mfa'), }); @@ -249,6 +279,17 @@ export class UserAPI extends AbstractAPI { }); } + public async getOrganizationInvitationList(params: GetOrganizationInvitationListParams) { + const { userId, ...queryParams } = params; + this.requireId(userId); + + return this.request>({ + method: 'GET', + path: joinPaths(basePath, userId, 'organization_invitations'), + queryParams, + }); + } + public async verifyPassword(params: VerifyPasswordParams) { const { userId, password } = params; this.requireId(userId); @@ -310,4 +351,47 @@ export class UserAPI extends AbstractAPI { path: joinPaths(basePath, userId, 'profile_image'), }); } + + public async deleteUserPasskey(params: DeleteUserPasskeyParams) { + this.requireId(params.userId); + this.requireId(params.passkeyIdentificationId); + return this.request({ + method: 'DELETE', + path: joinPaths(basePath, params.userId, 'passkeys', params.passkeyIdentificationId), + }); + } + + public async deleteUserWeb3Wallet(params: DeleteWeb3WalletParams) { + this.requireId(params.userId); + this.requireId(params.web3WalletIdentificationId); + return this.request({ + method: 'DELETE', + path: joinPaths(basePath, params.userId, 'web3_wallets', params.web3WalletIdentificationId), + }); + } + + public async deleteUserExternalAccount(params: DeleteUserExternalAccountParams) { + this.requireId(params.userId); + this.requireId(params.externalAccountId); + return this.request({ + method: 'DELETE', + path: joinPaths(basePath, params.userId, 'external_accounts', params.externalAccountId), + }); + } + + public async deleteUserBackupCodes(userId: string) { + this.requireId(userId); + return this.request({ + method: 'DELETE', + path: joinPaths(basePath, userId, 'backup_code'), + }); + } + + public async deleteUserTOTP(userId: string) { + this.requireId(userId); + return this.request({ + method: 'DELETE', + path: joinPaths(basePath, userId, 'totp'), + }); + } } diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index 5ad5f0cc078..0f5ed01623d 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -207,13 +207,16 @@ export interface OrganizationDomainVerificationJSON { export interface OrganizationInvitationJSON extends ClerkResourceJSON { email_address: string; role: OrganizationMembershipRole; + role_name: string; organization_id: string; public_organization_data?: PublicOrganizationDataJSON | null; status?: OrganizationInvitationStatus; public_metadata: OrganizationInvitationPublicMetadata; private_metadata: OrganizationInvitationPrivateMetadata; + url: string | null; created_at: number; updated_at: number; + expires_at: number; } export interface PublicOrganizationDataJSON extends ClerkResourceJSON { diff --git a/packages/backend/src/api/resources/OrganizationInvitation.ts b/packages/backend/src/api/resources/OrganizationInvitation.ts index 1a0943111e5..6d6c7d9ee1f 100644 --- a/packages/backend/src/api/resources/OrganizationInvitation.ts +++ b/packages/backend/src/api/resources/OrganizationInvitation.ts @@ -1,5 +1,5 @@ import type { OrganizationInvitationStatus, OrganizationMembershipRole } from './Enums'; -import type { OrganizationInvitationJSON } from './JSON'; +import type { OrganizationInvitationJSON, PublicOrganizationDataJSON } from './JSON'; export class OrganizationInvitation { private _raw: OrganizationInvitationJSON | null = null; @@ -12,12 +12,16 @@ export class OrganizationInvitation { readonly id: string, readonly emailAddress: string, readonly role: OrganizationMembershipRole, + readonly roleName: string, readonly organizationId: string, readonly createdAt: number, readonly updatedAt: number, + readonly expiresAt: number, + readonly url: string | null, readonly status?: OrganizationInvitationStatus, readonly publicMetadata: OrganizationInvitationPublicMetadata = {}, readonly privateMetadata: OrganizationInvitationPrivateMetadata = {}, + readonly publicOrganizationData?: PublicOrganizationDataJSON | null, ) {} static fromJSON(data: OrganizationInvitationJSON) { @@ -25,12 +29,16 @@ export class OrganizationInvitation { data.id, data.email_address, data.role, + data.role_name, data.organization_id, data.created_at, data.updated_at, + data.expires_at, + data.url, data.status, data.public_metadata, data.private_metadata, + data.public_organization_data, ); res._raw = data; return res; From 62fc7cb535fdfdabefe2999262018ae46790ad7e Mon Sep 17 00:00:00 2001 From: Tom Milewski Date: Thu, 10 Apr 2025 18:26:31 -0400 Subject: [PATCH 2/2] chore: Update changeset --- .changeset/thin-foxes-exist.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.changeset/thin-foxes-exist.md b/.changeset/thin-foxes-exist.md index 7d0e4fe5c37..14f7390acc0 100644 --- a/.changeset/thin-foxes-exist.md +++ b/.changeset/thin-foxes-exist.md @@ -14,18 +14,18 @@ Adds the following User-centric functionality to the Backend API client. userId: 'user_xxxxxx', status: 'pending', }); - await clerkClient.actorTokens.deleteUserPasskey({ + await clerkClient.users.deleteUserPasskey({ userId: 'user_xxxxxx', passkeyIdentificationId: 'xxxxxxx', }); - await clerkClient.actorTokens.deleteUserWeb3Wallet({ + await clerkClient.users.deleteUserWeb3Wallet({ userId: 'user_xxxxxx', web3WalletIdentificationId: 'xxxxxxx', }); - await clerkClient.actorTokens.deleteUserExternalAccount({ + await clerkClient.users.deleteUserExternalAccount({ userId: 'user_xxxxxx', externalAccountId: 'xxxxxxx', }); - await clerkClient.actorTokens.deleteUserBackupCodes('user_xxxxxx'); - await clerkClient.actorTokens.deleteUserTOTP('user_xxxxxx'); + await clerkClient.users.deleteUserBackupCodes('user_xxxxxx'); + await clerkClient.users.deleteUserTOTP('user_xxxxxx'); ``` \ No newline at end of file