@nuxt-customer-portal/core 0.3.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/LICENSE +21 -0
  3. package/README.md +35 -0
  4. package/app/composables/useAutoPagination.ts +30 -0
  5. package/app/composables/useOrganization.ts +117 -0
  6. package/app/composables/usePaginatedResource.ts +167 -0
  7. package/app/composables/usePortalFeatures.ts +74 -0
  8. package/app/composables/usePortalSession.ts +20 -0
  9. package/app/composables/useUserStore.ts +460 -0
  10. package/app/middleware/auth.global.ts +32 -0
  11. package/app/plugins/auth.ts +28 -0
  12. package/app/plugins/portal-core-feature.ts +3 -0
  13. package/app/types/index.d.ts +75 -0
  14. package/app/utils/auth-client.ts +31 -0
  15. package/app/utils/permission-utils.ts +21 -0
  16. package/app/utils/random.ts +9 -0
  17. package/i18n/locales/en.json +81 -0
  18. package/i18n/locales/nl.json +81 -0
  19. package/migrations/0000_baseline.sql +119 -0
  20. package/migrations/0001_organization_types.sql +12 -0
  21. package/migrations/0002_invitation_created_at.sql +1 -0
  22. package/migrations/0003_system_admin_role.sql +5 -0
  23. package/migrations/0004_provider_organization_type.sql +16 -0
  24. package/migrations/0005_portal_email_settings.sql +15 -0
  25. package/migrations/meta/0000_snapshot.json +880 -0
  26. package/migrations/meta/_journal.json +13 -0
  27. package/nuxt.config.ts +55 -0
  28. package/package.json +66 -0
  29. package/portal.manifest.mjs +8 -0
  30. package/server/api/auth/[...].ts +18 -0
  31. package/server/api/auth/permissions.get.ts +64 -0
  32. package/server/api/notifications.ts +294 -0
  33. package/server/db/core-schema.ts +1 -0
  34. package/server/db/schema/auth-schema.ts +198 -0
  35. package/server/db/schema/index.ts +1 -0
  36. package/server/middleware/api-docs-auth.ts +26 -0
  37. package/server/middleware/auth.ts +61 -0
  38. package/server/portal.ts +212 -0
  39. package/server/routes/api-docs/openapi.json.get.ts +22 -0
  40. package/server/utils/admin-email-allowlist.ts +12 -0
  41. package/server/utils/auth.ts +326 -0
  42. package/server/utils/business-hooks.ts +28 -0
  43. package/server/utils/db.ts +18 -0
  44. package/server/utils/email-template.html +105 -0
  45. package/server/utils/email-texts.ts +110 -0
  46. package/server/utils/email.ts +37 -0
  47. package/server/utils/invitation-management.ts +29 -0
  48. package/server/utils/openapi-auth.ts +67 -0
  49. package/server/utils/openapi-contracts.ts +129 -0
  50. package/server/utils/openapi-order.ts +136 -0
  51. package/server/utils/permissions.ts +179 -0
  52. package/server/utils/portal-email.ts +424 -0
  53. package/server/utils/query-builder.ts +270 -0
  54. package/server/utils/route-meta.ts +28 -0
  55. package/shared/core-feature.ts +152 -0
  56. package/shared/feature-registry.ts +62 -0
  57. package/shared/invitation-validation.ts +8 -0
  58. package/shared/organization-avatar.ts +11 -0
  59. package/shared/permissions.ts +26 -0
  60. package/shared/portal-session.ts +17 -0
  61. package/shared/types/feature.ts +130 -0
  62. package/shared/types/index.ts +40 -0
  63. package/shared/types/sections/api.ts +134 -0
  64. package/shared/types/sections/auth.ts +71 -0
  65. package/shared/types/sections/database.ts +86 -0
  66. package/shared/types/sections/queryBuilder.ts +31 -0
@@ -0,0 +1,460 @@
1
+ import { defineStore } from 'pinia'
2
+ import type { AuthSession, AuthUser, AuthSessionResponse } from '@nuxt-customer-portal/core/app/utils/auth-client'
3
+ import { authClient } from '@nuxt-customer-portal/core/app/utils/auth-client'
4
+ import type { Organization } from '@nuxt-customer-portal/core/shared/types/index'
5
+ import type { DashboardUser } from '@nuxt-customer-portal/core/app/types'
6
+
7
+ interface UserPermissions {
8
+ [key: string]: boolean
9
+ }
10
+
11
+ interface PermissionsResponse {
12
+ permissions: UserPermissions
13
+ role: string
14
+ organizationRole: string | null
15
+ activeOrganization: Organization | null
16
+ organizationType: 'PROVIDER' | 'CLIENT' | null
17
+ }
18
+
19
+ export const useUserStore = defineStore('user', () => {
20
+ const currentSession = ref<AuthSession | null>(null)
21
+ const currentUser = ref<AuthUser | null>(null)
22
+
23
+ // Manual organizations state to prevent 401 errors for unauthenticated users
24
+ const organizationsData = ref<Organization[] | null>(null)
25
+ const organizationsError = ref<Error | null>(null)
26
+ const organizationsIsPending = ref(false)
27
+ const organizationsIsRefetching = ref(false)
28
+
29
+ // Fetch organizations manually - only when authenticated
30
+ const fetchOrganizations = async () => {
31
+ if (!currentUser.value) {
32
+ organizationsData.value = null
33
+ organizationsError.value = null
34
+ return
35
+ }
36
+
37
+ try {
38
+ organizationsIsPending.value = true
39
+ organizationsError.value = null
40
+ const { data, error } = await authClient.organization.list()
41
+ if (error) {
42
+ organizationsError.value = new Error(error.message || 'Failed to fetch organizations')
43
+ organizationsData.value = null
44
+ } else if (data) {
45
+ // Convert null logos to undefined to match Organization type
46
+ organizationsData.value = data.map((org) => ({
47
+ ...org,
48
+ logo: org.logo ?? undefined
49
+ })) as Organization[]
50
+ } else {
51
+ organizationsData.value = null
52
+ }
53
+ } catch (error) {
54
+ organizationsError.value = error instanceof Error ? error : new Error('Failed to fetch organizations')
55
+ organizationsData.value = null
56
+ } finally {
57
+ organizationsIsPending.value = false
58
+ organizationsIsRefetching.value = false
59
+ }
60
+ }
61
+
62
+ // Create a helper object that matches the composable interface
63
+ const organizationsHelper = computed(() => ({
64
+ data: organizationsData.value,
65
+ error: organizationsError.value,
66
+ isPending: organizationsIsPending.value,
67
+ isRefetching: organizationsIsRefetching.value,
68
+ refetch: async () => {
69
+ organizationsIsRefetching.value = true
70
+ await fetchOrganizations()
71
+ }
72
+ }))
73
+
74
+ const refreshOrganizations = async () => {
75
+ await fetchOrganizations()
76
+ }
77
+
78
+ // Watch authentication status and fetch organizations when user becomes authenticated
79
+ watch(
80
+ currentUser,
81
+ (user, oldUser) => {
82
+ if (user && !oldUser) {
83
+ // User just logged in, fetch organizations
84
+ fetchOrganizations()
85
+ } else if (!user) {
86
+ // User logged out, clear organizations
87
+ organizationsData.value = null
88
+ organizationsError.value = null
89
+ }
90
+ },
91
+ { immediate: true }
92
+ )
93
+
94
+ const permissions = ref<UserPermissions>({})
95
+ const role = ref<string | null>(null)
96
+ const organizationRole = ref<string | null>(null)
97
+ const activeOrganizationFromPermissions = ref<Organization | null>(null)
98
+ const isLoading = ref(false)
99
+ const activeOrganization = computed(() => {
100
+ // Prefer the one from permissions API if available, otherwise use organizationsHelper
101
+ return (
102
+ activeOrganizationFromPermissions.value ||
103
+ organizationsHelper.value.data?.find((org: Organization) => org.id === activeOrganizationId.value)
104
+ )
105
+ })
106
+
107
+ const colorMode = useColorMode() // Use the colorMode composable
108
+ const theme = ref(colorMode.preference)
109
+
110
+ // Watch for changes in colorMode.preference from @nuxtjs/color-mode
111
+ watch(
112
+ () => colorMode.preference,
113
+ (newValue) => {
114
+ theme.value = newValue // Update our store's theme ref
115
+ }
116
+ )
117
+
118
+ const hasPermission = (subject: string, action: string): boolean => {
119
+ return permissions.value[`${subject}.${action}`] ?? false
120
+ }
121
+
122
+ // This is the global platform role. It is deliberately independent from
123
+ // activeOrganizationRole, so organization owners do not become system admins.
124
+ const isAdmin = computed(() => role.value === 'admin')
125
+
126
+ const userInitials = computed(() => {
127
+ const user = currentUser.value
128
+ if (!user) {
129
+ return undefined
130
+ }
131
+ const name = user.name || user.email || ''
132
+ if (!name) {
133
+ return ''
134
+ }
135
+ const parts = name.split(' ').filter((part) => part.length > 0)
136
+ if (parts.length === 0) {
137
+ return ''
138
+ }
139
+ if (parts.length === 1) {
140
+ return parts[0]?.charAt(0).toUpperCase() || ''
141
+ }
142
+ // Multiple parts: return first char of first part + first char of last part
143
+ const first = parts[0]?.charAt(0) || ''
144
+ const last = parts[parts.length - 1]?.charAt(0) || ''
145
+ return (first + last).toUpperCase()
146
+ })
147
+
148
+ const activeOrganizationId = computed(() => {
149
+ return currentSession.value?.activeOrganizationId
150
+ })
151
+
152
+ // Store the active organization role
153
+ const activeOrganizationRoleValue = ref<string | null>(null)
154
+
155
+ // Watch for changes in activeOrganizationId and fetch the role
156
+ watch(
157
+ [activeOrganizationId, currentUser],
158
+ async ([newOrgId, user]) => {
159
+ if (newOrgId && user) {
160
+ try {
161
+ const { data: roleData } = await authClient.organization.getActiveMemberRole()
162
+ activeOrganizationRoleValue.value = roleData?.role || null
163
+ } catch (error) {
164
+ console.error('Error fetching active organization role:', error)
165
+ activeOrganizationRoleValue.value = null
166
+ }
167
+ } else {
168
+ activeOrganizationRoleValue.value = null
169
+ }
170
+ },
171
+ { immediate: true }
172
+ )
173
+
174
+ // Computed property that returns the active organization role
175
+ const activeOrganizationRole = computed(() => {
176
+ return activeOrganizationRoleValue.value
177
+ })
178
+ const activeOrganizationType = computed(() => activeOrganization.value?.organizationType ?? null)
179
+
180
+ const loggedInUsingEmail = computed(() => {
181
+ const user = currentUser.value
182
+ if (!user || typeof user.providerId === 'undefined') {
183
+ return false
184
+ }
185
+ return user.providerId === 'credential'
186
+ })
187
+
188
+ const fetchUserPermissions = async () => {
189
+ // If there is no current user, or permissions have already been fetched, do nothing.
190
+ if (!currentUser.value) {
191
+ permissions.value = {}
192
+ role.value = null
193
+ return
194
+ }
195
+
196
+ // console.log('Fetching user permissions from API for user:', currentUser.value.id)
197
+ try {
198
+ const data = await $fetch<PermissionsResponse>('/api/auth/permissions')
199
+ if (data) {
200
+ permissions.value = data.permissions
201
+ role.value = data.role
202
+ organizationRole.value = data.organizationRole
203
+ activeOrganizationFromPermissions.value = data.activeOrganization
204
+ } else {
205
+ console.warn('Permissions API returned no data. Clearing user data as a precaution.')
206
+ clearUserData() // Clear data if API returns nothing, as state is uncertain
207
+ }
208
+ } catch (error) {
209
+ console.error('Error in fetchUserPermissions:', error)
210
+ clearUserData()
211
+ }
212
+ }
213
+
214
+ const clearUserData = () => {
215
+ console.log('Clearing user data and permissions fetch status.')
216
+ permissions.value = {}
217
+ role.value = null
218
+ organizationRole.value = null
219
+ activeOrganizationFromPermissions.value = null
220
+ currentSession.value = null
221
+ currentUser.value = null
222
+ activeOrganizationRoleValue.value = null
223
+ }
224
+
225
+ const setActiveOrganizationId = async (organizationId: string) => {
226
+ try {
227
+ isLoading.value = true
228
+ const { data, error } = await authClient.organization.setActive({ organizationId })
229
+ if (error) {
230
+ console.error('Error setting active organization:', error)
231
+ throw error
232
+ }
233
+ if (data) {
234
+ console.log('Active organization set:', data)
235
+ // Fetch the updated session - retry a few times if needed
236
+ let retries = 3
237
+ let sessionResponse = null
238
+ while (retries > 0) {
239
+ sessionResponse = await authClient.getSession()
240
+ // Check if the session has been updated with the new organizationId
241
+ const sessionData = sessionResponse?.data as unknown as AuthSessionResponse & {
242
+ activeOrganizationId?: string
243
+ }
244
+ if (sessionData && sessionData.activeOrganizationId === organizationId) {
245
+ break
246
+ }
247
+ console.log('ActiverOganizationId not yet set in Session data, retrying (retries left: ${retries -1})...')
248
+ await new Promise((resolve) => setTimeout(resolve, 50))
249
+ retries--
250
+ }
251
+
252
+ if (sessionResponse?.data) {
253
+ const sessionData = sessionResponse.data as unknown as AuthSessionResponse
254
+ await setSession(sessionData)
255
+ } else {
256
+ console.warn('Session data not available after setting active organization')
257
+ // Still try to fetch once more
258
+ const finalSession = await authClient.getSession()
259
+ if (finalSession?.data) {
260
+ await setSession(finalSession.data as unknown as AuthSessionResponse)
261
+ }
262
+ }
263
+ }
264
+ } catch (error) {
265
+ console.error('Error setting active organization:', error)
266
+ throw error
267
+ } finally {
268
+ isLoading.value = false
269
+ }
270
+ }
271
+
272
+ const setSession = async (data: AuthSessionResponse | null) => {
273
+ if (data) {
274
+ try {
275
+ const { user, ...session } = data
276
+ currentSession.value = session
277
+ currentUser.value = data.user as AuthUser
278
+
279
+ if (currentUser.value) {
280
+ await fetchUserPermissions()
281
+ }
282
+ } catch (error) {
283
+ console.error('Error setting session:', error)
284
+ clearUserData()
285
+ throw error
286
+ } finally {
287
+ isLoading.value = false
288
+ }
289
+ } else {
290
+ // Session is null/undefined - clear user data (e.g., on logout)
291
+ clearUserData()
292
+ }
293
+ }
294
+
295
+ const isAuthenticated = computed(() => {
296
+ return currentUser.value !== null
297
+ })
298
+
299
+ // Store roles for each organization
300
+ const organizationRoles = ref<Record<string, string>>({})
301
+
302
+ // Track if we've attempted to auto-set the first organization
303
+ const hasAttemptedAutoSet = ref(false)
304
+
305
+ // Auto-set first organization when activeOrganizationId is null but user has organizations
306
+ watch(
307
+ [activeOrganizationId, () => organizationsHelper.value.data, currentUser],
308
+ async ([orgId, organizations, user]) => {
309
+ // Only run on client side
310
+ if (import.meta.server) {
311
+ return
312
+ }
313
+
314
+ // Only proceed if:
315
+ // 1. User is authenticated
316
+ // 2. activeOrganizationId is null
317
+ // 3. User has at least one organization
318
+ // 4. We haven't already attempted to set it
319
+ // 5. Organizations are not still loading
320
+ if (
321
+ user &&
322
+ !orgId &&
323
+ organizations &&
324
+ organizations.length > 0 &&
325
+ !hasAttemptedAutoSet.value &&
326
+ !organizationsHelper.value.isPending
327
+ ) {
328
+ hasAttemptedAutoSet.value = true
329
+ try {
330
+ // A provider user may also belong to one or more client organizations.
331
+ // Prefer the provider workspace as the initial context so provider-only
332
+ // modules (Clients, administration, and sales invoices) are available
333
+ // after a session without an active organization is restored.
334
+ const firstOrg = organizations.find((org) => org.organizationType === 'PROVIDER') ?? organizations[0]
335
+ if (firstOrg?.id) {
336
+ console.log('Auto-setting first organization as active:', firstOrg.id)
337
+ await setActiveOrganizationId(firstOrg.id)
338
+ }
339
+ } catch (error) {
340
+ console.error('Error auto-setting first organization:', error)
341
+ // Reset flag on error so we can retry later
342
+ hasAttemptedAutoSet.value = false
343
+ }
344
+ }
345
+ },
346
+ { immediate: true }
347
+ )
348
+
349
+ // Reset the auto-set flag when activeOrganizationId changes to a non-null value or user logs out
350
+ watch([activeOrganizationId, currentUser], ([orgId, user]) => {
351
+ if (orgId) {
352
+ hasAttemptedAutoSet.value = false
353
+ }
354
+ if (!user) {
355
+ hasAttemptedAutoSet.value = false
356
+ }
357
+ })
358
+
359
+ // Fetch roles for all organizations when organizations are loaded
360
+ watch(
361
+ () => organizationsHelper.value.data,
362
+ async (organizations) => {
363
+ if (!organizations || !currentUser.value) {
364
+ organizationRoles.value = {}
365
+ return
366
+ }
367
+
368
+ // Fetch only the current user's role. Listing the full member directory is
369
+ // restricted to organization owners and admins.
370
+ const roles: Record<string, string> = {}
371
+ for (const org of organizations) {
372
+ try {
373
+ const { data } = await authClient.organization.getActiveMemberRole({
374
+ query: { organizationId: org.id }
375
+ })
376
+ if (data?.role) {
377
+ roles[org.id] = data.role
378
+ }
379
+ } catch (error) {
380
+ console.error(`Error fetching role for organization ${org.id}:`, error)
381
+ }
382
+ }
383
+ organizationRoles.value = roles
384
+ },
385
+ { immediate: true }
386
+ )
387
+
388
+ // Enhanced organizations with roles
389
+ const myOrganizations = computed(() => {
390
+ const orgs = organizationsHelper.value.data as Organization[] | null | undefined
391
+ if (!orgs) {
392
+ return orgs
393
+ }
394
+
395
+ return orgs.map((org) => ({
396
+ ...org,
397
+ role: organizationRoles.value[org.id] || null
398
+ }))
399
+ })
400
+
401
+ const loadingOrganization = computed(() => {
402
+ return organizationsHelper.value.isPending ?? false
403
+ })
404
+
405
+ const setCurrentUser = (data: AuthUser | null) => {
406
+ currentUser.value = data
407
+ }
408
+
409
+ const dashboardUser = computed(() => {
410
+ if (!currentUser.value) {
411
+ return {
412
+ name: 'User',
413
+ avatar: {
414
+ src: undefined,
415
+ alt: 'User'
416
+ }
417
+ } as DashboardUser
418
+ }
419
+ return {
420
+ name: currentUser.value.name || currentUser.value?.email || 'User',
421
+ avatar: {
422
+ src: currentUser.value.image,
423
+ alt: currentUser.value.name || currentUser.value?.email || 'User'
424
+ }
425
+ } as DashboardUser
426
+ })
427
+
428
+ const changePasswordAllowed = computed(() => {
429
+ return currentUser.value?.providerId === 'credential'
430
+ })
431
+ // Fetch current session data using getSession()
432
+ return {
433
+ permissions,
434
+ role,
435
+ organizationRole,
436
+ isAuthenticated,
437
+ isLoading,
438
+ currentSession,
439
+ currentUser,
440
+ isAdmin,
441
+ userInitials,
442
+ loggedInUsingEmail,
443
+ theme,
444
+ activeOrganizationId,
445
+ activeOrganization,
446
+ activeOrganizationRole,
447
+ activeOrganizationType,
448
+ myOrganizations,
449
+ loadingOrganization,
450
+ refreshOrganizations,
451
+ fetchUserPermissions,
452
+ hasPermission,
453
+ clearUserData,
454
+ setSession,
455
+ setActiveOrganizationId,
456
+ setCurrentUser,
457
+ dashboardUser,
458
+ changePasswordAllowed
459
+ }
460
+ })
@@ -0,0 +1,32 @@
1
+ import { authClient } from '@nuxt-customer-portal/core/app/utils/auth-client'
2
+
3
+ export default defineNuxtRouteMiddleware(async (to) => {
4
+ if (to.meta.public === true) {
5
+ return
6
+ }
7
+ try {
8
+ const { data: session } = await authClient.useSession(useFetch)
9
+
10
+ if (!session.value) {
11
+ console.log('No active session found, redirecting to login')
12
+ // No active session, redirect the user to the login page
13
+ // Preserve the original intended path via a redirect query parameter
14
+ const redirectPath = `/login?redirect=${encodeURIComponent(to.fullPath)}`
15
+ return navigateTo(redirectPath, {
16
+ external: false,
17
+ replace: true // Use replace to avoid adding the original protected path to history
18
+ })
19
+ } else {
20
+ console.log('Active session found, allowing access to:', to.path)
21
+ }
22
+ } catch (error) {
23
+ console.error('Auth middleware error:', error)
24
+ // If there's an error checking auth, redirect to login
25
+ return navigateTo('/login', {
26
+ external: false,
27
+ replace: true
28
+ })
29
+ }
30
+
31
+ // User is authenticated, allow access to the protected route
32
+ })
@@ -0,0 +1,28 @@
1
+ import { authClient } from '@nuxt-customer-portal/core/app/utils/auth-client'
2
+ import type { AuthSessionResponse } from '@nuxt-customer-portal/core/app/utils/auth-client'
3
+
4
+ export default defineNuxtPlugin({
5
+ name: 'auth',
6
+ async setup() {
7
+ const userStore = useUserStore()
8
+ // const organizationStore = useOrganization()
9
+ try {
10
+ const sessionData = await authClient.getSession()
11
+ await userStore.setSession(sessionData?.data as unknown as AuthSessionResponse)
12
+ } catch (error) {
13
+ console.log('getSession error:', error)
14
+ }
15
+
16
+ const { data: session } = await authClient.useSession(useFetch)
17
+
18
+ // Also watch for session changes to handle OTP verification and logout
19
+ watch(
20
+ () => session.value,
21
+ async (newSession) => {
22
+ const sessionData = newSession as unknown as AuthSessionResponse | null
23
+ await userStore.setSession(sessionData)
24
+ },
25
+ { deep: true }
26
+ )
27
+ }
28
+ })
@@ -0,0 +1,3 @@
1
+ import { coreFeature } from '../../shared/core-feature'
2
+
3
+ export default defineNuxtPlugin(() => usePortalFeatures().registerFeature(coreFeature))
@@ -0,0 +1,75 @@
1
+ import type { ParsedContent } from '@nuxt/content'
2
+ import type { Avatar, Badge, Link } from '#ui/types'
3
+ import type { AvatarProps } from '@nuxt/ui'
4
+
5
+ export interface BlogPost extends ParsedContent {
6
+ title: string
7
+ description: string
8
+ date: string
9
+ image?: HTMLImageElement
10
+ badge?: Badge
11
+ authors?: ({
12
+ name: string
13
+ description?: string
14
+ avatar: Avatar
15
+ } & Link)[]
16
+ }
17
+
18
+ export type UserStatus = 'subscribed' | 'unsubscribed' | 'bounced'
19
+ export type SaleStatus = 'paid' | 'failed' | 'refunded'
20
+
21
+ export interface DashboardUser {
22
+ id: number
23
+ name: string
24
+ email: string
25
+ avatar?: AvatarProps
26
+ status: UserStatus
27
+ location: string
28
+ }
29
+
30
+ export interface Mail {
31
+ id: number
32
+ unread?: boolean
33
+ from: DashboardUser
34
+ subject: string
35
+ body: string
36
+ date: string
37
+ }
38
+
39
+ export interface Member {
40
+ name: string
41
+ username: string
42
+ role: 'member' | 'owner'
43
+ avatar: AvatarProps
44
+ }
45
+
46
+ export interface Stat {
47
+ title: string
48
+ icon: string
49
+ value: number | string
50
+ variation: number
51
+ formatter?: (value: number) => string
52
+ }
53
+
54
+ export interface Sale {
55
+ id: string
56
+ date: string
57
+ status: SaleStatus
58
+ email: string
59
+ amount: number
60
+ }
61
+
62
+ export interface Notification {
63
+ id: number
64
+ unread?: boolean
65
+ sender: DashboardUser
66
+ body: string
67
+ date: string
68
+ }
69
+
70
+ export type Period = 'daily' | 'weekly' | 'monthly'
71
+
72
+ export interface Range {
73
+ start: Date
74
+ end: Date
75
+ }
@@ -0,0 +1,31 @@
1
+ import { createAuthClient } from 'better-auth/vue' // Use the Vue-specific import
2
+ import { adminClient, emailOTPClient, organizationClient } from 'better-auth/client/plugins'
3
+ import { ac, user, admin as adminRole } from '@nuxt-customer-portal/core/shared/permissions'
4
+
5
+ const baseURL = process.env.BETTER_AUTH_URL
6
+
7
+ export const authClient = createAuthClient({
8
+ baseURL: baseURL,
9
+ plugins: [
10
+ adminClient({
11
+ ac,
12
+ roles: {
13
+ user,
14
+ admin: adminRole
15
+ }
16
+ }),
17
+ emailOTPClient(),
18
+ organizationClient() // Add organization client
19
+ ]
20
+ })
21
+
22
+ export type AuthSession = typeof authClient.$Infer.Session.session
23
+ export type AuthUser = typeof authClient.$Infer.Session.user & { providerId?: string }
24
+ export type AuthSessionResponse = typeof authClient.$Infer.Session.session & { user: User }
25
+ export type AuthActiveOganization = typeof authClient.$Infer.ActiveOrganization
26
+ export type AuthMember = typeof authClient.$Infer.Member
27
+ export type AuthOrganization = typeof authClient.$Infer.Organization
28
+ export type AuthInvitation = typeof authClient.$Infer.Invitation
29
+ export type AuthTeam = typeof authClient.$Infer.Team
30
+
31
+ export const { signIn, signOut, signUp, useSession, getSession, emailOtp, sendVerificationEmail } = authClient
@@ -0,0 +1,21 @@
1
+ import { authClient } from './auth-client'
2
+
3
+ /**
4
+ * Checks if a role has a specific permission for a subject
5
+ * @param subject The subject to check permissions against
6
+ * @param permission The specific permission to check
7
+ * @returns boolean indicating if the role has the permission
8
+ */
9
+ export const hasPermission = async (subject: string, permission: string): Promise<boolean> => {
10
+ try {
11
+ const response = await authClient.admin.hasPermission({
12
+ permissions: {
13
+ [subject]: [permission]
14
+ }
15
+ })
16
+ return response?.data?.success || false
17
+ } catch (error) {
18
+ console.error('Error checking permission:', error)
19
+ return false
20
+ }
21
+ }
@@ -0,0 +1,9 @@
1
+ export const randomInt = (min: number, max: number): number => Math.floor(Math.random() * (max - min + 1)) + min
2
+
3
+ export const randomFrom = <T>(items: readonly T[]): T => {
4
+ const item = items[randomInt(0, items.length - 1)]
5
+ if (item === undefined) {
6
+ throw new Error('Cannot select a random item from an empty array')
7
+ }
8
+ return item
9
+ }