From 5f5bf7eaf75bf5296fb603db3f4f3cb85e7b126a Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 23 Oct 2023 22:24:48 +0300 Subject: [PATCH 01/26] feat(nextjs,shared,backend,clerk-react): Support permissions in Gate --- packages/backend/src/tokens/authObjects.ts | 34 +++++++++++++++---- .../app-router/server/controlComponents.tsx | 4 +-- .../src/components/controlComponents.tsx | 4 +-- packages/react/src/contexts/AuthContext.ts | 4 ++- .../src/contexts/ClerkContextProvider.tsx | 5 +-- packages/react/src/hooks/useAuth.ts | 17 ++++++---- packages/react/src/utils/deriveState.ts | 4 +++ packages/types/src/jwtv2.ts | 8 ++++- packages/types/src/session.ts | 26 ++++++++++---- packages/types/src/ssr.ts | 6 ++-- 10 files changed, 83 insertions(+), 29 deletions(-) diff --git a/packages/backend/src/tokens/authObjects.ts b/packages/backend/src/tokens/authObjects.ts index 54e57809f8..a7998c2fde 100644 --- a/packages/backend/src/tokens/authObjects.ts +++ b/packages/backend/src/tokens/authObjects.ts @@ -1,7 +1,8 @@ import type { ActClaim, - experimental__CheckAuthorizationWithoutPermission, + experimental__CheckAuthorizationWithCustomPermissions, JwtPayload, + OrganizationPermission, ServerGetToken, ServerGetTokenOptions, } from '@clerk/types'; @@ -30,12 +31,14 @@ export type SignedInAuthObject = { orgId: string | undefined; orgRole: string | undefined; orgSlug: string | undefined; + // TODO(@panteliselef): Typesafe + orgPermissions: OrganizationPermission[] | undefined; organization: Organization | undefined; getToken: ServerGetToken; /** * @experimental The method is experimental and subject to change in future releases. */ - experimental__has: experimental__CheckAuthorizationWithoutPermission; + experimental__has: experimental__CheckAuthorizationWithCustomPermissions; debug: AuthObjectDebug; }; @@ -49,12 +52,13 @@ export type SignedOutAuthObject = { orgId: null; orgRole: null; orgSlug: null; + orgPermissions: null; organization: null; getToken: ServerGetToken; /** * @experimental The method is experimental and subject to change in future releases. */ - experimental__has: experimental__CheckAuthorizationWithoutPermission; + experimental__has: experimental__CheckAuthorizationWithCustomPermissions; debug: AuthObjectDebug; }; @@ -80,6 +84,7 @@ export function signedInAuthObject( org_id: orgId, org_role: orgRole, org_slug: orgSlug, + org_permissions: orgPermissions, sub: userId, } = sessionClaims; const { secretKey, apiUrl, apiVersion, token, session, user, organization } = options; @@ -105,9 +110,10 @@ export function signedInAuthObject( orgId, orgRole, orgSlug, + orgPermissions, organization, getToken, - experimental__has: createHasAuthorization({ orgId, orgRole, userId }), + experimental__has: createHasAuthorization({ orgId, orgRole, orgPermissions, userId }), debug: createDebug({ ...options, ...debugData }), }; } @@ -123,6 +129,7 @@ export function signedOutAuthObject(debugData?: AuthObjectDebugData): SignedOutA orgId: null, orgRole: null, orgSlug: null, + orgPermissions: null, organization: null, getToken: () => Promise.resolve(null), experimental__has: () => false, @@ -200,22 +207,37 @@ const createHasAuthorization = orgId, orgRole, userId, + orgPermissions, }: { userId: string; orgId: string | undefined; orgRole: string | undefined; - }): experimental__CheckAuthorizationWithoutPermission => + orgPermissions: string[] | undefined; + }): experimental__CheckAuthorizationWithCustomPermissions => params => { - if (!orgId || !userId) { + // console.log({ + // orgId, + // userId, + // orgRole, + // orgPermissions, + // }); + if (!orgId || !userId || !orgPermissions) { return false; } + if (params.permission) { + return orgPermissions.includes(params.permission); + } + if (params.role) { return orgRole === params.role; } if (params.some) { return !!params.some.find(permObj => { + if (permObj.permission) { + return orgPermissions.includes(permObj.permission); + } if (permObj.role) { return orgRole === permObj.role; } diff --git a/packages/nextjs/src/app-router/server/controlComponents.tsx b/packages/nextjs/src/app-router/server/controlComponents.tsx index ac506a1ec8..8805d736bb 100644 --- a/packages/nextjs/src/app-router/server/controlComponents.tsx +++ b/packages/nextjs/src/app-router/server/controlComponents.tsx @@ -1,4 +1,4 @@ -import type { experimental__CheckAuthorizationWithoutPermission } from '@clerk/types'; +import type { experimental__CheckAuthorizationWithCustomPermissions } from '@clerk/types'; import { redirect } from 'next/navigation'; import React from 'react'; @@ -17,7 +17,7 @@ export function SignedOut(props: React.PropsWithChildren) { } type GateServerComponentProps = React.PropsWithChildren< - Parameters[0] & { + Parameters[0] & { fallback?: React.ReactNode; redirectTo?: string; } diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx index 983a647210..138857dd6c 100644 --- a/packages/react/src/components/controlComponents.tsx +++ b/packages/react/src/components/controlComponents.tsx @@ -1,4 +1,4 @@ -import type { experimental__CheckAuthorizationWithoutPermission, HandleOAuthCallbackParams } from '@clerk/types'; +import type { experimental__CheckAuthorizationWithCustomPermissions, HandleOAuthCallbackParams } from '@clerk/types'; import React from 'react'; import { useAuthContext } from '../contexts/AuthContext'; @@ -42,7 +42,7 @@ export const ClerkLoading = ({ children }: React.PropsWithChildren): JS }; type GateProps = React.PropsWithChildren< - Parameters[0] & { + Parameters[0] & { fallback?: React.ReactNode; } >; diff --git a/packages/react/src/contexts/AuthContext.ts b/packages/react/src/contexts/AuthContext.ts index ce346388e4..40808c9cd8 100644 --- a/packages/react/src/contexts/AuthContext.ts +++ b/packages/react/src/contexts/AuthContext.ts @@ -1,5 +1,5 @@ import { createContextAndHook } from '@clerk/shared/react'; -import type { ActJWTClaim, MembershipRole } from '@clerk/types'; +import type { ActJWTClaim, Autocomplete, MembershipRole, OrganizationPermission } from '@clerk/types'; export const [AuthContext, useAuthContext] = createContextAndHook<{ userId: string | null | undefined; @@ -8,4 +8,6 @@ export const [AuthContext, useAuthContext] = createContextAndHook<{ orgId: string | null | undefined; orgRole: MembershipRole | null | undefined; orgSlug: string | null | undefined; + // TODO(@panteliselef): Typesafe + orgPermissions: Autocomplete[] | null | undefined; }>('AuthContext'); diff --git a/packages/react/src/contexts/ClerkContextProvider.tsx b/packages/react/src/contexts/ClerkContextProvider.tsx index 9cd14735eb..a31bcb18ed 100644 --- a/packages/react/src/contexts/ClerkContextProvider.tsx +++ b/packages/react/src/contexts/ClerkContextProvider.tsx @@ -35,10 +35,11 @@ export function ClerkContextProvider(props: ClerkContextProvider): JSX.Element | const clerkCtx = React.useMemo(() => ({ value: clerk }), [clerkLoaded]); const clientCtx = React.useMemo(() => ({ value: state.client }), [state.client]); - const { sessionId, session, userId, user, orgId, actor, organization, orgRole, orgSlug } = derivedState; + const { sessionId, session, userId, user, orgId, actor, organization, orgRole, orgSlug, orgPermissions } = + derivedState; const authCtx = React.useMemo(() => { - const value = { sessionId, userId, actor, orgId, orgRole, orgSlug }; + const value = { sessionId, userId, actor, orgId, orgRole, orgSlug, orgPermissions }; return { value }; }, [sessionId, userId, actor, orgId, orgRole, orgSlug]); const userCtx = React.useMemo(() => ({ value: user }), [userId, user]); diff --git a/packages/react/src/hooks/useAuth.ts b/packages/react/src/hooks/useAuth.ts index d1f735fea5..4fa51ba45a 100644 --- a/packages/react/src/hooks/useAuth.ts +++ b/packages/react/src/hooks/useAuth.ts @@ -1,6 +1,6 @@ import type { ActJWTClaim, - experimental__CheckAuthorizationWithoutPermission, + experimental__CheckAuthorizationWithCustomPermissions, GetToken, MembershipRole, SignOut, @@ -14,7 +14,7 @@ import { errorThrower } from '../utils'; import { createGetToken, createSignOut } from './utils'; type experimental__CheckAuthorizationSignedOut = ( - params?: Parameters[0], + params?: Parameters[0], ) => false; type UseAuthReturn = @@ -78,7 +78,7 @@ type UseAuthReturn = /** * @experimental The method is experimental and subject to change in future releases. */ - experimental__has: experimental__CheckAuthorizationWithoutPermission; + experimental__has: experimental__CheckAuthorizationWithCustomPermissions; signOut: SignOut; getToken: GetToken; }; @@ -122,15 +122,15 @@ type UseAuth = () => UseAuthReturn; * } */ export const useAuth: UseAuth = () => { - const { sessionId, userId, actor, orgId, orgRole, orgSlug } = useAuthContext(); + const { sessionId, userId, actor, orgId, orgRole, orgSlug, orgPermissions } = useAuthContext(); const isomorphicClerk = useIsomorphicClerkContext(); const getToken: GetToken = useCallback(createGetToken(isomorphicClerk), [isomorphicClerk]); const signOut: SignOut = useCallback(createSignOut(isomorphicClerk), [isomorphicClerk]); const has = useCallback( - (params?: Parameters[0]) => { - if (!orgId || !userId || !orgRole) { + (params?: Parameters[0]) => { + if (!orgId || !userId || !orgRole || !orgPermissions) { return false; } @@ -138,12 +138,15 @@ export const useAuth: UseAuth = () => { return false; } + if (params.permission) { + return orgPermissions.includes(params.permission); + } if (params.role) { return orgRole === params.role; } return false; }, - [orgId, orgRole, userId], + [orgId, orgRole, userId, orgPermissions], ); if (sessionId === undefined && userId === undefined) { diff --git a/packages/react/src/utils/deriveState.ts b/packages/react/src/utils/deriveState.ts index d699efcaf9..00117b9473 100644 --- a/packages/react/src/utils/deriveState.ts +++ b/packages/react/src/utils/deriveState.ts @@ -2,6 +2,7 @@ import type { ActiveSessionResource, InitialState, MembershipRole, + OrganizationPermission, OrganizationResource, Resources, UserResource, @@ -22,6 +23,8 @@ const deriveFromSsrInitialState = (initialState: InitialState) => { const organization = initialState.organization as OrganizationResource; const orgId = initialState.orgId; const orgRole = initialState.orgRole as MembershipRole; + // TODO(@panteliselef): Typesafe + const orgPermissions = initialState.orgPermissions as OrganizationPermission[]; const orgSlug = initialState.orgSlug; const actor = initialState.actor; @@ -33,6 +36,7 @@ const deriveFromSsrInitialState = (initialState: InitialState) => { organization, orgId, orgRole, + orgPermissions, orgSlug, actor, }; diff --git a/packages/types/src/jwtv2.ts b/packages/types/src/jwtv2.ts index e7f1db949c..96d3f18b13 100644 --- a/packages/types/src/jwtv2.ts +++ b/packages/types/src/jwtv2.ts @@ -1,4 +1,4 @@ -import type { MembershipRole } from './organizationMembership'; +import type { MembershipRole, OrganizationPermission } from './organizationMembership'; export interface Jwt { header: JwtHeader; @@ -96,6 +96,12 @@ export interface JwtPayload extends CustomJwtSessionClaims { */ org_role?: MembershipRole; + // TODO(@panteliselef): Typesafe + /** + * Active organization role + */ + org_permissions?: OrganizationPermission[]; + /** * Any other JWT Claim Set member. */ diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index d7dd6dc9e9..bab864b304 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -6,20 +6,34 @@ import type { ClerkResource } from './resource'; import type { TokenResource } from './token'; import type { UserResource } from './user'; -export type experimental__CheckAuthorizationWithoutPermission = ( - isAuthorizedParams: CheckAuthorizationParamsWithoutPermission, +export type experimental__CheckAuthorizationWithCustomPermissions = ( + isAuthorizedParams: CheckAuthorizationParamsWithCustomPermissions, ) => boolean; -type CheckAuthorizationParamsWithoutPermission = +type CheckAuthorizationParamsWithCustomPermissions = | { - some: { - role: string; - }[]; + some: ( + | { + role: string; + permission?: never; + } + | { + role?: never; + permission: Autocomplete; + } + )[]; role?: never; + permission?: never; } | { some?: never; role: string; + permission?: never; + } + | { + some?: never; + role?: never; + permission: Autocomplete; }; export type CheckAuthorization = (isAuthorizedParams: CheckAuthorizationParams) => boolean; diff --git a/packages/types/src/ssr.ts b/packages/types/src/ssr.ts index 3cad74c1de..637472f0c0 100644 --- a/packages/types/src/ssr.ts +++ b/packages/types/src/ssr.ts @@ -1,9 +1,9 @@ import type { ActClaim, JwtPayload } from './jwtv2'; import type { OrganizationResource } from './organization'; -import type { MembershipRole } from './organizationMembership'; +import type { MembershipRole, OrganizationPermission } from './organizationMembership'; import type { SessionResource } from './session'; import type { UserResource } from './user'; -import type { Serializable } from './utils'; +import type { Autocomplete, Serializable } from './utils'; export type ServerGetTokenOptions = { template?: string }; export type ServerGetToken = (options?: ServerGetTokenOptions) => Promise; @@ -18,5 +18,7 @@ export type InitialState = Serializable<{ orgId: string | undefined; orgRole: MembershipRole | undefined; orgSlug: string | undefined; + // TODO(@panteliselef): Typesafe + orgPermissions: Autocomplete[] | undefined; organization: OrganizationResource | undefined; }>; From 98933702ccfd8fd410da182f9cdef25d30dacefd Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 20 Nov 2023 14:45:49 +0200 Subject: [PATCH 02/26] chore(types,backend,clerk-react): Create type for OrganizationCustomPermissions --- packages/backend/src/tokens/authObjects.ts | 7 ------- .../src/core/resources/OrganizationMembership.ts | 3 +-- packages/react/src/contexts/AuthContext.ts | 5 ++--- packages/react/src/utils/deriveState.ts | 1 - packages/types/src/json.ts | 4 ++-- packages/types/src/jwtv2.ts | 1 - packages/types/src/organizationMembership.ts | 12 ++++++++++-- packages/types/src/session.ts | 12 +++++------- packages/types/src/ssr.ts | 5 ++--- 9 files changed, 22 insertions(+), 28 deletions(-) diff --git a/packages/backend/src/tokens/authObjects.ts b/packages/backend/src/tokens/authObjects.ts index a7998c2fde..dd9ae03594 100644 --- a/packages/backend/src/tokens/authObjects.ts +++ b/packages/backend/src/tokens/authObjects.ts @@ -31,7 +31,6 @@ export type SignedInAuthObject = { orgId: string | undefined; orgRole: string | undefined; orgSlug: string | undefined; - // TODO(@panteliselef): Typesafe orgPermissions: OrganizationPermission[] | undefined; organization: Organization | undefined; getToken: ServerGetToken; @@ -215,12 +214,6 @@ const createHasAuthorization = orgPermissions: string[] | undefined; }): experimental__CheckAuthorizationWithCustomPermissions => params => { - // console.log({ - // orgId, - // userId, - // orgRole, - // orgPermissions, - // }); if (!orgId || !userId || !orgPermissions) { return false; } diff --git a/packages/clerk-js/src/core/resources/OrganizationMembership.ts b/packages/clerk-js/src/core/resources/OrganizationMembership.ts index cc327fcd0f..956aeb94e7 100644 --- a/packages/clerk-js/src/core/resources/OrganizationMembership.ts +++ b/packages/clerk-js/src/core/resources/OrganizationMembership.ts @@ -1,5 +1,4 @@ import type { - Autocomplete, ClerkPaginatedResponse, ClerkResourceReloadParams, GetUserOrganizationMembershipParams, @@ -21,7 +20,7 @@ export class OrganizationMembership extends BaseResource implements Organization /** * @experimental The property is experimental and subject to change in future releases. */ - permissions: Autocomplete[] = []; + permissions: OrganizationPermission[] = []; role!: MembershipRole; createdAt!: Date; updatedAt!: Date; diff --git a/packages/react/src/contexts/AuthContext.ts b/packages/react/src/contexts/AuthContext.ts index 40808c9cd8..d7e475a0c5 100644 --- a/packages/react/src/contexts/AuthContext.ts +++ b/packages/react/src/contexts/AuthContext.ts @@ -1,5 +1,5 @@ import { createContextAndHook } from '@clerk/shared/react'; -import type { ActJWTClaim, Autocomplete, MembershipRole, OrganizationPermission } from '@clerk/types'; +import type { ActJWTClaim, MembershipRole, OrganizationPermission } from '@clerk/types'; export const [AuthContext, useAuthContext] = createContextAndHook<{ userId: string | null | undefined; @@ -8,6 +8,5 @@ export const [AuthContext, useAuthContext] = createContextAndHook<{ orgId: string | null | undefined; orgRole: MembershipRole | null | undefined; orgSlug: string | null | undefined; - // TODO(@panteliselef): Typesafe - orgPermissions: Autocomplete[] | null | undefined; + orgPermissions: OrganizationPermission[] | null | undefined; }>('AuthContext'); diff --git a/packages/react/src/utils/deriveState.ts b/packages/react/src/utils/deriveState.ts index 00117b9473..5ff4d1f18f 100644 --- a/packages/react/src/utils/deriveState.ts +++ b/packages/react/src/utils/deriveState.ts @@ -23,7 +23,6 @@ const deriveFromSsrInitialState = (initialState: InitialState) => { const organization = initialState.organization as OrganizationResource; const orgId = initialState.orgId; const orgRole = initialState.orgRole as MembershipRole; - // TODO(@panteliselef): Typesafe const orgPermissions = initialState.orgPermissions as OrganizationPermission[]; const orgSlug = initialState.orgSlug; const actor = initialState.actor; diff --git a/packages/types/src/json.ts b/packages/types/src/json.ts index 1bce0fc2f1..4263c5c28e 100644 --- a/packages/types/src/json.ts +++ b/packages/types/src/json.ts @@ -17,7 +17,7 @@ import type { SignUpField, SignUpIdentificationField, SignUpStatus } from './sig import type { OAuthStrategy } from './strategies'; import type { BoxShadow, Color, EmUnit, FontWeight, HexColor } from './theme'; import type { UserSettingsJSON } from './userSettings'; -import type { Autocomplete, CamelToSnake } from './utils'; +import type { CamelToSnake } from './utils'; import type { VerificationStatus } from './verification'; export interface ClerkResourceJSON { @@ -303,7 +303,7 @@ export interface OrganizationMembershipJSON extends ClerkResourceJSON { /** * @experimental The property is experimental and subject to change in future releases. */ - permissions: Autocomplete[]; + permissions: OrganizationPermission[]; public_metadata: OrganizationMembershipPublicMetadata; public_user_data: PublicUserDataJSON; role: MembershipRole; diff --git a/packages/types/src/jwtv2.ts b/packages/types/src/jwtv2.ts index 96d3f18b13..179ade7f76 100644 --- a/packages/types/src/jwtv2.ts +++ b/packages/types/src/jwtv2.ts @@ -96,7 +96,6 @@ export interface JwtPayload extends CustomJwtSessionClaims { */ org_role?: MembershipRole; - // TODO(@panteliselef): Typesafe /** * Active organization role */ diff --git a/packages/types/src/organizationMembership.ts b/packages/types/src/organizationMembership.ts index adb18e3bf0..982f4d462e 100644 --- a/packages/types/src/organizationMembership.ts +++ b/packages/types/src/organizationMembership.ts @@ -22,6 +22,10 @@ declare global { interface OrganizationMembershipPrivateMetadata { [k: string]: unknown; } + + interface OrganizationCustomPermissions { + [k: string]: string; + } } export interface OrganizationMembershipResource extends ClerkResource { @@ -30,7 +34,7 @@ export interface OrganizationMembershipResource extends ClerkResource { /** * @experimental The property is experimental and subject to change in future releases. */ - permissions: Autocomplete[]; + permissions: OrganizationPermission[]; publicMetadata: OrganizationMembershipPublicMetadata; publicUserData: PublicUserData; role: MembershipRole; @@ -40,9 +44,11 @@ export interface OrganizationMembershipResource extends ClerkResource { update: (updateParams: UpdateOrganizationMembershipParams) => Promise; } +export type OrganizationCustomPermission = OrganizationCustomPermissions[keyof OrganizationCustomPermissions]; + export type MembershipRole = Autocomplete<'admin' | 'basic_member' | 'guest_member'>; -export type OrganizationPermission = +export type OrganizationSystemPermission = | 'org:sys_domains:manage' | 'org:sys_profile:manage' | 'org:sys_profile:delete' @@ -50,6 +56,8 @@ export type OrganizationPermission = | 'org:sys_memberships:manage' | 'org:sys_domains:read'; +export type OrganizationPermission = OrganizationSystemPermission | OrganizationCustomPermission; + export type UpdateOrganizationMembershipParams = { role: MembershipRole; }; diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index bab864b304..7c2daea926 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -1,7 +1,5 @@ -import type { Autocomplete } from 'utils'; - import type { ActJWTClaim } from './jwt'; -import type { OrganizationPermission } from './organizationMembership'; +import type { OrganizationCustomPermission, OrganizationPermission } from './organizationMembership'; import type { ClerkResource } from './resource'; import type { TokenResource } from './token'; import type { UserResource } from './user'; @@ -19,7 +17,7 @@ type CheckAuthorizationParamsWithCustomPermissions = } | { role?: never; - permission: Autocomplete; + permission: OrganizationCustomPermission; } )[]; role?: never; @@ -33,7 +31,7 @@ type CheckAuthorizationParamsWithCustomPermissions = | { some?: never; role?: never; - permission: Autocomplete; + permission: OrganizationCustomPermission; }; export type CheckAuthorization = (isAuthorizedParams: CheckAuthorizationParams) => boolean; @@ -47,7 +45,7 @@ type CheckAuthorizationParams = } | { role?: never; - permission: Autocomplete; + permission: OrganizationPermission; } )[]; role?: never; @@ -61,7 +59,7 @@ type CheckAuthorizationParams = | { some?: never; role?: never; - permission: Autocomplete; + permission: OrganizationPermission; }; export interface SessionResource extends ClerkResource { diff --git a/packages/types/src/ssr.ts b/packages/types/src/ssr.ts index 637472f0c0..4bc24b99bf 100644 --- a/packages/types/src/ssr.ts +++ b/packages/types/src/ssr.ts @@ -3,7 +3,7 @@ import type { OrganizationResource } from './organization'; import type { MembershipRole, OrganizationPermission } from './organizationMembership'; import type { SessionResource } from './session'; import type { UserResource } from './user'; -import type { Autocomplete, Serializable } from './utils'; +import type { Serializable } from './utils'; export type ServerGetTokenOptions = { template?: string }; export type ServerGetToken = (options?: ServerGetTokenOptions) => Promise; @@ -18,7 +18,6 @@ export type InitialState = Serializable<{ orgId: string | undefined; orgRole: MembershipRole | undefined; orgSlug: string | undefined; - // TODO(@panteliselef): Typesafe - orgPermissions: Autocomplete[] | undefined; + orgPermissions: OrganizationPermission[] | undefined; organization: OrganizationResource | undefined; }>; From fb1bed5631cf48031d4abeeceae42a23eacd0b66 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 20 Nov 2023 15:08:50 +0200 Subject: [PATCH 03/26] chore(types,backend,clerk-react): Create type for custom roles --- packages/backend/src/tokens/authObjects.ts | 7 +++--- packages/types/src/organizationMembership.ts | 23 +++++++++++++++++--- packages/types/src/session.ts | 15 ++++++++----- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/packages/backend/src/tokens/authObjects.ts b/packages/backend/src/tokens/authObjects.ts index dd9ae03594..6241b6d88a 100644 --- a/packages/backend/src/tokens/authObjects.ts +++ b/packages/backend/src/tokens/authObjects.ts @@ -2,7 +2,8 @@ import type { ActClaim, experimental__CheckAuthorizationWithCustomPermissions, JwtPayload, - OrganizationPermission, + OrganizationCustomPermission, + OrganizationCustomRole, ServerGetToken, ServerGetTokenOptions, } from '@clerk/types'; @@ -29,9 +30,9 @@ export type SignedInAuthObject = { userId: string; user: User | undefined; orgId: string | undefined; - orgRole: string | undefined; + orgRole: OrganizationCustomRole | undefined; orgSlug: string | undefined; - orgPermissions: OrganizationPermission[] | undefined; + orgPermissions: OrganizationCustomPermission[] | undefined; organization: Organization | undefined; getToken: ServerGetToken; /** diff --git a/packages/types/src/organizationMembership.ts b/packages/types/src/organizationMembership.ts index 982f4d462e..5fa95feb35 100644 --- a/packages/types/src/organizationMembership.ts +++ b/packages/types/src/organizationMembership.ts @@ -1,5 +1,3 @@ -import type { Autocomplete } from 'utils'; - import type { OrganizationResource } from './organization'; import type { ClerkResource } from './resource'; import type { PublicUserData } from './session'; @@ -26,6 +24,10 @@ declare global { interface OrganizationCustomPermissions { [k: string]: string; } + + interface OrganizationCustomRoles { + [k: string]: string; + } } export interface OrganizationMembershipResource extends ClerkResource { @@ -45,8 +47,23 @@ export interface OrganizationMembershipResource extends ClerkResource { } export type OrganizationCustomPermission = OrganizationCustomPermissions[keyof OrganizationCustomPermissions]; +export type OrganizationCustomRole = OrganizationCustomRoles[keyof OrganizationCustomRoles]; -export type MembershipRole = Autocomplete<'admin' | 'basic_member' | 'guest_member'>; +/** + * @deprecated This type is deprecated and will be removed in the next major release. + * Instead, override the type like this + * ``` + * declare global { + * interface OrganizationCustomRoles { + * "org:custom_role1": "org:custom_role1"; + * "org:custom_role2": "org:custom_role2"; + * } + * } + * ``` + * MembershipRole includes `admin`, `basic_member`, `guest_member`. With the introduction of "Custom roles" + * these types will no longer match a developer's custom logic. + */ +export type MembershipRole = 'admin' | 'basic_member' | 'guest_member' | OrganizationCustomRole; export type OrganizationSystemPermission = | 'org:sys_domains:manage' diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index 7c2daea926..891fea2007 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -1,5 +1,10 @@ import type { ActJWTClaim } from './jwt'; -import type { OrganizationCustomPermission, OrganizationPermission } from './organizationMembership'; +import type { + MembershipRole, + OrganizationCustomPermission, + OrganizationCustomRole, + OrganizationPermission, +} from './organizationMembership'; import type { ClerkResource } from './resource'; import type { TokenResource } from './token'; import type { UserResource } from './user'; @@ -12,7 +17,7 @@ type CheckAuthorizationParamsWithCustomPermissions = | { some: ( | { - role: string; + role: OrganizationCustomRole; permission?: never; } | { @@ -25,7 +30,7 @@ type CheckAuthorizationParamsWithCustomPermissions = } | { some?: never; - role: string; + role: OrganizationCustomRole; permission?: never; } | { @@ -40,7 +45,7 @@ type CheckAuthorizationParams = | { some: ( | { - role: string; + role: MembershipRole; permission?: never; } | { @@ -53,7 +58,7 @@ type CheckAuthorizationParams = } | { some?: never; - role: string; + role: MembershipRole; permission?: never; } | { From 153140ba90ffd281400582b738ce73b935fae886 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 20 Nov 2023 15:18:04 +0200 Subject: [PATCH 04/26] chore(types,backend,clerk-react): Add changeset --- .changeset/chatty-beds-doubt.md | 41 +++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .changeset/chatty-beds-doubt.md diff --git a/.changeset/chatty-beds-doubt.md b/.changeset/chatty-beds-doubt.md new file mode 100644 index 0000000000..d57af0d495 --- /dev/null +++ b/.changeset/chatty-beds-doubt.md @@ -0,0 +1,41 @@ +--- +'@clerk/clerk-js': minor +'@clerk/backend': minor +'@clerk/nextjs': minor +'@clerk/shared': minor +'@clerk/clerk-react': minor +'@clerk/types': minor +--- + +Support for permission checks with Gate. + +Example usage: +//index.d.ts +```ts +declare global { + interface OrganizationCustomPermissions { + "org:appointment:accept":"org:appointment:accept"; + "org:appointment:decline":"org:appointment:decline"; + "org:patients:create":"org:patients:create"; + } + + interface OrganizationCustomPermissions { + "org:doctor":"org:doctor"; + "org:nurse":"org:nurse"; + } +} + +``` + + +```tsx + + + +``` + +```tsx + + + +``` From ca8c64d6ac71711c33f379e81185df0fed9fe459 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 20 Nov 2023 15:25:12 +0200 Subject: [PATCH 05/26] chore(types,backend,clerk-react): Add comments --- packages/types/src/organizationMembership.ts | 25 ++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/types/src/organizationMembership.ts b/packages/types/src/organizationMembership.ts index 5fa95feb35..c2fc139724 100644 --- a/packages/types/src/organizationMembership.ts +++ b/packages/types/src/organizationMembership.ts @@ -21,10 +21,35 @@ declare global { [k: string]: unknown; } + /** + * If you want to provide custom types for the organizationMembership.permissions + * array, simply redeclare this rule in the global namespace. + * Every utility function or component will use the provided type. + * ``` + * interface OrganizationCustomPermissions { + * "org:appointment:accept":"org:appointment:accept"; + * "org:appointment:decline":"org:appointment:decline"; + * "org:patients:create":"org:patients:create"; + * } + * ``` + */ interface OrganizationCustomPermissions { [k: string]: string; } + /** + * If you want to provide custom types for the organizationMembership.role + * property, simply redeclare this rule in the global namespace. + * Every utility function or component will use the provided type. + * ``` + * interface OrganizationCustomPermissions { + * "org:role1":"org:role1"; + * "org:role2":"org:role2"; + * } + * ``` + * `organizationMembership.role` will be equal to "org:role1" | "org:role2" + * + */ interface OrganizationCustomRoles { [k: string]: string; } From be5aaeeed6167319a3323be1a8bef85538b03216 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 24 Nov 2023 12:33:56 +0200 Subject: [PATCH 06/26] chore(types,nextjs): Remove custom types --- .changeset/chatty-beds-doubt.md | 19 ------- .../app-router/server/controlComponents.tsx | 23 +++++--- packages/types/src/organizationMembership.ts | 52 +++---------------- 3 files changed, 21 insertions(+), 73 deletions(-) diff --git a/.changeset/chatty-beds-doubt.md b/.changeset/chatty-beds-doubt.md index d57af0d495..8d1952394c 100644 --- a/.changeset/chatty-beds-doubt.md +++ b/.changeset/chatty-beds-doubt.md @@ -9,25 +9,6 @@ Support for permission checks with Gate. -Example usage: -//index.d.ts -```ts -declare global { - interface OrganizationCustomPermissions { - "org:appointment:accept":"org:appointment:accept"; - "org:appointment:decline":"org:appointment:decline"; - "org:patients:create":"org:patients:create"; - } - - interface OrganizationCustomPermissions { - "org:doctor":"org:doctor"; - "org:nurse":"org:nurse"; - } -} - -``` - - ```tsx diff --git a/packages/nextjs/src/app-router/server/controlComponents.tsx b/packages/nextjs/src/app-router/server/controlComponents.tsx index 8805d736bb..26a719ee21 100644 --- a/packages/nextjs/src/app-router/server/controlComponents.tsx +++ b/packages/nextjs/src/app-router/server/controlComponents.tsx @@ -17,10 +17,17 @@ export function SignedOut(props: React.PropsWithChildren) { } type GateServerComponentProps = React.PropsWithChildren< - Parameters[0] & { - fallback?: React.ReactNode; - redirectTo?: string; - } + Parameters[0] & + ( + | { + fallback: React.ReactNode; + redirectTo?: never; + } + | { + fallback?: never; + redirectTo: string; + } + ) >; /** @@ -32,11 +39,11 @@ export function experimental__Gate(gateProps: GateServerComponentProps) { const isAuthorizedUser = experimental__has(restAuthorizedParams); - const handleFallback = () => { - if (!redirectTo && !fallback) { - throw new Error('Provide `` with a `fallback` or `redirectTo`'); - } + if (!redirectTo && !fallback) { + throw new Error('Provide `` with a `fallback` or `redirectTo`'); + } + const handleFallback = () => { if (redirectTo) { return redirect(redirectTo); } diff --git a/packages/types/src/organizationMembership.ts b/packages/types/src/organizationMembership.ts index c2fc139724..e5b5feee56 100644 --- a/packages/types/src/organizationMembership.ts +++ b/packages/types/src/organizationMembership.ts @@ -1,6 +1,7 @@ import type { OrganizationResource } from './organization'; import type { ClerkResource } from './resource'; import type { PublicUserData } from './session'; +import type { Autocomplete } from './utils'; declare global { /** @@ -20,39 +21,6 @@ declare global { interface OrganizationMembershipPrivateMetadata { [k: string]: unknown; } - - /** - * If you want to provide custom types for the organizationMembership.permissions - * array, simply redeclare this rule in the global namespace. - * Every utility function or component will use the provided type. - * ``` - * interface OrganizationCustomPermissions { - * "org:appointment:accept":"org:appointment:accept"; - * "org:appointment:decline":"org:appointment:decline"; - * "org:patients:create":"org:patients:create"; - * } - * ``` - */ - interface OrganizationCustomPermissions { - [k: string]: string; - } - - /** - * If you want to provide custom types for the organizationMembership.role - * property, simply redeclare this rule in the global namespace. - * Every utility function or component will use the provided type. - * ``` - * interface OrganizationCustomPermissions { - * "org:role1":"org:role1"; - * "org:role2":"org:role2"; - * } - * ``` - * `organizationMembership.role` will be equal to "org:role1" | "org:role2" - * - */ - interface OrganizationCustomRoles { - [k: string]: string; - } } export interface OrganizationMembershipResource extends ClerkResource { @@ -71,24 +39,16 @@ export interface OrganizationMembershipResource extends ClerkResource { update: (updateParams: UpdateOrganizationMembershipParams) => Promise; } -export type OrganizationCustomPermission = OrganizationCustomPermissions[keyof OrganizationCustomPermissions]; -export type OrganizationCustomRole = OrganizationCustomRoles[keyof OrganizationCustomRoles]; +export type OrganizationCustomPermission = string; +export type OrganizationCustomRole = string; /** * @deprecated This type is deprecated and will be removed in the next major release. - * Instead, override the type like this - * ``` - * declare global { - * interface OrganizationCustomRoles { - * "org:custom_role1": "org:custom_role1"; - * "org:custom_role2": "org:custom_role2"; - * } - * } - * ``` + * Use `OrganizationCustomRole` instead. * MembershipRole includes `admin`, `basic_member`, `guest_member`. With the introduction of "Custom roles" * these types will no longer match a developer's custom logic. */ -export type MembershipRole = 'admin' | 'basic_member' | 'guest_member' | OrganizationCustomRole; +export type MembershipRole = Autocomplete<'admin' | 'basic_member' | 'guest_member'>; export type OrganizationSystemPermission = | 'org:sys_domains:manage' @@ -98,7 +58,7 @@ export type OrganizationSystemPermission = | 'org:sys_memberships:manage' | 'org:sys_domains:read'; -export type OrganizationPermission = OrganizationSystemPermission | OrganizationCustomPermission; +export type OrganizationPermission = Autocomplete; export type UpdateOrganizationMembershipParams = { role: MembershipRole; From bf3cf34643174927f98a236a0de311e14dcb59ed Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 24 Nov 2023 13:12:19 +0200 Subject: [PATCH 07/26] fix(clerk-react): Missing `some` support for has in useAuth --- packages/react/src/hooks/useAuth.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/react/src/hooks/useAuth.ts b/packages/react/src/hooks/useAuth.ts index 4fa51ba45a..a3aefa6790 100644 --- a/packages/react/src/hooks/useAuth.ts +++ b/packages/react/src/hooks/useAuth.ts @@ -141,9 +141,22 @@ export const useAuth: UseAuth = () => { if (params.permission) { return orgPermissions.includes(params.permission); } + if (params.role) { return orgRole === params.role; } + + if (params.some) { + return !!params.some.find(permObj => { + if (permObj.permission) { + return orgPermissions.includes(permObj.permission); + } + if (permObj.role) { + return orgRole === permObj.role; + } + return false; + }); + } return false; }, [orgId, orgRole, userId, orgPermissions], From 1a112db9d42f9fe28ef97bfb917e1096380b7784 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 24 Nov 2023 14:05:31 +0200 Subject: [PATCH 08/26] chore(types,clerk-react): Use OrganizationCustomPermission for permissions in ssr --- packages/react/src/contexts/AuthContext.ts | 4 ++-- packages/react/src/utils/deriveState.ts | 4 ++-- packages/types/src/jwtv2.ts | 4 ++-- packages/types/src/organizationMembership.ts | 4 ++++ packages/types/src/ssr.ts | 4 ++-- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/react/src/contexts/AuthContext.ts b/packages/react/src/contexts/AuthContext.ts index d7e475a0c5..d829bee2dd 100644 --- a/packages/react/src/contexts/AuthContext.ts +++ b/packages/react/src/contexts/AuthContext.ts @@ -1,5 +1,5 @@ import { createContextAndHook } from '@clerk/shared/react'; -import type { ActJWTClaim, MembershipRole, OrganizationPermission } from '@clerk/types'; +import type { ActJWTClaim, MembershipRole, OrganizationCustomPermission } from '@clerk/types'; export const [AuthContext, useAuthContext] = createContextAndHook<{ userId: string | null | undefined; @@ -8,5 +8,5 @@ export const [AuthContext, useAuthContext] = createContextAndHook<{ orgId: string | null | undefined; orgRole: MembershipRole | null | undefined; orgSlug: string | null | undefined; - orgPermissions: OrganizationPermission[] | null | undefined; + orgPermissions: OrganizationCustomPermission[] | null | undefined; }>('AuthContext'); diff --git a/packages/react/src/utils/deriveState.ts b/packages/react/src/utils/deriveState.ts index 5ff4d1f18f..db7c31e71c 100644 --- a/packages/react/src/utils/deriveState.ts +++ b/packages/react/src/utils/deriveState.ts @@ -2,7 +2,7 @@ import type { ActiveSessionResource, InitialState, MembershipRole, - OrganizationPermission, + OrganizationCustomPermission, OrganizationResource, Resources, UserResource, @@ -23,7 +23,7 @@ const deriveFromSsrInitialState = (initialState: InitialState) => { const organization = initialState.organization as OrganizationResource; const orgId = initialState.orgId; const orgRole = initialState.orgRole as MembershipRole; - const orgPermissions = initialState.orgPermissions as OrganizationPermission[]; + const orgPermissions = initialState.orgPermissions as OrganizationCustomPermission[]; const orgSlug = initialState.orgSlug; const actor = initialState.actor; diff --git a/packages/types/src/jwtv2.ts b/packages/types/src/jwtv2.ts index 179ade7f76..ac543f6f46 100644 --- a/packages/types/src/jwtv2.ts +++ b/packages/types/src/jwtv2.ts @@ -1,4 +1,4 @@ -import type { MembershipRole, OrganizationPermission } from './organizationMembership'; +import type { MembershipRole, OrganizationCustomPermission } from './organizationMembership'; export interface Jwt { header: JwtHeader; @@ -99,7 +99,7 @@ export interface JwtPayload extends CustomJwtSessionClaims { /** * Active organization role */ - org_permissions?: OrganizationPermission[]; + org_permissions?: OrganizationCustomPermission[]; /** * Any other JWT Claim Set member. diff --git a/packages/types/src/organizationMembership.ts b/packages/types/src/organizationMembership.ts index e5b5feee56..bf9dab93b3 100644 --- a/packages/types/src/organizationMembership.ts +++ b/packages/types/src/organizationMembership.ts @@ -58,6 +58,10 @@ export type OrganizationSystemPermission = | 'org:sys_memberships:manage' | 'org:sys_domains:read'; +/** + * OrganizationPermission is a combination of system and custom permissions. + * System permissions are only accessible from FAPI and client-side operations/utils + */ export type OrganizationPermission = Autocomplete; export type UpdateOrganizationMembershipParams = { diff --git a/packages/types/src/ssr.ts b/packages/types/src/ssr.ts index 4bc24b99bf..b4616d0f72 100644 --- a/packages/types/src/ssr.ts +++ b/packages/types/src/ssr.ts @@ -1,6 +1,6 @@ import type { ActClaim, JwtPayload } from './jwtv2'; import type { OrganizationResource } from './organization'; -import type { MembershipRole, OrganizationPermission } from './organizationMembership'; +import type { MembershipRole, OrganizationCustomPermission } from './organizationMembership'; import type { SessionResource } from './session'; import type { UserResource } from './user'; import type { Serializable } from './utils'; @@ -18,6 +18,6 @@ export type InitialState = Serializable<{ orgId: string | undefined; orgRole: MembershipRole | undefined; orgSlug: string | undefined; - orgPermissions: OrganizationPermission[] | undefined; + orgPermissions: OrganizationCustomPermission[] | undefined; organization: OrganizationResource | undefined; }>; From a91dc742cbaaaee3d1106fed40f57cd34f9aa164 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 24 Nov 2023 14:15:36 +0200 Subject: [PATCH 09/26] chore(nextjs): Drop redirect from RSC `` --- .../app-router/server/controlComponents.tsx | 37 ++++--------------- 1 file changed, 7 insertions(+), 30 deletions(-) diff --git a/packages/nextjs/src/app-router/server/controlComponents.tsx b/packages/nextjs/src/app-router/server/controlComponents.tsx index 26a719ee21..12136aaf01 100644 --- a/packages/nextjs/src/app-router/server/controlComponents.tsx +++ b/packages/nextjs/src/app-router/server/controlComponents.tsx @@ -1,5 +1,4 @@ import type { experimental__CheckAuthorizationWithCustomPermissions } from '@clerk/types'; -import { redirect } from 'next/navigation'; import React from 'react'; import { auth } from './auth'; @@ -17,43 +16,21 @@ export function SignedOut(props: React.PropsWithChildren) { } type GateServerComponentProps = React.PropsWithChildren< - Parameters[0] & - ( - | { - fallback: React.ReactNode; - redirectTo?: never; - } - | { - fallback?: never; - redirectTo: string; - } - ) + Parameters[0] & { + fallback?: React.ReactNode; + } >; /** * @experimental The component is experimental and subject to change in future releases. */ export function experimental__Gate(gateProps: GateServerComponentProps) { - const { children, fallback, redirectTo, ...restAuthorizedParams } = gateProps; + const { children, fallback, ...restAuthorizedParams } = gateProps; const { experimental__has } = auth(); - const isAuthorizedUser = experimental__has(restAuthorizedParams); - - if (!redirectTo && !fallback) { - throw new Error('Provide `` with a `fallback` or `redirectTo`'); - } - - const handleFallback = () => { - if (redirectTo) { - return redirect(redirectTo); - } - - return <>{fallback}; - }; - - if (!isAuthorizedUser) { - return handleFallback(); + if (experimental__has(restAuthorizedParams)) { + return <>{children}; } - return <>{children}; + return <>{fallback ?? null}; } From a55afe8b98427879b8824e5d6c6a31acf0fd98ce Mon Sep 17 00:00:00 2001 From: panteliselef Date: Sun, 3 Dec 2023 15:09:47 +0200 Subject: [PATCH 10/26] feat(types,nextjs,clerk-react,backend): Rename Gate to Protect - Drop `some` from the `has` utility and Protect. Protect now accepts a `condition` prop where a function is expected with the `has` being exposed as the param. - Protect can now be used without required props. In this chae behaves as `` if no authorization props are passed. - `has` will throw an error if neither `permission` or `role` is passed. --- packages/backend/src/tokens/authObjects.ts | 40 ++++-------- .../src/__snapshots__/exports.test.ts.snap | 2 +- .../app-router/server/controlComponents.tsx | 58 +++++++++++++++-- .../src/client-boundary/controlComponents.ts | 2 +- packages/nextjs/src/components.client.ts | 2 +- packages/nextjs/src/components.server.ts | 6 +- packages/nextjs/src/index.ts | 3 +- .../src/components/controlComponents.tsx | 64 ++++++++++++++++--- packages/react/src/components/index.ts | 2 +- packages/react/src/hooks/useAuth.ts | 54 +++++----------- packages/types/src/session.ts | 24 ++----- 11 files changed, 149 insertions(+), 108 deletions(-) diff --git a/packages/backend/src/tokens/authObjects.ts b/packages/backend/src/tokens/authObjects.ts index 6241b6d88a..f9ce3d7b29 100644 --- a/packages/backend/src/tokens/authObjects.ts +++ b/packages/backend/src/tokens/authObjects.ts @@ -1,6 +1,6 @@ import type { ActClaim, - experimental__CheckAuthorizationWithCustomPermissions, + CheckAuthorizationWithCustomPermissions, JwtPayload, OrganizationCustomPermission, OrganizationCustomRole, @@ -35,10 +35,7 @@ export type SignedInAuthObject = { orgPermissions: OrganizationCustomPermission[] | undefined; organization: Organization | undefined; getToken: ServerGetToken; - /** - * @experimental The method is experimental and subject to change in future releases. - */ - experimental__has: experimental__CheckAuthorizationWithCustomPermissions; + has: CheckAuthorizationWithCustomPermissions; debug: AuthObjectDebug; }; @@ -55,10 +52,7 @@ export type SignedOutAuthObject = { orgPermissions: null; organization: null; getToken: ServerGetToken; - /** - * @experimental The method is experimental and subject to change in future releases. - */ - experimental__has: experimental__CheckAuthorizationWithCustomPermissions; + has: CheckAuthorizationWithCustomPermissions; debug: AuthObjectDebug; }; @@ -113,7 +107,7 @@ export function signedInAuthObject( orgPermissions, organization, getToken, - experimental__has: createHasAuthorization({ orgId, orgRole, orgPermissions, userId }), + has: createHasAuthorization({ orgId, orgRole, orgPermissions, userId }), debug: createDebug({ ...options, ...debugData }), }; } @@ -132,7 +126,7 @@ export function signedOutAuthObject(debugData?: AuthObjectDebugData): SignedOutA orgPermissions: null, organization: null, getToken: () => Promise.resolve(null), - experimental__has: () => false, + has: () => false, debug: createDebug(debugData), }; } @@ -178,7 +172,7 @@ export function sanitizeAuthObject>(authObject: T): T export const makeAuthObjectSerializable = >(obj: T): T => { // remove any non-serializable props from the returned object - const { debug, getToken, experimental__has, ...rest } = obj as unknown as AuthObject; + const { debug, getToken, has, ...rest } = obj as unknown as AuthObject; return rest as unknown as T; }; @@ -202,6 +196,7 @@ const createGetToken: CreateGetToken = params => { }; }; +//MAYBE move this to @shared const createHasAuthorization = ({ orgId, @@ -213,9 +208,14 @@ const createHasAuthorization = orgId: string | undefined; orgRole: string | undefined; orgPermissions: string[] | undefined; - }): experimental__CheckAuthorizationWithCustomPermissions => + }): CheckAuthorizationWithCustomPermissions => params => { - if (!orgId || !userId || !orgPermissions) { + // TODO: assert + if (!params?.permission && !params?.role) { + throw 'Permission or role is required'; + } + + if (!orgId || !userId || !orgRole || !orgPermissions) { return false; } @@ -227,17 +227,5 @@ const createHasAuthorization = return orgRole === params.role; } - if (params.some) { - return !!params.some.find(permObj => { - if (permObj.permission) { - return orgPermissions.includes(permObj.permission); - } - if (permObj.role) { - return orgRole === permObj.role; - } - return false; - }); - } - return false; }; diff --git a/packages/chrome-extension/src/__snapshots__/exports.test.ts.snap b/packages/chrome-extension/src/__snapshots__/exports.test.ts.snap index 5850b5cf88..a8554f210c 100644 --- a/packages/chrome-extension/src/__snapshots__/exports.test.ts.snap +++ b/packages/chrome-extension/src/__snapshots__/exports.test.ts.snap @@ -8,11 +8,11 @@ exports[`public exports should not include a breaking change 1`] = ` "ClerkProvider", "CreateOrganization", "EmailLinkErrorCode", - "Experimental__Gate", "MultisessionAppSupport", "OrganizationList", "OrganizationProfile", "OrganizationSwitcher", + "Protect", "RedirectToCreateOrganization", "RedirectToOrganizationProfile", "RedirectToSignIn", diff --git a/packages/nextjs/src/app-router/server/controlComponents.tsx b/packages/nextjs/src/app-router/server/controlComponents.tsx index 12136aaf01..12bf93bf34 100644 --- a/packages/nextjs/src/app-router/server/controlComponents.tsx +++ b/packages/nextjs/src/app-router/server/controlComponents.tsx @@ -1,4 +1,8 @@ -import type { experimental__CheckAuthorizationWithCustomPermissions } from '@clerk/types'; +import type { + CheckAuthorizationWithCustomPermissions, + OrganizationCustomPermission, + OrganizationCustomRole, +} from '@clerk/types'; import React from 'react'; import { auth } from './auth'; @@ -16,7 +20,28 @@ export function SignedOut(props: React.PropsWithChildren) { } type GateServerComponentProps = React.PropsWithChildren< - Parameters[0] & { + ( + | { + condition?: never; + role: OrganizationCustomRole; + permission?: never; + } + | { + condition?: never; + role?: never; + permission: OrganizationCustomPermission; + } + | { + condition: (has: CheckAuthorizationWithCustomPermissions) => boolean; + role?: never; + permission?: never; + } + | { + condition?: never; + role?: never; + permission?: never; + } + ) & { fallback?: React.ReactNode; } >; @@ -24,13 +49,36 @@ type GateServerComponentProps = React.PropsWithChildren< /** * @experimental The component is experimental and subject to change in future releases. */ -export function experimental__Gate(gateProps: GateServerComponentProps) { +export function Protect(gateProps: GateServerComponentProps) { const { children, fallback, ...restAuthorizedParams } = gateProps; - const { experimental__has } = auth(); + const { has, userId, sessionId } = auth(); - if (experimental__has(restAuthorizedParams)) { + /** + * If neither of the authorization params are passed behave as the `` + */ + if (!restAuthorizedParams.condition && !restAuthorizedParams.role && !restAuthorizedParams.permission) { + if (userId && sessionId) { + return <>{children}; + } + return <>{fallback ?? null}; + } + + /** + * Check against the results of `has` called inside the callback + */ + if (typeof restAuthorizedParams.condition === 'function') { + if (restAuthorizedParams.condition(has)) { + return <>{children}; + } + return <>{fallback ?? null}; + } + + if (has(restAuthorizedParams)) { return <>{children}; } + /** + * Fallback to custom ui or null if authorization checks failed + */ return <>{fallback ?? null}; } diff --git a/packages/nextjs/src/client-boundary/controlComponents.ts b/packages/nextjs/src/client-boundary/controlComponents.ts index 2ce6bb6333..58eec00a1f 100644 --- a/packages/nextjs/src/client-boundary/controlComponents.ts +++ b/packages/nextjs/src/client-boundary/controlComponents.ts @@ -5,7 +5,7 @@ export { ClerkLoading, SignedOut, SignedIn, - Experimental__Gate, + Protect, RedirectToSignIn, RedirectToSignUp, RedirectToUserProfile, diff --git a/packages/nextjs/src/components.client.ts b/packages/nextjs/src/components.client.ts index 3bdb446014..aac3f82f65 100644 --- a/packages/nextjs/src/components.client.ts +++ b/packages/nextjs/src/components.client.ts @@ -1,2 +1,2 @@ export { ClerkProvider } from './client-boundary/ClerkProvider'; -export { SignedIn, SignedOut, Experimental__Gate } from './client-boundary/controlComponents'; +export { SignedIn, SignedOut, Protect } from './client-boundary/controlComponents'; diff --git a/packages/nextjs/src/components.server.ts b/packages/nextjs/src/components.server.ts index e002e1b086..f73c8cc91c 100644 --- a/packages/nextjs/src/components.server.ts +++ b/packages/nextjs/src/components.server.ts @@ -1,11 +1,11 @@ import { ClerkProvider } from './app-router/server/ClerkProvider'; -import { experimental__Gate, SignedIn, SignedOut } from './app-router/server/controlComponents'; +import { Protect, SignedIn, SignedOut } from './app-router/server/controlComponents'; -export { ClerkProvider, SignedOut, SignedIn, experimental__Gate as Experimental__Gate }; +export { ClerkProvider, SignedOut, SignedIn, Protect }; export type ServerComponentsServerModuleTypes = { ClerkProvider: typeof ClerkProvider; SignedIn: typeof SignedIn; SignedOut: typeof SignedOut; - Experimental__Gate: typeof experimental__Gate; + Protect: typeof Protect; }; diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts index a4cd040d5d..1b2e1442ff 100644 --- a/packages/nextjs/src/index.ts +++ b/packages/nextjs/src/index.ts @@ -92,8 +92,7 @@ export const SignedOut = ComponentsModule.SignedOut as ServerComponentsServerMod /** * @experimental */ -export const Experimental__Gate = - ComponentsModule.Experimental__Gate as ServerComponentsServerModuleTypes['Experimental__Gate']; +export const Protect = ComponentsModule.Protect as ServerComponentsServerModuleTypes['Protect']; export const auth = ServerHelperModule.auth as ServerHelpersServerModuleTypes['auth']; export const currentUser = ServerHelperModule.currentUser as ServerHelpersServerModuleTypes['currentUser']; diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx index 138857dd6c..4b5ace7e05 100644 --- a/packages/react/src/components/controlComponents.tsx +++ b/packages/react/src/components/controlComponents.tsx @@ -1,4 +1,9 @@ -import type { experimental__CheckAuthorizationWithCustomPermissions, HandleOAuthCallbackParams } from '@clerk/types'; +import type { + CheckAuthorizationWithCustomPermissions, + HandleOAuthCallbackParams, + OrganizationCustomPermission, + OrganizationCustomRole, +} from '@clerk/types'; import React from 'react'; import { useAuthContext } from '../contexts/AuthContext'; @@ -41,22 +46,63 @@ export const ClerkLoading = ({ children }: React.PropsWithChildren): JS return <>{children}; }; -type GateProps = React.PropsWithChildren< - Parameters[0] & { +type ProtectProps = React.PropsWithChildren< + ( + | { + condition?: never; + role: OrganizationCustomRole; + permission?: never; + } + | { + condition?: never; + role?: never; + permission: OrganizationCustomPermission; + } + | { + condition: (has: CheckAuthorizationWithCustomPermissions) => boolean; + role?: never; + permission?: never; + } + | { + condition?: never; + role?: never; + permission?: never; + } + ) & { fallback?: React.ReactNode; } >; -/** - * @experimental The component is experimental and subject to change in future releases. - */ -export const experimental__Gate = ({ children, fallback, ...restAuthorizedParams }: GateProps) => { - const { experimental__has } = useAuth(); +export const Protect = ({ children, fallback, ...restAuthorizedParams }: ProtectProps) => { + const { has, userId, sessionId } = useAuth(); - if (experimental__has(restAuthorizedParams)) { + /** + * If neither of the authorization params are passed behave as the `` + */ + if (!restAuthorizedParams.condition && !restAuthorizedParams.role && !restAuthorizedParams.permission) { + if (userId && sessionId) { + return <>{children}; + } + return <>{fallback ?? null}; + } + + /** + * Check against the results of `has` called inside the callback + */ + if (typeof restAuthorizedParams.condition === 'function') { + if (restAuthorizedParams.condition(has)) { + return <>{children}; + } + return <>{fallback ?? null}; + } + + if (has(restAuthorizedParams)) { return <>{children}; } + /** + * Fallback to custom ui or null if authorization checks failed + */ return <>{fallback ?? null}; }; diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index 55c0949dbb..439452bb52 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -14,7 +14,7 @@ export { ClerkLoading, SignedOut, SignedIn, - experimental__Gate as Experimental__Gate, + Protect, RedirectToSignIn, RedirectToSignUp, RedirectToUserProfile, diff --git a/packages/react/src/hooks/useAuth.ts b/packages/react/src/hooks/useAuth.ts index a3aefa6790..46f84f30cb 100644 --- a/packages/react/src/hooks/useAuth.ts +++ b/packages/react/src/hooks/useAuth.ts @@ -1,6 +1,6 @@ import type { ActJWTClaim, - experimental__CheckAuthorizationWithCustomPermissions, + CheckAuthorizationWithCustomPermissions, GetToken, MembershipRole, SignOut, @@ -13,9 +13,7 @@ import { invalidStateError } from '../errors'; import { errorThrower } from '../utils'; import { createGetToken, createSignOut } from './utils'; -type experimental__CheckAuthorizationSignedOut = ( - params?: Parameters[0], -) => false; +type CheckAuthorizationSignedOut = (params?: Parameters[0]) => false; type UseAuthReturn = | { @@ -27,10 +25,7 @@ type UseAuthReturn = orgId: undefined; orgRole: undefined; orgSlug: undefined; - /** - * @experimental The method is experimental and subject to change in future releases. - */ - experimental__has: experimental__CheckAuthorizationSignedOut; + has: CheckAuthorizationSignedOut; signOut: SignOut; getToken: GetToken; } @@ -43,10 +38,7 @@ type UseAuthReturn = orgId: null; orgRole: null; orgSlug: null; - /** - * @experimental The method is experimental and subject to change in future releases. - */ - experimental__has: experimental__CheckAuthorizationSignedOut; + has: CheckAuthorizationSignedOut; signOut: SignOut; getToken: GetToken; } @@ -59,10 +51,7 @@ type UseAuthReturn = orgId: null; orgRole: null; orgSlug: null; - /** - * @experimental The method is experimental and subject to change in future releases. - */ - experimental__has: experimental__CheckAuthorizationSignedOut; + has: CheckAuthorizationSignedOut; signOut: SignOut; getToken: GetToken; } @@ -75,10 +64,7 @@ type UseAuthReturn = orgId: string; orgRole: MembershipRole; orgSlug: string | null; - /** - * @experimental The method is experimental and subject to change in future releases. - */ - experimental__has: experimental__CheckAuthorizationWithCustomPermissions; + has: CheckAuthorizationWithCustomPermissions; signOut: SignOut; getToken: GetToken; }; @@ -129,12 +115,13 @@ export const useAuth: UseAuth = () => { const signOut: SignOut = useCallback(createSignOut(isomorphicClerk), [isomorphicClerk]); const has = useCallback( - (params?: Parameters[0]) => { - if (!orgId || !userId || !orgRole || !orgPermissions) { - return false; + (params: Parameters[0]) => { + // TODO: assert + if (!params?.permission && !params?.role) { + throw 'Permission or role is required'; } - if (!params) { + if (!orgId || !userId || !orgRole || !orgPermissions) { return false; } @@ -146,17 +133,6 @@ export const useAuth: UseAuth = () => { return orgRole === params.role; } - if (params.some) { - return !!params.some.find(permObj => { - if (permObj.permission) { - return orgPermissions.includes(permObj.permission); - } - if (permObj.role) { - return orgRole === permObj.role; - } - return false; - }); - } return false; }, [orgId, orgRole, userId, orgPermissions], @@ -172,7 +148,7 @@ export const useAuth: UseAuth = () => { orgId: undefined, orgRole: undefined, orgSlug: undefined, - experimental__has: () => false, + has: () => false, signOut, getToken, }; @@ -188,7 +164,7 @@ export const useAuth: UseAuth = () => { orgId: null, orgRole: null, orgSlug: null, - experimental__has: () => false, + has: () => false, signOut, getToken, }; @@ -204,7 +180,7 @@ export const useAuth: UseAuth = () => { orgId, orgRole, orgSlug: orgSlug || null, - experimental__has: has, + has, signOut, getToken, }; @@ -220,7 +196,7 @@ export const useAuth: UseAuth = () => { orgId: null, orgRole: null, orgSlug: null, - experimental__has: () => false, + has: () => false, signOut, getToken, }; diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index 891fea2007..74070654cf 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -8,38 +8,22 @@ import type { import type { ClerkResource } from './resource'; import type { TokenResource } from './token'; import type { UserResource } from './user'; +export type CheckAuthorizationFn = (isAuthorizedParams: Params) => boolean; -export type experimental__CheckAuthorizationWithCustomPermissions = ( - isAuthorizedParams: CheckAuthorizationParamsWithCustomPermissions, -) => boolean; +export type CheckAuthorizationWithCustomPermissions = + CheckAuthorizationFn; type CheckAuthorizationParamsWithCustomPermissions = | { - some: ( - | { - role: OrganizationCustomRole; - permission?: never; - } - | { - role?: never; - permission: OrganizationCustomPermission; - } - )[]; - role?: never; - permission?: never; - } - | { - some?: never; role: OrganizationCustomRole; permission?: never; } | { - some?: never; role?: never; permission: OrganizationCustomPermission; }; -export type CheckAuthorization = (isAuthorizedParams: CheckAuthorizationParams) => boolean; +export type CheckAuthorization = CheckAuthorizationFn; type CheckAuthorizationParams = | { From 698439069c81055430bcd329c751e6322e91e13f Mon Sep 17 00:00:00 2001 From: panteliselef Date: Sun, 3 Dec 2023 16:53:49 +0200 Subject: [PATCH 11/26] feat(nextjs): Introduce `auth().protect()` for App Router Allow per page protection in app router. This utility will automatically throw a 404 error if user is not authorized or authenticated. When `auth().protect()` is called - inside a page or layout file it will render the nearest `not-found` component set by the developer - inside a route handler it will return empty response body with a 404 status code --- packages/nextjs/src/app-router/server/auth.ts | 74 ++++++++++++++++++- packages/nextjs/src/server/getAuth.ts | 6 +- packages/nextjs/src/server/types.ts | 4 +- 3 files changed, 77 insertions(+), 7 deletions(-) diff --git a/packages/nextjs/src/app-router/server/auth.ts b/packages/nextjs/src/app-router/server/auth.ts index 8d8563a90a..3e8982c534 100644 --- a/packages/nextjs/src/app-router/server/auth.ts +++ b/packages/nextjs/src/app-router/server/auth.ts @@ -1,12 +1,82 @@ +import type { SignedInAuthObject, SignedOutAuthObject } from '@clerk/backend'; +import type { + CheckAuthorizationWithCustomPermissions, + OrganizationCustomPermission, + OrganizationCustomRole, +} from '@clerk/types'; +import { notFound } from 'next/navigation'; + import { authAuthHeaderMissing } from '../../server/errors'; import { buildClerkProps, createGetAuth } from '../../server/getAuth'; +import type { AuthObjectWithoutResources } from '../../server/types'; import { buildRequestLike } from './utils'; export const auth = () => { - return createGetAuth({ + const authObject = createGetAuth({ debugLoggerName: 'auth()', noAuthStatusMessage: authAuthHeaderMissing(), - })(buildRequestLike()); + })(buildRequestLike()) as + | AuthObjectWithoutResources< + SignedInAuthObject & { + protect: ( + params?: + | { + role: OrganizationCustomRole; + permission?: never; + } + | { + role?: never; + permission: OrganizationCustomPermission; + } + | ((has: CheckAuthorizationWithCustomPermissions) => boolean), + ) => AuthObjectWithoutResources; + } + > + /** + * Add a comment + */ + | AuthObjectWithoutResources< + SignedOutAuthObject & { + protect: never; + } + >; + + authObject.protect = params => { + /** + * User is not authenticated + */ + if (!authObject.userId) { + notFound(); + } + + /** + * User is authenticated + */ + if (!params) { + return { ...authObject }; + } + + /** + * if a function is passed and returns false then throw not found + */ + if (typeof params === 'function') { + if (params(authObject.has)) { + return { ...authObject }; + } + return notFound(); + } + + /** + * Checking if user is authorized when permission or role is passed + */ + if (authObject.has(params)) { + return { ...authObject }; + } + + notFound(); + }; + + return authObject; }; export const initialState = () => { diff --git a/packages/nextjs/src/server/getAuth.ts b/packages/nextjs/src/server/getAuth.ts index 8d088917c0..f6deeeee0d 100644 --- a/packages/nextjs/src/server/getAuth.ts +++ b/packages/nextjs/src/server/getAuth.ts @@ -1,4 +1,4 @@ -import type { AuthObject, Organization, Session, SignedInAuthObject, SignedOutAuthObject, User } from '@clerk/backend'; +import type { Organization, Session, SignedInAuthObject, SignedOutAuthObject, User } from '@clerk/backend'; import { AuthStatus, constants, @@ -12,11 +12,9 @@ import { import { withLogger } from '../utils/debugLogger'; import { API_URL, API_VERSION, SECRET_KEY } from './constants'; import { getAuthAuthHeaderMissing } from './errors'; -import type { RequestLike } from './types'; +import type { AuthObjectWithoutResources, RequestLike } from './types'; import { getAuthKeyFromRequest, getCookie, getHeader, injectSSRStateIntoObject } from './utils'; -type AuthObjectWithoutResources = Omit; - export const createGetAuth = ({ debugLoggerName, noAuthStatusMessage, diff --git a/packages/nextjs/src/server/types.ts b/packages/nextjs/src/server/types.ts index 18e9b9c04c..b960002590 100644 --- a/packages/nextjs/src/server/types.ts +++ b/packages/nextjs/src/server/types.ts @@ -1,4 +1,4 @@ -import type { OptionalVerifyTokenOptions } from '@clerk/backend'; +import type { AuthObject, OptionalVerifyTokenOptions } from '@clerk/backend'; import type { MultiDomainAndOrProxy } from '@clerk/types'; import type { IncomingMessage } from 'http'; import type { NextApiRequest } from 'next'; @@ -20,3 +20,5 @@ export type WithAuthOptions = OptionalVerifyTokenOptions & }; export type NextMiddlewareResult = Awaited>; + +export type AuthObjectWithoutResources = Omit; From 367a3b32746910c0e788789841648605883532d7 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 4 Dec 2023 11:44:20 +0200 Subject: [PATCH 12/26] chore(types): Add `Key` prefix to OrganizationCustomPermission --- packages/backend/src/tokens/authObjects.ts | 8 ++++---- .../core/resources/OrganizationMembership.ts | 9 +++------ packages/nextjs/src/app-router/server/auth.ts | 8 ++++---- .../src/app-router/server/controlComponents.tsx | 17 +++++++---------- packages/nextjs/src/index.ts | 3 --- .../react/src/components/controlComponents.tsx | 8 ++++---- packages/types/src/json.ts | 7 ++----- packages/types/src/jwtv2.ts | 4 ++-- packages/types/src/organizationMembership.ts | 13 +++++-------- packages/types/src/session.ts | 15 ++++++++------- packages/types/src/ssr.ts | 4 ++-- 11 files changed, 41 insertions(+), 55 deletions(-) diff --git a/packages/backend/src/tokens/authObjects.ts b/packages/backend/src/tokens/authObjects.ts index f9ce3d7b29..6fd8d8f27e 100644 --- a/packages/backend/src/tokens/authObjects.ts +++ b/packages/backend/src/tokens/authObjects.ts @@ -2,8 +2,8 @@ import type { ActClaim, CheckAuthorizationWithCustomPermissions, JwtPayload, - OrganizationCustomPermission, - OrganizationCustomRole, + OrganizationCustomPermissionKey, + OrganizationCustomRoleKey, ServerGetToken, ServerGetTokenOptions, } from '@clerk/types'; @@ -30,9 +30,9 @@ export type SignedInAuthObject = { userId: string; user: User | undefined; orgId: string | undefined; - orgRole: OrganizationCustomRole | undefined; + orgRole: OrganizationCustomRoleKey | undefined; orgSlug: string | undefined; - orgPermissions: OrganizationCustomPermission[] | undefined; + orgPermissions: OrganizationCustomPermissionKey[] | undefined; organization: Organization | undefined; getToken: ServerGetToken; has: CheckAuthorizationWithCustomPermissions; diff --git a/packages/clerk-js/src/core/resources/OrganizationMembership.ts b/packages/clerk-js/src/core/resources/OrganizationMembership.ts index 956aeb94e7..a6815fb896 100644 --- a/packages/clerk-js/src/core/resources/OrganizationMembership.ts +++ b/packages/clerk-js/src/core/resources/OrganizationMembership.ts @@ -5,7 +5,7 @@ import type { MembershipRole, OrganizationMembershipJSON, OrganizationMembershipResource, - OrganizationPermission, + OrganizationPermissionKey, } from '@clerk/types'; import { unixEpochToDate } from '../../utils/date'; @@ -17,10 +17,7 @@ export class OrganizationMembership extends BaseResource implements Organization publicMetadata: OrganizationMembershipPublicMetadata = {}; publicUserData!: PublicUserData; organization!: Organization; - /** - * @experimental The property is experimental and subject to change in future releases. - */ - permissions: OrganizationPermission[] = []; + permissions: OrganizationPermissionKey[] = []; role!: MembershipRole; createdAt!: Date; updatedAt!: Date; @@ -36,7 +33,7 @@ export class OrganizationMembership extends BaseResource implements Organization method: 'GET', // `paginated` is used in some legacy endpoints to support clerk paginated responses // The parameter will be dropped in FAPI v2 - search: convertPageToOffset({ ...retrieveMembershipsParams, paginated: true }) as any, + search: convertPageToOffset({ ...retrieveMembershipsParams, paginated: true }), }) .then(res => { if (!res?.response) { diff --git a/packages/nextjs/src/app-router/server/auth.ts b/packages/nextjs/src/app-router/server/auth.ts index 3e8982c534..8c0210315b 100644 --- a/packages/nextjs/src/app-router/server/auth.ts +++ b/packages/nextjs/src/app-router/server/auth.ts @@ -1,8 +1,8 @@ import type { SignedInAuthObject, SignedOutAuthObject } from '@clerk/backend'; import type { CheckAuthorizationWithCustomPermissions, - OrganizationCustomPermission, - OrganizationCustomRole, + OrganizationCustomPermissionKey, + OrganizationCustomRoleKey, } from '@clerk/types'; import { notFound } from 'next/navigation'; @@ -21,12 +21,12 @@ export const auth = () => { protect: ( params?: | { - role: OrganizationCustomRole; + role: OrganizationCustomRoleKey; permission?: never; } | { role?: never; - permission: OrganizationCustomPermission; + permission: OrganizationCustomPermissionKey; } | ((has: CheckAuthorizationWithCustomPermissions) => boolean), ) => AuthObjectWithoutResources; diff --git a/packages/nextjs/src/app-router/server/controlComponents.tsx b/packages/nextjs/src/app-router/server/controlComponents.tsx index 12bf93bf34..ee19a6396f 100644 --- a/packages/nextjs/src/app-router/server/controlComponents.tsx +++ b/packages/nextjs/src/app-router/server/controlComponents.tsx @@ -1,7 +1,7 @@ import type { CheckAuthorizationWithCustomPermissions, - OrganizationCustomPermission, - OrganizationCustomRole, + OrganizationCustomPermissionKey, + OrganizationCustomRoleKey, } from '@clerk/types'; import React from 'react'; @@ -19,17 +19,17 @@ export function SignedOut(props: React.PropsWithChildren) { return userId ? null : <>{children}; } -type GateServerComponentProps = React.PropsWithChildren< +type ProtectServerComponentProps = React.PropsWithChildren< ( | { condition?: never; - role: OrganizationCustomRole; + role: OrganizationCustomRoleKey; permission?: never; } | { condition?: never; role?: never; - permission: OrganizationCustomPermission; + permission: OrganizationCustomPermissionKey; } | { condition: (has: CheckAuthorizationWithCustomPermissions) => boolean; @@ -46,11 +46,8 @@ type GateServerComponentProps = React.PropsWithChildren< } >; -/** - * @experimental The component is experimental and subject to change in future releases. - */ -export function Protect(gateProps: GateServerComponentProps) { - const { children, fallback, ...restAuthorizedParams } = gateProps; +export function Protect(props: ProtectServerComponentProps) { + const { children, fallback, ...restAuthorizedParams } = props; const { has, userId, sessionId } = auth(); /** diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts index 1b2e1442ff..038b6e2f51 100644 --- a/packages/nextjs/src/index.ts +++ b/packages/nextjs/src/index.ts @@ -89,9 +89,6 @@ export const ClerkProvider = ComponentsModule.ClerkProvider as ServerComponentsS export const SignedIn = ComponentsModule.SignedIn as ServerComponentsServerModuleTypes['SignedIn']; export const SignedOut = ComponentsModule.SignedOut as ServerComponentsServerModuleTypes['SignedOut']; -/** - * @experimental - */ export const Protect = ComponentsModule.Protect as ServerComponentsServerModuleTypes['Protect']; export const auth = ServerHelperModule.auth as ServerHelpersServerModuleTypes['auth']; diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx index 4b5ace7e05..c9247d1f6d 100644 --- a/packages/react/src/components/controlComponents.tsx +++ b/packages/react/src/components/controlComponents.tsx @@ -1,8 +1,8 @@ import type { CheckAuthorizationWithCustomPermissions, HandleOAuthCallbackParams, - OrganizationCustomPermission, - OrganizationCustomRole, + OrganizationCustomPermissionKey, + OrganizationCustomRoleKey, } from '@clerk/types'; import React from 'react'; @@ -50,13 +50,13 @@ type ProtectProps = React.PropsWithChildren< ( | { condition?: never; - role: OrganizationCustomRole; + role: OrganizationCustomRoleKey; permission?: never; } | { condition?: never; role?: never; - permission: OrganizationCustomPermission; + permission: OrganizationCustomPermissionKey; } | { condition: (has: CheckAuthorizationWithCustomPermissions) => boolean; diff --git a/packages/types/src/json.ts b/packages/types/src/json.ts index 4263c5c28e..6e42a887d7 100644 --- a/packages/types/src/json.ts +++ b/packages/types/src/json.ts @@ -7,7 +7,7 @@ import type { ActJWTClaim } from './jwt'; import type { OAuthProvider } from './oauth'; import type { OrganizationDomainVerificationStatus, OrganizationEnrollmentMode } from './organizationDomain'; import type { OrganizationInvitationStatus } from './organizationInvitation'; -import type { MembershipRole, OrganizationPermission } from './organizationMembership'; +import type { MembershipRole, OrganizationPermissionKey } from './organizationMembership'; import type { OrganizationSettingsJSON } from './organizationSettings'; import type { OrganizationSuggestionStatus } from './organizationSuggestion'; import type { SamlIdpSlug } from './saml'; @@ -300,10 +300,7 @@ export interface OrganizationMembershipJSON extends ClerkResourceJSON { object: 'organization_membership'; id: string; organization: OrganizationJSON; - /** - * @experimental The property is experimental and subject to change in future releases. - */ - permissions: OrganizationPermission[]; + permissions: OrganizationPermissionKey[]; public_metadata: OrganizationMembershipPublicMetadata; public_user_data: PublicUserDataJSON; role: MembershipRole; diff --git a/packages/types/src/jwtv2.ts b/packages/types/src/jwtv2.ts index ac543f6f46..f1acea3d9b 100644 --- a/packages/types/src/jwtv2.ts +++ b/packages/types/src/jwtv2.ts @@ -1,4 +1,4 @@ -import type { MembershipRole, OrganizationCustomPermission } from './organizationMembership'; +import type { MembershipRole, OrganizationCustomPermissionKey } from './organizationMembership'; export interface Jwt { header: JwtHeader; @@ -99,7 +99,7 @@ export interface JwtPayload extends CustomJwtSessionClaims { /** * Active organization role */ - org_permissions?: OrganizationCustomPermission[]; + org_permissions?: OrganizationCustomPermissionKey[]; /** * Any other JWT Claim Set member. diff --git a/packages/types/src/organizationMembership.ts b/packages/types/src/organizationMembership.ts index bf9dab93b3..9b8459cb12 100644 --- a/packages/types/src/organizationMembership.ts +++ b/packages/types/src/organizationMembership.ts @@ -26,10 +26,7 @@ declare global { export interface OrganizationMembershipResource extends ClerkResource { id: string; organization: OrganizationResource; - /** - * @experimental The property is experimental and subject to change in future releases. - */ - permissions: OrganizationPermission[]; + permissions: OrganizationPermissionKey[]; publicMetadata: OrganizationMembershipPublicMetadata; publicUserData: PublicUserData; role: MembershipRole; @@ -39,8 +36,8 @@ export interface OrganizationMembershipResource extends ClerkResource { update: (updateParams: UpdateOrganizationMembershipParams) => Promise; } -export type OrganizationCustomPermission = string; -export type OrganizationCustomRole = string; +export type OrganizationCustomPermissionKey = string; +export type OrganizationCustomRoleKey = string; /** * @deprecated This type is deprecated and will be removed in the next major release. @@ -50,7 +47,7 @@ export type OrganizationCustomRole = string; */ export type MembershipRole = Autocomplete<'admin' | 'basic_member' | 'guest_member'>; -export type OrganizationSystemPermission = +export type OrganizationSystemPermissionKey = | 'org:sys_domains:manage' | 'org:sys_profile:manage' | 'org:sys_profile:delete' @@ -62,7 +59,7 @@ export type OrganizationSystemPermission = * OrganizationPermission is a combination of system and custom permissions. * System permissions are only accessible from FAPI and client-side operations/utils */ -export type OrganizationPermission = Autocomplete; +export type OrganizationPermissionKey = Autocomplete; export type UpdateOrganizationMembershipParams = { role: MembershipRole; diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index 74070654cf..4d8387c7b9 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -1,13 +1,14 @@ import type { ActJWTClaim } from './jwt'; import type { MembershipRole, - OrganizationCustomPermission, - OrganizationCustomRole, - OrganizationPermission, + OrganizationCustomPermissionKey, + OrganizationCustomRoleKey, + OrganizationPermissionKey, } from './organizationMembership'; import type { ClerkResource } from './resource'; import type { TokenResource } from './token'; import type { UserResource } from './user'; + export type CheckAuthorizationFn = (isAuthorizedParams: Params) => boolean; export type CheckAuthorizationWithCustomPermissions = @@ -15,12 +16,12 @@ export type CheckAuthorizationWithCustomPermissions = type CheckAuthorizationParamsWithCustomPermissions = | { - role: OrganizationCustomRole; + role: OrganizationCustomRoleKey; permission?: never; } | { role?: never; - permission: OrganizationCustomPermission; + permission: OrganizationCustomPermissionKey; }; export type CheckAuthorization = CheckAuthorizationFn; @@ -34,7 +35,7 @@ type CheckAuthorizationParams = } | { role?: never; - permission: OrganizationPermission; + permission: OrganizationPermissionKey; } )[]; role?: never; @@ -48,7 +49,7 @@ type CheckAuthorizationParams = | { some?: never; role?: never; - permission: OrganizationPermission; + permission: OrganizationPermissionKey; }; export interface SessionResource extends ClerkResource { diff --git a/packages/types/src/ssr.ts b/packages/types/src/ssr.ts index b4616d0f72..7f69f07e67 100644 --- a/packages/types/src/ssr.ts +++ b/packages/types/src/ssr.ts @@ -1,6 +1,6 @@ import type { ActClaim, JwtPayload } from './jwtv2'; import type { OrganizationResource } from './organization'; -import type { MembershipRole, OrganizationCustomPermission } from './organizationMembership'; +import type { MembershipRole, OrganizationCustomPermissionKey } from './organizationMembership'; import type { SessionResource } from './session'; import type { UserResource } from './user'; import type { Serializable } from './utils'; @@ -18,6 +18,6 @@ export type InitialState = Serializable<{ orgId: string | undefined; orgRole: MembershipRole | undefined; orgSlug: string | undefined; - orgPermissions: OrganizationCustomPermission[] | undefined; + orgPermissions: OrganizationCustomPermissionKey[] | undefined; organization: OrganizationResource | undefined; }>; From 4dc9b9fbe7dcf2152a3cb88b3573e59e9373318c Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 4 Dec 2023 12:07:38 +0200 Subject: [PATCH 13/26] chore(nextjs): Remove duplicate types --- .../app-router/server/controlComponents.tsx | 33 ++----------------- 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/packages/nextjs/src/app-router/server/controlComponents.tsx b/packages/nextjs/src/app-router/server/controlComponents.tsx index ee19a6396f..a04568f0c8 100644 --- a/packages/nextjs/src/app-router/server/controlComponents.tsx +++ b/packages/nextjs/src/app-router/server/controlComponents.tsx @@ -1,8 +1,4 @@ -import type { - CheckAuthorizationWithCustomPermissions, - OrganizationCustomPermissionKey, - OrganizationCustomRoleKey, -} from '@clerk/types'; +import type { Protect as ProtectClientComponent } from '@clerk/clerk-react'; import React from 'react'; import { auth } from './auth'; @@ -19,32 +15,7 @@ export function SignedOut(props: React.PropsWithChildren) { return userId ? null : <>{children}; } -type ProtectServerComponentProps = React.PropsWithChildren< - ( - | { - condition?: never; - role: OrganizationCustomRoleKey; - permission?: never; - } - | { - condition?: never; - role?: never; - permission: OrganizationCustomPermissionKey; - } - | { - condition: (has: CheckAuthorizationWithCustomPermissions) => boolean; - role?: never; - permission?: never; - } - | { - condition?: never; - role?: never; - permission?: never; - } - ) & { - fallback?: React.ReactNode; - } ->; +type ProtectServerComponentProps = React.ComponentProps; export function Protect(props: ProtectServerComponentProps) { const { children, fallback, ...restAuthorizedParams } = props; From 7a218c70cbe4d21cdc2c05ac660586d00153f238 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 4 Dec 2023 12:43:25 +0200 Subject: [PATCH 14/26] chore(nextjs): Minor improvements in readability --- packages/backend/src/tokens/authObjects.ts | 4 +- packages/nextjs/src/app-router/server/auth.ts | 54 ++++++++----------- packages/react/src/errors.ts | 3 ++ packages/react/src/hooks/useAuth.ts | 5 +- 4 files changed, 27 insertions(+), 39 deletions(-) diff --git a/packages/backend/src/tokens/authObjects.ts b/packages/backend/src/tokens/authObjects.ts index 6fd8d8f27e..b062700091 100644 --- a/packages/backend/src/tokens/authObjects.ts +++ b/packages/backend/src/tokens/authObjects.ts @@ -196,7 +196,6 @@ const createGetToken: CreateGetToken = params => { }; }; -//MAYBE move this to @shared const createHasAuthorization = ({ orgId, @@ -210,9 +209,8 @@ const createHasAuthorization = orgPermissions: string[] | undefined; }): CheckAuthorizationWithCustomPermissions => params => { - // TODO: assert if (!params?.permission && !params?.role) { - throw 'Permission or role is required'; + throw new Error('Missing params. `has` from `useAuth` requires a permission or role key to be passed.'); } if (!orgId || !userId || !orgRole || !orgPermissions) { diff --git a/packages/nextjs/src/app-router/server/auth.ts b/packages/nextjs/src/app-router/server/auth.ts index 8c0210315b..c4ce820289 100644 --- a/packages/nextjs/src/app-router/server/auth.ts +++ b/packages/nextjs/src/app-router/server/auth.ts @@ -1,9 +1,5 @@ import type { SignedInAuthObject, SignedOutAuthObject } from '@clerk/backend'; -import type { - CheckAuthorizationWithCustomPermissions, - OrganizationCustomPermissionKey, - OrganizationCustomRoleKey, -} from '@clerk/types'; +import type { CheckAuthorizationWithCustomPermissions } from '@clerk/types'; import { notFound } from 'next/navigation'; import { authAuthHeaderMissing } from '../../server/errors'; @@ -11,37 +7,29 @@ import { buildClerkProps, createGetAuth } from '../../server/getAuth'; import type { AuthObjectWithoutResources } from '../../server/types'; import { buildRequestLike } from './utils'; +type AuthSignedIn = AuthObjectWithoutResources< + SignedInAuthObject & { + protect: ( + params?: + | Parameters[0] + | ((has: CheckAuthorizationWithCustomPermissions) => boolean), + ) => AuthObjectWithoutResources; + } +>; + +type AuthSignedOut = AuthObjectWithoutResources< + SignedOutAuthObject & { + protect: never; + } +>; + export const auth = () => { const authObject = createGetAuth({ debugLoggerName: 'auth()', noAuthStatusMessage: authAuthHeaderMissing(), - })(buildRequestLike()) as - | AuthObjectWithoutResources< - SignedInAuthObject & { - protect: ( - params?: - | { - role: OrganizationCustomRoleKey; - permission?: never; - } - | { - role?: never; - permission: OrganizationCustomPermissionKey; - } - | ((has: CheckAuthorizationWithCustomPermissions) => boolean), - ) => AuthObjectWithoutResources; - } - > - /** - * Add a comment - */ - | AuthObjectWithoutResources< - SignedOutAuthObject & { - protect: never; - } - >; + })(buildRequestLike()); - authObject.protect = params => { + (authObject as AuthSignedIn).protect = params => { /** * User is not authenticated */ @@ -63,7 +51,7 @@ export const auth = () => { if (params(authObject.has)) { return { ...authObject }; } - return notFound(); + notFound(); } /** @@ -76,7 +64,7 @@ export const auth = () => { notFound(); }; - return authObject; + return authObject as AuthSignedIn | AuthSignedOut; }; export const initialState = () => { diff --git a/packages/react/src/errors.ts b/packages/react/src/errors.ts index 498e47d280..3266ea6787 100644 --- a/packages/react/src/errors.ts +++ b/packages/react/src/errors.ts @@ -43,3 +43,6 @@ export const customPageWrongProps = (componentName: string) => export const customLinkWrongProps = (componentName: string) => `Missing props. <${componentName}.Link /> component requires the following props: url, label and labelIcon.`; + +export const useAuthHasRequiresRoleOrPermission = + 'Missing params. `has` from `useAuth` requires a permission or role key to be passed.'; diff --git a/packages/react/src/hooks/useAuth.ts b/packages/react/src/hooks/useAuth.ts index 46f84f30cb..2c8e1a9904 100644 --- a/packages/react/src/hooks/useAuth.ts +++ b/packages/react/src/hooks/useAuth.ts @@ -9,7 +9,7 @@ import { useCallback } from 'react'; import { useAuthContext } from '../contexts/AuthContext'; import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext'; -import { invalidStateError } from '../errors'; +import { invalidStateError, useAuthHasRequiresRoleOrPermission } from '../errors'; import { errorThrower } from '../utils'; import { createGetToken, createSignOut } from './utils'; @@ -116,9 +116,8 @@ export const useAuth: UseAuth = () => { const has = useCallback( (params: Parameters[0]) => { - // TODO: assert if (!params?.permission && !params?.role) { - throw 'Permission or role is required'; + errorThrower.throw(useAuthHasRequiresRoleOrPermission); } if (!orgId || !userId || !orgRole || !orgPermissions) { From 3f154ad97c6cf65c7998622c65179f8fb838f96f Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 4 Dec 2023 12:57:55 +0200 Subject: [PATCH 15/26] chore(nextjs): Mark protect utility as experimental for Nextjs --- packages/nextjs/src/app-router/server/auth.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/nextjs/src/app-router/server/auth.ts b/packages/nextjs/src/app-router/server/auth.ts index c4ce820289..83e9112f32 100644 --- a/packages/nextjs/src/app-router/server/auth.ts +++ b/packages/nextjs/src/app-router/server/auth.ts @@ -9,6 +9,10 @@ import { buildRequestLike } from './utils'; type AuthSignedIn = AuthObjectWithoutResources< SignedInAuthObject & { + /** + * @experimental + * This function is experimental as it throws a Nextjs notFound error if user is not authenticated or authorized + */ protect: ( params?: | Parameters[0] @@ -19,6 +23,10 @@ type AuthSignedIn = AuthObjectWithoutResources< type AuthSignedOut = AuthObjectWithoutResources< SignedOutAuthObject & { + /** + * @experimental + * This function is experimental as it throws a Nextjs notFound error if user is not authenticated or authorized + */ protect: never; } >; From d4f7d49acafbbde7f2ed7d462ed36ebb721cd11e Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 4 Dec 2023 19:14:27 +0200 Subject: [PATCH 16/26] chore(nextjs): Minor improvements --- packages/backend/src/tokens/authObjects.ts | 4 +++- .../src/app-router/server/controlComponents.tsx | 16 ++++++++++++++-- .../react/src/components/controlComponents.tsx | 16 ++++++++++++++-- packages/react/src/errors.ts | 2 +- packages/types/src/organizationMembership.ts | 4 ++-- 5 files changed, 34 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/tokens/authObjects.ts b/packages/backend/src/tokens/authObjects.ts index b062700091..7baeea67ab 100644 --- a/packages/backend/src/tokens/authObjects.ts +++ b/packages/backend/src/tokens/authObjects.ts @@ -210,7 +210,9 @@ const createHasAuthorization = }): CheckAuthorizationWithCustomPermissions => params => { if (!params?.permission && !params?.role) { - throw new Error('Missing params. `has` from `useAuth` requires a permission or role key to be passed.'); + throw new Error( + 'Missing parameters. `has` from `auth` or `getAuth` requires a permission or role key to be passed.', + ); } if (!orgId || !userId || !orgRole || !orgPermissions) { diff --git a/packages/nextjs/src/app-router/server/controlComponents.tsx b/packages/nextjs/src/app-router/server/controlComponents.tsx index a04568f0c8..aed6511fee 100644 --- a/packages/nextjs/src/app-router/server/controlComponents.tsx +++ b/packages/nextjs/src/app-router/server/controlComponents.tsx @@ -17,15 +17,27 @@ export function SignedOut(props: React.PropsWithChildren) { type ProtectServerComponentProps = React.ComponentProps; +/** + * Use `` in order to prevent unauthenticated or unauthorized user from accessing the children passed to the component. + * + * Examples: + * ``` + * + * + * has({permission:"a_permission_key"})} /> + * has({role:"a_role_key"})} /> + * Unauthorized

}/> + * ``` + */ export function Protect(props: ProtectServerComponentProps) { const { children, fallback, ...restAuthorizedParams } = props; - const { has, userId, sessionId } = auth(); + const { has, userId } = auth(); /** * If neither of the authorization params are passed behave as the `` */ if (!restAuthorizedParams.condition && !restAuthorizedParams.role && !restAuthorizedParams.permission) { - if (userId && sessionId) { + if (userId) { return <>{children}; } return <>{fallback ?? null}; diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx index c9247d1f6d..f37e147fd7 100644 --- a/packages/react/src/components/controlComponents.tsx +++ b/packages/react/src/components/controlComponents.tsx @@ -73,14 +73,26 @@ type ProtectProps = React.PropsWithChildren< } >; +/** + * Use `` in order to prevent unauthenticated or unauthorized user from accessing the children passed to the component. + * + * Examples: + * ``` + * + * + * has({permission:"a_permission_key"})} /> + * has({role:"a_role_key"})} /> + * Unauthorized

}/> + * ``` + */ export const Protect = ({ children, fallback, ...restAuthorizedParams }: ProtectProps) => { - const { has, userId, sessionId } = useAuth(); + const { has, userId } = useAuth(); /** * If neither of the authorization params are passed behave as the `` */ if (!restAuthorizedParams.condition && !restAuthorizedParams.role && !restAuthorizedParams.permission) { - if (userId && sessionId) { + if (userId) { return <>{children}; } return <>{fallback ?? null}; diff --git a/packages/react/src/errors.ts b/packages/react/src/errors.ts index 3266ea6787..c6d783507c 100644 --- a/packages/react/src/errors.ts +++ b/packages/react/src/errors.ts @@ -45,4 +45,4 @@ export const customLinkWrongProps = (componentName: string) => `Missing props. <${componentName}.Link /> component requires the following props: url, label and labelIcon.`; export const useAuthHasRequiresRoleOrPermission = - 'Missing params. `has` from `useAuth` requires a permission or role key to be passed.'; + 'Missing parameters. `has` from `useAuth` requires a permission or role key to be passed.'; diff --git a/packages/types/src/organizationMembership.ts b/packages/types/src/organizationMembership.ts index 9b8459cb12..19838b8475 100644 --- a/packages/types/src/organizationMembership.ts +++ b/packages/types/src/organizationMembership.ts @@ -41,7 +41,7 @@ export type OrganizationCustomRoleKey = string; /** * @deprecated This type is deprecated and will be removed in the next major release. - * Use `OrganizationCustomRole` instead. + * Use `OrganizationCustomRoleKey` instead. * MembershipRole includes `admin`, `basic_member`, `guest_member`. With the introduction of "Custom roles" * these types will no longer match a developer's custom logic. */ @@ -56,7 +56,7 @@ export type OrganizationSystemPermissionKey = | 'org:sys_domains:read'; /** - * OrganizationPermission is a combination of system and custom permissions. + * OrganizationPermissionKey is a combination of system and custom permissions. * System permissions are only accessible from FAPI and client-side operations/utils */ export type OrganizationPermissionKey = Autocomplete; From 1d323ad5076ba014face31b634c4d7d16a036bbb Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 5 Dec 2023 10:56:48 +0200 Subject: [PATCH 17/26] fix(nextjs,clerk-react,backend): Utility `has` is undefined when user is signed out --- packages/backend/src/tokens/authObjects.ts | 5 +++-- .../nextjs/src/app-router/server/controlComponents.tsx | 4 ++-- packages/react/src/components/controlComponents.tsx | 4 ++-- packages/react/src/hooks/useAuth.ts | 9 +++++---- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/backend/src/tokens/authObjects.ts b/packages/backend/src/tokens/authObjects.ts index 7baeea67ab..3d831d2f45 100644 --- a/packages/backend/src/tokens/authObjects.ts +++ b/packages/backend/src/tokens/authObjects.ts @@ -14,6 +14,7 @@ import { createBackendApiClient } from '../api'; type AuthObjectDebugData = Record; type CreateAuthObjectDebug = (data?: AuthObjectDebugData) => AuthObjectDebug; type AuthObjectDebug = () => AuthObjectDebugData; +type CheckAuthorizationSignedOut = undefined; export type SignedInAuthObjectOptions = CreateBackendApiOptions & { token: string; @@ -52,7 +53,7 @@ export type SignedOutAuthObject = { orgPermissions: null; organization: null; getToken: ServerGetToken; - has: CheckAuthorizationWithCustomPermissions; + has: CheckAuthorizationSignedOut; debug: AuthObjectDebug; }; @@ -126,7 +127,7 @@ export function signedOutAuthObject(debugData?: AuthObjectDebugData): SignedOutA orgPermissions: null, organization: null, getToken: () => Promise.resolve(null), - has: () => false, + has: undefined, debug: createDebug(debugData), }; } diff --git a/packages/nextjs/src/app-router/server/controlComponents.tsx b/packages/nextjs/src/app-router/server/controlComponents.tsx index aed6511fee..5647d8f199 100644 --- a/packages/nextjs/src/app-router/server/controlComponents.tsx +++ b/packages/nextjs/src/app-router/server/controlComponents.tsx @@ -47,13 +47,13 @@ export function Protect(props: ProtectServerComponentProps) { * Check against the results of `has` called inside the callback */ if (typeof restAuthorizedParams.condition === 'function') { - if (restAuthorizedParams.condition(has)) { + if (userId && restAuthorizedParams.condition(has)) { return <>{children}; } return <>{fallback ?? null}; } - if (has(restAuthorizedParams)) { + if (userId && has(restAuthorizedParams)) { return <>{children}; } diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx index f37e147fd7..ffdb1a3fc3 100644 --- a/packages/react/src/components/controlComponents.tsx +++ b/packages/react/src/components/controlComponents.tsx @@ -102,13 +102,13 @@ export const Protect = ({ children, fallback, ...restAuthorizedParams }: Protect * Check against the results of `has` called inside the callback */ if (typeof restAuthorizedParams.condition === 'function') { - if (restAuthorizedParams.condition(has)) { + if (userId && restAuthorizedParams.condition(has)) { return <>{children}; } return <>{fallback ?? null}; } - if (has(restAuthorizedParams)) { + if (userId && has(restAuthorizedParams)) { return <>{children}; } diff --git a/packages/react/src/hooks/useAuth.ts b/packages/react/src/hooks/useAuth.ts index 2c8e1a9904..3781db518c 100644 --- a/packages/react/src/hooks/useAuth.ts +++ b/packages/react/src/hooks/useAuth.ts @@ -13,7 +13,8 @@ import { invalidStateError, useAuthHasRequiresRoleOrPermission } from '../errors import { errorThrower } from '../utils'; import { createGetToken, createSignOut } from './utils'; -type CheckAuthorizationSignedOut = (params?: Parameters[0]) => false; +type CheckAuthorizationSignedOut = undefined; +type CheckAuthorizationWithoutOrg = (params?: Parameters[0]) => false; type UseAuthReturn = | { @@ -51,7 +52,7 @@ type UseAuthReturn = orgId: null; orgRole: null; orgSlug: null; - has: CheckAuthorizationSignedOut; + has: CheckAuthorizationWithoutOrg; signOut: SignOut; getToken: GetToken; } @@ -147,7 +148,7 @@ export const useAuth: UseAuth = () => { orgId: undefined, orgRole: undefined, orgSlug: undefined, - has: () => false, + has: undefined, signOut, getToken, }; @@ -163,7 +164,7 @@ export const useAuth: UseAuth = () => { orgId: null, orgRole: null, orgSlug: null, - has: () => false, + has: undefined, signOut, getToken, }; From a8d61dc372e18942fa7932da9c787a145cb76c44 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 6 Dec 2023 11:57:41 +0200 Subject: [PATCH 18/26] fix(clerk-react): Utility `has` returns false when user isLoaded is true and no user or org --- packages/react/src/hooks/useAuth.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react/src/hooks/useAuth.ts b/packages/react/src/hooks/useAuth.ts index 3781db518c..b8e60b3d1e 100644 --- a/packages/react/src/hooks/useAuth.ts +++ b/packages/react/src/hooks/useAuth.ts @@ -14,7 +14,7 @@ import { errorThrower } from '../utils'; import { createGetToken, createSignOut } from './utils'; type CheckAuthorizationSignedOut = undefined; -type CheckAuthorizationWithoutOrg = (params?: Parameters[0]) => false; +type CheckAuthorizationWithoutOrgOrUser = (params?: Parameters[0]) => false; type UseAuthReturn = | { @@ -39,7 +39,7 @@ type UseAuthReturn = orgId: null; orgRole: null; orgSlug: null; - has: CheckAuthorizationSignedOut; + has: CheckAuthorizationWithoutOrgOrUser; signOut: SignOut; getToken: GetToken; } @@ -52,7 +52,7 @@ type UseAuthReturn = orgId: null; orgRole: null; orgSlug: null; - has: CheckAuthorizationWithoutOrg; + has: CheckAuthorizationWithoutOrgOrUser; signOut: SignOut; getToken: GetToken; } @@ -164,7 +164,7 @@ export const useAuth: UseAuth = () => { orgId: null, orgRole: null, orgSlug: null, - has: undefined, + has: () => false, signOut, getToken, }; From 48d64c54382e21823b6bd3e15e0c58a499551048 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 6 Dec 2023 12:25:29 +0200 Subject: [PATCH 19/26] chore(clerk-react,nextjs): Improve comments --- packages/nextjs/src/app-router/server/auth.ts | 6 ++++-- .../nextjs/src/app-router/server/controlComponents.tsx | 9 +++++---- packages/react/src/components/controlComponents.tsx | 9 +++++---- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/nextjs/src/app-router/server/auth.ts b/packages/nextjs/src/app-router/server/auth.ts index 83e9112f32..4629f0f470 100644 --- a/packages/nextjs/src/app-router/server/auth.ts +++ b/packages/nextjs/src/app-router/server/auth.ts @@ -11,7 +11,8 @@ type AuthSignedIn = AuthObjectWithoutResources< SignedInAuthObject & { /** * @experimental - * This function is experimental as it throws a Nextjs notFound error if user is not authenticated or authorized + * This function is experimental as it throws a Nextjs notFound error if user is not authenticated or authorized. + * In the future we would investigate a way to throw a more appropriate error that clearly describes the not authorized of authenticated status. */ protect: ( params?: @@ -25,7 +26,8 @@ type AuthSignedOut = AuthObjectWithoutResources< SignedOutAuthObject & { /** * @experimental - * This function is experimental as it throws a Nextjs notFound error if user is not authenticated or authorized + * This function is experimental as it throws a Nextjs notFound error if user is not authenticated or authorized. + * In the future we would investigate a way to throw a more appropriate error that clearly describes the not authorized of authenticated status. */ protect: never; } diff --git a/packages/nextjs/src/app-router/server/controlComponents.tsx b/packages/nextjs/src/app-router/server/controlComponents.tsx index 5647d8f199..2177216ff6 100644 --- a/packages/nextjs/src/app-router/server/controlComponents.tsx +++ b/packages/nextjs/src/app-router/server/controlComponents.tsx @@ -18,7 +18,7 @@ export function SignedOut(props: React.PropsWithChildren) { type ProtectServerComponentProps = React.ComponentProps; /** - * Use `` in order to prevent unauthenticated or unauthorized user from accessing the children passed to the component. + * Use `` in order to prevent unauthenticated or unauthorized users from accessing the children passed to the component. * * Examples: * ``` @@ -26,7 +26,7 @@ type ProtectServerComponentProps = React.ComponentProps * has({permission:"a_permission_key"})} /> * has({role:"a_role_key"})} /> - * Unauthorized

}/> + * Unauthorized

} /> * ``` */ export function Protect(props: ProtectServerComponentProps) { @@ -34,7 +34,8 @@ export function Protect(props: ProtectServerComponentProps) { const { has, userId } = auth(); /** - * If neither of the authorization params are passed behave as the `` + * If neither of the authorization params are passed behave as the ``. + * If fallback is present render that instead of rendering nothing. */ if (!restAuthorizedParams.condition && !restAuthorizedParams.role && !restAuthorizedParams.permission) { if (userId) { @@ -58,7 +59,7 @@ export function Protect(props: ProtectServerComponentProps) { } /** - * Fallback to custom ui or null if authorization checks failed + * Fallback to UI provided by user or `null` if authorization checks failed */ return <>{fallback ?? null}; } diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx index ffdb1a3fc3..780750a556 100644 --- a/packages/react/src/components/controlComponents.tsx +++ b/packages/react/src/components/controlComponents.tsx @@ -74,7 +74,7 @@ type ProtectProps = React.PropsWithChildren< >; /** - * Use `` in order to prevent unauthenticated or unauthorized user from accessing the children passed to the component. + * Use `` in order to prevent unauthenticated or unauthorized users from accessing the children passed to the component. * * Examples: * ``` @@ -82,14 +82,15 @@ type ProtectProps = React.PropsWithChildren< * * has({permission:"a_permission_key"})} /> * has({role:"a_role_key"})} /> - * Unauthorized

}/> + * Unauthorized

} /> * ``` */ export const Protect = ({ children, fallback, ...restAuthorizedParams }: ProtectProps) => { const { has, userId } = useAuth(); /** - * If neither of the authorization params are passed behave as the `` + * If neither of the authorization params are passed behave as the ``. + * If fallback is present render that instead of rendering nothing. */ if (!restAuthorizedParams.condition && !restAuthorizedParams.role && !restAuthorizedParams.permission) { if (userId) { @@ -113,7 +114,7 @@ export const Protect = ({ children, fallback, ...restAuthorizedParams }: Protect } /** - * Fallback to custom ui or null if authorization checks failed + * Fallback to UI provided by user or `null` if authorization checks failed */ return <>{fallback ?? null}; }; From e3a4c231ef420639e500e3ac24346d67a77cf692 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 6 Dec 2023 13:56:26 +0200 Subject: [PATCH 20/26] fix(clerk-react): Eliminate flickering of fallback for CSR applications --- packages/react/src/components/controlComponents.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx index 780750a556..ce571a3fc6 100644 --- a/packages/react/src/components/controlComponents.tsx +++ b/packages/react/src/components/controlComponents.tsx @@ -86,7 +86,14 @@ type ProtectProps = React.PropsWithChildren< * ``` */ export const Protect = ({ children, fallback, ...restAuthorizedParams }: ProtectProps) => { - const { has, userId } = useAuth(); + const { isLoaded, has, userId } = useAuth(); + + /** + * Avoid flickering children or fallback while clerk is loading sessionId or userId + */ + if (!isLoaded) { + return null; + } /** * If neither of the authorization params are passed behave as the ``. From 819b6a0d036df1324f3698134c7387fddda8c01c Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 5 Dec 2023 21:54:58 +0200 Subject: [PATCH 21/26] feat(types): Allow overriding of types for custom roles and permissions --- packages/types/src/organizationMembership.ts | 27 +++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/types/src/organizationMembership.ts b/packages/types/src/organizationMembership.ts index 19838b8475..e31e33633a 100644 --- a/packages/types/src/organizationMembership.ts +++ b/packages/types/src/organizationMembership.ts @@ -3,6 +3,15 @@ import type { ClerkResource } from './resource'; import type { PublicUserData } from './session'; import type { Autocomplete } from './utils'; +interface Base { + permission: string; + role: string; +} + +declare global { + interface ClerkAuthorization {} +} + declare global { /** * If you want to provide custom types for the organizationMembership.publicMetadata @@ -36,8 +45,15 @@ export interface OrganizationMembershipResource extends ClerkResource { update: (updateParams: UpdateOrganizationMembershipParams) => Promise; } -export type OrganizationCustomPermissionKey = string; -export type OrganizationCustomRoleKey = string; +export type OrganizationCustomPermissionKey = 'permission' extends keyof ClerkAuthorization + ? // @ts-expect-error Typescript cannot infer the existence of the `permission` key even if we checking it above + ClerkAuthorization['permission'] + : Base['permission']; + +export type OrganizationCustomRoleKey = 'role' extends keyof ClerkAuthorization + ? // @ts-expect-error Typescript cannot infer the existence of the `role` key even if we checking it above + ClerkAuthorization['role'] + : Base['role']; /** * @deprecated This type is deprecated and will be removed in the next major release. @@ -59,7 +75,12 @@ export type OrganizationSystemPermissionKey = * OrganizationPermissionKey is a combination of system and custom permissions. * System permissions are only accessible from FAPI and client-side operations/utils */ -export type OrganizationPermissionKey = Autocomplete; +export type OrganizationPermissionKey = 'permission' extends keyof ClerkAuthorization + ? // @ts-expect-error Typescript cannot infer the existence of the `permission` key even if we checking it above + // Disabling eslint rule because the error causes the type to become any when accessing a property that does not exist + // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents + ClerkAuthorization['permission'] | OrganizationSystemPermissionKey + : Autocomplete; export type UpdateOrganizationMembershipParams = { role: MembershipRole; From 3c6e17e4f1ba9e58e32495ad036ca7081f861d93 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 20 Nov 2023 15:18:04 +0200 Subject: [PATCH 22/26] chore(repo): Update changeset file --- .changeset/chatty-beds-doubt.md | 22 ---------------------- .changeset/short-eagles-search.md | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 22 deletions(-) delete mode 100644 .changeset/chatty-beds-doubt.md create mode 100644 .changeset/short-eagles-search.md diff --git a/.changeset/chatty-beds-doubt.md b/.changeset/chatty-beds-doubt.md deleted file mode 100644 index 8d1952394c..0000000000 --- a/.changeset/chatty-beds-doubt.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@clerk/clerk-js': minor -'@clerk/backend': minor -'@clerk/nextjs': minor -'@clerk/shared': minor -'@clerk/clerk-react': minor -'@clerk/types': minor ---- - -Support for permission checks with Gate. - -```tsx - - - -``` - -```tsx - - - -``` diff --git a/.changeset/short-eagles-search.md b/.changeset/short-eagles-search.md new file mode 100644 index 0000000000..956ec761ab --- /dev/null +++ b/.changeset/short-eagles-search.md @@ -0,0 +1,20 @@ +--- +'@clerk/chrome-extension': minor +'@clerk/clerk-js': minor +'@clerk/backend': minor +'@clerk/nextjs': minor +'@clerk/clerk-react': minor +'@clerk/types': minor +--- + +Introduce Protect for authorization. +Changes in public APIs: +- Rename Gate to Protect +- Support for permission checks. (Previously only roles could be used) +- Remove the `experimental` tags and prefixes +- Drop `some` from the `has` utility and Protect. Protect now accepts a `condition` prop where a function is expected with the `has` being exposed as the param. +- Protect can now be used without required props. In this case behaves as ``, if no authorization props are passed. +- `has` will throw an error if neither `permission` or `role` is passed. +- `auth().protect()` for Nextjs App Router. Allow per page protection in app router. This utility will automatically throw a 404 error if user is not authorized or authenticated. + - inside a page or layout file it will render the nearest `not-found` component set by the developer + - inside a route handler it will return empty response body with a 404 status code From 64ad3234736c1ae92dd6d17d3b1a3ef5173a8384 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 7 Dec 2023 21:01:29 +0200 Subject: [PATCH 23/26] fix(types): `MembershipRole` will include custom roles if applicable --- packages/types/src/organizationMembership.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/types/src/organizationMembership.ts b/packages/types/src/organizationMembership.ts index e31e33633a..b8d6ea8235 100644 --- a/packages/types/src/organizationMembership.ts +++ b/packages/types/src/organizationMembership.ts @@ -61,7 +61,12 @@ export type OrganizationCustomRoleKey = 'role' extends keyof ClerkAuthorization * MembershipRole includes `admin`, `basic_member`, `guest_member`. With the introduction of "Custom roles" * these types will no longer match a developer's custom logic. */ -export type MembershipRole = Autocomplete<'admin' | 'basic_member' | 'guest_member'>; +export type MembershipRole = 'role' extends keyof ClerkAuthorization + ? // @ts-expect-error Typescript cannot infer the existence of the `role` key even if we checking it above + // Disabling eslint rule because the error causes the type to become any when accessing a property that does not exist + // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents + ClerkAuthorization['role'] | 'admin' | 'basic_member' | 'guest_member' + : Autocomplete<'admin' | 'basic_member' | 'guest_member'>; export type OrganizationSystemPermissionKey = | 'org:sys_domains:manage' From 87ea85202fd3090f1bf1e06f94dd7baf8815f56f Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 11 Dec 2023 16:47:28 +0200 Subject: [PATCH 24/26] chore(nextjs): Improve readability of conditionals --- packages/backend/src/tokens/authObjects.ts | 2 +- .../app-router/server/controlComponents.tsx | 32 +++++++++++-------- .../src/components/controlComponents.tsx | 32 +++++++++++-------- packages/react/src/errors.ts | 2 +- 4 files changed, 38 insertions(+), 30 deletions(-) diff --git a/packages/backend/src/tokens/authObjects.ts b/packages/backend/src/tokens/authObjects.ts index 3d831d2f45..8277926fae 100644 --- a/packages/backend/src/tokens/authObjects.ts +++ b/packages/backend/src/tokens/authObjects.ts @@ -212,7 +212,7 @@ const createHasAuthorization = params => { if (!params?.permission && !params?.role) { throw new Error( - 'Missing parameters. `has` from `auth` or `getAuth` requires a permission or role key to be passed.', + 'Missing parameters. `has` from `auth` or `getAuth` requires a permission or role key to be passed. Example usage: `has({permission: "org:posts:edit"`', ); } diff --git a/packages/nextjs/src/app-router/server/controlComponents.tsx b/packages/nextjs/src/app-router/server/controlComponents.tsx index 2177216ff6..7455bb5769 100644 --- a/packages/nextjs/src/app-router/server/controlComponents.tsx +++ b/packages/nextjs/src/app-router/server/controlComponents.tsx @@ -34,32 +34,36 @@ export function Protect(props: ProtectServerComponentProps) { const { has, userId } = auth(); /** - * If neither of the authorization params are passed behave as the ``. - * If fallback is present render that instead of rendering nothing. + * Fallback to UI provided by user or `null` if authorization checks failed */ - if (!restAuthorizedParams.condition && !restAuthorizedParams.role && !restAuthorizedParams.permission) { - if (userId) { - return <>{children}; - } - return <>{fallback ?? null}; + const unauthorized = <>{fallback ?? null}; + + const authorized = <>{children}; + + if (!userId) { + return unauthorized; } /** * Check against the results of `has` called inside the callback */ if (typeof restAuthorizedParams.condition === 'function') { - if (userId && restAuthorizedParams.condition(has)) { - return <>{children}; + if (restAuthorizedParams.condition(has)) { + return authorized; } - return <>{fallback ?? null}; + return unauthorized; } - if (userId && has(restAuthorizedParams)) { - return <>{children}; + if (restAuthorizedParams.role || restAuthorizedParams.permission) { + if (has(restAuthorizedParams)) { + return authorized; + } + return unauthorized; } /** - * Fallback to UI provided by user or `null` if authorization checks failed + * If neither of the authorization params are passed behave as the ``. + * If fallback is present render that instead of rendering nothing. */ - return <>{fallback ?? null}; + return authorized; } diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx index ce571a3fc6..4b05c0cfd3 100644 --- a/packages/react/src/components/controlComponents.tsx +++ b/packages/react/src/components/controlComponents.tsx @@ -96,34 +96,38 @@ export const Protect = ({ children, fallback, ...restAuthorizedParams }: Protect } /** - * If neither of the authorization params are passed behave as the ``. - * If fallback is present render that instead of rendering nothing. + * Fallback to UI provided by user or `null` if authorization checks failed */ - if (!restAuthorizedParams.condition && !restAuthorizedParams.role && !restAuthorizedParams.permission) { - if (userId) { - return <>{children}; - } - return <>{fallback ?? null}; + const unauthorized = <>{fallback ?? null}; + + const authorized = <>{children}; + + if (!userId) { + return unauthorized; } /** * Check against the results of `has` called inside the callback */ if (typeof restAuthorizedParams.condition === 'function') { - if (userId && restAuthorizedParams.condition(has)) { - return <>{children}; + if (restAuthorizedParams.condition(has)) { + return authorized; } - return <>{fallback ?? null}; + return unauthorized; } - if (userId && has(restAuthorizedParams)) { - return <>{children}; + if (restAuthorizedParams.role || restAuthorizedParams.permission) { + if (has(restAuthorizedParams)) { + return authorized; + } + return unauthorized; } /** - * Fallback to UI provided by user or `null` if authorization checks failed + * If neither of the authorization params are passed behave as the ``. + * If fallback is present render that instead of rendering nothing. */ - return <>{fallback ?? null}; + return authorized; }; export const RedirectToSignIn = withClerk(({ clerk, ...props }: WithClerkProp) => { diff --git a/packages/react/src/errors.ts b/packages/react/src/errors.ts index c6d783507c..6f94acf99b 100644 --- a/packages/react/src/errors.ts +++ b/packages/react/src/errors.ts @@ -45,4 +45,4 @@ export const customLinkWrongProps = (componentName: string) => `Missing props. <${componentName}.Link /> component requires the following props: url, label and labelIcon.`; export const useAuthHasRequiresRoleOrPermission = - 'Missing parameters. `has` from `useAuth` requires a permission or role key to be passed.'; + 'Missing parameters. `has` from `useAuth` requires a permission or role key to be passed. Example usage: `has({permission: "org:posts:edit"`'; From 584c71c8acbd0451066c5e17d6fda35efcf36855 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 11 Dec 2023 16:56:00 +0200 Subject: [PATCH 25/26] Revert "fix(nextjs,clerk-react,backend): Utility `has` is undefined when user is signed out" This reverts commit cf736cc8 --- packages/backend/src/tokens/authObjects.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/tokens/authObjects.ts b/packages/backend/src/tokens/authObjects.ts index 8277926fae..13aa72582b 100644 --- a/packages/backend/src/tokens/authObjects.ts +++ b/packages/backend/src/tokens/authObjects.ts @@ -14,7 +14,6 @@ import { createBackendApiClient } from '../api'; type AuthObjectDebugData = Record; type CreateAuthObjectDebug = (data?: AuthObjectDebugData) => AuthObjectDebug; type AuthObjectDebug = () => AuthObjectDebugData; -type CheckAuthorizationSignedOut = undefined; export type SignedInAuthObjectOptions = CreateBackendApiOptions & { token: string; @@ -53,7 +52,7 @@ export type SignedOutAuthObject = { orgPermissions: null; organization: null; getToken: ServerGetToken; - has: CheckAuthorizationSignedOut; + has: CheckAuthorizationWithCustomPermissions; debug: AuthObjectDebug; }; @@ -127,7 +126,7 @@ export function signedOutAuthObject(debugData?: AuthObjectDebugData): SignedOutA orgPermissions: null, organization: null, getToken: () => Promise.resolve(null), - has: undefined, + has: () => false, debug: createDebug(debugData), }; } From 79fff5ae50ebfa0641f9bb821c21363cf50cd3db Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 11 Dec 2023 19:21:45 +0200 Subject: [PATCH 26/26] fix(clerk-js,types): Remove `experimental` from checkAuthorization --- .../src/core/resources/Session.test.ts | 4 +- .../clerk-js/src/core/resources/Session.ts | 17 +------- .../clerk-js/src/ui.retheme/common/Gate.tsx | 42 ++++++++++++++++--- .../OrganizationProfileNavbar.tsx | 14 +++---- .../OrganizationProfileRoutes.tsx | 8 ++-- .../src/ui.retheme/utils/test/mockHelpers.ts | 2 +- packages/clerk-js/src/ui/common/Gate.tsx | 42 ++++++++++++++++--- .../OrganizationProfileNavbar.tsx | 14 +++---- .../OrganizationProfileRoutes.tsx | 6 ++- .../clerk-js/src/ui/utils/test/mockHelpers.ts | 2 +- packages/types/src/session.ts | 21 +--------- 11 files changed, 99 insertions(+), 73 deletions(-) diff --git a/packages/clerk-js/src/core/resources/Session.test.ts b/packages/clerk-js/src/core/resources/Session.test.ts index b3f5c889d8..f705632e76 100644 --- a/packages/clerk-js/src/core/resources/Session.test.ts +++ b/packages/clerk-js/src/core/resources/Session.test.ts @@ -73,7 +73,7 @@ describe('Session', () => { updated_at: new Date().getTime(), } as SessionJSON); - const isAuthorized = await session.experimental__checkAuthorization({ permission: 'org:sys_profile:delete' }); + const isAuthorized = await session.checkAuthorization({ permission: 'org:sys_profile:delete' }); expect(isAuthorized).toBe(true); }); @@ -93,7 +93,7 @@ describe('Session', () => { updated_at: new Date().getTime(), } as SessionJSON); - const isAuthorized = await session.experimental__checkAuthorization({ permission: 'org:sys_profile:delete' }); + const isAuthorized = await session.checkAuthorization({ permission: 'org:sys_profile:delete' }); expect(isAuthorized).toBe(false); }); diff --git a/packages/clerk-js/src/core/resources/Session.ts b/packages/clerk-js/src/core/resources/Session.ts index a153414c41..60860feccf 100644 --- a/packages/clerk-js/src/core/resources/Session.ts +++ b/packages/clerk-js/src/core/resources/Session.ts @@ -75,10 +75,7 @@ export class Session extends BaseResource implements SessionResource { }); }; - /** - * @experimental The method is experimental and subject to change in future releases. - */ - experimental__checkAuthorization: CheckAuthorization = params => { + checkAuthorization: CheckAuthorization = params => { // if there is no active organization user can not be authorized if (!this.lastActiveOrganizationId || !this.user) { return false; @@ -103,18 +100,6 @@ export class Session extends BaseResource implements SessionResource { return activeOrganizationRole === params.role; } - if (params.some) { - return !!params.some.find(permObj => { - if (permObj.permission) { - return activeOrganizationPermissions.includes(permObj.permission); - } - if (permObj.role) { - return activeOrganizationRole === permObj.role; - } - return false; - }); - } - return false; }; diff --git a/packages/clerk-js/src/ui.retheme/common/Gate.tsx b/packages/clerk-js/src/ui.retheme/common/Gate.tsx index 1644b6c7b0..9aefb2801d 100644 --- a/packages/clerk-js/src/ui.retheme/common/Gate.tsx +++ b/packages/clerk-js/src/ui.retheme/common/Gate.tsx @@ -1,13 +1,29 @@ import { useSession } from '@clerk/shared/react'; -import type { CheckAuthorization } from '@clerk/types'; +import type { CheckAuthorization, MembershipRole, OrganizationPermissionKey } from '@clerk/types'; import type { ComponentType, PropsWithChildren, ReactNode } from 'react'; import React, { useEffect } from 'react'; import { useRouter } from '../router'; -type GateParams = Parameters[0]; +type GateParams = Parameters[0] | ((has: CheckAuthorization) => boolean); type GateProps = PropsWithChildren< - GateParams & { + ( + | { + condition?: never; + role: MembershipRole; + permission?: never; + } + | { + condition?: never; + role?: never; + permission: OrganizationPermissionKey; + } + | { + condition: (has: CheckAuthorization) => boolean; + role?: never; + permission?: never; + } + ) & { fallback?: ReactNode; redirectTo?: string; } @@ -16,15 +32,31 @@ type GateProps = PropsWithChildren< export const useGate = (params: GateParams) => { const { session } = useSession(); + if (!session?.id) { + return { isAuthorizedUser: false }; + } + + /** + * if a function is passed and returns false then throw not found + */ + if (typeof params === 'function') { + if (params(session.checkAuthorization)) { + return { isAuthorizedUser: true }; + } + return { isAuthorizedUser: false }; + } + return { - isAuthorizedUser: session?.experimental__checkAuthorization(params), + isAuthorizedUser: session?.checkAuthorization(params), }; }; export const Gate = (gateProps: GateProps) => { const { children, fallback, redirectTo, ...restAuthorizedParams } = gateProps; - const { isAuthorizedUser } = useGate(restAuthorizedParams); + const { isAuthorizedUser } = useGate( + typeof restAuthorizedParams.condition === 'function' ? restAuthorizedParams.condition : restAuthorizedParams, + ); const { navigate } = useRouter(); diff --git a/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/OrganizationProfileNavbar.tsx b/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/OrganizationProfileNavbar.tsx index 36ab17339d..6f7a90db4b 100644 --- a/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/OrganizationProfileNavbar.tsx +++ b/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/OrganizationProfileNavbar.tsx @@ -14,16 +14,12 @@ export const OrganizationProfileNavbar = ( const { organization } = useOrganization(); const { pages } = useOrganizationProfileContext(); - const { isAuthorizedUser: allowMembersRoute } = useGate({ - some: [ - { + const { isAuthorizedUser: allowMembersRoute } = useGate( + has => + has({ permission: 'org:sys_memberships:read', - }, - { - permission: 'org:sys_memberships:manage', - }, - ], - }); + }) || has({ permission: 'org:sys_memberships:manage' }), + ); if (!organization) { return null; diff --git a/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/OrganizationProfileRoutes.tsx b/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/OrganizationProfileRoutes.tsx index ae2799d57a..ed257be5c4 100644 --- a/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/OrganizationProfileRoutes.tsx +++ b/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/OrganizationProfileRoutes.tsx @@ -77,7 +77,7 @@ export const OrganizationProfileRoutes = (props: PropsOfComponent @@ -130,8 +130,10 @@ export const OrganizationProfileRoutes = (props: PropsOfComponent + has({ permission: 'org:sys_memberships:read' }) || has({ permission: 'org:sys_memberships:manage' }) + } + redirectTo={isSettingsPageRoot ? '../' : './organization-settings'} > diff --git a/packages/clerk-js/src/ui.retheme/utils/test/mockHelpers.ts b/packages/clerk-js/src/ui.retheme/utils/test/mockHelpers.ts index 8f4d5e38ac..c509469eff 100644 --- a/packages/clerk-js/src/ui.retheme/utils/test/mockHelpers.ts +++ b/packages/clerk-js/src/ui.retheme/utils/test/mockHelpers.ts @@ -35,7 +35,7 @@ export const mockClerkMethods = (clerk: LoadedClerk): DeepJestMocked { mockMethodsOf(session, { - exclude: ['experimental__checkAuthorization'], + exclude: ['checkAuthorization'], }); mockMethodsOf(session.user); session.user?.emailAddresses.forEach(m => mockMethodsOf(m)); diff --git a/packages/clerk-js/src/ui/common/Gate.tsx b/packages/clerk-js/src/ui/common/Gate.tsx index 1644b6c7b0..9aefb2801d 100644 --- a/packages/clerk-js/src/ui/common/Gate.tsx +++ b/packages/clerk-js/src/ui/common/Gate.tsx @@ -1,13 +1,29 @@ import { useSession } from '@clerk/shared/react'; -import type { CheckAuthorization } from '@clerk/types'; +import type { CheckAuthorization, MembershipRole, OrganizationPermissionKey } from '@clerk/types'; import type { ComponentType, PropsWithChildren, ReactNode } from 'react'; import React, { useEffect } from 'react'; import { useRouter } from '../router'; -type GateParams = Parameters[0]; +type GateParams = Parameters[0] | ((has: CheckAuthorization) => boolean); type GateProps = PropsWithChildren< - GateParams & { + ( + | { + condition?: never; + role: MembershipRole; + permission?: never; + } + | { + condition?: never; + role?: never; + permission: OrganizationPermissionKey; + } + | { + condition: (has: CheckAuthorization) => boolean; + role?: never; + permission?: never; + } + ) & { fallback?: ReactNode; redirectTo?: string; } @@ -16,15 +32,31 @@ type GateProps = PropsWithChildren< export const useGate = (params: GateParams) => { const { session } = useSession(); + if (!session?.id) { + return { isAuthorizedUser: false }; + } + + /** + * if a function is passed and returns false then throw not found + */ + if (typeof params === 'function') { + if (params(session.checkAuthorization)) { + return { isAuthorizedUser: true }; + } + return { isAuthorizedUser: false }; + } + return { - isAuthorizedUser: session?.experimental__checkAuthorization(params), + isAuthorizedUser: session?.checkAuthorization(params), }; }; export const Gate = (gateProps: GateProps) => { const { children, fallback, redirectTo, ...restAuthorizedParams } = gateProps; - const { isAuthorizedUser } = useGate(restAuthorizedParams); + const { isAuthorizedUser } = useGate( + typeof restAuthorizedParams.condition === 'function' ? restAuthorizedParams.condition : restAuthorizedParams, + ); const { navigate } = useRouter(); diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileNavbar.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileNavbar.tsx index b8300940aa..03d2ab8cdd 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileNavbar.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileNavbar.tsx @@ -13,16 +13,12 @@ export const OrganizationProfileNavbar = ( const { organization } = useOrganization(); const { pages } = useOrganizationProfileContext(); - const { isAuthorizedUser: allowMembersRoute } = useGate({ - some: [ - { + const { isAuthorizedUser: allowMembersRoute } = useGate( + has => + has({ permission: 'org:sys_memberships:read', - }, - { - permission: 'org:sys_memberships:manage', - }, - ], - }); + }) || has({ permission: 'org:sys_memberships:manage' }), + ); if (!organization) { return null; diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileRoutes.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileRoutes.tsx index 3a56d86fda..ed257be5c4 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileRoutes.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileRoutes.tsx @@ -77,7 +77,7 @@ export const OrganizationProfileRoutes = (props: PropsOfComponent @@ -130,7 +130,9 @@ export const OrganizationProfileRoutes = (props: PropsOfComponent + has({ permission: 'org:sys_memberships:read' }) || has({ permission: 'org:sys_memberships:manage' }) + } redirectTo={isSettingsPageRoot ? '../' : './organization-settings'} > diff --git a/packages/clerk-js/src/ui/utils/test/mockHelpers.ts b/packages/clerk-js/src/ui/utils/test/mockHelpers.ts index 8f4d5e38ac..c509469eff 100644 --- a/packages/clerk-js/src/ui/utils/test/mockHelpers.ts +++ b/packages/clerk-js/src/ui/utils/test/mockHelpers.ts @@ -35,7 +35,7 @@ export const mockClerkMethods = (clerk: LoadedClerk): DeepJestMocked { mockMethodsOf(session, { - exclude: ['experimental__checkAuthorization'], + exclude: ['checkAuthorization'], }); mockMethodsOf(session.user); session.user?.emailAddresses.forEach(m => mockMethodsOf(m)); diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index 4d8387c7b9..caafd9c723 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -28,26 +28,10 @@ export type CheckAuthorization = CheckAuthorizationFn; type CheckAuthorizationParams = | { - some: ( - | { - role: MembershipRole; - permission?: never; - } - | { - role?: never; - permission: OrganizationPermissionKey; - } - )[]; - role?: never; - permission?: never; - } - | { - some?: never; role: MembershipRole; permission?: never; } | { - some?: never; role?: never; permission: OrganizationPermissionKey; }; @@ -67,10 +51,7 @@ export interface SessionResource extends ClerkResource { remove: () => Promise; touch: () => Promise; getToken: GetToken; - /** - * @experimental The method is experimental and subject to change in future releases. - */ - experimental__checkAuthorization: CheckAuthorization; + checkAuthorization: CheckAuthorization; clearCache: () => void; createdAt: Date; updatedAt: Date;