// Copied from https://github.com/atinux/nuxthub-better-auth import { defu } from 'defu' import { createAuthClient } from 'better-auth/vue' import type { InferSessionFromClient, InferUserFromClient, ClientOptions } from 'better-auth/client' import { organizationClient, jwtClient } from 'better-auth/client/plugins' import type { RouteLocationRaw } from 'vue-router' import type { RouteLocationNormalizedLoaded } from '#vue-router' import { accessControl, employerRole, worksCouncilMemberRole, employeeRole, adminRole, ownerRole, ROLES } from '~/server/utils/permissions' interface RuntimeAuthConfig { redirectUserTo: RouteLocationRaw | string redirectGuestTo: RouteLocationRaw | string } // Types can be found here: https://github.com/better-auth/better-auth/blob/3f574ec70bb15c155a78673d42c5e25f7376ced3/packages/better-auth/src/plugins/organization/routes/crud-invites.ts#L531 type Client = ReturnType['client'] export type Session = Client['$Infer']['Session'] export type User = Session['user'] export type ActiveOrganization = Client['$Infer']['ActiveOrganization'] export type Organization = Client['$Infer']['Organization'] export type Invitation = Client['$Infer']['Invitation'] // Extended invitation type with additional organization and inviter details export type CustomInvitation = | (Invitation & { organizationName: string organizationSlug: string inviterEmail: string }) | null // TODO: Move into pinia store const session = ref | null>(null) const user = ref | null>(null) const sessionFetching = import.meta.server ? ref(false) : ref(false) const jwt = ref(null) const organizations = ref< { id: string; name: string; createdAt: Date; slug: string; metadata?: any; logo?: string | null }[] >([]) const selectedOrganization = ref<{ id: string name: string createdAt: Date slug: string metadata?: any logo?: string | null } | null>(null) const activeMember = ref<{ role: string } | null>(null) export function useAuth() { const url = useRequestURL() const route = useRoute() const headers = import.meta.server ? useRequestHeaders() : undefined const client = createAuthClient({ baseURL: url.origin, fetchOptions: { headers }, user: { deleteUser: { enabled: true } }, plugins: [ organizationClient({ // Pass the same access control instance and roles to client ac: accessControl, roles: { [ROLES.EMPLOYER]: employerRole, [ROLES.WORKS_COUNCIL_MEMBER]: worksCouncilMemberRole, [ROLES.EMPLOYEE]: employeeRole, [ROLES.ADMIN]: adminRole, [ROLES.OWNER]: ownerRole } }), jwtClient() ] }) const options = defu(useRuntimeConfig().public.auth as Partial, { redirectUserTo: '/', redirectGuestTo: '/login' }) async function fetchSession(targetPath?: string) { if (sessionFetching.value) { console.log('already fetching session') return } sessionFetching.value = true const { data } = await client.getSession({ fetchOptions: { headers } }) session.value = data?.session || null user.value = data?.user || null sessionFetching.value = false // Only fetch JWT and organizations if we have a session and not on public routes if (session.value && !isPublicPath(targetPath)) { await fetchJwtAndOrganizations() } return data } async function fetchJwtAndOrganizations() { // Fetch JWT const tokenResult = await client.token() jwt.value = tokenResult.data?.token ?? null // Fetch organization const orgResult = await client.organization.list({ fetchOptions: { headers } }) organizations.value = orgResult.data ?? [] if (!selectedOrganization.value && organizations.value.length > 0) { selectedOrganization.value = organizations.value[0] } // Fetch active member const activeMemberResult = await client.organization.getActiveMember({ fetchOptions: { headers } }) activeMember.value = activeMemberResult.data || null } watch( () => selectedOrganization.value, async (newValue) => { if (newValue) { await client.organization.setActive({ organizationId: newValue?.id }) } } ) if (import.meta.client) { client.$store.listen('$sessionSignal', async (signal) => { if (!signal) return await fetchSession() }) } function isPublicPath(path?: string) { const finalPath = path ?? route.path const publicRoutes = ['/login', '/signup', '/accept-invitation'] return publicRoutes.some((path) => finalPath.startsWith(path)) } async function signOut({ redirectTo }: { redirectTo?: RouteLocationRaw } = {}) { const res = await client.signOut() if (res.error) { console.error('Error signing out:', res.error) return res } session.value = null user.value = null if (redirectTo) { await navigateTo(redirectTo, { external: true }) } return res } return { session, user, loggedIn: computed(() => !!session.value), signIn: client.signIn, signUp: client.signUp, deleteUser: client.deleteUser, signOut, organization: client.organization, organizations, selectedOrganization, options, fetchSession, client, jwt, isPublicPath, activeMember } }