@xenterprises/nuxt-x-auth-better 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -257,6 +257,199 @@ This layer uses Better Auth's official Vue client (`better-auth/vue`) via `creat
257
257
 
258
258
  5. **Token access**: `getToken()` and `getAuthHeaders()` extract the access token from the Better Auth session for use with your own API calls.
259
259
 
260
+ ## Organizations & Teams
261
+
262
+ The layer includes a full BetterAuth Organizations & Teams implementation. Enable it in `app.config.ts`:
263
+
264
+ ```ts
265
+ xAuth: {
266
+ features: {
267
+ organization: true,
268
+ teams: true,
269
+ },
270
+ organization: {
271
+ enabled: true,
272
+ teams: true,
273
+ },
274
+ }
275
+ ```
276
+
277
+ ### `useOrganization()` Composable
278
+
279
+ ```vue
280
+ <script setup>
281
+ const {
282
+ // State
283
+ organizations,
284
+ organizationsLoading,
285
+ activeOrganization,
286
+ activeLoading,
287
+ activeMember,
288
+ activeMemberRole,
289
+ members,
290
+ invitations,
291
+ teams,
292
+ isOrgAdmin,
293
+ isOrgOwner,
294
+ canCreateOrganization,
295
+
296
+ // Org CRUD
297
+ createOrganization,
298
+ updateOrganization,
299
+ deleteOrganization,
300
+ setActiveOrganization,
301
+ getFullOrganization,
302
+ checkSlug,
303
+
304
+ // Members
305
+ listMembers,
306
+ updateMemberRole,
307
+ removeMember,
308
+ leaveOrganization,
309
+
310
+ // Invitations
311
+ inviteMember,
312
+ cancelInvitation,
313
+ acceptInvitation,
314
+ rejectInvitation,
315
+ getInvitation,
316
+ listInvitations,
317
+ listUserInvitations,
318
+
319
+ // Teams
320
+ listTeams,
321
+ createTeam,
322
+ updateTeam,
323
+ deleteTeam,
324
+ setActiveTeam,
325
+ listTeamMembers,
326
+ addTeamMember,
327
+ removeTeamMember,
328
+ listUserTeams,
329
+
330
+ // Access control
331
+ hasPermission,
332
+ checkRolePermission,
333
+ getDefaultPermissions,
334
+ } = useOrganization()
335
+ </script>
336
+ ```
337
+
338
+ #### Reactive State
339
+
340
+ | Property | Type | Description |
341
+ |----------|------|-------------|
342
+ | `organizations` | `Ref<Organization[]>` | All orgs the user belongs to |
343
+ | `organizationsLoading` | `Ref<boolean>` | Whether the org list is loading |
344
+ | `activeOrganization` | `Ref<Organization \| null>` | Currently active organization |
345
+ | `activeLoading` | `Ref<boolean>` | Whether the active org is loading |
346
+ | `activeMember` | `Ref<OrganizationMember \| null>` | Current user's membership in active org |
347
+ | `activeMemberRole` | `Ref<string \| null>` | Role of current user in active org |
348
+ | `members` | `ComputedRef<OrganizationMember[]>` | Members of the active org |
349
+ | `invitations` | `ComputedRef<OrganizationInvitation[]>` | Pending invitations for the active org |
350
+ | `teams` | `ComputedRef<OrganizationTeam[]>` | Teams in the active org |
351
+ | `isOrgAdmin` | `ComputedRef<boolean>` | User is owner or admin of active org |
352
+ | `isOrgOwner` | `ComputedRef<boolean>` | User is owner of active org |
353
+ | `canCreateOrganization` | `ComputedRef<boolean>` | User can create new organizations |
354
+
355
+ #### Methods
356
+
357
+ | Method | Description |
358
+ |--------|-------------|
359
+ | `createOrganization(name, slug?, logo?)` | Create a new organization |
360
+ | `updateOrganization(data)` | Update organization details |
361
+ | `deleteOrganization(organizationId?)` | Delete an organization |
362
+ | `setActiveOrganization(organizationId?, organizationSlug?)` | Set the active organization |
363
+ | `getFullOrganization(organizationId?, organizationSlug?)` | Fetch full org details |
364
+ | `checkSlug(slug)` | Check if a slug is available |
365
+ | `listMembers(organizationId?)` | List organization members |
366
+ | `updateMemberRole(memberId, role, organizationId?)` | Update a member's role |
367
+ | `removeMember(memberId, organizationId?)` | Remove a member |
368
+ | `leaveOrganization()` | Leave the active organization |
369
+ | `inviteMember(email, role, organizationId?)` | Invite a user by email |
370
+ | `cancelInvitation(invitationId)` | Cancel a pending invitation |
371
+ | `acceptInvitation(invitationId)` | Accept an invitation |
372
+ | `rejectInvitation(invitationId)` | Reject an invitation |
373
+ | `getInvitation(invitationId)` | Fetch invitation details |
374
+ | `listInvitations(organizationId?)` | List org invitations |
375
+ | `listUserInvitations()` | List current user's invitations |
376
+ | `listTeams(organizationId?)` | List teams in an org |
377
+ | `createTeam(name, organizationId?)` | Create a team |
378
+ | `updateTeam(teamId, data, organizationId?)` | Update a team |
379
+ | `deleteTeam(teamId, organizationId?)` | Delete a team |
380
+ | `setActiveTeam(teamId)` | Set the active team |
381
+ | `listTeamMembers(teamId)` | List members of a team |
382
+ | `addTeamMember(teamId, memberId, organizationId?)` | Add a member to a team |
383
+ | `removeTeamMember(teamId, memberId)` | Remove a member from a team |
384
+ | `listUserTeams()` | List teams for current user |
385
+ | `hasPermission(permissionCheck)` | Check if active member has a permission |
386
+ | `checkRolePermission(role, permissions)` | Check permissions for a specific role |
387
+ | `getDefaultPermissions(role)` | Get default permissions for a role |
388
+
389
+ ### Components
390
+
391
+ | Component | Description |
392
+ |-----------|-------------|
393
+ | `XAuthOrganizationSwitcher` | List and switch between organizations |
394
+ | `XAuthOrganizationCreate` | Create a new organization |
395
+ | `XAuthOrganizationSettings` | Edit / delete organization |
396
+ | `XAuthOrganizationMembers` | Manage organization members |
397
+ | `XAuthOrganizationInvite` | Invite a new member |
398
+ | `XAuthOrganizationInvitations` | List pending invitations |
399
+ | `XAuthTeamList` | List teams in an organization |
400
+ | `XAuthTeamCreate` | Create a team |
401
+ | `XAuthTeamSettings` | Edit / delete team |
402
+ | `XAuthTeamMembers` | Manage team members |
403
+
404
+ ### Pages (auto-registered)
405
+
406
+ | Path | Description |
407
+ |------|-------------|
408
+ | `/org` | Organization list / switcher |
409
+ | `/org/create` | Create a new organization |
410
+ | `/org/:slug` | Organization dashboard |
411
+ | `/org/:slug/settings` | Organization settings |
412
+ | `/org/:slug/members` | Member list |
413
+ | `/org/:slug/invite` | Invite member |
414
+ | `/org/:slug/invitations` | Pending invitations |
415
+ | `/org/:slug/teams` | Team list |
416
+ | `/org/:slug/teams/create` | Create team |
417
+ | `/org/:slug/teams/:teamId` | Team settings + members |
418
+ | `/auth/handler/invitation?id=...` | Public invitation accept handler |
419
+
420
+ ### Middleware
421
+
422
+ `organization.global.ts` runs on every navigation. For routes under `/org` (or any path configured in `organization.protectedRoutes`), it:
423
+
424
+ 1. Verifies organizations are enabled.
425
+ 2. Redirects unauthenticated users to the login page.
426
+ 3. Resolves the requested organization by slug and sets it active.
427
+ 4. Redirects non-members to the `notMember` redirect path.
428
+ 5. Enforces `requireOwner`, `requireAdmin`, and `requiredPermissions` rules on matched `OrganizationRoute` objects.
429
+
430
+ ### Server Setup & Custom Roles
431
+
432
+ Add the `organization` plugin to your BetterAuth server config and enable teams on both server and client:
433
+
434
+ ```ts
435
+ // server/auth.ts
436
+ import { betterAuth } from 'better-auth'
437
+ import { organization } from 'better-auth/plugins'
438
+
439
+ export const auth = betterAuth({
440
+ plugins: [
441
+ organization({
442
+ allowUserToCreateOrganization: true,
443
+ teams: {
444
+ enabled: true,
445
+ },
446
+ }),
447
+ ],
448
+ })
449
+ ```
450
+
451
+ Custom access-control roles must be configured **on both server and client**. See `INTEGRATION.md` for details.
452
+
260
453
  ## Layer Architecture
261
454
 
262
455
  - **`nuxt.config.ts`**: Registers `@nuxt/ui` module, loads the auth CSS, and defines `runtimeConfig.public.x.auth` for base URL and auth path.
package/app/app.config.ts CHANGED
@@ -21,6 +21,25 @@ export default defineAppConfig({
21
21
  otp: false,
22
22
  forgotPassword: true,
23
23
  signup: true,
24
+ organization: false,
25
+ teams: false,
26
+ },
27
+
28
+ organization: {
29
+ enabled: false,
30
+ teams: true,
31
+ allowUserToCreateOrganization: true,
32
+ requireMemberEmailVerification: true,
33
+ defaultRole: 'member',
34
+ protectedRoutes: ['/org'],
35
+ redirects: {
36
+ create: '/org/create',
37
+ list: '/org',
38
+ settings: '/org/:slug/settings',
39
+ invitations: '/org/:slug/invitations',
40
+ notMember: '/org',
41
+ noPermission: '/org',
42
+ },
24
43
  },
25
44
 
26
45
  oauthProviders: [] as Array<{
@@ -0,0 +1,65 @@
1
+ <template>
2
+ <div class="w-full space-y-6">
3
+ <div class="text-center">
4
+ <h1 class="text-2xl font-semibold text-highlighted">Create organization</h1>
5
+ <p class="text-sm text-muted mt-1">Set up a new organization for your team.</p>
6
+ </div>
7
+
8
+ <UForm :schema="schema" :state="state" class="space-y-4" @submit="handleCreate">
9
+ <UFormField label="Name" name="name">
10
+ <UInput v-model="state.name" placeholder="Acme Inc." class="w-full" />
11
+ </UFormField>
12
+
13
+ <UFormField label="Slug" name="slug">
14
+ <UInput v-model="state.slug" placeholder="acme" class="w-full" />
15
+ </UFormField>
16
+
17
+ <UFormField label="Logo URL" name="logo">
18
+ <UInput v-model="state.logo" placeholder="https://example.com/logo.png" class="w-full" />
19
+ </UFormField>
20
+
21
+ <UButton
22
+ type="submit"
23
+ block
24
+ size="lg"
25
+ label="Create organization"
26
+ :loading="creating"
27
+ />
28
+ </UForm>
29
+ </div>
30
+ </template>
31
+
32
+ <script setup>
33
+ import { z } from "zod"
34
+
35
+ const router = useRouter()
36
+ const { createOrganization, setActiveOrganization } = useOrganization()
37
+
38
+ const creating = ref(false)
39
+
40
+ const state = reactive({
41
+ name: '',
42
+ slug: '',
43
+ logo: '',
44
+ })
45
+
46
+ const schema = z.object({
47
+ name: z.string().min(1, 'Organization name is required'),
48
+ slug: z.string().min(1, 'Slug is required').regex(/^[a-z0-9-]+$/, 'Slug must be lowercase letters, numbers, and hyphens'),
49
+ logo: z.string().url().optional().or(z.literal('')),
50
+ })
51
+
52
+ async function handleCreate() {
53
+ creating.value = true
54
+ try {
55
+ const result = await createOrganization(state.name, state.slug || state.name.toLowerCase().replace(/\s+/g, '-'), state.logo || undefined)
56
+ if (result && !result.error) {
57
+ const slug = result?.slug || state.slug
58
+ await setActiveOrganization(result?.id)
59
+ router.push(`/org/${slug}`)
60
+ }
61
+ } finally {
62
+ creating.value = false
63
+ }
64
+ }
65
+ </script>
@@ -0,0 +1,48 @@
1
+ <template>
2
+ <div class="w-full space-y-6">
3
+ <div class="flex items-center justify-between">
4
+ <h2 class="text-lg font-semibold text-highlighted">Invitations</h2>
5
+ <UButton
6
+ v-if="isOrgAdmin"
7
+ size="sm"
8
+ to="invite"
9
+ label="Invite"
10
+ icon="i-lucide-plus"
11
+ />
12
+ </div>
13
+
14
+ <div v-if="invitations.length === 0" class="text-center py-8 text-muted">
15
+ No pending invitations.
16
+ </div>
17
+
18
+ <div v-else class="divide-y divide-neutral-200 dark:divide-neutral-800">
19
+ <div
20
+ v-for="invitation in invitations"
21
+ :key="invitation.id"
22
+ class="flex items-center justify-between py-3"
23
+ >
24
+ <div>
25
+ <p class="text-sm font-medium text-highlighted">{{ invitation.email }}</p>
26
+ <p class="text-xs text-muted">
27
+ Role: {{ invitation.role }}
28
+ <span v-if="invitation.status">· {{ invitation.status }}</span>
29
+ </p>
30
+ </div>
31
+
32
+ <UButton
33
+ v-if="isOrgAdmin"
34
+ color="error"
35
+ variant="soft"
36
+ size="xs"
37
+ label="Cancel"
38
+ icon="i-lucide-x"
39
+ @click="cancelInvitation(invitation.id)"
40
+ />
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </template>
45
+
46
+ <script setup>
47
+ const { invitations, isOrgAdmin, cancelInvitation } = useOrganization()
48
+ </script>
@@ -0,0 +1,73 @@
1
+ <template>
2
+ <div class="w-full space-y-6">
3
+ <div class="text-center">
4
+ <h2 class="text-lg font-semibold text-highlighted">Invite member</h2>
5
+ <p class="text-sm text-muted mt-1">Invite a new member to this organization.</p>
6
+ </div>
7
+
8
+ <UForm :schema="schema" :state="state" class="space-y-4" @submit="handleInvite">
9
+ <UFormField label="Email" name="email">
10
+ <UInput v-model="state.email" type="email" placeholder="colleague@example.com" class="w-full" />
11
+ </UFormField>
12
+
13
+ <UFormField label="Role" name="role">
14
+ <USelect
15
+ v-model="state.role"
16
+ :items="roleOptions"
17
+ class="w-full"
18
+ />
19
+ </UFormField>
20
+
21
+ <UButton
22
+ type="submit"
23
+ block
24
+ size="lg"
25
+ label="Send invitation"
26
+ :loading="inviting"
27
+ />
28
+ </UForm>
29
+ </div>
30
+ </template>
31
+
32
+ <script setup>
33
+ import { z } from "zod"
34
+
35
+ const props = defineProps({
36
+ organization: {
37
+ type: Object,
38
+ required: true,
39
+ },
40
+ })
41
+
42
+ const router = useRouter()
43
+ const { inviteMember } = useOrganization()
44
+
45
+ const inviting = ref(false)
46
+
47
+ const state = reactive({
48
+ email: '',
49
+ role: 'member',
50
+ })
51
+
52
+ const roleOptions = [
53
+ { label: 'Member', value: 'member' },
54
+ { label: 'Admin', value: 'admin' },
55
+ ]
56
+
57
+ const schema = z.object({
58
+ email: z.string().email('Please enter a valid email address'),
59
+ role: z.enum(['member', 'admin']),
60
+ })
61
+
62
+ async function handleInvite() {
63
+ inviting.value = true
64
+ try {
65
+ const result = await inviteMember(state.email, state.role, props.organization.id)
66
+ if (result && !result.error) {
67
+ router.push(`/org/${props.organization.slug}/invitations`)
68
+ }
69
+ } finally {
70
+ inviting.value = false
71
+ }
72
+ }
73
+ </script>
@@ -0,0 +1,103 @@
1
+ <template>
2
+ <div class="w-full space-y-6">
3
+ <div class="flex items-center justify-between">
4
+ <h2 class="text-lg font-semibold text-highlighted">Members</h2>
5
+ <UButton
6
+ v-if="isOrgAdmin"
7
+ size="sm"
8
+ to="invite"
9
+ label="Invite"
10
+ icon="i-lucide-user-plus"
11
+ />
12
+ </div>
13
+
14
+ <div v-if="members.length === 0" class="text-center py-8 text-muted">
15
+ No members yet.
16
+ </div>
17
+
18
+ <div v-else class="divide-y divide-neutral-200 dark:divide-neutral-800">
19
+ <div
20
+ v-for="member in members"
21
+ :key="member.id"
22
+ class="flex items-center justify-between py-3"
23
+ >
24
+ <div class="flex items-center gap-3">
25
+ <div class="flex size-9 items-center justify-center rounded-full bg-primary/10 text-primary">
26
+ <UIcon name="i-lucide-user" class="size-4" />
27
+ </div>
28
+ <div>
29
+ <p class="text-sm font-medium text-highlighted">{{ member.name || member.email }}</p>
30
+ <p class="text-xs text-muted">{{ member.email }}</p>
31
+ </div>
32
+ </div>
33
+
34
+ <div class="flex items-center gap-2">
35
+ <UBadge
36
+ :label="member.role"
37
+ :color="roleColor(member.role)"
38
+ size="sm"
39
+ variant="soft"
40
+ />
41
+ <UDropdownMenu
42
+ v-if="isOrgAdmin && !isCurrentUser(member)"
43
+ :items="memberActions(member)"
44
+ >
45
+ <UButton
46
+ color="neutral"
47
+ variant="ghost"
48
+ size="xs"
49
+ icon="i-lucide-ellipsis-vertical"
50
+ />
51
+ </UDropdownMenu>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ </template>
57
+
58
+ <script setup>
59
+ const props = defineProps({
60
+ organization: {
61
+ type: Object,
62
+ required: true,
63
+ },
64
+ })
65
+
66
+ const { user } = useXAuth()
67
+ const { members, isOrgAdmin, isOrgOwner, updateMemberRole, removeMember } = useOrganization()
68
+
69
+ const roleColor = (role) => {
70
+ const roles = role.split(',')
71
+ if (roles.includes('owner')) return 'primary'
72
+ if (roles.includes('admin')) return 'warning'
73
+ return 'neutral'
74
+ }
75
+
76
+ const isCurrentUser = (member) => member.userId === user.value?.id
77
+
78
+ const memberActions = (member) => [
79
+ ...(isOrgOwner.value
80
+ ? [{
81
+ label: 'Make owner',
82
+ icon: 'i-lucide-crown',
83
+ onSelect: () => updateMemberRole(member.id, 'owner'),
84
+ }]
85
+ : []),
86
+ {
87
+ label: 'Make admin',
88
+ icon: 'i-lucide-shield',
89
+ onSelect: () => updateMemberRole(member.id, 'admin'),
90
+ },
91
+ {
92
+ label: 'Make member',
93
+ icon: 'i-lucide-user',
94
+ onSelect: () => updateMemberRole(member.id, 'member'),
95
+ },
96
+ {
97
+ label: 'Remove',
98
+ icon: 'i-lucide-trash',
99
+ color: 'error',
100
+ onSelect: () => removeMember(member.id, props.organization.id),
101
+ },
102
+ ]
103
+ </script>
@@ -0,0 +1,85 @@
1
+ <template>
2
+ <div class="w-full space-y-6">
3
+ <div class="flex items-center justify-between">
4
+ <h2 class="text-lg font-semibold text-highlighted">Settings</h2>
5
+ <UButton
6
+ v-if="isOrgOwner"
7
+ color="error"
8
+ variant="soft"
9
+ size="sm"
10
+ label="Delete"
11
+ icon="i-lucide-trash"
12
+ @click="handleDelete"
13
+ />
14
+ </div>
15
+
16
+ <UForm :schema="schema" :state="state" class="space-y-4" @submit="handleUpdate">
17
+ <UFormField label="Name" name="name">
18
+ <UInput v-model="state.name" class="w-full" />
19
+ </UFormField>
20
+
21
+ <UFormField label="Slug" name="slug">
22
+ <UInput v-model="state.slug" class="w-full" />
23
+ </UFormField>
24
+
25
+ <UFormField label="Logo URL" name="logo">
26
+ <UInput v-model="state.logo" class="w-full" />
27
+ </UFormField>
28
+
29
+ <UButton
30
+ type="submit"
31
+ size="sm"
32
+ label="Save changes"
33
+ :loading="updating"
34
+ />
35
+ </UForm>
36
+ </div>
37
+ </template>
38
+
39
+ <script setup>
40
+ import { z } from "zod"
41
+
42
+ const props = defineProps({
43
+ organization: {
44
+ type: Object,
45
+ required: true,
46
+ },
47
+ })
48
+
49
+ const { updateOrganization, deleteOrganization, isOrgOwner } = useOrganization()
50
+
51
+ const updating = ref(false)
52
+
53
+ const state = reactive({
54
+ name: props.organization.name || '',
55
+ slug: props.organization.slug || '',
56
+ logo: props.organization.logo || '',
57
+ })
58
+
59
+ const schema = z.object({
60
+ name: z.string().min(1, 'Name is required'),
61
+ slug: z.string().min(1, 'Slug is required').regex(/^[a-z0-9-]+$/, 'Slug must be lowercase letters, numbers, and hyphens'),
62
+ logo: z.string().url().optional().or(z.literal('')),
63
+ })
64
+
65
+ async function handleUpdate() {
66
+ updating.value = true
67
+ try {
68
+ await updateOrganization({
69
+ organizationId: props.organization.id,
70
+ name: state.name,
71
+ slug: state.slug,
72
+ logo: state.logo || undefined,
73
+ })
74
+ } finally {
75
+ updating.value = false
76
+ }
77
+ }
78
+
79
+ async function handleDelete() {
80
+ if (!confirm('Are you sure you want to delete this organization? This action cannot be undone.')) {
81
+ return
82
+ }
83
+ await deleteOrganization(props.organization.id)
84
+ }
85
+ </script>
@@ -0,0 +1,61 @@
1
+ <template>
2
+ <div class="w-full space-y-4">
3
+ <div class="flex items-center justify-between">
4
+ <h2 class="text-lg font-semibold text-highlighted">Organizations</h2>
5
+ <UButton
6
+ v-if="canCreateOrganization"
7
+ size="sm"
8
+ to="/org/create"
9
+ label="New"
10
+ icon="i-lucide-plus"
11
+ />
12
+ </div>
13
+
14
+ <div v-if="organizationsLoading" class="flex items-center justify-center py-8">
15
+ <UIcon name="i-lucide-loader-2" class="size-6 animate-spin text-primary" />
16
+ </div>
17
+
18
+ <div v-else-if="organizations.length === 0" class="text-center py-8">
19
+ <div class="mx-auto mb-4 flex size-14 items-center justify-center rounded-full bg-primary/10 ring-1 ring-primary/20">
20
+ <UIcon name="i-lucide-building-2" class="size-7 text-primary" />
21
+ </div>
22
+ <h3 class="text-base font-semibold text-highlighted">No organizations yet</h3>
23
+ <p class="text-sm text-muted mt-1">Create an organization to get started.</p>
24
+ <UButton
25
+ v-if="canCreateOrganization"
26
+ class="mt-4"
27
+ size="sm"
28
+ to="/org/create"
29
+ label="Create organization"
30
+ />
31
+ </div>
32
+
33
+ <div v-else class="space-y-2">
34
+ <NuxtLink
35
+ v-for="org in organizations"
36
+ :key="org.id"
37
+ :to="`/org/${org.slug}`"
38
+ class="flex items-center gap-3 p-3 rounded-lg border border-neutral-200 dark:border-neutral-800 hover:bg-neutral-50 dark:hover:bg-neutral-900 transition-colors"
39
+ :class="{ 'bg-primary/5 border-primary/20 dark:border-primary/20': activeOrganization?.id === org.id }"
40
+ >
41
+ <div class="flex size-10 items-center justify-center rounded-lg bg-primary/10 text-primary">
42
+ <img v-if="org.logo" :src="org.logo" alt="" class="size-10 rounded-lg object-cover" />
43
+ <UIcon v-else name="i-lucide-building-2" class="size-5" />
44
+ </div>
45
+ <div class="flex-1 min-w-0">
46
+ <p class="font-medium text-highlighted truncate">{{ org.name }}</p>
47
+ <p class="text-xs text-muted">@{{ org.slug }}</p>
48
+ </div>
49
+ <UIcon
50
+ v-if="activeOrganization?.id === org.id"
51
+ name="i-lucide-check"
52
+ class="size-5 text-primary"
53
+ />
54
+ </NuxtLink>
55
+ </div>
56
+ </div>
57
+ </template>
58
+
59
+ <script setup>
60
+ const { organizations, organizationsLoading, activeOrganization, canCreateOrganization } = useOrganization()
61
+ </script>