// 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 { UserDto } from '~/.api-client' import type { RouteLocationNormalizedLoaded } from '#vue-router' interface RuntimeAuthConfig { redirectUserTo: RouteLocationRaw | string redirectGuestTo: RouteLocationRaw | string } 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) export function useAuth() { const url = useRequestURL() const route = useRoute() const headers = import.meta.server ? useRequestHeaders() : undefined const client = createAuthClient({ baseURL: url.origin, fetchOptions: { headers }, plugins: [organizationClient(), jwtClient()] }) const options = defu(useRuntimeConfig().public.auth as Partial, { redirectUserTo: '/', redirectGuestTo: '/login' }) async function fetchSession() { 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 && !isPublicRoute()) { 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] } } 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 isPublicRoute(routeToCheck?: RouteLocationNormalizedLoaded) { const finalRoute = routeToCheck ?? route const publicRoutes = ['/login', '/signup', '/accept-invitation'] return publicRoutes.some((path) => finalRoute.path.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 } const userDto = computed(() => ({ id: user.value?.id ?? '', name: user.value?.name ?? 'Unknown' })) return { session, user, userDto, loggedIn: computed(() => !!session.value), signIn: client.signIn, signUp: client.signUp, signOut, organization: client.organization, organizations, selectedOrganization, options, fetchSession, fetchJwtAndOrganizations, client, jwt, isPublicRoute } }