@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.
@@ -0,0 +1,125 @@
1
+ import type { OrganizationRoute } from "../composables/types";
2
+
3
+ function isOrganizationRoute(rule: string | OrganizationRoute): rule is OrganizationRoute {
4
+ return (
5
+ typeof rule === "object" &&
6
+ rule !== null &&
7
+ typeof rule.path === "string"
8
+ );
9
+ }
10
+
11
+ function routeMatches(path: string, rule: string | OrganizationRoute): boolean {
12
+ if (typeof rule === "string") {
13
+ return path === rule || path.startsWith(`${rule}/`);
14
+ }
15
+ return path === rule.path || path.startsWith(`${rule.path}/`);
16
+ }
17
+
18
+ export default defineNuxtRouteMiddleware(async (to) => {
19
+ const config = useAppConfig();
20
+ const { getCurrentUser } = useXAuth();
21
+ const {
22
+ isEnabled,
23
+ activeOrganization,
24
+ setActiveOrganization,
25
+ getFullOrganization,
26
+ activeMember,
27
+ hasPermission,
28
+ } = useOrganization();
29
+
30
+ if (!isEnabled.value) {
31
+ return;
32
+ }
33
+
34
+ const orgConfig = config.xAuth?.organization || {};
35
+ const redirects = orgConfig.redirects || {};
36
+ const notMemberPath = redirects.notMember || "/org";
37
+ const noPermissionPath = redirects.noPermission || "/org";
38
+ const loginPath = config.xAuth?.redirects?.login || "/auth/login";
39
+
40
+ const protectedRoutes: Array<string | OrganizationRoute> =
41
+ orgConfig.protectedRoutes || ["/org"];
42
+
43
+ const isProtectedRoute = protectedRoutes.some((rule) =>
44
+ routeMatches(to.path, rule)
45
+ );
46
+
47
+ if (!isProtectedRoute) {
48
+ return;
49
+ }
50
+
51
+ let user: { id: string } | null = null;
52
+ try {
53
+ user = await getCurrentUser();
54
+ } catch {
55
+ user = null;
56
+ }
57
+
58
+ if (!user) {
59
+ return navigateTo(loginPath);
60
+ }
61
+
62
+ const slug = to.params?.slug;
63
+ if (!slug || typeof slug !== "string") {
64
+ return;
65
+ }
66
+
67
+ if (!activeOrganization.value || activeOrganization.value.slug !== slug) {
68
+ let org: { id: string } | null = null;
69
+ try {
70
+ const result = await getFullOrganization(undefined, slug);
71
+ org = result && typeof result === "object" && !("error" in result)
72
+ ? (result as { id: string })
73
+ : null;
74
+ } catch {
75
+ org = null;
76
+ }
77
+
78
+ if (!org) {
79
+ return navigateTo(notMemberPath);
80
+ }
81
+
82
+ await setActiveOrganization(org.id);
83
+ }
84
+
85
+ if (!activeMember.value) {
86
+ return navigateTo(notMemberPath);
87
+ }
88
+
89
+ const matchedRule = protectedRoutes.find((rule) =>
90
+ routeMatches(to.path, rule)
91
+ );
92
+
93
+ if (matchedRule && isOrganizationRoute(matchedRule)) {
94
+ const role = activeMember.value.role || "";
95
+ const roles = role.split(",").map((r) => r.trim());
96
+
97
+ if (matchedRule.requireOwner === true && !roles.includes("owner")) {
98
+ return navigateTo(noPermissionPath);
99
+ }
100
+
101
+ if (
102
+ matchedRule.requireAdmin === true &&
103
+ !roles.includes("owner") &&
104
+ !roles.includes("admin")
105
+ ) {
106
+ return navigateTo(noPermissionPath);
107
+ }
108
+
109
+ if (matchedRule.requireMember === true && !activeMember.value) {
110
+ return navigateTo(notMemberPath);
111
+ }
112
+
113
+ if (
114
+ matchedRule.requiredPermissions &&
115
+ Array.isArray(matchedRule.requiredPermissions)
116
+ ) {
117
+ const hasAll = matchedRule.requiredPermissions.every((check) =>
118
+ hasPermission({ role, permissions: check.permissions })
119
+ );
120
+ if (!hasAll) {
121
+ return navigateTo(noPermissionPath);
122
+ }
123
+ }
124
+ }
125
+ });
@@ -0,0 +1,63 @@
1
+ <template>
2
+ <div class="min-h-screen flex items-center justify-center p-4 bg-neutral-50 dark:bg-neutral-950">
3
+ <div class="w-full max-w-sm text-center">
4
+ <div v-if="loading" class="py-8">
5
+ <UIcon name="i-lucide-loader-2" class="size-10 animate-spin text-primary mx-auto" />
6
+ <p class="mt-4 text-muted">Processing invitation...</p>
7
+ </div>
8
+
9
+ <div v-else-if="success" class="py-8">
10
+ <div class="mx-auto mb-4 flex size-16 items-center justify-center rounded-full bg-success/10 ring-1 ring-success/20">
11
+ <UIcon name="i-lucide-check" class="size-8 text-success" />
12
+ </div>
13
+ <h3 class="text-lg font-semibold text-highlighted">Invitation accepted</h3>
14
+ <p class="text-sm text-muted mt-1">Redirecting you to the organization...</p>
15
+ </div>
16
+
17
+ <div v-else class="py-8">
18
+ <div class="mx-auto mb-4 flex size-16 items-center justify-center rounded-full bg-error/10 ring-1 ring-error/20">
19
+ <UIcon name="i-lucide-x" class="size-8 text-error" />
20
+ </div>
21
+ <h3 class="text-lg font-semibold text-highlighted">Invitation failed</h3>
22
+ <p class="text-sm text-muted mt-1">{{ error }}</p>
23
+ <UButton class="mt-6" to="/org" label="Go to organizations" />
24
+ </div>
25
+ </div>
26
+ </div>
27
+ </template>
28
+
29
+ <script setup>
30
+ const route = useRoute()
31
+ const router = useRouter()
32
+ const { acceptInvitation, isEnabled } = useOrganization()
33
+
34
+ const loading = ref(true)
35
+ const success = ref(false)
36
+ const error = ref('')
37
+
38
+ onMounted(async () => {
39
+ const id = route.query.id
40
+ if (!id || typeof id !== 'string') {
41
+ error.value = 'Invitation ID is missing.'
42
+ loading.value = false
43
+ return
44
+ }
45
+
46
+ if (!isEnabled.value) {
47
+ error.value = 'Organizations are not enabled.'
48
+ loading.value = false
49
+ return
50
+ }
51
+
52
+ const result = await acceptInvitation(id)
53
+ if (result && !result.error) {
54
+ success.value = true
55
+ setTimeout(() => {
56
+ router.push('/org')
57
+ }, 1500)
58
+ } else {
59
+ error.value = result?.error || 'Could not accept invitation.'
60
+ }
61
+ loading.value = false
62
+ })
63
+ </script>
@@ -0,0 +1,53 @@
1
+ <template>
2
+ <div class="min-h-screen bg-neutral-50 dark:bg-neutral-950">
3
+ <nav class="border-b border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900">
4
+ <div class="max-w-5xl mx-auto px-4 py-3 flex items-center justify-between">
5
+ <div class="flex items-center gap-3">
6
+ <UButton to="/org" variant="ghost" color="neutral" icon="i-lucide-arrow-left" size="sm" />
7
+ <h1 class="font-semibold text-lg">{{ organization?.name || 'Organization' }}</h1>
8
+ </div>
9
+ <div class="flex items-center gap-2">
10
+ <UButton size="sm" variant="outline" :to="`/org/${slug}/members`" label="Members" />
11
+ <UButton size="sm" variant="outline" :to="`/org/${slug}/invitations`" label="Invitations" />
12
+ <UButton size="sm" variant="outline" :to="`/org/${slug}/teams`" label="Teams" />
13
+ <UButton size="sm" :to="`/org/${slug}/settings`" label="Settings" />
14
+ </div>
15
+ </div>
16
+ </nav>
17
+
18
+ <div class="max-w-5xl mx-auto px-4 py-8">
19
+ <div class="grid md:grid-cols-3 gap-6">
20
+ <div class="md:col-span-2 space-y-6">
21
+ <XAuthTeamList :organization="organization" />
22
+ </div>
23
+ <div class="space-y-6">
24
+ <XAuthOrganizationSwitcher />
25
+ </div>
26
+ </div>
27
+ </div>
28
+ </div>
29
+ </template>
30
+
31
+ <script setup>
32
+ const route = useRoute()
33
+ const slug = route.params.slug
34
+
35
+ const { activeOrganization, setActiveOrganization, getFullOrganization } = useOrganization()
36
+ const organization = ref(null)
37
+
38
+ onMounted(async () => {
39
+ if (!activeOrganization.value || activeOrganization.value.slug !== slug) {
40
+ const result = await getFullOrganization(undefined, slug)
41
+ if (result && !result.error) {
42
+ organization.value = result
43
+ await setActiveOrganization(result.id)
44
+ }
45
+ } else {
46
+ organization.value = activeOrganization.value
47
+ }
48
+ })
49
+
50
+ definePageMeta({
51
+ middleware: ['auth'],
52
+ })
53
+ </script>
@@ -0,0 +1,23 @@
1
+ <template>
2
+ <div class="min-h-screen bg-neutral-50 dark:bg-neutral-950">
3
+ <nav class="border-b border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900">
4
+ <div class="max-w-5xl mx-auto px-4 py-3 flex items-center gap-3">
5
+ <UButton :to="`/org/${slug}`" variant="ghost" color="neutral" icon="i-lucide-arrow-left" size="sm" />
6
+ <h1 class="font-semibold text-lg">Invitations</h1>
7
+ </div>
8
+ </nav>
9
+
10
+ <div class="max-w-5xl mx-auto px-4 py-8">
11
+ <XAuthOrganizationInvitations />
12
+ </div>
13
+ </div>
14
+ </template>
15
+
16
+ <script setup>
17
+ const route = useRoute()
18
+ const slug = route.params.slug
19
+
20
+ definePageMeta({
21
+ middleware: ['auth'],
22
+ })
23
+ </script>
@@ -0,0 +1,30 @@
1
+ <template>
2
+ <div class="min-h-screen bg-neutral-50 dark:bg-neutral-950 flex items-center justify-center p-4">
3
+ <div class="w-full max-w-md p-6 rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900">
4
+ <XAuthOrganizationInvite :organization="organization" />
5
+ </div>
6
+ </div>
7
+ </template>
8
+
9
+ <script setup>
10
+ const route = useRoute()
11
+ const slug = route.params.slug
12
+
13
+ const { activeOrganization, getFullOrganization } = useOrganization()
14
+ const organization = ref(null)
15
+
16
+ onMounted(async () => {
17
+ if (!activeOrganization.value || activeOrganization.value.slug !== slug) {
18
+ const result = await getFullOrganization(undefined, slug)
19
+ if (result && !result.error) {
20
+ organization.value = result
21
+ }
22
+ } else {
23
+ organization.value = activeOrganization.value
24
+ }
25
+ })
26
+
27
+ definePageMeta({
28
+ middleware: ['auth'],
29
+ })
30
+ </script>
@@ -0,0 +1,37 @@
1
+ <template>
2
+ <div class="min-h-screen bg-neutral-50 dark:bg-neutral-950">
3
+ <nav class="border-b border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900">
4
+ <div class="max-w-5xl mx-auto px-4 py-3 flex items-center gap-3">
5
+ <UButton :to="`/org/${slug}`" variant="ghost" color="neutral" icon="i-lucide-arrow-left" size="sm" />
6
+ <h1 class="font-semibold text-lg">Members</h1>
7
+ </div>
8
+ </nav>
9
+
10
+ <div class="max-w-5xl mx-auto px-4 py-8">
11
+ <XAuthOrganizationMembers :organization="organization" />
12
+ </div>
13
+ </div>
14
+ </template>
15
+
16
+ <script setup>
17
+ const route = useRoute()
18
+ const slug = route.params.slug
19
+
20
+ const { activeOrganization, getFullOrganization } = useOrganization()
21
+ const organization = ref(null)
22
+
23
+ onMounted(async () => {
24
+ if (!activeOrganization.value || activeOrganization.value.slug !== slug) {
25
+ const result = await getFullOrganization(undefined, slug)
26
+ if (result && !result.error) {
27
+ organization.value = result
28
+ }
29
+ } else {
30
+ organization.value = activeOrganization.value
31
+ }
32
+ })
33
+
34
+ definePageMeta({
35
+ middleware: ['auth'],
36
+ })
37
+ </script>
@@ -0,0 +1,39 @@
1
+ <template>
2
+ <div class="min-h-screen bg-neutral-50 dark:bg-neutral-950">
3
+ <nav class="border-b border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900">
4
+ <div class="max-w-5xl mx-auto px-4 py-3 flex items-center gap-3">
5
+ <UButton :to="`/org/${slug}`" variant="ghost" color="neutral" icon="i-lucide-arrow-left" size="sm" />
6
+ <h1 class="font-semibold text-lg">Settings</h1>
7
+ </div>
8
+ </nav>
9
+
10
+ <div class="max-w-5xl mx-auto px-4 py-8">
11
+ <div class="max-w-2xl p-6 rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900">
12
+ <XAuthOrganizationSettings :organization="organization" />
13
+ </div>
14
+ </div>
15
+ </div>
16
+ </template>
17
+
18
+ <script setup>
19
+ const route = useRoute()
20
+ const slug = route.params.slug
21
+
22
+ const { activeOrganization, getFullOrganization } = useOrganization()
23
+ const organization = ref(null)
24
+
25
+ onMounted(async () => {
26
+ if (!activeOrganization.value || activeOrganization.value.slug !== slug) {
27
+ const result = await getFullOrganization(undefined, slug)
28
+ if (result && !result.error) {
29
+ organization.value = result
30
+ }
31
+ } else {
32
+ organization.value = activeOrganization.value
33
+ }
34
+ })
35
+
36
+ definePageMeta({
37
+ middleware: ['auth'],
38
+ })
39
+ </script>
@@ -0,0 +1,51 @@
1
+ <template>
2
+ <div class="min-h-screen bg-neutral-50 dark:bg-neutral-950">
3
+ <nav class="border-b border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900">
4
+ <div class="max-w-5xl mx-auto px-4 py-3 flex items-center gap-3">
5
+ <UButton :to="`/org/${slug}`" variant="ghost" color="neutral" icon="i-lucide-arrow-left" size="sm" />
6
+ <h1 class="font-semibold text-lg">Team</h1>
7
+ </div>
8
+ </nav>
9
+
10
+ <div class="max-w-5xl mx-auto px-4 py-8">
11
+ <div class="grid md:grid-cols-2 gap-6">
12
+ <div class="p-6 rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900">
13
+ <XAuthTeamSettings :team="team" :organization="organization" />
14
+ </div>
15
+ <div class="p-6 rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900">
16
+ <XAuthTeamMembers :team="team" :organization="organization" />
17
+ </div>
18
+ </div>
19
+ </div>
20
+ </div>
21
+ </template>
22
+
23
+ <script setup>
24
+ const route = useRoute()
25
+ const slug = route.params.slug
26
+ const teamId = route.params.teamId
27
+
28
+ const { activeOrganization, getFullOrganization, listTeams } = useOrganization()
29
+ const organization = ref(null)
30
+ const team = ref(null)
31
+
32
+ onMounted(async () => {
33
+ if (!activeOrganization.value || activeOrganization.value.slug !== slug) {
34
+ const result = await getFullOrganization(undefined, slug)
35
+ if (result && !result.error) {
36
+ organization.value = result
37
+ }
38
+ } else {
39
+ organization.value = activeOrganization.value
40
+ }
41
+
42
+ const teams = await listTeams(organization.value?.id)
43
+ if (teams && !teams.error) {
44
+ team.value = teams.find((t) => t.id === teamId) || null
45
+ }
46
+ })
47
+
48
+ definePageMeta({
49
+ middleware: ['auth'],
50
+ })
51
+ </script>
@@ -0,0 +1,30 @@
1
+ <template>
2
+ <div class="min-h-screen bg-neutral-50 dark:bg-neutral-950 flex items-center justify-center p-4">
3
+ <div class="w-full max-w-md p-6 rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900">
4
+ <XAuthTeamCreate :organization="organization" />
5
+ </div>
6
+ </div>
7
+ </template>
8
+
9
+ <script setup>
10
+ const route = useRoute()
11
+ const slug = route.params.slug
12
+
13
+ const { activeOrganization, getFullOrganization } = useOrganization()
14
+ const organization = ref(null)
15
+
16
+ onMounted(async () => {
17
+ if (!activeOrganization.value || activeOrganization.value.slug !== slug) {
18
+ const result = await getFullOrganization(undefined, slug)
19
+ if (result && !result.error) {
20
+ organization.value = result
21
+ }
22
+ } else {
23
+ organization.value = activeOrganization.value
24
+ }
25
+ })
26
+
27
+ definePageMeta({
28
+ middleware: ['auth'],
29
+ })
30
+ </script>
@@ -0,0 +1,37 @@
1
+ <template>
2
+ <div class="min-h-screen bg-neutral-50 dark:bg-neutral-950">
3
+ <nav class="border-b border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900">
4
+ <div class="max-w-5xl mx-auto px-4 py-3 flex items-center gap-3">
5
+ <UButton :to="`/org/${slug}`" variant="ghost" color="neutral" icon="i-lucide-arrow-left" size="sm" />
6
+ <h1 class="font-semibold text-lg">Teams</h1>
7
+ </div>
8
+ </nav>
9
+
10
+ <div class="max-w-5xl mx-auto px-4 py-8">
11
+ <XAuthTeamList :organization="organization" />
12
+ </div>
13
+ </div>
14
+ </template>
15
+
16
+ <script setup>
17
+ const route = useRoute()
18
+ const slug = route.params.slug
19
+
20
+ const { activeOrganization, getFullOrganization } = useOrganization()
21
+ const organization = ref(null)
22
+
23
+ onMounted(async () => {
24
+ if (!activeOrganization.value || activeOrganization.value.slug !== slug) {
25
+ const result = await getFullOrganization(undefined, slug)
26
+ if (result && !result.error) {
27
+ organization.value = result
28
+ }
29
+ } else {
30
+ organization.value = activeOrganization.value
31
+ }
32
+ })
33
+
34
+ definePageMeta({
35
+ middleware: ['auth'],
36
+ })
37
+ </script>
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <div class="min-h-screen bg-neutral-50 dark:bg-neutral-950 flex items-center justify-center p-4">
3
+ <div class="w-full max-w-md p-6 rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900">
4
+ <XAuthOrganizationCreate />
5
+ </div>
6
+ </div>
7
+ </template>
8
+
9
+ <script setup>
10
+ definePageMeta({
11
+ middleware: ['auth'],
12
+ })
13
+ </script>
@@ -0,0 +1,20 @@
1
+ <template>
2
+ <div class="min-h-screen bg-neutral-50 dark:bg-neutral-950">
3
+ <nav class="border-b border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900">
4
+ <div class="max-w-5xl mx-auto px-4 py-3 flex items-center justify-between">
5
+ <h1 class="font-semibold text-lg">Organizations</h1>
6
+ <UButton size="sm" to="/org/create" label="Create" icon="i-lucide-plus" />
7
+ </div>
8
+ </nav>
9
+
10
+ <div class="max-w-5xl mx-auto px-4 py-8">
11
+ <XAuthOrganizationSwitcher />
12
+ </div>
13
+ </div>
14
+ </template>
15
+
16
+ <script setup>
17
+ definePageMeta({
18
+ middleware: ['auth'],
19
+ })
20
+ </script>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xenterprises/nuxt-x-auth-better",
3
3
  "type": "module",
4
- "version": "0.3.1",
4
+ "version": "0.4.0",
5
5
  "description": "BetterAuth authentication layer for Nuxt",
6
6
  "license": "SEE LICENSE IN LICENSE",
7
7
  "author": "X Enterprises",
@@ -24,7 +24,7 @@
24
24
  "generate": "nuxt generate .playground",
25
25
  "preview": "nuxt preview .playground",
26
26
  "lint": "eslint .",
27
- "typecheck": "nuxi typecheck",
27
+ "typecheck": "nuxt prepare && nuxi typecheck",
28
28
  "test": "vitest",
29
29
  "test:run": "vitest run",
30
30
  "test:coverage": "vitest run --coverage"
@@ -52,11 +52,14 @@
52
52
  "@playwright/test": "^1.57.0",
53
53
  "@vitejs/plugin-vue": "^6.0.0",
54
54
  "@vitest/coverage-v8": "^3.0.0",
55
- "better-auth": "^1.4.10",
55
+ "better-auth": "^1.6.5",
56
56
  "happy-dom": "^16.0.0",
57
57
  "nuxt": "^4.0.3",
58
58
  "typescript": "^5.6.3",
59
59
  "vitest": "^3.0.0",
60
60
  "vue": "latest"
61
+ },
62
+ "publishConfig": {
63
+ "access": "public"
61
64
  }
62
- }
65
+ }