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;