From aaaf24a55a51acd501c956fb2a42e0af6230955a Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 22 Nov 2023 16:59:29 +0200 Subject: [PATCH] feat(backend): Introduce OrganizationRoleAPI (#2177) (#2187) --- .changeset/cold-comics-serve.md | 5 + .../src/api/endpoints/OrganizationRoleApi.ts | 138 ++++++++++++++++++ packages/backend/src/api/endpoints/index.ts | 1 + packages/backend/src/api/factory.ts | 2 + packages/backend/src/api/resources/JSON.ts | 11 ++ packages/backend/src/api/resources/Role.ts | 26 ++++ packages/backend/src/api/resources/index.ts | 1 + packages/backend/src/exports.test.ts | 1 + .../__snapshots__/exports.test.ts.snap | 1 + .../__snapshots__/exports.test.ts.snap | 1 + .../__snapshots__/exports.test.ts.snap | 1 + .../__snapshots__/exports.test.ts.snap | 2 + packages/sdk-node/src/index.ts | 2 + 13 files changed, 192 insertions(+) create mode 100644 .changeset/cold-comics-serve.md create mode 100644 packages/backend/src/api/endpoints/OrganizationRoleApi.ts create mode 100644 packages/backend/src/api/resources/Role.ts diff --git a/.changeset/cold-comics-serve.md b/.changeset/cold-comics-serve.md new file mode 100644 index 0000000000..9a81d8d480 --- /dev/null +++ b/.changeset/cold-comics-serve.md @@ -0,0 +1,5 @@ +--- +'@clerk/backend': patch +--- + +Add OrganizationRoleAPI for CRUD operations regarding instance level organization roles. diff --git a/packages/backend/src/api/endpoints/OrganizationRoleApi.ts b/packages/backend/src/api/endpoints/OrganizationRoleApi.ts new file mode 100644 index 0000000000..5385bcc0b7 --- /dev/null +++ b/packages/backend/src/api/endpoints/OrganizationRoleApi.ts @@ -0,0 +1,138 @@ +import { joinPaths } from '../../util/path'; +import type { DeletedObject, Role } from '../resources'; +import { AbstractAPI } from './AbstractApi'; + +const basePath = '/organizations_roles'; + +type GetRoleListParams = { + limit?: number; + offset?: number; + query?: string; + order_by?: string; +}; + +type CreateParams = { + /** + * A name of a role in a readable friendly format. + * F.e. `Teacher` or `Administrator` + */ + name: string; + + /** + * A unique identifier that represents the role. + * F.e. `org:administrator` + */ + key: string; + + /** + * A brief description of what the role represents or its intended use. + */ + description: string; + + /** + * An array of permission ids that will be assigned to this role. + */ + permissions: string[]; +}; + +type GetOrganizationRoleParams = { roleId: string }; + +type UpdateParams = { + /** + * A name of a role in a readable friendly format. + * F.e. `Teacher` or `Administrator` + * Passing undefined has no effect to the existing value. + */ + name?: string; + + /** + * A unique identifier that represents the role. + * F.e. `org:administrator` + * Passing undefined has no effect to the existing value. + */ + key?: string; + + /** + * A brief description of what the role represents or its intended use. + * Passing undefined has no effect to the existing value. + */ + description?: string; + + /** + * An array of permission ids that will be assigned to this role. + * Passing undefined has no effect to the permission that already exist. + * Passing an empty array will override the existing permissions. + */ + permissions?: string[]; +}; + +type RemovePermissionParams = { + permissionId: string; + roleId: string; +}; + +type AssignPermissionParams = RemovePermissionParams; + +export class OrganizationRoleAPI extends AbstractAPI { + public async getOrganizationRoleList(params?: GetRoleListParams) { + return this.request({ + method: 'GET', + path: basePath, + queryParams: params, + }); + } + + public async createOrganizationRole(params: CreateParams) { + return this.request({ + method: 'POST', + path: basePath, + bodyParams: params, + }); + } + + public async getOrganizationRole(params: GetOrganizationRoleParams) { + this.requireId(params.roleId); + + return this.request({ + method: 'GET', + path: joinPaths(basePath, params.roleId), + }); + } + + public async updateOrganizationRole(roleId: string, params: UpdateParams) { + this.requireId(roleId); + return this.request({ + method: 'PATCH', + path: joinPaths(basePath, roleId), + bodyParams: params, + }); + } + + public async deleteOrganizationRole(roleId: string) { + this.requireId(roleId); + return this.request({ + method: 'DELETE', + path: joinPaths(basePath, roleId), + }); + } + + public async assignPermissionToRole(params: AssignPermissionParams) { + const { roleId, permissionId } = params; + this.requireId(roleId); + this.requireId(permissionId); + return this.request({ + method: 'POST', + path: joinPaths(basePath, roleId, 'permission', permissionId), + }); + } + + public async removePermissionFromRole(params: RemovePermissionParams) { + const { roleId, permissionId } = params; + this.requireId(roleId); + this.requireId(permissionId); + return this.request({ + method: 'DELETE', + path: joinPaths(basePath, roleId, 'permission', permissionId), + }); + } +} diff --git a/packages/backend/src/api/endpoints/index.ts b/packages/backend/src/api/endpoints/index.ts index a498b1e835..e0cc684b48 100644 --- a/packages/backend/src/api/endpoints/index.ts +++ b/packages/backend/src/api/endpoints/index.ts @@ -7,6 +7,7 @@ export * from './EmailApi'; export * from './InterstitialApi'; export * from './InvitationApi'; export * from './OrganizationApi'; +export * from './OrganizationRoleApi'; export * from './OrganizationPermissionApi'; export * from './PhoneNumberApi'; export * from './RedirectUrlApi'; diff --git a/packages/backend/src/api/factory.ts b/packages/backend/src/api/factory.ts index e4d887cc24..f4453cc704 100644 --- a/packages/backend/src/api/factory.ts +++ b/packages/backend/src/api/factory.ts @@ -8,6 +8,7 @@ import { InvitationAPI, OrganizationAPI, OrganizationPermissionAPI, + OrganizationRoleAPI, PhoneNumberAPI, RedirectUrlAPI, SessionAPI, @@ -50,6 +51,7 @@ export function createBackendApiClient(options: CreateBackendApiOptions) { interstitial: new InterstitialAPI(request), invitations: new InvitationAPI(request), organizations: new OrganizationAPI(request), + organizationRoles: new OrganizationRoleAPI(request), organizationPermissions: new OrganizationPermissionAPI(request), phoneNumbers: new PhoneNumberAPI(request), redirectUrls: new RedirectUrlAPI(request), diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index 12c0878e78..1b9aa8d62c 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -21,6 +21,7 @@ export enum ObjectType { Organization = 'organization', OrganizationInvitation = 'organization_invitation', OrganizationMembership = 'organization_membership', + Role = 'role', Permission = 'permission', PhoneNumber = 'phone_number', RedirectUrl = 'redirect_url', @@ -186,6 +187,16 @@ export interface OrganizationMembershipPublicUserDataJSON { user_id: string; } +export interface RoleJSON extends ClerkResourceJSON { + object: ObjectType.Role; + name: string; + key: string; + description: string; + permissions: PermissionJSON[]; + created_at: number; + updated_at: number; +} + export interface PermissionJSON extends ClerkResourceJSON { object: ObjectType.Permission; id: string; diff --git a/packages/backend/src/api/resources/Role.ts b/packages/backend/src/api/resources/Role.ts new file mode 100644 index 0000000000..cd26153146 --- /dev/null +++ b/packages/backend/src/api/resources/Role.ts @@ -0,0 +1,26 @@ +import type { RoleJSON } from './JSON'; +import { Permission } from './Permission'; + +export class Role { + constructor( + readonly id: string, + readonly name: string, + readonly key: string, + readonly description: string, + readonly permissions: Permission[] = [], + readonly createdAt: number, + readonly updatedAt: number, + ) {} + + static fromJSON(data: RoleJSON): Role { + return new Role( + data.id, + data.name, + data.key, + data.description, + (data.permissions || []).map(x => Permission.fromJSON(x)), + data.created_at, + data.updated_at, + ); + } +} diff --git a/packages/backend/src/api/resources/index.ts b/packages/backend/src/api/resources/index.ts index 14d0367f98..6c95ba9cb3 100644 --- a/packages/backend/src/api/resources/index.ts +++ b/packages/backend/src/api/resources/index.ts @@ -22,6 +22,7 @@ export * from './Invitation'; export * from './JSON'; export * from './OauthAccessToken'; export * from './Organization'; +export * from './Role'; export * from './Permission'; export * from './OrganizationInvitation'; export * from './OrganizationMembership'; diff --git a/packages/backend/src/exports.test.ts b/packages/backend/src/exports.test.ts index dc98d732c7..98d1218687 100644 --- a/packages/backend/src/exports.test.ts +++ b/packages/backend/src/exports.test.ts @@ -27,6 +27,7 @@ export default (QUnit: QUnit) => { 'Permission', 'PhoneNumber', 'RedirectUrl', + 'Role', 'SMSMessage', 'Session', 'SignInToken', diff --git a/packages/nextjs/src/api/__tests__/__snapshots__/exports.test.ts.snap b/packages/nextjs/src/api/__tests__/__snapshots__/exports.test.ts.snap index c40f5106b4..b3288acdd6 100644 --- a/packages/nextjs/src/api/__tests__/__snapshots__/exports.test.ts.snap +++ b/packages/nextjs/src/api/__tests__/__snapshots__/exports.test.ts.snap @@ -21,6 +21,7 @@ exports[`/api public exports should not include a breaking change 1`] = ` "Permission", "PhoneNumber", "RedirectUrl", + "Role", "SMSMessage", "Session", "SignInToken", diff --git a/packages/nextjs/src/edge-middleware/__tests__/__snapshots__/exports.test.ts.snap b/packages/nextjs/src/edge-middleware/__tests__/__snapshots__/exports.test.ts.snap index a29f3fcecd..e7af66329d 100644 --- a/packages/nextjs/src/edge-middleware/__tests__/__snapshots__/exports.test.ts.snap +++ b/packages/nextjs/src/edge-middleware/__tests__/__snapshots__/exports.test.ts.snap @@ -21,6 +21,7 @@ exports[`/edge-middleware public exports should not include a breaking change 1` "Permission", "PhoneNumber", "RedirectUrl", + "Role", "SMSMessage", "Session", "SignInToken", diff --git a/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap b/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap index 5841225076..4888c8db52 100644 --- a/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap +++ b/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap @@ -32,6 +32,7 @@ exports[`/server public exports should not include a breaking change 1`] = ` "Permission", "PhoneNumber", "RedirectUrl", + "Role", "SECRET_KEY", "SIGN_IN_URL", "SIGN_UP_URL", diff --git a/packages/sdk-node/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/sdk-node/src/__tests__/__snapshots__/exports.test.ts.snap index b1e1aa246b..0fba806185 100644 --- a/packages/sdk-node/src/__tests__/__snapshots__/exports.test.ts.snap +++ b/packages/sdk-node/src/__tests__/__snapshots__/exports.test.ts.snap @@ -23,6 +23,7 @@ exports[`module exports should not change unless explicitly set 1`] = ` "Permission", "PhoneNumber", "RedirectUrl", + "Role", "SMSMessage", "Session", "SignInToken", @@ -51,6 +52,7 @@ exports[`module exports should not change unless explicitly set 1`] = ` "loadInterstitialFromLocal", "makeAuthObjectSerializable", "organizationPermissions", + "organizationRoles", "organizations", "phoneNumbers", "prunePrivateMetadata", diff --git a/packages/sdk-node/src/index.ts b/packages/sdk-node/src/index.ts index 425e14776d..4fc05faabb 100644 --- a/packages/sdk-node/src/index.ts +++ b/packages/sdk-node/src/index.ts @@ -47,6 +47,7 @@ const { emails, invitations, organizations, + organizationRoles, organizationPermissions, clients, allowlistIdentifiers, @@ -62,6 +63,7 @@ export { emails, invitations, organizations, + organizationRoles, organizationPermissions, clients, allowlistIdentifiers,