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
+
+
+
+
+```
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/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);
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;