From c505edac36dfb0a3440e4341344b69aa7b4d637f Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Thu, 8 May 2025 18:59:04 -0700 Subject: [PATCH 1/3] feat(vue): Introduce billing in useAuth and Protect --- .../vue/src/components/controlComponents.ts | 25 ++++++++++- packages/vue/src/composables/useAuth.ts | 44 ++++++------------- packages/vue/src/types.ts | 1 + 3 files changed, 39 insertions(+), 31 deletions(-) diff --git a/packages/vue/src/components/controlComponents.ts b/packages/vue/src/components/controlComponents.ts index 40d6f19f80b..2838de895ec 100644 --- a/packages/vue/src/components/controlComponents.ts +++ b/packages/vue/src/components/controlComponents.ts @@ -1,5 +1,6 @@ import { deprecated } from '@clerk/shared/deprecated'; import type { + Autocomplete, CheckAuthorizationWithCustomPermissions, HandleOAuthCallbackParams, OrganizationCustomPermissionKey, @@ -111,21 +112,43 @@ export type ProtectProps = ( condition?: never; role: OrganizationCustomRoleKey; permission?: never; + feature?: never; + plan?: never; } | { condition?: never; role?: never; + feature?: never; + plan?: never; permission: OrganizationCustomPermissionKey; } | { condition: (has: CheckAuthorizationWithCustomPermissions) => boolean; role?: never; permission?: never; + feature?: never; + plan?: never; } | { condition?: never; role?: never; permission?: never; + feature: Autocomplete<`user:${string}` | `org:${string}`>; + plan?: never; + } + | { + condition?: never; + role?: never; + permission?: never; + feature?: never; + plan: Autocomplete<`user:${string}` | `org:${string}`>; + } + | { + condition?: never; + role?: never; + permission?: never; + feature?: never; + plan?: never; } ) & PendingSessionOptions; @@ -160,7 +183,7 @@ export const Protect = defineComponent((props: ProtectProps, { slots }) => { return slots.fallback?.(); } - if (props.role || props.permission) { + if (props.role || props.permission || props.feature || props.plan) { if (has.value?.(props)) { return slots.default?.(); } diff --git a/packages/vue/src/composables/useAuth.ts b/packages/vue/src/composables/useAuth.ts index 8d577a57cc6..e5902f8a4a5 100644 --- a/packages/vue/src/composables/useAuth.ts +++ b/packages/vue/src/composables/useAuth.ts @@ -1,16 +1,9 @@ -import { resolveAuthState } from '@clerk/shared/authorization'; -import type { - CheckAuthorizationWithCustomPermissions, - Clerk, - GetToken, - PendingSessionOptions, - SignOut, - UseAuthReturn, -} from '@clerk/types'; +import { createCheckAuthorization, resolveAuthState } from '@clerk/shared/authorization'; +import type { Clerk, GetToken, JwtPayload, PendingSessionOptions, SignOut, UseAuthReturn } from '@clerk/types'; import { computed, type ShallowRef, watch } from 'vue'; import { errorThrower } from '../errors/errorThrower'; -import { invalidStateError, useAuthHasRequiresRoleOrPermission } from '../errors/messages'; +import { invalidStateError } from '../errors/messages'; import type { ToComputedRefs } from '../utils'; import { toComputedRefs } from '../utils'; import { useClerkContext } from './useClerkContext'; @@ -87,26 +80,17 @@ export const useAuth: UseAuth = (options = {}) => { const signOut: SignOut = createSignOut(clerk); const result = computed(() => { - const { userId, orgId, orgRole, orgPermissions } = authCtx.value; - - const has = (params: Parameters[0]) => { - if (!params?.permission && !params?.role) { - return errorThrower.throw(useAuthHasRequiresRoleOrPermission); - } - if (!orgId || !userId || !orgRole || !orgPermissions) { - return false; - } - - if (params.permission) { - return orgPermissions.includes(params.permission); - } - - if (params.role) { - return orgRole === params.role; - } - - return false; - }; + const { userId, orgId, orgRole, orgPermissions, sessionClaims, factorVerificationAge } = authCtx.value; + + const has = createCheckAuthorization({ + userId, + orgId, + orgRole, + orgPermissions, + factorVerificationAge, + features: ((sessionClaims as JwtPayload | undefined)?.fea as string) || '', + plans: ((sessionClaims as JwtPayload | undefined)?.pla as string) || '', + }); const payload = resolveAuthState({ authObject: { diff --git a/packages/vue/src/types.ts b/packages/vue/src/types.ts index aef4e97ece1..75f4c5b46ec 100644 --- a/packages/vue/src/types.ts +++ b/packages/vue/src/types.ts @@ -28,6 +28,7 @@ export interface VueClerkInjectionKeyType { orgRole: OrganizationCustomRoleKey | null | undefined; orgSlug: string | null | undefined; orgPermissions: OrganizationCustomPermissionKey[] | null | undefined; + factorVerificationAge: [number, number] | null; }>; clientCtx: ComputedRef; sessionCtx: ComputedRef; From be2d7763a6d33c067a47f4cc060c2a495628f77b Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Thu, 8 May 2025 19:03:05 -0700 Subject: [PATCH 2/3] chore: add missing prop --- packages/vue/src/plugin.ts | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/vue/src/plugin.ts b/packages/vue/src/plugin.ts index 01356647f0e..90997d4c6b7 100644 --- a/packages/vue/src/plugin.ts +++ b/packages/vue/src/plugin.ts @@ -79,9 +79,30 @@ export const clerkPlugin: Plugin<[PluginOptions]> = { const derivedState = computed(() => deriveState(loaded.value, resources.value, initialState)); const authCtx = computed(() => { - const { sessionId, userId, orgId, actor, orgRole, orgSlug, orgPermissions, sessionStatus, sessionClaims } = - derivedState.value; - return { sessionId, userId, actor, orgId, orgRole, orgSlug, orgPermissions, sessionStatus, sessionClaims }; + const { + sessionId, + userId, + orgId, + actor, + orgRole, + orgSlug, + orgPermissions, + sessionStatus, + sessionClaims, + factorVerificationAge, + } = derivedState.value; + return { + sessionId, + userId, + actor, + orgId, + orgRole, + orgSlug, + orgPermissions, + sessionStatus, + sessionClaims, + factorVerificationAge, + }; }); const clientCtx = computed(() => resources.value.client); const userCtx = computed(() => derivedState.value.user); From 02d0e42e78813533ff9c8efd9f744c02888c2265 Mon Sep 17 00:00:00 2001 From: Robert Soriano Date: Thu, 8 May 2025 19:11:39 -0700 Subject: [PATCH 3/3] choire: add changeset --- .changeset/five-tips-own.md | 54 +++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .changeset/five-tips-own.md diff --git a/.changeset/five-tips-own.md b/.changeset/five-tips-own.md new file mode 100644 index 00000000000..c46ee04ef82 --- /dev/null +++ b/.changeset/five-tips-own.md @@ -0,0 +1,54 @@ +--- +'@clerk/vue': minor +--- + +Introduce feature or plan based authorization + +## `useAuth()` +### Plan + +```ts +const { has } = useAuth() +has.value({ plan: "my-plan" }) +``` + +### Feature + +```ts +const { has } = useAuth() +has.value({ feature: "my-feature" }) +``` + +### Scoped per user or per org + +```ts +const { has } = useAuth() + +has.value({ feature: "org:my-feature" }) +has.value({ feature: "user:my-feature" }) +has.value({ plan: "user:my-plan" }) +has.value({ plan: "org:my-plan" }) +``` + +## `` + +### Plan + +```html + +``` + +### Feature + +```html + +``` + +### Scoped per user or per org + +```html + + + + +```