Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
1a58d2e
feat: switch to better auth
onmax Feb 5, 2026
811c140
feat(auth): add migration e2e
onmax Feb 6, 2026
0dd93f6
Merge remote-tracking branch 'origin/main' into feat/better-auth-migr…
onmax Feb 7, 2026
a9d9ddb
fix(auth): unblock build and typecheck
onmax Feb 7, 2026
0c7a19a
fix: satisfy lint + auth hook types
onmax Feb 7, 2026
be54d13
fix(auth): avoid ts path aliases in auth config
onmax Feb 7, 2026
ac8582b
fix(auth): keep invite social login
onmax Feb 7, 2026
040e03a
chore(shelve): harden auth migration base
onmax Feb 15, 2026
52ce948
chore(app): add migration rehearsal scripts
onmax Feb 15, 2026
ea37af4
fix(db): rewrite 0004 migration without do blocks
onmax Feb 15, 2026
fd041cb
test(migration): fail compare on missing auth fks
onmax Feb 15, 2026
a29034b
test(e2e): improve otp errors
onmax Feb 15, 2026
5e8b0d3
fix(auth): use auth schema in hub
onmax Feb 15, 2026
40806fa
fix(api): avoid db.query.user
onmax Feb 15, 2026
890f79f
fix(team): hydrate member users
onmax Feb 15, 2026
8c43674
test(migration): harden otp flows
onmax Feb 15, 2026
869814f
chore(auth): bump nuxt better auth
onmax Feb 16, 2026
853e9e7
refactor(auth): remove provider hook collision
onmax Feb 16, 2026
858086e
chore(auth): unslop auth core files
onmax Feb 16, 2026
9c25ee0
refactor(auth): drop user schema shim
onmax Feb 16, 2026
a4f88fb
docs(pr): add auth review map
onmax Feb 16, 2026
21ce0f2
refactor(auth): aggressive unslop core
onmax Feb 16, 2026
c725b5a
chore(auth): normalize auth typings
onmax Feb 16, 2026
6024571
chore(auth): fix nullable hydrate types
onmax Feb 16, 2026
a2ebd6a
test(auth): verify runtime checks
onmax Feb 16, 2026
c6e5fbd
refactor(auth): remove compat fallbacks
onmax Feb 17, 2026
6a0c2b9
fix(auth): use explicit user table imports
onmax Feb 22, 2026
3383eac
feat(rehearsal): add migration run scripts
onmax Feb 22, 2026
08a575d
chore(app): bump better-auth pkg to @185
onmax Feb 22, 2026
00523ab
refactor(auth): align guards and lint cleanup
onmax Feb 23, 2026
1a6ecda
Merge remote-tracking branch 'origin/main' into feat/better-auth-migr…
onmax Feb 23, 2026
f227389
refactor(auth): use better-auth action composables
onmax Feb 23, 2026
60a2d3a
refactor(app): reuse login router instance
onmax Feb 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,9 @@ dist
.data

apps/vault/.data
apps/shelve/.data
apps/shelve/.data
.vercel
.env*.local
.tmp/
.worktrees/
.nuxtrc
6 changes: 6 additions & 0 deletions apps/shelve/.env.e2e.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
E2E_BASE_URL=""
E2E_DATABASE_URL=""
E2E_TEST_EMAIL=""
E2E_ADMIN_EMAIL=""
E2E_TEAM_SLUG=""
E2E_VERCEL_BYPASS=""
3 changes: 2 additions & 1 deletion apps/shelve/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ NUXT_OAUTH_GOOGLE_CLIENT_ID=your_value
TURBO_TEAM=your_value
NUXT_OAUTH_GITHUB_CLIENT_ID=your_value
NUXT_PRIVATE_ENCRYPTION_KEY=your_value
NUXT_AUTH_ALLOW_LEGACY_CLI=true
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
Expand Down
7 changes: 6 additions & 1 deletion apps/shelve/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,9 @@ dist
.turbo

.data
.wrangler
.wrangler
.vercel
.env*.local
.nuxtrc
playwright-report/
test-results/
6 changes: 6 additions & 0 deletions apps/shelve/app/auth.config.ts
Original file line number Diff line number Diff line change
@@ -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()],
})
4 changes: 2 additions & 2 deletions apps/shelve/app/components/CliInstall.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
const { user, fetch } = useUserSession()
const { user, fetchSession } = useUserSession()

const closed = ref(false)
const loading = ref(false)
Expand All @@ -8,7 +8,7 @@ async function installCli() {
await $fetch('/api/user/cli', {
method: 'POST',
})
await fetch()
await fetchSession()
toast.success('Thank you for installing the Shelve CLI')
loading.value = false
}
Expand Down
6 changes: 3 additions & 3 deletions apps/shelve/app/components/UserDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ const colorMode = useColorMode()
const items = computed<DropdownMenuItem[][]>(() => [
[
{
label: user.value?.username || '',
label: user.value?.name || '',
avatar: {
src: user.value?.avatar || ''
src: user.value?.image || ''
},
type: 'label'
}
Expand Down Expand Up @@ -63,6 +63,6 @@ const items = computed<DropdownMenuItem[][]>(() => [
content: 'w-48'
}"
>
<UAvatar :src="user.avatar" :alt="user.username" size="sm" />
<UAvatar :src="user.image" :alt="user.name" size="sm" />
</UDropdownMenu>
</template>
23 changes: 14 additions & 9 deletions apps/shelve/app/components/auth/Button.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
const loading = ref(false)
const signInSocial = useSignIn('social')
const props = defineProps({
label: {
type: String,
Expand All @@ -15,24 +15,29 @@ const props = defineProps({
},
redirectUrl: {
type: String,
default: '',
required: false,
default: undefined,
},
})

function open() {
loading.value = true
const url = props.redirectUrl
? `/auth/${props.provider}?redirect=${encodeURIComponent(props.redirectUrl)}`
: `/auth/${props.provider}`
window.location.href = url
const loading = computed(() => signInSocial.status.value === 'pending')

async function open() {
await signInSocial.execute({
provider: props.provider,
callbackURL: props.redirectUrl || undefined,
})

if (signInSocial.status.value === 'error') {
toast.error(getAuthErrorMessage(signInSocial.error.value, `Failed to sign in with ${props.provider}`))
}
}
</script>

<template>
<UButton
:loading
:disabled="loading"
external
:icon
block
class="rounded-none"
Expand Down
22 changes: 10 additions & 12 deletions apps/shelve/app/components/auth/EmailForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,20 @@ const state = reactive<Partial<Schema>>({
email: undefined
})

const loading = ref(false)
const sendVerificationOtp = useAuthClientAction((authClient) => authClient.emailOtp.sendVerificationOtp)
const loading = computed(() => sendVerificationOtp.status.value === 'pending')

async function onSubmit(event: FormSubmitEvent<Schema>) {
loading.value = true
try {
await $fetch('/api/auth/otp/send', {
method: 'POST',
body: { email: event.data.email }
})

await sendVerificationOtp.execute({
email: event.data.email,
type: 'sign-in'
})

if (sendVerificationOtp.status.value === 'error') {
toast.error(getAuthErrorMessage(sendVerificationOtp.error.value, 'Failed to send verification code'))
} else {
emit('emailSubmitted', event.data.email)
toast.success('Check your email for the verification code')
} catch (error: any) {
toast.error(error.data?.message || 'Failed to send verification code')
} finally {
loading.value = false
}
}
</script>
Expand Down
54 changes: 26 additions & 28 deletions apps/shelve/app/components/auth/OtpForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ const emit = defineEmits<{

const router = useRouter()
const otp = ref<string[]>(props.prefilledOtp ? props.prefilledOtp.split('') : [])
const loading = ref(false)
const signInEmailOtp = useSignIn('emailOtp')
const sendVerificationOtp = useAuthClientAction((authClient) => authClient.emailOtp.sendVerificationOtp)
const loading = computed(() => signInEmailOtp.status.value === 'pending' || sendVerificationOtp.status.value === 'pending')

onMounted(async () => {
if (props.prefilledOtp && props.prefilledOtp.length === 6) {
Expand All @@ -23,41 +25,37 @@ onMounted(async () => {
async function handleOtpComplete(value: string[]) {
if (value.length !== 6) return

loading.value = true
try {
await $fetch('/api/auth/otp/verify', {
method: 'POST',
body: {
email: props.email,
code: value.join('')
await signInEmailOtp.execute({
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')
})

if (signInEmailOtp.status.value === 'error') {
toast.error(getAuthErrorMessage(signInEmailOtp.error.value, 'Invalid verification code'))
otp.value = []
} finally {
loading.value = false
}
}

async function resendCode() {
try {
await $fetch('/api/auth/otp/send', {
method: 'POST',
body: { email: props.email }
})
await sendVerificationOtp.execute({
email: props.email,
type: 'sign-in'
})

if (sendVerificationOtp.status.value === 'error') {
toast.error(getAuthErrorMessage(sendVerificationOtp.error.value, 'Failed to resend code'))
} else {
toast.success('New verification code sent')
} catch (error: any) {
toast.error('Failed to resend code')
}
}
</script>
Expand Down
5 changes: 4 additions & 1 deletion apps/shelve/app/composables/useLogout.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export async function useLogout() {
await useUserSession().clear()
const { signOut } = useUserSession()
const defaultTeamSlug = useCookie<string | null>('defaultTeamSlug')
defaultTeamSlug.value = null
await signOut()
navigateTo('/login')
}
4 changes: 2 additions & 2 deletions apps/shelve/app/error.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const { error } = defineProps<ErrorProps>()
console.error(error)

const router = useRouter()
const { clear } = useUserSession()
const { signOut } = useUserSession()

const handleError = () => clearError({ redirect: '/' })
const goBack = () => {
Expand All @@ -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')
}

Expand Down
2 changes: 1 addition & 1 deletion apps/shelve/app/layouts/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
</script>

Expand Down
1 change: 1 addition & 0 deletions apps/shelve/app/middleware/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Role } from '@types'

export default defineNuxtRouteMiddleware(() => {
const { user } = useUserSession()

if (user.value && user.value.role !== Role.ADMIN) {
toast.error('You are not authorized to access this page')
return navigateTo('/')
Expand Down
8 changes: 0 additions & 8 deletions apps/shelve/app/middleware/auth.ts

This file was deleted.

7 changes: 0 additions & 7 deletions apps/shelve/app/middleware/guest.ts

This file was deleted.

6 changes: 3 additions & 3 deletions apps/shelve/app/middleware/index-redirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
if (from.path && from.path !== '/' && !from.path.startsWith('/auth')) return

try {
const teams = await $fetch('/api/teams')
if (teams.length === 1) {
return navigateTo(`/${teams[0].slug}`)
const teams = await $fetch<Array<{ slug: string }>>('/api/teams')
if (teams?.length === 1) {
return navigateTo(`/${teams[0]?.slug}`)
}
} catch (error) {
console.error('Failed to fetch teams in index-redirect middleware:', error)
Expand Down
3 changes: 2 additions & 1 deletion apps/shelve/app/pages/[teamSlug].vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script setup lang="ts">
definePageMeta({
layout: 'app',
middleware: ['auth', 'onboarding', 'default-team']
auth: 'user',
middleware: ['onboarding', 'default-team']
})

const route = useRoute()
Expand Down
18 changes: 9 additions & 9 deletions apps/shelve/app/pages/[teamSlug]/team/members.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand All @@ -75,12 +75,12 @@ const modal = overlay.create(ConfirmModal)

const columns: TableColumn<Member>[] = [
{
accessorKey: 'avatar',
accessorKey: 'image',
header: 'Avatar',
},
{
accessorKey: 'username',
header: 'Username',
accessorKey: 'name',
header: 'Name',
},
{
accessorKey: 'email',
Expand Down Expand Up @@ -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), {
Expand Down Expand Up @@ -290,12 +290,12 @@ definePageMeta({
td: 'border-b border-default'
}"
>
<template #avatar-cell="{ row }">
<UAvatar :src="row.original.user.avatar" :alt="row.original.user.username" size="sm" img-class="object-cover" />
<template #image-cell="{ row }">
<UAvatar :src="row.original.user.image" :alt="row.original.user.name" size="sm" img-class="object-cover" />
</template>
<template #username-cell="{ row }">
<template #name-cell="{ row }">
<span class="font-semibold">
{{ row.original.user.username }}
{{ row.original.user.name }}
</span>
</template>
<template #email-cell="{ row }">
Expand Down
4 changes: 2 additions & 2 deletions apps/shelve/app/pages/admin.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script setup lang="ts">
definePageMeta({
layout: 'app',
middleware: ['auth', 'admin']
auth: 'user',
middleware: ['admin']
})

useSeoMeta({
Expand All @@ -12,4 +13,3 @@ useSeoMeta({
<template>
<NuxtPage />
</template>

Loading
Loading