Skip to content

Commit 4bef73d

Browse files
committed
feat: upgrade better-auth to 1.4.7, expand playground
- Upgrade better-auth and @better-auth/passkey to 1.4.7 - Fix version mismatch causing passkey TypeScript errors - Add twoFactor and multiSession plugins - Expand dashboard with sessions, 2FA setup, passkeys - Add full admin page with user management - Fix server imports (useRuntimeConfig from nitropack/runtime) - Update API calls to match 1.4.7 signatures
1 parent 7429e4d commit 4bef73d

File tree

21 files changed

+1025
-489
lines changed

21 files changed

+1025
-489
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
"@nuxthub/core": "^0.10.0",
7070
"@types/better-sqlite3": "^7.6.13",
7171
"@types/node": "latest",
72-
"better-auth": "^1.2.8",
72+
"better-auth": "^1.4.7",
7373
"better-sqlite3": "^11.9.1",
7474
"changelogen": "^0.6.2",
7575
"consola": "^3.4.2",

playground/app/auth.client.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
import { passkeyClient } from '@better-auth/passkey/client'
2-
import { adminClient } from 'better-auth/client/plugins'
2+
import { adminClient, multiSessionClient, twoFactorClient } from 'better-auth/client/plugins'
33
import { createAuthClient } from 'better-auth/vue'
4+
import { navigateTo } from '#imports'
45

56
export function createAppAuthClient(baseURL: string) {
67
return createAuthClient({
78
baseURL,
8-
plugins: [adminClient(), passkeyClient()],
9+
plugins: [
10+
adminClient(),
11+
passkeyClient(),
12+
multiSessionClient(),
13+
twoFactorClient({
14+
onTwoFactorRedirect() {
15+
navigateTo('/two-factor')
16+
},
17+
}),
18+
],
919
})
1020
}
Lines changed: 226 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,232 @@
11
<script setup lang="ts">
2-
const { user } = useUserSession()
2+
const { client } = useUserSession()
3+
const toast = useToast()
4+
5+
// Type assertion for admin plugin methods
6+
const adminClient = client as typeof client & {
7+
admin: {
8+
listUsers: Function, createUser: Function, banUser: Function
9+
unbanUser: Function, impersonateUser: Function, removeUser: Function
10+
}
11+
}
12+
13+
// Users
14+
const users = ref<any[]>([])
15+
const loading = ref(true)
16+
const search = ref('')
17+
18+
async function loadUsers() {
19+
loading.value = true
20+
try {
21+
const res = await adminClient?.admin.listUsers({
22+
query: { limit: 50, sortBy: 'createdAt', sortDirection: 'desc' },
23+
})
24+
users.value = res?.data?.users || []
25+
}
26+
catch { users.value = [] }
27+
loading.value = false
28+
}
29+
30+
const filteredUsers = computed(() => {
31+
if (!search.value) return users.value
32+
const s = search.value.toLowerCase()
33+
return users.value.filter(u => u.email?.toLowerCase().includes(s) || u.name?.toLowerCase().includes(s))
34+
})
35+
36+
const userColumns = [
37+
{ id: 'email', header: 'Email', accessorKey: 'email' },
38+
{ id: 'name', header: 'Name', accessorKey: 'name' },
39+
{ id: 'role', header: 'Role', accessorKey: 'role' },
40+
{ id: 'banned', header: 'Status', accessorKey: 'banned' },
41+
{ id: 'actions', header: '', accessorKey: 'actions' },
42+
]
43+
44+
// Create user
45+
const createOpen = ref(false)
46+
const createForm = reactive({ email: '', password: '', name: '', role: 'user' as 'user' | 'admin' })
47+
const createLoading = ref(false)
48+
49+
async function createUser() {
50+
createLoading.value = true
51+
try {
52+
await adminClient?.admin.createUser({
53+
email: createForm.email,
54+
password: createForm.password,
55+
name: createForm.name,
56+
role: createForm.role,
57+
})
58+
toast.add({ title: 'User created', color: 'success' })
59+
createOpen.value = false
60+
createForm.email = ''
61+
createForm.password = ''
62+
createForm.name = ''
63+
createForm.role = 'user'
64+
await loadUsers()
65+
}
66+
catch (e: any) {
67+
toast.add({ title: 'Error', description: e.message, color: 'error' })
68+
}
69+
createLoading.value = false
70+
}
71+
72+
// Ban user
73+
const banOpen = ref(false)
74+
const banForm = reactive({ userId: '', reason: '', expiresIn: '' })
75+
const banLoading = ref(false)
76+
77+
function openBan(userId: string) {
78+
banForm.userId = userId
79+
banForm.reason = ''
80+
banForm.expiresIn = ''
81+
banOpen.value = true
82+
}
83+
84+
async function banUser() {
85+
banLoading.value = true
86+
try {
87+
const expiresIn = banForm.expiresIn ? Number.parseInt(banForm.expiresIn) * 60 * 60 * 1000 : undefined
88+
await adminClient?.admin.banUser({
89+
userId: banForm.userId,
90+
banReason: banForm.reason || undefined,
91+
banExpiresIn: expiresIn,
92+
})
93+
toast.add({ title: 'User banned', color: 'success' })
94+
banOpen.value = false
95+
await loadUsers()
96+
}
97+
catch (e: any) {
98+
toast.add({ title: 'Error', description: e.message, color: 'error' })
99+
}
100+
banLoading.value = false
101+
}
102+
103+
async function unbanUser(userId: string) {
104+
try {
105+
await adminClient?.admin.unbanUser({ userId })
106+
toast.add({ title: 'User unbanned', color: 'success' })
107+
await loadUsers()
108+
}
109+
catch (e: any) {
110+
toast.add({ title: 'Error', description: e.message, color: 'error' })
111+
}
112+
}
113+
114+
// Impersonate
115+
async function impersonate(userId: string) {
116+
try {
117+
await adminClient?.admin.impersonateUser({ userId })
118+
toast.add({ title: 'Now impersonating user', color: 'success' })
119+
navigateTo('/app')
120+
}
121+
catch (e: any) {
122+
toast.add({ title: 'Error', description: e.message, color: 'error' })
123+
}
124+
}
125+
126+
// Delete user
127+
async function deleteUser(userId: string) {
128+
if (!confirm('Are you sure you want to delete this user?')) return
129+
try {
130+
await adminClient?.admin.removeUser({ userId })
131+
toast.add({ title: 'User deleted', color: 'success' })
132+
await loadUsers()
133+
}
134+
catch (e: any) {
135+
toast.add({ title: 'Error', description: e.message, color: 'error' })
136+
}
137+
}
138+
139+
onMounted(loadUsers)
3140
</script>
4141

5142
<template>
6-
<div class="max-w-2xl mx-auto py-12 px-4">
7-
<h2 class="text-2xl font-semibold mb-4">
8-
Admin Dashboard
9-
</h2>
10-
<p class="text-muted-foreground mb-2">
11-
Welcome, admin {{ user?.name }}!
12-
</p>
13-
<p class="text-muted-foreground">
14-
This page is only accessible to admins.
15-
</p>
143+
<div class="max-w-4xl mx-auto py-8 px-4">
144+
<UCard>
145+
<template #header>
146+
<div class="flex justify-between items-center gap-4">
147+
<h1 class="text-xl font-semibold">Admin Dashboard</h1>
148+
<div class="flex gap-2">
149+
<UInput v-model="search" placeholder="Search users..." class="w-48" />
150+
<UButton @click="createOpen = true">
151+
<UIcon name="i-lucide-plus" />
152+
Create User
153+
</UButton>
154+
</div>
155+
</div>
156+
</template>
157+
158+
<UTable :loading="loading" :data="filteredUsers" :columns="userColumns">
159+
<template #email-cell="{ row }">
160+
<div class="flex items-center gap-2">
161+
<UAvatar :src="row.original.image" :alt="row.original.name" size="xs" />
162+
<span>{{ row.original.email }}</span>
163+
</div>
164+
</template>
165+
<template #role-cell="{ row }">
166+
<UBadge :color="row.original.role === 'admin' ? 'primary' : 'neutral'" size="xs">
167+
{{ row.original.role }}
168+
</UBadge>
169+
</template>
170+
<template #banned-cell="{ row }">
171+
<UBadge :color="row.original.banned ? 'error' : 'success'" size="xs">
172+
{{ row.original.banned ? 'Banned' : 'Active' }}
173+
</UBadge>
174+
</template>
175+
<template #actions-cell="{ row }">
176+
<div class="flex gap-1">
177+
<UButton size="xs" variant="ghost" @click="impersonate(row.original.id)">
178+
<UIcon name="i-lucide-user-check" />
179+
</UButton>
180+
<UButton v-if="row.original.banned" size="xs" variant="ghost" color="success" @click="unbanUser(row.original.id)">
181+
Unban
182+
</UButton>
183+
<UButton v-else size="xs" variant="ghost" color="warning" @click="openBan(row.original.id)">
184+
Ban
185+
</UButton>
186+
<UButton size="xs" variant="ghost" color="error" @click="deleteUser(row.original.id)">
187+
<UIcon name="i-lucide-trash-2" />
188+
</UButton>
189+
</div>
190+
</template>
191+
</UTable>
192+
</UCard>
193+
194+
<!-- Create User Modal -->
195+
<UModal v-model:open="createOpen">
196+
<template #header>Create New User</template>
197+
<template #body>
198+
<div class="space-y-4 p-4">
199+
<UFormField label="Email">
200+
<UInput v-model="createForm.email" type="email" />
201+
</UFormField>
202+
<UFormField label="Password">
203+
<UInput v-model="createForm.password" type="password" />
204+
</UFormField>
205+
<UFormField label="Name">
206+
<UInput v-model="createForm.name" />
207+
</UFormField>
208+
<UFormField label="Role">
209+
<USelect v-model="createForm.role" :options="['user', 'admin']" />
210+
</UFormField>
211+
<UButton block :loading="createLoading" @click="createUser">Create User</UButton>
212+
</div>
213+
</template>
214+
</UModal>
215+
216+
<!-- Ban User Modal -->
217+
<UModal v-model:open="banOpen">
218+
<template #header>Ban User</template>
219+
<template #body>
220+
<div class="space-y-4 p-4">
221+
<UFormField label="Reason (optional)">
222+
<UInput v-model="banForm.reason" placeholder="Reason for ban" />
223+
</UFormField>
224+
<UFormField label="Duration (hours, leave empty for permanent)">
225+
<UInput v-model="banForm.expiresIn" type="number" placeholder="e.g. 24" />
226+
</UFormField>
227+
<UButton block color="error" :loading="banLoading" @click="banUser">Ban User</UButton>
228+
</div>
229+
</template>
230+
</UModal>
16231
</div>
17232
</template>

0 commit comments

Comments
 (0)