From 1a58d2e5b814d3aea6e3482e2fa15c4df9dbf174 Mon Sep 17 00:00:00 2001 From: onmax Date: Thu, 5 Feb 2026 13:22:25 +0100 Subject: [PATCH 01/31] feat: switch to better auth --- apps/shelve/.env.example | 2 +- apps/shelve/app/auth.config.ts | 6 + apps/shelve/app/components/CliInstall.vue | 4 +- apps/shelve/app/components/UserDropdown.vue | 6 +- apps/shelve/app/components/auth/Button.vue | 14 +- apps/shelve/app/components/auth/EmailForm.vue | 7 +- apps/shelve/app/components/auth/OtpForm.vue | 33 +- apps/shelve/app/composables/useLogout.ts | 5 +- apps/shelve/app/error.vue | 4 +- apps/shelve/app/layouts/app.vue | 2 +- apps/shelve/app/middleware/admin.ts | 10 +- apps/shelve/app/middleware/auth.ts | 5 +- apps/shelve/app/middleware/guest.ts | 5 +- apps/shelve/app/middleware/onboarding.ts | 5 +- .../app/pages/[teamSlug]/team/members.vue | 18 +- apps/shelve/app/pages/admin/index.vue | 24 +- apps/shelve/app/pages/index.vue | 6 +- apps/shelve/app/pages/user/profile.vue | 19 +- apps/shelve/app/plugins/botid.client.ts | 3 +- apps/shelve/modules/auth/index.ts | 10 +- apps/shelve/nuxt.config.ts | 9 +- apps/shelve/package.json | 3 +- .../server/api/admin/users/[id].delete.ts | 6 +- .../shelve/server/api/admin/users/[id].put.ts | 8 +- .../server/api/admin/users/index.get.ts | 2 +- apps/shelve/server/api/auth/otp/send.post.ts | 48 - .../shelve/server/api/auth/otp/verify.post.ts | 56 - .../apps/[installationId]/index.delete.ts | 2 +- .../server/api/github/apps/complete.get.ts | 2 +- .../server/api/github/apps/index.get.ts | 2 +- .../server/api/github/repos/index.get.ts | 2 +- .../server/api/github/secrets/index.post.ts | 2 +- apps/shelve/server/api/teams/index.get.ts | 2 +- apps/shelve/server/api/teams/index.post.ts | 2 +- apps/shelve/server/api/tokens/[id].delete.ts | 2 +- apps/shelve/server/api/tokens/index.get.ts | 2 +- apps/shelve/server/api/tokens/index.post.ts | 16 +- apps/shelve/server/api/user/cli.post.ts | 12 +- apps/shelve/server/api/user/index.delete.ts | 4 +- apps/shelve/server/api/user/index.put.ts | 24 +- apps/shelve/server/api/user/me.ts | 2 +- .../shelve/server/api/user/onboarding.post.ts | 12 +- apps/shelve/server/auth.config.ts | 107 + .../postgresql/0003_better_auth.sql | 117 ++ .../migrations/postgresql/meta/_journal.json | 9 +- apps/shelve/server/db/schema.ts | 59 +- apps/shelve/server/db/zod.ts | 14 +- apps/shelve/server/middleware/2.auth.ts | 24 - apps/shelve/server/plugins/session.ts | 5 - apps/shelve/server/routes/auth/github.get.ts | 52 - apps/shelve/server/routes/auth/google.get.ts | 52 - apps/shelve/server/routes/auth/otp.get.ts | 32 - apps/shelve/server/services/github.ts | 10 +- apps/shelve/server/services/members.ts | 4 +- apps/shelve/server/services/otp.ts | 154 -- apps/shelve/server/services/resend.ts | 4 +- apps/shelve/server/services/teams.ts | 2 +- apps/shelve/server/services/user.ts | 130 +- apps/shelve/server/services/variables.ts | 2 +- apps/shelve/server/utils/auth.ts | 4 +- apps/shelve/server/utils/session.ts | 28 + apps/shelve/server/utils/stats.ts | 2 +- apps/shelve/shared/types/auth.d.ts | 36 - packages/types/src/Team.ts | 2 +- packages/types/src/Token.ts | 2 +- packages/types/src/User.ts | 12 +- packages/types/src/integrations/Github.ts | 2 +- pnpm-lock.yaml | 1856 ++++++++++++++--- 68 files changed, 2097 insertions(+), 1032 deletions(-) create mode 100644 apps/shelve/app/auth.config.ts delete mode 100644 apps/shelve/server/api/auth/otp/send.post.ts delete mode 100644 apps/shelve/server/api/auth/otp/verify.post.ts create mode 100644 apps/shelve/server/auth.config.ts create mode 100644 apps/shelve/server/db/migrations/postgresql/0003_better_auth.sql delete mode 100644 apps/shelve/server/middleware/2.auth.ts delete mode 100644 apps/shelve/server/plugins/session.ts delete mode 100644 apps/shelve/server/routes/auth/github.get.ts delete mode 100644 apps/shelve/server/routes/auth/google.get.ts delete mode 100644 apps/shelve/server/routes/auth/otp.get.ts delete mode 100644 apps/shelve/server/services/otp.ts create mode 100644 apps/shelve/server/utils/session.ts delete mode 100644 apps/shelve/shared/types/auth.d.ts diff --git a/apps/shelve/.env.example b/apps/shelve/.env.example index c8d2e0739..6dd9dc88e 100644 --- a/apps/shelve/.env.example +++ b/apps/shelve/.env.example @@ -7,7 +7,7 @@ TURBO_TEAM=your_value NUXT_OAUTH_GITHUB_CLIENT_ID=your_value NUXT_PRIVATE_ENCRYPTION_KEY=your_value NUXT_OAUTH_GITHUB_CLIENT_SECRET=your_value -NUXT_SESSION_PASSWORD=your_value +BETTER_AUTH_SECRET=your_value TURBO_TOKEN=your_value NUXT_PRIVATE_RESEND_API_KEY=your_value NUXT_PRIVATE_RESEND_WEBHOOK_SECRET=your_value diff --git a/apps/shelve/app/auth.config.ts b/apps/shelve/app/auth.config.ts new file mode 100644 index 000000000..76b4bc0f7 --- /dev/null +++ b/apps/shelve/app/auth.config.ts @@ -0,0 +1,6 @@ +import { defineClientAuth } from '@onmax/nuxt-better-auth/config' +import { adminClient, emailOTPClient } from 'better-auth/client/plugins' + +export default defineClientAuth({ + plugins: [emailOTPClient(), adminClient()], +}) diff --git a/apps/shelve/app/components/CliInstall.vue b/apps/shelve/app/components/CliInstall.vue index 4056777ce..36c2b6366 100644 --- a/apps/shelve/app/components/CliInstall.vue +++ b/apps/shelve/app/components/CliInstall.vue @@ -1,5 +1,5 @@ @@ -32,7 +29,6 @@ function open() { >({ }) const loading = ref(false) +const { client } = useUserSession() async function onSubmit(event: FormSubmitEvent) { loading.value = true try { - await $fetch('/api/auth/otp/send', { - method: 'POST', - body: { email: event.data.email } + await client.emailOtp.sendVerificationOtp({ + email: event.data.email, + type: 'sign-in' }) emit('emailSubmitted', event.data.email) diff --git a/apps/shelve/app/components/auth/OtpForm.vue b/apps/shelve/app/components/auth/OtpForm.vue index 23c6da67f..e76fb49a4 100644 --- a/apps/shelve/app/components/auth/OtpForm.vue +++ b/apps/shelve/app/components/auth/OtpForm.vue @@ -13,6 +13,7 @@ const emit = defineEmits<{ const router = useRouter() const otp = ref(props.prefilledOtp ? props.prefilledOtp.split('') : []) const loading = ref(false) +const { signIn, client } = useUserSession() onMounted(async () => { if (props.prefilledOtp && props.prefilledOtp.length === 6) { @@ -25,22 +26,20 @@ async function handleOtpComplete(value: string[]) { loading.value = true try { - await $fetch('/api/auth/otp/verify', { - method: 'POST', - body: { - email: props.email, - code: value.join('') + await signIn.emailOtp({ + email: props.email, + otp: value.join('') + }, { + onSuccess: async () => { + emit('otpVerified') + toast.success('Login successful!') + if (props.redirectUrl) { + await router.push(props.redirectUrl) + } else { + await reloadNuxtApp() + } } }) - - emit('otpVerified') - toast.success('Login successful!') - - if (props.redirectUrl) { - await router.push(props.redirectUrl) - } else { - reloadNuxtApp() - } } catch (error: any) { toast.error(error.data?.message || 'Invalid verification code') otp.value = [] @@ -51,9 +50,9 @@ async function handleOtpComplete(value: string[]) { async function resendCode() { try { - await $fetch('/api/auth/otp/send', { - method: 'POST', - body: { email: props.email } + await client.emailOtp.sendVerificationOtp({ + email: props.email, + type: 'sign-in' }) toast.success('New verification code sent') } catch (error: any) { diff --git a/apps/shelve/app/composables/useLogout.ts b/apps/shelve/app/composables/useLogout.ts index 699291705..43a55010a 100644 --- a/apps/shelve/app/composables/useLogout.ts +++ b/apps/shelve/app/composables/useLogout.ts @@ -1,4 +1,7 @@ export async function useLogout() { - await useUserSession().clear() + const { signOut } = useUserSession() + const defaultTeamSlug = useCookie('defaultTeamSlug') + defaultTeamSlug.value = null + await signOut() navigateTo('/login') } diff --git a/apps/shelve/app/error.vue b/apps/shelve/app/error.vue index 7b5e1e7a3..da4c157ef 100644 --- a/apps/shelve/app/error.vue +++ b/apps/shelve/app/error.vue @@ -9,7 +9,7 @@ const { error } = defineProps() console.error(error) const router = useRouter() -const { clear } = useUserSession() +const { signOut } = useUserSession() const handleError = () => clearError({ redirect: '/' }) const goBack = () => { @@ -34,7 +34,7 @@ const clearCookies = async () => { const [name] = cookie.split('=') document.cookie = `${name?.trim()}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/` }) - await clear() + await signOut() navigateTo('/login') } diff --git a/apps/shelve/app/layouts/app.vue b/apps/shelve/app/layouts/app.vue index 7eb1b05b3..2c8ae5803 100644 --- a/apps/shelve/app/layouts/app.vue +++ b/apps/shelve/app/layouts/app.vue @@ -21,7 +21,7 @@ const routeTitle = computed(() => { }) const title = computed(() => { - return route.path === `/${teamSlug.value}` ? `Welcome, ${user.value?.username}` : navigation.value?.title || routeTitle.value + return route.path === `/${teamSlug.value}` ? `Welcome, ${user.value?.name}` : navigation.value?.title || routeTitle.value }) diff --git a/apps/shelve/app/middleware/admin.ts b/apps/shelve/app/middleware/admin.ts index a8372f12e..b0480c2a7 100644 --- a/apps/shelve/app/middleware/admin.ts +++ b/apps/shelve/app/middleware/admin.ts @@ -1,7 +1,13 @@ import { Role } from '@types' -export default defineNuxtRouteMiddleware(() => { - const { user } = useUserSession() +export default defineNuxtRouteMiddleware(async () => { + const { user, loggedIn, waitForSession } = useUserSession() + await waitForSession() + + if (!loggedIn.value) { + return navigateTo('/login') + } + if (user.value && user.value.role !== Role.ADMIN) { toast.error('You are not authorized to access this page') return navigateTo('/') diff --git a/apps/shelve/app/middleware/auth.ts b/apps/shelve/app/middleware/auth.ts index c8c29e299..1740747e5 100644 --- a/apps/shelve/app/middleware/auth.ts +++ b/apps/shelve/app/middleware/auth.ts @@ -1,5 +1,6 @@ -export default defineNuxtRouteMiddleware(() => { - const { loggedIn } = useUserSession() +export default defineNuxtRouteMiddleware(async () => { + const { loggedIn, waitForSession } = useUserSession() + await waitForSession() if (!loggedIn.value) { toast.error('You need to be logged in to access this page.') diff --git a/apps/shelve/app/middleware/guest.ts b/apps/shelve/app/middleware/guest.ts index ffef76ae0..d167dd4d8 100644 --- a/apps/shelve/app/middleware/guest.ts +++ b/apps/shelve/app/middleware/guest.ts @@ -1,5 +1,6 @@ -export default defineNuxtRouteMiddleware(() => { - const { loggedIn } = useUserSession() +export default defineNuxtRouteMiddleware(async () => { + const { loggedIn, waitForSession } = useUserSession() + await waitForSession() if (loggedIn.value) { return navigateTo('/') diff --git a/apps/shelve/app/middleware/onboarding.ts b/apps/shelve/app/middleware/onboarding.ts index 3cbe8a4ff..bfee6442f 100644 --- a/apps/shelve/app/middleware/onboarding.ts +++ b/apps/shelve/app/middleware/onboarding.ts @@ -1,5 +1,6 @@ -export default defineNuxtRouteMiddleware(() => { - const { user } = useUserSession() +export default defineNuxtRouteMiddleware(async () => { + const { user, waitForSession } = useUserSession() + await waitForSession() if (user.value && !user.value.onboarding) { toast.error('You need to complete the onboarding process to access this page.') diff --git a/apps/shelve/app/pages/[teamSlug]/team/members.vue b/apps/shelve/app/pages/[teamSlug]/team/members.vue index 000545f43..1860bafbb 100644 --- a/apps/shelve/app/pages/[teamSlug]/team/members.vue +++ b/apps/shelve/app/pages/[teamSlug]/team/members.vue @@ -49,7 +49,7 @@ onMounted(() => { }) // Members logic -const members = computed(() => currentTeam.value?.members.filter((member) => member.user.username.toLowerCase().includes(search.value.toLowerCase()))) +const members = computed(() => currentTeam.value?.members.filter((member) => member.user.name.toLowerCase().includes(search.value.toLowerCase()))) const canDelete = computed(() => hasAccess(teamRole.value, TeamRole.OWNER)) const canUpdate = computed(() => hasAccess(teamRole.value, TeamRole.ADMIN)) @@ -75,12 +75,12 @@ const modal = overlay.create(ConfirmModal) const columns: TableColumn[] = [ { - accessorKey: 'avatar', + accessorKey: 'image', header: 'Avatar', }, { - accessorKey: 'username', - header: 'Username', + accessorKey: 'name', + header: 'Name', }, { accessorKey: 'email', @@ -182,7 +182,7 @@ const items = (row: Member) => [ } modal.open({ title: 'Are you sure?', - description: `You are about to remove ${row.user.username} from the team.`, + description: `You are about to remove ${row.user.name} from the team.`, danger: true, onSuccess() { toast.promise(removeMember(row.id), { @@ -290,12 +290,12 @@ definePageMeta({ td: 'border-b border-default' }" > -