feat(frontend): Use betterAuth implementation from nuxthub-better-auth project

This commit is contained in:
2025-04-20 09:54:16 +02:00
parent 4e7a962a06
commit eec15dd7ef
17 changed files with 209 additions and 66 deletions

View File

@@ -4,6 +4,7 @@
<component
:is="getResolvedComponent(formElement)"
:form-options="formElement.options"
:disabled="props.disabled"
@update:form-options="updateFormOptions($event, index)"
/>
</UFormField>
@@ -17,6 +18,7 @@ import { resolveComponent } from 'vue'
const props = defineProps<{
modelValue: FormElementDto[]
disabled?: boolean
}>()
const emit = defineEmits<{

View File

@@ -60,13 +60,13 @@ const colors = [
]
const neutrals = ['slate', 'gray', 'zinc', 'neutral', 'stone']
const { data: session } = await useSession(useFetch)
const { user: betterAuthUser, signOut } = await useAuth()
const user = ref({
name: session?.value?.user?.name,
name: betterAuthUser.value?.name,
avatar: {
src: '/_nuxt/public/favicon.ico',
alt: session?.value?.user?.name
alt: betterAuthUser.value?.name
}
})
@@ -115,7 +115,6 @@ const items = computed<DropdownMenuItem[][]>(() => [
type: 'checkbox',
onSelect: (e) => {
e.preventDefault()
appConfig.ui.colors.primary = color
}
}))
@@ -136,7 +135,6 @@ const items = computed<DropdownMenuItem[][]>(() => [
checked: appConfig.ui.colors.neutral === color,
onSelect: (e) => {
e.preventDefault()
appConfig.ui.colors.neutral = color
}
}))
@@ -154,7 +152,6 @@ const items = computed<DropdownMenuItem[][]>(() => [
checked: colorMode.value === 'light',
onSelect(e: Event) {
e.preventDefault()
colorMode.preference = 'light'
}
},
@@ -182,7 +179,6 @@ const items = computed<DropdownMenuItem[][]>(() => [
async onSelect(e: Event) {
e.preventDefault()
signOut()
await navigateTo('/login')
}
}
]

View File

@@ -0,0 +1,84 @@
// Copied from https://github.com/atinux/nuxthub-better-auth
import { defu } from 'defu'
import { createAuthClient } from 'better-auth/client'
import type { InferSessionFromClient, InferUserFromClient, ClientOptions } from 'better-auth/client'
import { organizationClient } from 'better-auth/client/plugins'
import type { RouteLocationRaw } from 'vue-router'
interface RuntimeAuthConfig {
redirectUserTo: RouteLocationRaw | string
redirectGuestTo: RouteLocationRaw | string
}
export function useAuth() {
const url = useRequestURL()
const headers = import.meta.server ? useRequestHeaders() : undefined
const client = createAuthClient({
baseURL: url.origin,
fetchOptions: {
headers
},
plugins: [organizationClient()]
})
const options = defu(useRuntimeConfig().public.auth as Partial<RuntimeAuthConfig>, {
redirectUserTo: '/',
redirectGuestTo: '/login'
})
const session = useState<InferSessionFromClient<ClientOptions> | null>('auth:session', () => null)
const user = useState<InferUserFromClient<ClientOptions> | null>('auth:user', () => null)
const sessionFetching = import.meta.server ? ref(false) : useState('auth:sessionFetching', () => false)
const fetchSession = async () => {
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
return data
}
if (import.meta.client) {
client.$store.listen('$sessionSignal', async (signal) => {
if (!signal) return
await fetchSession()
})
}
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)
}
return res
}
return {
session,
user,
loggedIn: computed(() => !!session.value),
signIn: client.signIn,
signUp: client.signUp,
signOut,
organization: client.organization,
options,
fetchSession,
client
}
}

View File

@@ -3,6 +3,7 @@ const selectedOrgId = ref<string | undefined>(undefined)
export function useBetterAuth() {
const toast = useToast()
const { organization } = useAuth()
async function createOrganization(name: string, slug: string, logo?: string) {
await organization.create(
@@ -25,7 +26,7 @@ export function useBetterAuth() {
}
async function deleteOrganization() {
await authClient.organization.delete(
await organization.delete(
{ organizationId: activeOrganization.value?.id ?? '' },
{
onSuccess: () => {
@@ -41,7 +42,7 @@ export function useBetterAuth() {
}
async function getInvitation(invitationId: string): Promise<CustomInvitation> {
return authClient.organization.getInvitation({
return organization.getInvitation({
query: { id: invitationId },
fetchOptions: {
throw: true,
@@ -56,7 +57,7 @@ export function useBetterAuth() {
}
async function inviteMember(email: string, role: 'member' | 'admin') {
await authClient.organization.inviteMember({
await organization.inviteMember({
email,
role,
fetchOptions: {
@@ -77,7 +78,7 @@ export function useBetterAuth() {
}
async function acceptInvitation(invitationId: string) {
await authClient.organization.acceptInvitation({
await organization.acceptInvitation({
invitationId,
fetchOptions: {
throw: true,
@@ -93,7 +94,7 @@ export function useBetterAuth() {
}
async function rejectInvitation(invitationId: string) {
await authClient.organization.rejectInvitation({
await organization.rejectInvitation({
invitationId,
fetchOptions: {
throw: true,

View File

@@ -0,0 +1,63 @@
// Copied from https://github.com/atinux/nuxthub-better-auth
import { defu } from 'defu'
type MiddlewareOptions =
| false
| {
/**
* Only apply auth middleware to guest or user
*/
only?: 'guest' | 'user'
/**
* Redirect authenticated user to this route
*/
redirectUserTo?: string
/**
* Redirect guest to this route
*/
redirectGuestTo?: string
}
declare module '#app' {
interface PageMeta {
auth?: MiddlewareOptions
}
}
declare module 'vue-router' {
interface RouteMeta {
auth?: MiddlewareOptions
}
}
export default defineNuxtRouteMiddleware(async (to) => {
// If auth is disabled, skip middleware
if (to.meta?.auth === false) {
return
}
const { loggedIn, options, fetchSession } = useAuth()
const { only, redirectUserTo, redirectGuestTo } = defu(to.meta?.auth, options)
// If guest mode, redirect if authenticated
if (only === 'guest' && loggedIn.value) {
// Avoid infinite redirect
if (to.path === redirectUserTo) {
return
}
return navigateTo(redirectUserTo)
}
// If client-side, fetch session between each navigation
if (import.meta.client) {
await fetchSession()
}
// If not authenticated, redirect to home
if (!loggedIn.value) {
// Avoid infinite redirect
if (to.path === redirectGuestTo) {
return
}
return navigateTo(redirectGuestTo)
}
})

View File

@@ -1,7 +0,0 @@
export default defineNuxtRouteMiddleware(async (_to, _from) => {
const { data: session } = await useSession(useFetch)
if (!session.value) {
return navigateTo('/login')
}
})

View File

@@ -6,7 +6,11 @@ export default defineNuxtConfig({
public: {
clientProxyBasePath: 'NOT_SET',
serverApiBaseUrl: 'NOT_SET',
serverApiBasePath: 'NOT_SET'
serverApiBasePath: 'NOT_SET',
auth: {
redirectUserTo: '/',
redirectGuestTo: '/login'
}
}
},
components: [

View File

@@ -69,16 +69,16 @@
<p class="text-xs text-gray-500">{{ member.role }}</p>
</div>
</div>
<div v-if="session && canRemove({ role: 'owner' }, member)">
<div v-if="user && canRemove({ role: 'owner' }, member)">
<UButton size="xs" color="error" @click="organization.removeMember({ memberIdOrEmail: member.id })">
{{ member.user.id === session.id ? 'Leave' : 'Remove' }}
{{ member.user.id === user.id ? 'Leave' : 'Remove' }}
</UButton>
</div>
</div>
<div v-if="!activeOrganization?.id" class="flex items-center gap-2">
<UAvatar :src="session?.image ?? undefined" />
<UAvatar :src="user?.image ?? undefined" />
<div>
<p class="text-sm">{{ session?.name }}</p>
<p class="text-sm">{{ user?.name }}</p>
<p class="text-xs text-gray-500">Owner</p>
</div>
</div>
@@ -141,7 +141,8 @@ import { useClipboard } from '@vueuse/core'
const { copy, copied } = useClipboard()
const toast = useToast()
const organizations = computed(() => useListOrganizations().value.data || [])
const { organization, client } = useAuth()
const organizations = computed(() => client.useListOrganizations.value?.data || [])
const { deleteOrganization: betterAuthDeleteOrganization, activeOrganization, selectedOrgId } = useBetterAuth()
const selectItems = computed(() => organizations.value.map((org) => ({ label: org.name, value: org.id })))
@@ -151,8 +152,7 @@ watch(selectedOrgId, async (newId) => {
activeOrganization.value = data
})
const { data: sessionData } = useSession().value
const session = computed(() => sessionData?.user)
const { user } = await useAuth()
const isRevoking = ref<string[]>([])

View File

@@ -22,8 +22,8 @@
<div class="flex flex-col gap-4 sm:gap-6 lg:gap-12 w-full lg:max-w-4xl mx-auto">
<UPageCard variant="subtle">
<UForm class="space-y-4" :state="{}" @submit="onSubmit">
<FormEngine v-if="applicationForm" v-model="applicationForm.formElements" />
<UButton type="submit">Submit</UButton>
<FormEngine v-if="applicationForm" v-model="applicationForm.formElements" :disabled="isReadOnly" />
<UButton type="submit" :disabled="isReadOnly">Submit</UButton>
</UForm>
</UPageCard>
</div>
@@ -33,8 +33,10 @@
<script setup lang="ts">
import type { ApplicationFormDto } from '~/.api-client'
const { getApplicationFormById, updateApplicationForm } = useApplicationForm()
const route = useRoute()
const { user } = useAuth()
const items = [
[
@@ -59,6 +61,10 @@ const applicationForm = computed({
}
})
const isReadOnly = computed(() => {
return applicationForm.value?.createdBy !== user.value?.name
})
async function onSubmit() {
if (data?.value) {
await updateApplicationForm(data.value.id, data.value)

View File

@@ -39,14 +39,10 @@ import { ComplianceStatus, type PagedApplicationFormDto } from '~/.api-client'
import { useApplicationFormValidator } from '~/composables/useApplicationFormValidator'
import type { FormElementId } from '~/types/FormElement'
definePageMeta({
middleware: ['auth']
})
const { getAllApplicationFormTemplates } = useApplicationFormTemplate()
const { createApplicationForm } = useApplicationForm()
const { validateFormElements, getHighestComplianceStatus } = useApplicationFormValidator()
const { data: session } = await useSession(useFetch)
const { user } = await useAuth()
const { data } = await useAsyncData<PagedApplicationFormDto>(async () => {
return await getAllApplicationFormTemplates()
@@ -96,8 +92,8 @@ const ampelStatusEmoji = computed(() => {
async function onSubmit() {
if (applicationFormTemplate.value) {
applicationFormTemplate.value.createdBy = session.value?.user?.name ?? 'Unknown'
applicationFormTemplate.value.lastModifiedBy = session.value?.user?.name ?? 'Unknown'
applicationFormTemplate.value.createdBy = user.value?.name ?? 'Unknown'
applicationFormTemplate.value.lastModifiedBy = user.value?.name ?? 'Unknown'
await createApplicationForm(applicationFormTemplate.value)
await navigateTo('/')

View File

@@ -56,10 +56,6 @@
<script setup lang="ts">
import type { ApplicationFormDto, PagedApplicationFormDto } from '~/.api-client'
definePageMeta({
middleware: ['auth']
})
const { getAllApplicationForms, deleteApplicationFormById } = useApplicationForm()
const route = useRoute()

View File

@@ -30,6 +30,7 @@ definePageMeta({ layout: 'auth' })
useSeoMeta({ title: 'Login' })
const toast = useToast()
const { signIn } = useAuth()
const fields = [
{
@@ -81,7 +82,7 @@ function onSubmit(payload: FormSubmitEvent<Schema>) {
alert('Bitte alle Felder ausfüllen')
return
}
authClient.signIn.email(
signIn.email(
{
email: payload.data.email,
password: payload.data.password

View File

@@ -26,6 +26,7 @@ definePageMeta({ layout: 'auth' })
useSeoMeta({ title: 'Sign up' })
const toast = useToast()
const { signUp } = useAuth()
const fields = [
{

View File

@@ -0,0 +1,12 @@
// Copied from https://github.com/atinux/nuxthub-better-auth
export default defineNuxtPlugin(async (nuxtApp) => {
if (!nuxtApp.payload.serverRendered) {
await useAuth().fetchSession()
} else if (Boolean(nuxtApp.payload.prerenderedAt) || Boolean(nuxtApp.payload.isCached)) {
// To avoid hydration mismatch
nuxtApp.hook('app:mounted', async () => {
await useAuth().fetchSession()
})
}
})

View File

@@ -0,0 +1,13 @@
// Copied from https://github.com/atinux/nuxthub-better-auth
export default defineNuxtPlugin({
name: 'better-auth-fetch-plugin',
enforce: 'pre',
async setup(nuxtApp) {
// Flag if request is cached
nuxtApp.payload.isCached = Boolean(useRequestEvent()?.context.cache)
if (nuxtApp.payload.serverRendered && !nuxtApp.payload.prerenderedAt && !nuxtApp.payload.isCached) {
await useAuth().fetchSession()
}
}
})

View File

@@ -1,18 +0,0 @@
import { createAuthClient } from 'better-auth/vue'
import { organizationClient } from 'better-auth/client/plugins'
export const authClient = createAuthClient({
baseURL: 'http://localhost:3001',
plugins: [organizationClient()]
})
export const {
signIn,
signOut,
signUp,
useSession,
forgetPassword,
resetPassword,
organization,
useListOrganizations
} = authClient

View File

@@ -1,10 +1,3 @@
import type { auth } from '../server/utils/auth'
import type { authClient } from './auth-client'
export type Session = typeof auth.$Infer.Session
export type ActiveOrganization = typeof authClient.$Infer.ActiveOrganization
export type Invitation = typeof authClient.$Infer.Invitation
// 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
export type CustomInvitation = {
organizationName: string