feat(frontend): Use betterAuth implementation from nuxthub-better-auth project
This commit is contained in:
@@ -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<{
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
84
legalconsenthub/composables/useAuth.ts
Normal file
84
legalconsenthub/composables/useAuth.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
63
legalconsenthub/middleware/auth.global.ts
Normal file
63
legalconsenthub/middleware/auth.global.ts
Normal 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)
|
||||
}
|
||||
})
|
||||
@@ -1,7 +0,0 @@
|
||||
export default defineNuxtRouteMiddleware(async (_to, _from) => {
|
||||
const { data: session } = await useSession(useFetch)
|
||||
|
||||
if (!session.value) {
|
||||
return navigateTo('/login')
|
||||
}
|
||||
})
|
||||
@@ -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: [
|
||||
|
||||
@@ -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[]>([])
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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('/')
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -26,6 +26,7 @@ definePageMeta({ layout: 'auth' })
|
||||
useSeoMeta({ title: 'Sign up' })
|
||||
|
||||
const toast = useToast()
|
||||
const { signUp } = useAuth()
|
||||
|
||||
const fields = [
|
||||
{
|
||||
|
||||
12
legalconsenthub/plugins/auth.client.ts
Normal file
12
legalconsenthub/plugins/auth.client.ts
Normal 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()
|
||||
})
|
||||
}
|
||||
})
|
||||
13
legalconsenthub/plugins/auth.server.ts
Normal file
13
legalconsenthub/plugins/auth.server.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user