From f2484cff2e318616b961a7f5cb49b6deba6ba480 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Mon, 23 Oct 2023 12:24:48 -0700 Subject: [PATCH 1/8] feat(nextjs,clerk-react,types,clerk-js): Add experimental support for `` --- packages/backend/src/tokens/authObjects.ts | 49 ++++++++++++- .../src/__snapshots__/exports.test.ts.snap | 1 + .../src/core/resources/Session.test.ts | 4 +- .../clerk-js/src/core/resources/Session.ts | 72 +++++++++---------- packages/clerk-js/src/ui/common/Gate.tsx | 10 ++- .../__tests__/OrganizationMembers.test.tsx | 2 +- .../__tests__/OrganizationSettings.test.tsx | 2 +- .../__tests__/OrganizationSwitcher.test.tsx | 2 +- .../app-router/server/controlComponents.tsx | 37 ++++++++++ .../src/client-boundary/controlComponents.ts | 1 + .../src/components/controlComponents.tsx | 22 +++++- packages/react/src/components/index.ts | 1 + packages/react/src/contexts/AuthContext.ts | 5 +- .../src/contexts/ClerkContextProvider.tsx | 3 +- packages/react/src/hooks/useAuth.ts | 47 +++++++++++- packages/react/src/hooks/utils.ts | 13 ++++ packages/react/src/utils/deriveState.ts | 13 +++- packages/types/src/jwtv2.ts | 7 +- packages/types/src/session.ts | 14 ++-- packages/types/src/ssr.ts | 5 +- 20 files changed, 243 insertions(+), 67 deletions(-) diff --git a/packages/backend/src/tokens/authObjects.ts b/packages/backend/src/tokens/authObjects.ts index ccecdd637b4..dddb801adc7 100644 --- a/packages/backend/src/tokens/authObjects.ts +++ b/packages/backend/src/tokens/authObjects.ts @@ -1,5 +1,12 @@ import { deprecated } from '@clerk/shared/deprecated'; -import type { ActClaim, JwtPayload, ServerGetToken, ServerGetTokenOptions } from '@clerk/types'; +import type { + ActClaim, + CheckAuthorization, + JwtPayload, + OrganizationPermission, + ServerGetToken, + ServerGetTokenOptions, +} from '@clerk/types'; import type { Organization, Session, User } from '../api'; import { createBackendApiClient } from '../api'; @@ -34,8 +41,13 @@ export type SignedInAuthObject = { orgId: string | undefined; orgRole: string | undefined; orgSlug: string | undefined; + orgPermissions: OrganizationPermission[] | undefined; organization: Organization | undefined; getToken: ServerGetToken; + /** + * @experimental The method is experimental and subject to change in future releases. + */ + experimental__has: CheckAuthorization; debug: AuthObjectDebug; }; @@ -50,7 +62,12 @@ export type SignedOutAuthObject = { orgRole: null; orgSlug: null; organization: null; + orgPermissions: null; getToken: ServerGetToken; + /** + * @experimental The method is experimental and subject to change in future releases. + */ + experimental__has: CheckAuthorization; debug: AuthObjectDebug; }; @@ -77,6 +94,7 @@ export function signedInAuthObject( org_id: orgId, org_role: orgRole, org_slug: orgSlug, + org_permissions: orgPermissions, sub: userId, } = sessionClaims; const { apiKey, secretKey, apiUrl, apiVersion, token, session, user, organization } = options; @@ -109,7 +127,9 @@ export function signedInAuthObject( orgRole, orgSlug, organization, + orgPermissions, getToken, + experimental__has: createHasAuthorization({ orgId, orgRole, orgPermissions, userId }), debug: createDebug({ ...options, ...debugData }), }; } @@ -130,7 +150,9 @@ export function signedOutAuthObject(debugData?: AuthObjectDebugData): SignedOutA orgRole: null, orgSlug: null, organization: null, + orgPermissions: null, getToken: () => Promise.resolve(null), + experimental__has: () => false, debug: createDebug(debugData), }; } @@ -190,3 +212,28 @@ const createGetToken: CreateGetToken = params => { return sessionToken; }; }; + +const createHasAuthorization = + ({ + orgId, + orgRole, + orgPermissions, + userId, + }: { + userId: string; + orgId: string | undefined; + orgRole: string | undefined; + orgPermissions: string[] | undefined; + }): CheckAuthorization => + params => { + if (!orgId || !userId || !orgPermissions) { + return false; + } + if (params.permission) { + return orgPermissions.includes(params.permission); + } + if (params.role) { + return orgRole === params.role; + } + 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 2d7332aaf87..0754eaca1c3 100644 --- a/packages/chrome-extension/src/__snapshots__/exports.test.ts.snap +++ b/packages/chrome-extension/src/__snapshots__/exports.test.ts.snap @@ -32,6 +32,7 @@ exports[`public exports should not include a breaking change 1`] = ` "WithSession", "WithUser", "__internal__setErrorThrowerOptions", + "experimental__Gate", "isClerkAPIResponseError", "isEmailLinkError", "isKnownError", diff --git a/packages/clerk-js/src/core/resources/Session.test.ts b/packages/clerk-js/src/core/resources/Session.test.ts index e098f5597d7..b3f5c889d8b 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.isAuthorized({ permission: 'org:sys_profile:delete' }); + const isAuthorized = await session.experimental__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.isAuthorized({ permission: 'org:sys_profile:delete' }); + const isAuthorized = await session.experimental__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 c37cfe97f3d..8f6be3c598d 100644 --- a/packages/clerk-js/src/core/resources/Session.ts +++ b/packages/clerk-js/src/core/resources/Session.ts @@ -2,9 +2,9 @@ import { runWithExponentialBackOff } from '@clerk/shared'; import { is4xxError } from '@clerk/shared/error'; import type { ActJWTClaim, + CheckAuthorization, GetToken, GetTokenOptions, - IsAuthorized, SessionJSON, SessionResource, SessionStatus, @@ -69,8 +69,6 @@ export class Session extends BaseResource implements SessionResource { return SessionTokenCache.clear(); }; - // TODO: Fix this eslint error - getToken: GetToken = async (options?: GetTokenOptions): Promise => { return runWithExponentialBackOff(() => this._getToken(options), { shouldRetry: (error: unknown, currentIteration: number) => !is4xxError(error) && currentIteration < 4, @@ -80,48 +78,44 @@ export class Session extends BaseResource implements SessionResource { /** * @experimental The method is experimental and subject to change in future releases. */ - isAuthorized: IsAuthorized = async params => { - return new Promise((resolve, reject) => { - // if there is no active organization user can not be authorized - if (!this.lastActiveOrganizationId || !this.user) { - return resolve(false); - } + experimental__checkAuthorization: CheckAuthorization = params => { + // if there is no active organization user can not be authorized + if (!this.lastActiveOrganizationId || !this.user) { + return false; + } - // loop through organizationMemberships from client piggybacking - const orgMemberships = this.user.organizationMemberships || []; - const activeMembership = orgMemberships.find(mem => mem.organization.id === this.lastActiveOrganizationId); + // loop through organizationMemberships from client piggybacking + const orgMemberships = this.user.organizationMemberships || []; + const activeMembership = orgMemberships.find(mem => mem.organization.id === this.lastActiveOrganizationId); - // Based on FAPI this should never happen, but we handle it anyway - if (!activeMembership) { - return resolve(false); - } + // Based on FAPI this should never happen, but we handle it anyway + if (!activeMembership) { + return false; + } - const activeOrganizationPermissions = activeMembership.permissions; - const activeOrganizationRole = activeMembership.role; + const activeOrganizationPermissions = activeMembership.permissions; + const activeOrganizationRole = activeMembership.role; - if (params.permission) { - return resolve(activeOrganizationPermissions.includes(params.permission)); - } - if (params.role) { - return resolve(activeOrganizationRole === params.role); - } + if (params.permission) { + return activeOrganizationPermissions.includes(params.permission); + } + if (params.role) { + return activeOrganizationRole === params.role; + } - if (params.any) { - return resolve( - !!params.any.find(permObj => { - if (permObj.permission) { - return activeOrganizationPermissions.includes(permObj.permission); - } - if (permObj.role) { - return activeOrganizationRole === permObj.role; - } - return false; - }), - ); - } + 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 reject(); - }); + return false; }; #hydrateCache = (token: TokenResource | null) => { diff --git a/packages/clerk-js/src/ui/common/Gate.tsx b/packages/clerk-js/src/ui/common/Gate.tsx index 7da69f6b22f..0c1a016742e 100644 --- a/packages/clerk-js/src/ui/common/Gate.tsx +++ b/packages/clerk-js/src/ui/common/Gate.tsx @@ -1,12 +1,11 @@ -import type { IsAuthorized } from '@clerk/types'; +import type { CheckAuthorization } from '@clerk/types'; import type { ComponentType, PropsWithChildren, ReactNode } from 'react'; import React, { useEffect } from 'react'; import { useCoreSession } from '../contexts'; -import { useFetch } from '../hooks'; import { useRouter } from '../router'; -type GateParams = Parameters[0]; +type GateParams = Parameters[0]; type GateProps = PropsWithChildren< GateParams & { fallback?: ReactNode; @@ -15,11 +14,10 @@ type GateProps = PropsWithChildren< >; export const useGate = (params: GateParams) => { - const { isAuthorized } = useCoreSession(); - const { data: isAuthorizedUser } = useFetch(isAuthorized, params); + const { experimental__checkAuthorization } = useCoreSession(); return { - isAuthorizedUser, + isAuthorizedUser: experimental__checkAuthorization(params), }; }; diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationMembers.test.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationMembers.test.tsx index 7fa40648391..a19683a39b8 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationMembers.test.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationMembers.test.tsx @@ -11,7 +11,7 @@ import { createFakeMember, createFakeOrganizationInvitation, createFakeOrganizat const { createFixtures } = bindCreateFixtures('OrganizationProfile'); -describe('OrganizationMembers', () => { +describe.skip('OrganizationMembers', () => { it('renders the Organization Members page', async () => { const { wrapper } = await createFixtures(f => { f.withOrganizations(); diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationSettings.test.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationSettings.test.tsx index f17a1c21953..a13f7ed4225 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationSettings.test.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationSettings.test.tsx @@ -9,7 +9,7 @@ import { createFakeDomain, createFakeMember } from './utils'; const { createFixtures } = bindCreateFixtures('OrganizationProfile'); -describe('OrganizationSettings', () => { +describe.skip('OrganizationSettings', () => { it.skip('enables organization profile button and disables leave when user is the only admin', async () => { const adminsList: OrganizationMembershipResource[] = [createFakeMember({ id: '1', orgId: '1', role: 'admin' })]; const domainList: OrganizationDomainResource[] = [ diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx index 711ee40fcd2..e26b24e6467 100644 --- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx @@ -8,7 +8,7 @@ import { createFakeUserOrganizationInvitation, createFakeUserOrganizationSuggest const { createFixtures } = bindCreateFixtures('OrganizationSwitcher'); -describe('OrganizationSwitcher', () => { +describe.skip('OrganizationSwitcher', () => { it('renders component', async () => { const { wrapper } = await createFixtures(f => { f.withOrganizations(); diff --git a/packages/nextjs/src/app-router/server/controlComponents.tsx b/packages/nextjs/src/app-router/server/controlComponents.tsx index ee3aceacae9..29119d532e1 100644 --- a/packages/nextjs/src/app-router/server/controlComponents.tsx +++ b/packages/nextjs/src/app-router/server/controlComponents.tsx @@ -1,3 +1,5 @@ +import type { CheckAuthorization } from '@clerk/types'; +import { redirect } from 'next/navigation'; import React from 'react'; import { auth } from './auth'; @@ -13,3 +15,38 @@ export function SignedOut(props: React.PropsWithChildren) { const { userId } = auth(); return userId ? null : <>{children}; } + +type GateServerComponentProps = React.PropsWithChildren< + Parameters[0] & { + fallback?: React.ReactNode; + redirectTo?: string; + } +>; + +/** + * @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 { experimental__has } = auth(); + + const isAuthorizedUser = experimental__has(restAuthorizedParams); + + const handleFallback = () => { + if (!redirectTo && !fallback) { + throw new Error('Provide `` with a `fallback` or `redirectTo`'); + } + + if (redirectTo) { + return redirect(redirectTo); + } + + return fallback; + }; + + if (!isAuthorizedUser) { + return handleFallback(); + } + + return children; +} diff --git a/packages/nextjs/src/client-boundary/controlComponents.ts b/packages/nextjs/src/client-boundary/controlComponents.ts index 5b7c92e29a2..020cd8c257c 100644 --- a/packages/nextjs/src/client-boundary/controlComponents.ts +++ b/packages/nextjs/src/client-boundary/controlComponents.ts @@ -5,6 +5,7 @@ export { ClerkLoading, SignedOut, SignedIn, + experimental__Gate, RedirectToSignIn, RedirectToSignUp, RedirectToUserProfile, diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx index 20791102102..b977a289865 100644 --- a/packages/react/src/components/controlComponents.tsx +++ b/packages/react/src/components/controlComponents.tsx @@ -1,10 +1,11 @@ -import type { HandleOAuthCallbackParams } from '@clerk/types'; +import type { CheckAuthorization, HandleOAuthCallbackParams } from '@clerk/types'; import React from 'react'; import { useAuthContext } from '../contexts/AuthContext'; import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext'; import { useSessionContext } from '../contexts/SessionContext'; import { LoadedGuarantee } from '../contexts/StructureContext'; +import { useAuth } from '../hooks'; import type { RedirectToSignInProps, RedirectToSignUpProps, WithClerkProp } from '../types'; import { withClerk } from './withClerk'; @@ -40,6 +41,25 @@ export const ClerkLoading = ({ children }: React.PropsWithChildren): JS return <>{children}; }; +type GateProps = React.PropsWithChildren< + Parameters[0] & { + 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(); + + if (experimental__has(restAuthorizedParams)) { + return <>{children}; + } + + return <>{fallback ?? null}; +}; + export const RedirectToSignIn = withClerk(({ clerk, ...props }: WithClerkProp) => { const { client, session } = clerk; // TODO: Remove temp use of __unstable__environment diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index 1d00b724a32..7127b2e0943 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -14,6 +14,7 @@ export { ClerkLoading, SignedOut, SignedIn, + experimental__Gate, RedirectToSignIn, RedirectToSignUp, RedirectToUserProfile, diff --git a/packages/react/src/contexts/AuthContext.ts b/packages/react/src/contexts/AuthContext.ts index ce346388e46..ab958736ce5 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, MembershipRole, OrganizationPermission } from '@clerk/types'; export const [AuthContext, useAuthContext] = createContextAndHook<{ userId: string | null | undefined; @@ -8,4 +8,7 @@ export const [AuthContext, useAuthContext] = createContextAndHook<{ orgId: string | null | undefined; orgRole: MembershipRole | null | undefined; orgSlug: string | null | undefined; + + // eslint-disable-next-line + orgPermissions: (OrganizationPermission | (string & {}))[] | null | undefined; }>('AuthContext'); diff --git a/packages/react/src/contexts/ClerkContextProvider.tsx b/packages/react/src/contexts/ClerkContextProvider.tsx index 432444fc406..f73fa43820b 100644 --- a/packages/react/src/contexts/ClerkContextProvider.tsx +++ b/packages/react/src/contexts/ClerkContextProvider.tsx @@ -57,10 +57,11 @@ export function ClerkContextProvider(props: ClerkContextProvider): JSX.Element | 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 293639e92d6..bd009882ee5 100644 --- a/packages/react/src/hooks/useAuth.ts +++ b/packages/react/src/hooks/useAuth.ts @@ -1,4 +1,4 @@ -import type { ActJWTClaim, GetToken, MembershipRole, SignOut } from '@clerk/types'; +import type { ActJWTClaim, CheckAuthorization, GetToken, MembershipRole, SignOut } from '@clerk/types'; import { useCallback } from 'react'; import { useAuthContext } from '../contexts/AuthContext'; @@ -7,6 +7,8 @@ import { invalidStateError } from '../errors'; import type IsomorphicClerk from '../isomorphicClerk'; import { createGetToken, createSignOut } from './utils'; +type CheckAuthorizationSignedOut = (params?: Parameters[0]) => false; + type UseAuthReturn = | { isLoaded: false; @@ -17,6 +19,10 @@ type UseAuthReturn = orgId: undefined; orgRole: undefined; orgSlug: undefined; + /** + * @experimental The method is experimental and subject to change in future releases. + */ + experimental__has: CheckAuthorizationSignedOut; signOut: SignOut; getToken: GetToken; } @@ -29,6 +35,10 @@ type UseAuthReturn = orgId: null; orgRole: null; orgSlug: null; + /** + * @experimental The method is experimental and subject to change in future releases. + */ + experimental__has: CheckAuthorizationSignedOut; signOut: SignOut; getToken: GetToken; } @@ -41,6 +51,10 @@ type UseAuthReturn = orgId: null; orgRole: null; orgSlug: null; + /** + * @experimental The method is experimental and subject to change in future releases. + */ + experimental__has: CheckAuthorizationSignedOut; signOut: SignOut; getToken: GetToken; } @@ -53,6 +67,10 @@ type UseAuthReturn = orgId: string; orgRole: MembershipRole; orgSlug: string | null; + /** + * @experimental The method is experimental and subject to change in future releases. + */ + experimental__has: CheckAuthorization; signOut: SignOut; getToken: GetToken; }; @@ -99,12 +117,33 @@ 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() as unknown as IsomorphicClerk; const getToken: GetToken = useCallback(createGetToken(isomorphicClerk), [isomorphicClerk]); const signOut: SignOut = useCallback(createSignOut(isomorphicClerk), [isomorphicClerk]); + const has = useCallback( + (params?: Parameters[0]) => { + if (!orgId || !userId || !orgRole || !orgPermissions) { + return false; + } + + if (!params) { + return false; + } + + if (params.permission) { + return orgPermissions.includes(params.permission); + } + if (params.role) { + return orgRole === params.role; + } + return false; + }, + [orgId, orgRole, userId, orgPermissions], + ); + if (sessionId === undefined && userId === undefined) { return { isLoaded: false, @@ -115,6 +154,7 @@ export const useAuth: UseAuth = () => { orgId: undefined, orgRole: undefined, orgSlug: undefined, + experimental__has: () => false, signOut, getToken, }; @@ -130,6 +170,7 @@ export const useAuth: UseAuth = () => { orgId: null, orgRole: null, orgSlug: null, + experimental__has: () => false, signOut, getToken, }; @@ -145,6 +186,7 @@ export const useAuth: UseAuth = () => { orgId, orgRole, orgSlug: orgSlug || null, + experimental__has: has, signOut, getToken, }; @@ -160,6 +202,7 @@ export const useAuth: UseAuth = () => { orgId: null, orgRole: null, orgSlug: null, + experimental__has: () => false, signOut, getToken, }; diff --git a/packages/react/src/hooks/utils.ts b/packages/react/src/hooks/utils.ts index 28b7cab86a2..955d42f9394 100644 --- a/packages/react/src/hooks/utils.ts +++ b/packages/react/src/hooks/utils.ts @@ -34,3 +34,16 @@ export const createSignOut = (isomorphicClerk: IsomorphicClerk) => { return isomorphicClerk.signOut(...args); }; }; + +/** + * @internal + */ +export const createHasAuthorization = (isomorphicClerk: IsomorphicClerk) => { + return async (options: any) => { + await clerkLoaded(isomorphicClerk); + if (!isomorphicClerk.session) { + return null; + } + return isomorphicClerk.session.getToken(options); + }; +}; diff --git a/packages/react/src/utils/deriveState.ts b/packages/react/src/utils/deriveState.ts index c22204d279d..981c1fcff13 100644 --- a/packages/react/src/utils/deriveState.ts +++ b/packages/react/src/utils/deriveState.ts @@ -1,4 +1,11 @@ -import type { ActiveSessionResource, InitialState, OrganizationResource, Resources, UserResource } from '@clerk/types'; +import type { + ActiveSessionResource, + InitialState, + OrganizationPermission, + OrganizationResource, + Resources, + UserResource, +} from '@clerk/types'; import type { MembershipRole } from '@clerk/types'; export const deriveState = (clerkLoaded: boolean, state: Resources, initialState: InitialState | undefined) => { @@ -18,6 +25,7 @@ const deriveFromSsrInitialState = (initialState: InitialState) => { const orgRole = initialState.orgRole as MembershipRole; const orgSlug = initialState.orgSlug; const actor = initialState.actor; + const orgPermissions = initialState.orgPermissions as OrganizationPermission[]; return { userId, @@ -28,6 +36,7 @@ const deriveFromSsrInitialState = (initialState: InitialState) => { orgId, orgRole, orgSlug, + orgPermissions, actor, lastOrganizationInvitation: null, lastOrganizationMember: null, @@ -46,6 +55,7 @@ const deriveFromClientSideState = (state: Resources) => { const membership = organization ? user?.organizationMemberships?.find(om => om.organization.id === orgId) : organization; + const orgPermissions = membership ? membership.permissions : membership; const orgRole = membership ? membership.role : membership; const lastOrganizationInvitation = state.lastOrganizationInvitation; @@ -60,6 +70,7 @@ const deriveFromClientSideState = (state: Resources) => { orgId, orgRole, orgSlug, + orgPermissions, actor, lastOrganizationInvitation, lastOrganizationMember, diff --git a/packages/types/src/jwtv2.ts b/packages/types/src/jwtv2.ts index 21963b85013..4805691fd96 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; @@ -101,6 +101,11 @@ export interface JwtPayload extends CustomJwtSessionClaims { */ org_role?: MembershipRole; + /** + * 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 3eaecba62ec..d7976c83e6d 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -4,11 +4,11 @@ import type { ClerkResource } from './resource'; import type { TokenResource } from './token'; import type { UserResource } from './user'; -export type IsAuthorized = (isAuthorizedParams: IsAuthorizedParams) => Promise; +export type CheckAuthorization = (isAuthorizedParams: CheckAuthorizationParams) => boolean; -type IsAuthorizedParams = +type CheckAuthorizationParams = | { - any: ( + some: ( | { role: string; permission?: never; @@ -24,20 +24,18 @@ type IsAuthorizedParams = permission?: never; } | { - any?: never; + some?: never; role: string; permission?: never; } | { - any?: never; + some?: never; role?: never; // Adding (string & {}) allows for getting eslint autocomplete but also accepts any string // eslint-disable-next-line permission: OrganizationPermission | (string & {}); }; -type IsAuthorizedReturnValues = boolean; - export interface SessionResource extends ClerkResource { id: string; status: SessionStatus; @@ -56,7 +54,7 @@ export interface SessionResource extends ClerkResource { /** * @experimental The method is experimental and subject to change in future releases. */ - isAuthorized: IsAuthorized; + experimental__checkAuthorization: CheckAuthorization; clearCache: () => void; createdAt: Date; updatedAt: Date; diff --git a/packages/types/src/ssr.ts b/packages/types/src/ssr.ts index a77a28e2a4c..61289508840 100644 --- a/packages/types/src/ssr.ts +++ b/packages/types/src/ssr.ts @@ -1,7 +1,7 @@ import type { ActJWTClaim, ClerkJWTClaims } from './jwt'; 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'; @@ -30,5 +30,8 @@ export type InitialState = Serializable<{ orgId: string | undefined; orgRole: MembershipRole | undefined; orgSlug: string | undefined; + + // eslint-disable-next-line + orgPermissions: (OrganizationPermission | (string & {}))[] | undefined; organization: OrganizationResource | undefined; }>; From 715914a3fc91ad8cc79d86f8facab268df1f62b2 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 2 Nov 2023 15:03:35 +0200 Subject: [PATCH 2/8] chore(clerk-js): Remove references to permissions --- packages/backend/src/tokens/authObjects.ts | 48 +++++++++++-------- packages/react/src/contexts/AuthContext.ts | 5 +- .../src/contexts/ClerkContextProvider.tsx | 3 +- packages/react/src/hooks/useAuth.ts | 9 ++-- packages/react/src/hooks/utils.ts | 13 ----- packages/react/src/utils/deriveState.ts | 17 ++----- packages/types/src/jwtv2.ts | 8 +--- packages/types/src/ssr.ts | 5 +- 8 files changed, 39 insertions(+), 69 deletions(-) diff --git a/packages/backend/src/tokens/authObjects.ts b/packages/backend/src/tokens/authObjects.ts index dddb801adc7..05bc0f64e04 100644 --- a/packages/backend/src/tokens/authObjects.ts +++ b/packages/backend/src/tokens/authObjects.ts @@ -1,12 +1,5 @@ import { deprecated } from '@clerk/shared/deprecated'; -import type { - ActClaim, - CheckAuthorization, - JwtPayload, - OrganizationPermission, - ServerGetToken, - ServerGetTokenOptions, -} from '@clerk/types'; +import type { ActClaim, CheckAuthorization, JwtPayload, ServerGetToken, ServerGetTokenOptions } from '@clerk/types'; import type { Organization, Session, User } from '../api'; import { createBackendApiClient } from '../api'; @@ -41,7 +34,6 @@ export type SignedInAuthObject = { orgId: string | undefined; orgRole: string | undefined; orgSlug: string | undefined; - orgPermissions: OrganizationPermission[] | undefined; organization: Organization | undefined; getToken: ServerGetToken; /** @@ -62,7 +54,6 @@ export type SignedOutAuthObject = { orgRole: null; orgSlug: null; organization: null; - orgPermissions: null; getToken: ServerGetToken; /** * @experimental The method is experimental and subject to change in future releases. @@ -94,7 +85,6 @@ export function signedInAuthObject( org_id: orgId, org_role: orgRole, org_slug: orgSlug, - org_permissions: orgPermissions, sub: userId, } = sessionClaims; const { apiKey, secretKey, apiUrl, apiVersion, token, session, user, organization } = options; @@ -127,9 +117,8 @@ export function signedInAuthObject( orgRole, orgSlug, organization, - orgPermissions, getToken, - experimental__has: createHasAuthorization({ orgId, orgRole, orgPermissions, userId }), + experimental__has: createHasAuthorization({ orgId, orgRole, userId }), debug: createDebug({ ...options, ...debugData }), }; } @@ -150,14 +139,24 @@ export function signedOutAuthObject(debugData?: AuthObjectDebugData): SignedOutA orgRole: null, orgSlug: null, organization: null, - orgPermissions: null, getToken: () => Promise.resolve(null), experimental__has: () => false, debug: createDebug(debugData), }; } -export function prunePrivateMetadata(resource?: { private_metadata: any } | { privateMetadata: any } | null) { +export function prunePrivateMetadata( + resource?: + | { + // eslint-disable-next-line + private_metadata: any; + } + | { + // eslint-disable-next-line + privateMetadata: any; + } + | null, +) { // Delete sensitive private metadata from resource before rendering in SSR if (resource) { // @ts-ignore @@ -169,6 +168,7 @@ export function prunePrivateMetadata(resource?: { private_metadata: any } | { pr return resource; } +// eslint-disable-next-line export function sanitizeAuthObject>(authObject: T): T { const user = authObject.user ? { ...authObject.user } : authObject.user; const organization = authObject.organization ? { ...authObject.organization } : authObject.organization; @@ -217,23 +217,29 @@ const createHasAuthorization = ({ orgId, orgRole, - orgPermissions, userId, }: { userId: string; orgId: string | undefined; orgRole: string | undefined; - orgPermissions: string[] | undefined; }): CheckAuthorization => params => { - if (!orgId || !userId || !orgPermissions) { + if (!orgId || !userId) { 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.role) { + return orgRole === permObj.role; + } + return false; + }); + } + return false; }; diff --git a/packages/react/src/contexts/AuthContext.ts b/packages/react/src/contexts/AuthContext.ts index ab958736ce5..ce346388e46 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 } from '@clerk/types'; export const [AuthContext, useAuthContext] = createContextAndHook<{ userId: string | null | undefined; @@ -8,7 +8,4 @@ export const [AuthContext, useAuthContext] = createContextAndHook<{ orgId: string | null | undefined; orgRole: MembershipRole | null | undefined; orgSlug: string | null | undefined; - - // eslint-disable-next-line - orgPermissions: (OrganizationPermission | (string & {}))[] | null | undefined; }>('AuthContext'); diff --git a/packages/react/src/contexts/ClerkContextProvider.tsx b/packages/react/src/contexts/ClerkContextProvider.tsx index f73fa43820b..432444fc406 100644 --- a/packages/react/src/contexts/ClerkContextProvider.tsx +++ b/packages/react/src/contexts/ClerkContextProvider.tsx @@ -57,11 +57,10 @@ export function ClerkContextProvider(props: ClerkContextProvider): JSX.Element | organization, orgRole, orgSlug, - orgPermissions, } = derivedState; const authCtx = React.useMemo(() => { - const value = { sessionId, userId, actor, orgId, orgRole, orgSlug, orgPermissions }; + const value = { sessionId, userId, actor, orgId, orgRole, orgSlug }; 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 bd009882ee5..58b794c78db 100644 --- a/packages/react/src/hooks/useAuth.ts +++ b/packages/react/src/hooks/useAuth.ts @@ -117,7 +117,7 @@ type UseAuth = () => UseAuthReturn; * } */ export const useAuth: UseAuth = () => { - const { sessionId, userId, actor, orgId, orgRole, orgSlug, orgPermissions } = useAuthContext(); + const { sessionId, userId, actor, orgId, orgRole, orgSlug } = useAuthContext(); const isomorphicClerk = useIsomorphicClerkContext() as unknown as IsomorphicClerk; const getToken: GetToken = useCallback(createGetToken(isomorphicClerk), [isomorphicClerk]); @@ -125,7 +125,7 @@ export const useAuth: UseAuth = () => { const has = useCallback( (params?: Parameters[0]) => { - if (!orgId || !userId || !orgRole || !orgPermissions) { + if (!orgId || !userId || !orgRole) { return false; } @@ -133,15 +133,12 @@ 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, orgPermissions], + [orgId, orgRole, userId], ); if (sessionId === undefined && userId === undefined) { diff --git a/packages/react/src/hooks/utils.ts b/packages/react/src/hooks/utils.ts index 955d42f9394..28b7cab86a2 100644 --- a/packages/react/src/hooks/utils.ts +++ b/packages/react/src/hooks/utils.ts @@ -34,16 +34,3 @@ export const createSignOut = (isomorphicClerk: IsomorphicClerk) => { return isomorphicClerk.signOut(...args); }; }; - -/** - * @internal - */ -export const createHasAuthorization = (isomorphicClerk: IsomorphicClerk) => { - return async (options: any) => { - await clerkLoaded(isomorphicClerk); - if (!isomorphicClerk.session) { - return null; - } - return isomorphicClerk.session.getToken(options); - }; -}; diff --git a/packages/react/src/utils/deriveState.ts b/packages/react/src/utils/deriveState.ts index 981c1fcff13..74770e84ac4 100644 --- a/packages/react/src/utils/deriveState.ts +++ b/packages/react/src/utils/deriveState.ts @@ -1,11 +1,4 @@ -import type { - ActiveSessionResource, - InitialState, - OrganizationPermission, - OrganizationResource, - Resources, - UserResource, -} from '@clerk/types'; +import type { ActiveSessionResource, InitialState, OrganizationResource, Resources, UserResource } from '@clerk/types'; import type { MembershipRole } from '@clerk/types'; export const deriveState = (clerkLoaded: boolean, state: Resources, initialState: InitialState | undefined) => { @@ -17,15 +10,14 @@ export const deriveState = (clerkLoaded: boolean, state: Resources, initialState const deriveFromSsrInitialState = (initialState: InitialState) => { const userId = initialState.userId; - const user = initialState.user as any as UserResource; + const user = initialState.user as UserResource; const sessionId = initialState.sessionId; - const session = initialState.session as any as ActiveSessionResource; - const organization = initialState.organization as any as OrganizationResource; + const session = initialState.session as ActiveSessionResource; + const organization = initialState.organization as OrganizationResource; const orgId = initialState.orgId; const orgRole = initialState.orgRole as MembershipRole; const orgSlug = initialState.orgSlug; const actor = initialState.actor; - const orgPermissions = initialState.orgPermissions as OrganizationPermission[]; return { userId, @@ -36,7 +28,6 @@ const deriveFromSsrInitialState = (initialState: InitialState) => { orgId, orgRole, orgSlug, - orgPermissions, actor, lastOrganizationInvitation: null, lastOrganizationMember: null, diff --git a/packages/types/src/jwtv2.ts b/packages/types/src/jwtv2.ts index 4805691fd96..14151e8d550 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 } from './organizationMembership'; export interface Jwt { header: JwtHeader; @@ -101,11 +101,6 @@ export interface JwtPayload extends CustomJwtSessionClaims { */ org_role?: MembershipRole; - /** - * Active organization role - */ - org_permissions?: OrganizationPermission[]; - /** * Any other JWT Claim Set member. */ @@ -117,5 +112,6 @@ export interface JwtPayload extends CustomJwtSessionClaims { */ export interface ActClaim { sub: string; + [x: string]: unknown; } diff --git a/packages/types/src/ssr.ts b/packages/types/src/ssr.ts index 61289508840..a77a28e2a4c 100644 --- a/packages/types/src/ssr.ts +++ b/packages/types/src/ssr.ts @@ -1,7 +1,7 @@ import type { ActJWTClaim, ClerkJWTClaims } from './jwt'; import type { ActClaim, JwtPayload } from './jwtv2'; import type { OrganizationResource } from './organization'; -import type { MembershipRole, OrganizationPermission } from './organizationMembership'; +import type { MembershipRole } from './organizationMembership'; import type { SessionResource } from './session'; import type { UserResource } from './user'; import type { Serializable } from './utils'; @@ -30,8 +30,5 @@ export type InitialState = Serializable<{ orgId: string | undefined; orgRole: MembershipRole | undefined; orgSlug: string | undefined; - - // eslint-disable-next-line - orgPermissions: (OrganizationPermission | (string & {}))[] | undefined; organization: OrganizationResource | undefined; }>; From f3bd2e9a7dbbd3b86d54ba30c889a7c033a2fade Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 2 Nov 2023 15:14:58 +0200 Subject: [PATCH 3/8] fix(clerk-js): Edit internal Gate to use some instead of any --- .../OrganizationProfile/OrganizationProfileRoutes.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileRoutes.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileRoutes.tsx index 53fd277bfb6..0af1a3cd555 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 From 211c8e8089d27f8cb89f185ea00f49ef9bde6480 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 2 Nov 2023 15:37:21 +0200 Subject: [PATCH 4/8] chore(clerk-js): Enable tests --- .../OrganizationProfile/__tests__/OrganizationMembers.test.tsx | 2 +- .../OrganizationProfile/__tests__/OrganizationSettings.test.tsx | 2 +- .../__tests__/OrganizationSwitcher.test.tsx | 2 +- packages/clerk-js/src/ui/utils/test/mockHelpers.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationMembers.test.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationMembers.test.tsx index a19683a39b8..7fa40648391 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationMembers.test.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationMembers.test.tsx @@ -11,7 +11,7 @@ import { createFakeMember, createFakeOrganizationInvitation, createFakeOrganizat const { createFixtures } = bindCreateFixtures('OrganizationProfile'); -describe.skip('OrganizationMembers', () => { +describe('OrganizationMembers', () => { it('renders the Organization Members page', async () => { const { wrapper } = await createFixtures(f => { f.withOrganizations(); diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationSettings.test.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationSettings.test.tsx index a13f7ed4225..f17a1c21953 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationSettings.test.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationSettings.test.tsx @@ -9,7 +9,7 @@ import { createFakeDomain, createFakeMember } from './utils'; const { createFixtures } = bindCreateFixtures('OrganizationProfile'); -describe.skip('OrganizationSettings', () => { +describe('OrganizationSettings', () => { it.skip('enables organization profile button and disables leave when user is the only admin', async () => { const adminsList: OrganizationMembershipResource[] = [createFakeMember({ id: '1', orgId: '1', role: 'admin' })]; const domainList: OrganizationDomainResource[] = [ diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx index e26b24e6467..711ee40fcd2 100644 --- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx @@ -8,7 +8,7 @@ import { createFakeUserOrganizationInvitation, createFakeUserOrganizationSuggest const { createFixtures } = bindCreateFixtures('OrganizationSwitcher'); -describe.skip('OrganizationSwitcher', () => { +describe('OrganizationSwitcher', () => { it('renders component', async () => { const { wrapper } = await createFixtures(f => { f.withOrganizations(); diff --git a/packages/clerk-js/src/ui/utils/test/mockHelpers.ts b/packages/clerk-js/src/ui/utils/test/mockHelpers.ts index e8dc6477cd9..bb03f1628ac 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: ['isAuthorized'], + exclude: ['experimental__checkAuthorization'], }); mockMethodsOf(session.user); session.user?.emailAddresses.forEach(m => mockMethodsOf(m)); From fba6c41af93c503f49769fc0a822ea3798f29eb5 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 2 Nov 2023 15:38:57 +0200 Subject: [PATCH 5/8] chore(clerk-js): Add changeset --- .changeset/orange-pumpkins-poke.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .changeset/orange-pumpkins-poke.md diff --git a/.changeset/orange-pumpkins-poke.md b/.changeset/orange-pumpkins-poke.md new file mode 100644 index 00000000000..6673bf5dc0d --- /dev/null +++ b/.changeset/orange-pumpkins-poke.md @@ -0,0 +1,10 @@ +--- +'@clerk/chrome-extension': minor +'@clerk/clerk-js': minor +'@clerk/backend': minor +'@clerk/nextjs': minor +'@clerk/clerk-react': minor +'@clerk/types': minor +--- + +Experimental support for `` with role checks. From 409690e5663ba5a81a0e675cf32037472aa1e752 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 2 Nov 2023 16:01:57 +0200 Subject: [PATCH 6/8] chore(types): Create experimental types --- packages/backend/src/tokens/authObjects.ts | 19 +++++++++++-------- packages/react/src/hooks/useAuth.ts | 22 +++++++++++++++------- packages/types/src/session.ts | 16 ++++++++++++++++ 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/packages/backend/src/tokens/authObjects.ts b/packages/backend/src/tokens/authObjects.ts index 05bc0f64e04..c9096b6b85f 100644 --- a/packages/backend/src/tokens/authObjects.ts +++ b/packages/backend/src/tokens/authObjects.ts @@ -1,5 +1,11 @@ import { deprecated } from '@clerk/shared/deprecated'; -import type { ActClaim, CheckAuthorization, JwtPayload, ServerGetToken, ServerGetTokenOptions } from '@clerk/types'; +import type { + ActClaim, + experimental__CheckAuthorizationWithoutPermission, + JwtPayload, + ServerGetToken, + ServerGetTokenOptions, +} from '@clerk/types'; import type { Organization, Session, User } from '../api'; import { createBackendApiClient } from '../api'; @@ -39,7 +45,7 @@ export type SignedInAuthObject = { /** * @experimental The method is experimental and subject to change in future releases. */ - experimental__has: CheckAuthorization; + experimental__has: experimental__CheckAuthorizationWithoutPermission; debug: AuthObjectDebug; }; @@ -58,7 +64,7 @@ export type SignedOutAuthObject = { /** * @experimental The method is experimental and subject to change in future releases. */ - experimental__has: CheckAuthorization; + experimental__has: experimental__CheckAuthorizationWithoutPermission; debug: AuthObjectDebug; }; @@ -148,11 +154,9 @@ export function signedOutAuthObject(debugData?: AuthObjectDebugData): SignedOutA export function prunePrivateMetadata( resource?: | { - // eslint-disable-next-line private_metadata: any; } | { - // eslint-disable-next-line privateMetadata: any; } | null, @@ -168,7 +172,6 @@ export function prunePrivateMetadata( return resource; } -// eslint-disable-next-line export function sanitizeAuthObject>(authObject: T): T { const user = authObject.user ? { ...authObject.user } : authObject.user; const organization = authObject.organization ? { ...authObject.organization } : authObject.organization; @@ -188,7 +191,7 @@ export function sanitizeAuthObject>(authObject: T): T */ export const makeAuthObjectSerializable = >(obj: T): T => { // remove any non-serializable props from the returned object - // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { debug, getToken, ...rest } = obj as unknown as AuthObject; return rest as unknown as T; }; @@ -222,7 +225,7 @@ const createHasAuthorization = userId: string; orgId: string | undefined; orgRole: string | undefined; - }): CheckAuthorization => + }): experimental__CheckAuthorizationWithoutPermission => params => { if (!orgId || !userId) { return false; diff --git a/packages/react/src/hooks/useAuth.ts b/packages/react/src/hooks/useAuth.ts index 58b794c78db..7cfb14f7881 100644 --- a/packages/react/src/hooks/useAuth.ts +++ b/packages/react/src/hooks/useAuth.ts @@ -1,4 +1,10 @@ -import type { ActJWTClaim, CheckAuthorization, GetToken, MembershipRole, SignOut } from '@clerk/types'; +import type { + ActJWTClaim, + experimental__CheckAuthorizationWithoutPermission, + GetToken, + MembershipRole, + SignOut, +} from '@clerk/types'; import { useCallback } from 'react'; import { useAuthContext } from '../contexts/AuthContext'; @@ -7,7 +13,9 @@ import { invalidStateError } from '../errors'; import type IsomorphicClerk from '../isomorphicClerk'; import { createGetToken, createSignOut } from './utils'; -type CheckAuthorizationSignedOut = (params?: Parameters[0]) => false; +type experimental__CheckAuthorizationSignedOut = ( + params?: Parameters[0], +) => false; type UseAuthReturn = | { @@ -22,7 +30,7 @@ type UseAuthReturn = /** * @experimental The method is experimental and subject to change in future releases. */ - experimental__has: CheckAuthorizationSignedOut; + experimental__has: experimental__CheckAuthorizationSignedOut; signOut: SignOut; getToken: GetToken; } @@ -38,7 +46,7 @@ type UseAuthReturn = /** * @experimental The method is experimental and subject to change in future releases. */ - experimental__has: CheckAuthorizationSignedOut; + experimental__has: experimental__CheckAuthorizationSignedOut; signOut: SignOut; getToken: GetToken; } @@ -54,7 +62,7 @@ type UseAuthReturn = /** * @experimental The method is experimental and subject to change in future releases. */ - experimental__has: CheckAuthorizationSignedOut; + experimental__has: experimental__CheckAuthorizationSignedOut; signOut: SignOut; getToken: GetToken; } @@ -70,7 +78,7 @@ type UseAuthReturn = /** * @experimental The method is experimental and subject to change in future releases. */ - experimental__has: CheckAuthorization; + experimental__has: experimental__CheckAuthorizationWithoutPermission; signOut: SignOut; getToken: GetToken; }; @@ -124,7 +132,7 @@ export const useAuth: UseAuth = () => { const signOut: SignOut = useCallback(createSignOut(isomorphicClerk), [isomorphicClerk]); const has = useCallback( - (params?: Parameters[0]) => { + (params?: Parameters[0]) => { if (!orgId || !userId || !orgRole) { return false; } diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index d7976c83e6d..e6455d24892 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -4,6 +4,22 @@ import type { ClerkResource } from './resource'; import type { TokenResource } from './token'; import type { UserResource } from './user'; +export type experimental__CheckAuthorizationWithoutPermission = ( + isAuthorizedParams: CheckAuthorizationParamsWithoutPermission, +) => boolean; + +type CheckAuthorizationParamsWithoutPermission = + | { + some: { + role: string; + }[]; + role?: never; + } + | { + some?: never; + role: string; + }; + export type CheckAuthorization = (isAuthorizedParams: CheckAuthorizationParams) => boolean; type CheckAuthorizationParams = From 228e031aabfe30feb270bae41478b8a6dc8329ac Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 2 Nov 2023 16:04:46 +0200 Subject: [PATCH 7/8] feat(nextjs): Export `` --- .../nextjs/src/app-router/server/controlComponents.tsx | 8 ++++---- packages/nextjs/src/client-boundary/controlComponents.ts | 2 +- packages/nextjs/src/components.client.ts | 2 +- packages/nextjs/src/components.server.ts | 5 +++-- packages/nextjs/src/index.ts | 6 ++++++ packages/react/src/components/controlComponents.tsx | 4 ++-- packages/react/src/components/index.ts | 2 +- 7 files changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/nextjs/src/app-router/server/controlComponents.tsx b/packages/nextjs/src/app-router/server/controlComponents.tsx index 29119d532e1..ac506a1ec8b 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 { CheckAuthorization } from '@clerk/types'; +import type { experimental__CheckAuthorizationWithoutPermission } 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; } @@ -41,12 +41,12 @@ export function experimental__Gate(gateProps: GateServerComponentProps) { return redirect(redirectTo); } - return fallback; + return <>{fallback}; }; if (!isAuthorizedUser) { return handleFallback(); } - return children; + return <>{children}; } diff --git a/packages/nextjs/src/client-boundary/controlComponents.ts b/packages/nextjs/src/client-boundary/controlComponents.ts index 020cd8c257c..2ce6bb63338 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, + Experimental__Gate, RedirectToSignIn, RedirectToSignUp, RedirectToUserProfile, diff --git a/packages/nextjs/src/components.client.ts b/packages/nextjs/src/components.client.ts index 2fcf82aa99c..3bdb446014f 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 } from './client-boundary/controlComponents'; +export { SignedIn, SignedOut, Experimental__Gate } from './client-boundary/controlComponents'; diff --git a/packages/nextjs/src/components.server.ts b/packages/nextjs/src/components.server.ts index a18963af32b..e002e1b0865 100644 --- a/packages/nextjs/src/components.server.ts +++ b/packages/nextjs/src/components.server.ts @@ -1,10 +1,11 @@ import { ClerkProvider } from './app-router/server/ClerkProvider'; -import { SignedIn, SignedOut } from './app-router/server/controlComponents'; +import { experimental__Gate, SignedIn, SignedOut } from './app-router/server/controlComponents'; -export { ClerkProvider, SignedOut, SignedIn }; +export { ClerkProvider, SignedOut, SignedIn, experimental__Gate as Experimental__Gate }; export type ServerComponentsServerModuleTypes = { ClerkProvider: typeof ClerkProvider; SignedIn: typeof SignedIn; SignedOut: typeof SignedOut; + Experimental__Gate: typeof experimental__Gate; }; diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts index ed565c78731..6b4c1714244 100644 --- a/packages/nextjs/src/index.ts +++ b/packages/nextjs/src/index.ts @@ -93,6 +93,12 @@ 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 Experimental__Gate = + ComponentsModule.Experimental__Gate as ServerComponentsServerModuleTypes['Experimental__Gate']; + export const auth = ServerHelperModule.auth as ServerHelpersServerModuleTypes['auth']; export const currentUser = ServerHelperModule.currentUser as ServerHelpersServerModuleTypes['currentUser']; // export const getAuth = ServerHelperModule.getAuth as ServerHelpersServerModuleTypes['getAuth']; diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx index b977a289865..983a6472105 100644 --- a/packages/react/src/components/controlComponents.tsx +++ b/packages/react/src/components/controlComponents.tsx @@ -1,4 +1,4 @@ -import type { CheckAuthorization, HandleOAuthCallbackParams } from '@clerk/types'; +import type { experimental__CheckAuthorizationWithoutPermission, 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/components/index.ts b/packages/react/src/components/index.ts index 7127b2e0943..55c0949dbb7 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, + experimental__Gate as Experimental__Gate, RedirectToSignIn, RedirectToSignUp, RedirectToUserProfile, From 1a30298eb10d34b839e0a393b206d270e64f5319 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 2 Nov 2023 16:09:41 +0200 Subject: [PATCH 8/8] chore(chrome-extension): Update snapshot --- .../chrome-extension/src/__snapshots__/exports.test.ts.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/chrome-extension/src/__snapshots__/exports.test.ts.snap b/packages/chrome-extension/src/__snapshots__/exports.test.ts.snap index 0754eaca1c3..3ac0e70526f 100644 --- a/packages/chrome-extension/src/__snapshots__/exports.test.ts.snap +++ b/packages/chrome-extension/src/__snapshots__/exports.test.ts.snap @@ -8,6 +8,7 @@ exports[`public exports should not include a breaking change 1`] = ` "ClerkProvider", "CreateOrganization", "EmailLinkErrorCode", + "Experimental__Gate", "MagicLinkErrorCode", "MultisessionAppSupport", "OrganizationList", @@ -32,7 +33,6 @@ exports[`public exports should not include a breaking change 1`] = ` "WithSession", "WithUser", "__internal__setErrorThrowerOptions", - "experimental__Gate", "isClerkAPIResponseError", "isEmailLinkError", "isKnownError",