@onmax/nuxt-better-auth 0.0.1 → 0.0.2-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +17 -170
  2. package/dist/module.d.mts +20 -2
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +432 -14
  5. package/dist/runtime/app/components/BetterAuthState.d.vue.ts +20 -0
  6. package/dist/runtime/app/components/BetterAuthState.vue +8 -0
  7. package/dist/runtime/app/components/BetterAuthState.vue.d.ts +20 -0
  8. package/dist/runtime/app/composables/useUserSession.d.ts +22 -0
  9. package/dist/runtime/app/composables/useUserSession.js +159 -0
  10. package/dist/runtime/app/middleware/auth.global.d.ts +13 -0
  11. package/dist/runtime/app/middleware/auth.global.js +37 -0
  12. package/dist/runtime/app/pages/__better-auth-devtools.d.vue.ts +3 -0
  13. package/dist/runtime/app/pages/__better-auth-devtools.vue +426 -0
  14. package/dist/runtime/app/pages/__better-auth-devtools.vue.d.ts +3 -0
  15. package/dist/runtime/app/plugins/session.client.d.ts +2 -0
  16. package/dist/runtime/app/plugins/session.client.js +16 -0
  17. package/dist/runtime/app/plugins/session.server.d.ts +2 -0
  18. package/dist/runtime/app/plugins/session.server.js +24 -0
  19. package/dist/runtime/config.d.ts +44 -0
  20. package/dist/runtime/config.js +6 -0
  21. package/dist/runtime/server/api/_better-auth/_schema.d.ts +8 -0
  22. package/dist/runtime/server/api/_better-auth/_schema.js +11 -0
  23. package/dist/runtime/server/api/_better-auth/accounts.get.d.ts +14 -0
  24. package/dist/runtime/server/api/_better-auth/accounts.get.js +28 -0
  25. package/dist/runtime/server/api/_better-auth/config.get.d.ts +35 -0
  26. package/dist/runtime/server/api/_better-auth/config.get.js +47 -0
  27. package/dist/runtime/server/api/_better-auth/sessions.delete.d.ts +4 -0
  28. package/dist/runtime/server/api/_better-auth/sessions.delete.js +22 -0
  29. package/dist/runtime/server/api/_better-auth/sessions.get.d.ts +14 -0
  30. package/dist/runtime/server/api/_better-auth/sessions.get.js +43 -0
  31. package/dist/runtime/server/api/_better-auth/users.get.d.ts +14 -0
  32. package/dist/runtime/server/api/_better-auth/users.get.js +34 -0
  33. package/dist/runtime/server/api/auth/[...all].d.ts +2 -0
  34. package/dist/runtime/server/api/auth/[...all].js +6 -0
  35. package/dist/runtime/server/middleware/route-access.d.ts +2 -0
  36. package/dist/runtime/server/middleware/route-access.js +28 -0
  37. package/dist/runtime/server/tsconfig.json +3 -0
  38. package/dist/runtime/server/utils/auth.d.ts +11 -0
  39. package/dist/runtime/server/utils/auth.js +32 -0
  40. package/dist/runtime/server/utils/session.d.ts +9 -0
  41. package/dist/runtime/server/utils/session.js +23 -0
  42. package/dist/runtime/types/augment.d.ts +42 -0
  43. package/dist/runtime/types/augment.js +0 -0
  44. package/dist/runtime/types.d.ts +23 -0
  45. package/dist/runtime/types.js +0 -0
  46. package/dist/runtime/utils/match-user.d.ts +2 -0
  47. package/dist/runtime/utils/match-user.js +13 -0
  48. package/dist/types.d.mts +8 -10
  49. package/package.json +40 -11
  50. package/dist/module.d.cts +0 -2
@@ -0,0 +1,426 @@
1
+ <script setup>
2
+ import { useDevtoolsClient } from "@nuxt/devtools-kit/iframe-client";
3
+ import { refDebounced } from "@vueuse/core";
4
+ definePageMeta({ layout: false });
5
+ const toast = useToast();
6
+ const devtoolsClient = useDevtoolsClient();
7
+ const runtimeConfig = useRuntimeConfig();
8
+ const hasDb = computed(() => runtimeConfig.public.auth?.useDatabase ?? false);
9
+ const isDark = computed(() => devtoolsClient.value?.host?.app?.colorMode?.value === "dark");
10
+ watchEffect(() => {
11
+ if (import.meta.client) {
12
+ document.documentElement.classList.toggle("dark", isDark.value);
13
+ }
14
+ });
15
+ const sessionsPage = ref(1);
16
+ const usersPage = ref(1);
17
+ const accountsPage = ref(1);
18
+ const deleteConfirm = ref(null);
19
+ const sessionsSearchRaw = ref("");
20
+ const usersSearchRaw = ref("");
21
+ const accountsSearchRaw = ref("");
22
+ const sessionsSearch = refDebounced(sessionsSearchRaw, 300);
23
+ const usersSearch = refDebounced(usersSearchRaw, 300);
24
+ const accountsSearch = refDebounced(accountsSearchRaw, 300);
25
+ watch(sessionsSearch, () => sessionsPage.value = 1);
26
+ watch(usersSearch, () => usersPage.value = 1);
27
+ watch(accountsSearch, () => accountsPage.value = 1);
28
+ const sessionsQuery = computed(() => ({ page: sessionsPage.value, limit: 20, search: sessionsSearch.value }));
29
+ const usersQuery = computed(() => ({ page: usersPage.value, limit: 20, search: usersSearch.value }));
30
+ const accountsQuery = computed(() => ({ page: accountsPage.value, limit: 20, search: accountsSearch.value }));
31
+ const { data: sessionsData, refresh: refreshSessions } = await useFetch("/api/_better-auth/sessions", { query: sessionsQuery, immediate: hasDb.value });
32
+ const { data: usersData, refresh: refreshUsers } = await useFetch("/api/_better-auth/users", { query: usersQuery, immediate: hasDb.value });
33
+ const { data: accountsData, refresh: refreshAccounts } = await useFetch("/api/_better-auth/accounts", { query: accountsQuery, immediate: hasDb.value });
34
+ const { data: configData } = await useFetch("/api/_better-auth/config");
35
+ const tabs = computed(() => {
36
+ const dbTabs = [
37
+ { label: "Sessions", value: "sessions", icon: "i-lucide-key", slot: "sessions" },
38
+ { label: "Users", value: "users", icon: "i-lucide-users", slot: "users" },
39
+ { label: "Accounts", value: "accounts", icon: "i-lucide-link", slot: "accounts" }
40
+ ];
41
+ const configTab = { label: "Config", value: "config", icon: "i-lucide-settings", slot: "config" };
42
+ return hasDb.value ? [...dbTabs, configTab] : [configTab];
43
+ });
44
+ function isExpired(date) {
45
+ if (!date)
46
+ return false;
47
+ return new Date(date) < /* @__PURE__ */ new Date();
48
+ }
49
+ function formatDate(date) {
50
+ if (!date)
51
+ return "-";
52
+ return new Date(date).toLocaleString();
53
+ }
54
+ function truncate(str, len = 12) {
55
+ if (!str)
56
+ return "-";
57
+ if (str.length <= len)
58
+ return str;
59
+ const half = Math.floor((len - 1) / 2);
60
+ return `${str.slice(0, half)}\u2026${str.slice(-half)}`;
61
+ }
62
+ async function copyToClipboard(text, label = "Value") {
63
+ try {
64
+ await navigator.clipboard.writeText(text);
65
+ toast.add({ title: `${label} copied`, icon: "i-lucide-check", color: "success" });
66
+ } catch {
67
+ toast.add({ title: "Copy failed", icon: "i-lucide-x", color: "error" });
68
+ }
69
+ }
70
+ function generateConfigMarkdown() {
71
+ const config = configData.value?.config;
72
+ if (!config)
73
+ return "";
74
+ const moduleJson = JSON.stringify(config.module, null, 2);
75
+ const serverJson = JSON.stringify(config.server, null, 2);
76
+ return `## Module Config (\`nuxt.config.ts\`)
77
+
78
+ \`\`\`json
79
+ ${moduleJson}
80
+ \`\`\`
81
+
82
+ ## Server Config (\`server/auth.config.ts\`)
83
+
84
+ \`\`\`json
85
+ ${serverJson}
86
+ \`\`\`
87
+ `;
88
+ }
89
+ async function deleteSession(id) {
90
+ try {
91
+ await $fetch("/api/_better-auth/sessions", { method: "DELETE", body: { id } });
92
+ toast.add({ title: "Session deleted", icon: "i-lucide-trash-2", color: "success" });
93
+ deleteConfirm.value = null;
94
+ refreshSessions();
95
+ } catch {
96
+ toast.add({ title: "Failed to delete session", icon: "i-lucide-x", color: "error" });
97
+ }
98
+ }
99
+ const sessionColumns = [
100
+ { accessorKey: "id", header: "ID", cell: ({ row }) => h("span", { class: "font-mono text-sm" }, truncate(row.original.id)) },
101
+ {
102
+ accessorKey: "userId",
103
+ header: "User",
104
+ cell: ({ row }) => h("div", { class: "min-w-0" }, [
105
+ h("p", { class: "font-mono text-sm truncate" }, truncate(row.original.userId)),
106
+ h("p", { class: "text-sm text-muted-foreground font-mono" }, row.original.ipAddress || "No IP")
107
+ ])
108
+ },
109
+ { accessorKey: "userAgent", header: "User Agent", cell: ({ row }) => h("span", { class: "text-sm text-muted-foreground max-w-48 truncate block" }, truncate(row.original.userAgent, 30)) },
110
+ {
111
+ accessorKey: "expiresAt",
112
+ header: "Status",
113
+ cell: ({ row }) => {
114
+ const expired = isExpired(row.original.expiresAt);
115
+ return h(resolveComponent("UBadge"), { color: expired ? "error" : "success", variant: "subtle", size: "sm" }, () => expired ? "Expired" : "Active");
116
+ }
117
+ },
118
+ { accessorKey: "createdAt", header: "Created", cell: ({ row }) => h("span", { class: "text-sm text-muted-foreground" }, formatDate(row.original.createdAt)) }
119
+ ];
120
+ const userColumns = [
121
+ { accessorKey: "id", header: "ID", cell: ({ row }) => h("span", { class: "font-mono text-sm" }, truncate(row.original.id)) },
122
+ {
123
+ accessorKey: "name",
124
+ header: "User",
125
+ cell: ({ row }) => h("div", { class: "min-w-0" }, [
126
+ h("p", { class: "font-medium truncate" }, row.original.name || "Unnamed"),
127
+ h("p", { class: "text-sm text-muted-foreground font-mono truncate" }, row.original.email)
128
+ ])
129
+ },
130
+ {
131
+ accessorKey: "emailVerified",
132
+ header: "Verified",
133
+ cell: ({ row }) => h(resolveComponent("UBadge"), { color: row.original.emailVerified ? "success" : "neutral", variant: "subtle", size: "sm" }, () => row.original.emailVerified ? "Yes" : "No")
134
+ },
135
+ { accessorKey: "createdAt", header: "Created", cell: ({ row }) => h("span", { class: "text-sm text-muted-foreground" }, formatDate(row.original.createdAt)) }
136
+ ];
137
+ const accountColumns = [
138
+ { accessorKey: "id", header: "ID", cell: ({ row }) => h("span", { class: "font-mono text-sm" }, truncate(row.original.id)) },
139
+ {
140
+ accessorKey: "providerId",
141
+ header: "Provider",
142
+ cell: ({ row }) => {
143
+ const provider = row.original.providerId;
144
+ const iconMap = { github: "i-simple-icons-github", google: "i-simple-icons-google", discord: "i-simple-icons-discord", twitter: "i-simple-icons-x", facebook: "i-simple-icons-facebook" };
145
+ return h("div", { class: "flex items-center gap-2" }, [
146
+ h(resolveComponent("UIcon"), { name: iconMap[provider] || "i-lucide-key", class: "size-4" }),
147
+ h("div", { class: "min-w-0" }, [
148
+ h("p", { class: "capitalize font-medium" }, provider),
149
+ h("p", { class: "text-sm text-muted-foreground font-mono truncate" }, truncate(row.original.accountId, 16))
150
+ ])
151
+ ]);
152
+ }
153
+ },
154
+ { accessorKey: "userId", header: "User ID", cell: ({ row }) => h("span", { class: "font-mono text-sm" }, truncate(row.original.userId)) },
155
+ { accessorKey: "createdAt", header: "Created", cell: ({ row }) => h("span", { class: "text-sm text-muted-foreground" }, formatDate(row.original.createdAt)) }
156
+ ];
157
+ function getSessionActions(row) {
158
+ return [
159
+ [{ label: "Copy ID", icon: "i-lucide-copy", click: () => copyToClipboard(row.id, "Session ID") }],
160
+ [{ label: "Delete", icon: "i-lucide-trash-2", color: "error", click: () => {
161
+ deleteConfirm.value = row.id;
162
+ } }]
163
+ ];
164
+ }
165
+ function getUserActions(row) {
166
+ return [
167
+ [{ label: "Copy ID", icon: "i-lucide-copy", click: () => copyToClipboard(row.id, "User ID") }],
168
+ [{ label: "Copy Email", icon: "i-lucide-mail", click: () => copyToClipboard(row.email, "Email") }]
169
+ ];
170
+ }
171
+ function getAccountActions(row) {
172
+ return [[{ label: "Copy ID", icon: "i-lucide-copy", click: () => copyToClipboard(row.id, "Account ID") }]];
173
+ }
174
+ </script>
175
+
176
+ <template>
177
+ <div class="min-h-screen bg-background text-foreground">
178
+ <!-- Header -->
179
+ <header class="flex items-center justify-between border-b border-border px-4 py-3">
180
+ <div class="flex items-center gap-3">
181
+ <svg width="60" height="45" viewBox="0 0 60 45" fill="none" class="h-4 w-auto" xmlns="http://www.w3.org/2000/svg">
182
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M0 0H15V15H30V30H15V45H0V30V15V0ZM45 30V15H30V0H45H60V15V30V45H45H30V30H45Z" class="fill-current" />
183
+ </svg>
184
+ <span class="font-medium text-sm">Better Auth DevTools</span>
185
+ </div>
186
+ <div class="flex items-center">
187
+ <a href="https://www.better-auth.com/docs" target="_blank" class="header-link border-r border-border">Docs</a>
188
+ <a href="https://github.com/onmax/nuxt-better-auth" target="_blank" class="header-link">
189
+ <UIcon name="i-simple-icons-github" class="size-4" />
190
+ </a>
191
+ </div>
192
+ </header>
193
+
194
+ <!-- Tabs -->
195
+ <UTabs :items="tabs" class="w-full" :ui="{ list: 'border-b border-border rounded-none bg-transparent justify-start', trigger: 'rounded-none data-[state=active]:shadow-none data-[state=active]:bg-[var(--tab-active-bg)] data-[state=active]:text-foreground flex-none text-muted-foreground' }">
196
+ <!-- Sessions Tab -->
197
+ <template #sessions>
198
+ <div class="p-4 space-y-4">
199
+ <div class="flex items-center justify-between gap-4">
200
+ <UInput v-model="sessionsSearchRaw" placeholder="Search by user ID or IP..." icon="i-lucide-search" class="max-w-xs" />
201
+ <div class="flex items-center gap-2 text-sm text-muted-foreground whitespace-nowrap">
202
+ <span>{{ sessionsData?.total ?? 0 }} sessions</span>
203
+ <UButton variant="ghost" size="xs" icon="i-lucide-refresh-cw" @click="() => refreshSessions()" />
204
+ </div>
205
+ </div>
206
+
207
+ <UAlert v-if="deleteConfirm" title="Delete session?" description="This will invalidate the session immediately." color="error" variant="soft" icon="i-lucide-alert-triangle" :actions="[{ label: 'Cancel', color: 'neutral', variant: 'outline', onClick: () => {
208
+ deleteConfirm = null;
209
+ } }, { label: 'Delete', color: 'error', onClick: () => deleteSession(deleteConfirm) }]" />
210
+
211
+ <p v-if="sessionsData?.error" class="text-destructive text-sm">
212
+ {{ sessionsData.error }}
213
+ </p>
214
+
215
+ <UTable v-else-if="sessionsData?.sessions?.length" :data="sessionsData.sessions" :columns="sessionColumns" class="rounded-none border border-border">
216
+ <template #actions="{ row }">
217
+ <UDropdownMenu :items="getSessionActions(row.original)">
218
+ <UButton variant="ghost" size="xs" icon="i-lucide-more-horizontal" />
219
+ </UDropdownMenu>
220
+ </template>
221
+ </UTable>
222
+
223
+ <p v-else class="text-muted-foreground text-sm py-8 text-center">
224
+ No sessions found
225
+ </p>
226
+
227
+ <UPagination v-if="(sessionsData?.total ?? 0) > 20" v-model:page="sessionsPage" :total="sessionsData?.total ?? 0" :items-per-page="20" />
228
+ </div>
229
+ </template>
230
+
231
+ <!-- Users Tab -->
232
+ <template #users>
233
+ <div class="p-4 space-y-4">
234
+ <div class="flex items-center justify-between gap-4">
235
+ <UInput v-model="usersSearchRaw" placeholder="Search by name or email..." icon="i-lucide-search" class="max-w-xs" />
236
+ <div class="flex items-center gap-2 text-sm text-muted-foreground whitespace-nowrap">
237
+ <span>{{ usersData?.total ?? 0 }} users</span>
238
+ <UButton variant="ghost" size="xs" icon="i-lucide-refresh-cw" @click="() => refreshUsers()" />
239
+ </div>
240
+ </div>
241
+
242
+ <p v-if="usersData?.error" class="text-destructive text-sm">
243
+ {{ usersData.error }}
244
+ </p>
245
+
246
+ <UTable v-else-if="usersData?.users?.length" :data="usersData.users" :columns="userColumns" class="rounded-none border border-border">
247
+ <template #actions="{ row }">
248
+ <UDropdownMenu :items="getUserActions(row.original)">
249
+ <UButton variant="ghost" size="xs" icon="i-lucide-more-horizontal" />
250
+ </UDropdownMenu>
251
+ </template>
252
+ </UTable>
253
+
254
+ <p v-else class="text-muted-foreground text-sm py-8 text-center">
255
+ No users found
256
+ </p>
257
+
258
+ <UPagination v-if="(usersData?.total ?? 0) > 20" v-model:page="usersPage" :total="usersData?.total ?? 0" :items-per-page="20" />
259
+ </div>
260
+ </template>
261
+
262
+ <!-- Accounts Tab -->
263
+ <template #accounts>
264
+ <div class="p-4 space-y-4">
265
+ <div class="flex items-center justify-between gap-4">
266
+ <UInput v-model="accountsSearchRaw" placeholder="Search by provider..." icon="i-lucide-search" class="max-w-xs" />
267
+ <div class="flex items-center gap-2 text-sm text-muted-foreground whitespace-nowrap">
268
+ <span>{{ accountsData?.total ?? 0 }} accounts</span>
269
+ <UButton variant="ghost" size="xs" icon="i-lucide-refresh-cw" @click="() => refreshAccounts()" />
270
+ </div>
271
+ </div>
272
+
273
+ <p v-if="accountsData?.error" class="text-destructive text-sm">
274
+ {{ accountsData.error }}
275
+ </p>
276
+
277
+ <UTable v-else-if="accountsData?.accounts?.length" :data="accountsData.accounts" :columns="accountColumns" class="rounded-none border border-border">
278
+ <template #actions="{ row }">
279
+ <UDropdownMenu :items="getAccountActions(row.original)">
280
+ <UButton variant="ghost" size="xs" icon="i-lucide-more-horizontal" />
281
+ </UDropdownMenu>
282
+ </template>
283
+ </UTable>
284
+
285
+ <p v-else class="text-muted-foreground text-sm py-8 text-center">
286
+ No accounts found
287
+ </p>
288
+
289
+ <UPagination v-if="(accountsData?.total ?? 0) > 20" v-model:page="accountsPage" :total="accountsData?.total ?? 0" :items-per-page="20" />
290
+ </div>
291
+ </template>
292
+
293
+ <!-- Config Tab -->
294
+ <template #config>
295
+ <div class="p-3 space-y-3">
296
+ <div class="flex items-center justify-end">
297
+ <UButton variant="ghost" size="xs" icon="i-lucide-copy" @click="copyToClipboard(generateConfigMarkdown(), 'Config')">
298
+ Copy
299
+ </UButton>
300
+ </div>
301
+
302
+ <p v-if="configData?.error" class="text-destructive text-sm">
303
+ {{ configData.error }}
304
+ </p>
305
+
306
+ <template v-else-if="configData?.config?.server">
307
+ <!-- Row 1: Endpoints + Session + Auth Methods -->
308
+ <div class="grid gap-3 md:grid-cols-3">
309
+ <div class="config-section">
310
+ <div class="config-header">
311
+ <UIcon name="i-lucide-globe" class="size-4" /><span>Endpoints</span>
312
+ </div>
313
+ <div class="config-row">
314
+ <span class="config-label">Base URL</span><span class="font-mono">{{ configData.config.server.baseURL || "auto" }}</span>
315
+ </div>
316
+ <div class="config-row">
317
+ <span class="config-label">Path</span><span class="font-mono">{{ configData.config.server.basePath }}</span>
318
+ </div>
319
+ </div>
320
+ <div class="config-section">
321
+ <div class="config-header">
322
+ <UIcon name="i-lucide-clock" class="size-4" /><span>Session</span>
323
+ </div>
324
+ <div class="config-row">
325
+ <span class="config-label">Expires</span><span class="font-mono">{{ configData.config.server.session?.expiresIn }}</span>
326
+ </div>
327
+ <div class="config-row">
328
+ <span class="config-label">Update</span><span class="font-mono">{{ configData.config.server.session?.updateAge }}</span>
329
+ </div>
330
+ <div class="config-row">
331
+ <span class="config-label">Cache</span><UBadge :color="configData.config.server.session?.cookieCache ? 'success' : 'neutral'" variant="subtle" size="sm">
332
+ {{ configData.config.server.session?.cookieCache ? "On" : "Off" }}
333
+ </UBadge>
334
+ </div>
335
+ </div>
336
+ <div class="config-section">
337
+ <div class="config-header">
338
+ <UIcon name="i-lucide-key-round" class="size-4" /><span>Auth</span>
339
+ </div>
340
+ <div class="flex flex-wrap gap-1">
341
+ <UBadge v-if="configData.config.server.emailAndPassword" variant="subtle" color="success" size="sm">
342
+ Email
343
+ </UBadge>
344
+ <UBadge v-for="provider in configData.config.server.socialProviders" :key="provider" variant="subtle" color="neutral" size="sm" class="capitalize">
345
+ {{ provider }}
346
+ </UBadge>
347
+ <span v-if="!configData.config.server.emailAndPassword && !configData.config.server.socialProviders?.length" class="text-muted-foreground text-sm">None</span>
348
+ </div>
349
+ </div>
350
+ </div>
351
+
352
+ <!-- Row 2: Security + Module + Plugins -->
353
+ <div class="grid gap-3 md:grid-cols-3">
354
+ <div class="config-section">
355
+ <div class="config-header">
356
+ <UIcon name="i-lucide-shield" class="size-4" /><span>Security</span>
357
+ </div>
358
+ <div class="config-row">
359
+ <span class="config-label">Cookies</span><span class="font-mono">{{ configData.config.server.advanced?.useSecureCookies }}</span>
360
+ </div>
361
+ <div class="config-row">
362
+ <span class="config-label">CSRF</span><UBadge :color="configData.config.server.advanced?.disableCSRFCheck ? 'error' : 'success'" variant="subtle" size="sm">
363
+ {{ configData.config.server.advanced?.disableCSRFCheck ? "Off" : "On" }}
364
+ </UBadge>
365
+ </div>
366
+ <div class="config-row">
367
+ <span class="config-label">Rate Limit</span><UBadge :color="configData.config.server.rateLimit ? 'success' : 'neutral'" variant="subtle" size="sm">
368
+ {{ configData.config.server.rateLimit ? "On" : "Off" }}
369
+ </UBadge>
370
+ </div>
371
+ </div>
372
+ <div class="config-section">
373
+ <div class="config-header">
374
+ <UIcon name="i-lucide-settings-2" class="size-4" /><span>Module</span>
375
+ </div>
376
+ <div class="config-row">
377
+ <span class="config-label">Login</span><span class="font-mono">{{ configData.config.module?.redirects?.login }}</span>
378
+ </div>
379
+ <div class="config-row">
380
+ <span class="config-label">Guest</span><span class="font-mono">{{ configData.config.module?.redirects?.guest }}</span>
381
+ </div>
382
+ <div class="config-row">
383
+ <span class="config-label">DB</span><UBadge :color="configData.config.module?.useDatabase ? 'success' : 'neutral'" variant="subtle" size="sm">
384
+ {{ configData.config.module?.useDatabase ? "Hub" : "Off" }}
385
+ </UBadge>
386
+ </div>
387
+ <div class="config-row">
388
+ <span class="config-label">KV</span><UBadge :color="configData.config.module?.secondaryStorage ? 'success' : 'neutral'" variant="subtle" size="sm">
389
+ {{ configData.config.module?.secondaryStorage ? "On" : "Off" }}
390
+ </UBadge>
391
+ </div>
392
+ </div>
393
+ <div class="config-section">
394
+ <div class="config-header">
395
+ <UIcon name="i-lucide-puzzle" class="size-4" /><span>Plugins</span>
396
+ </div>
397
+ <div class="flex flex-wrap gap-1">
398
+ <UBadge v-for="plugin in configData.config.server.plugins" :key="plugin" variant="subtle" color="neutral" size="sm">
399
+ {{ plugin }}
400
+ </UBadge>
401
+ <span v-if="!configData.config.server.plugins?.length" class="text-muted-foreground text-sm">None</span>
402
+ </div>
403
+ </div>
404
+ </div>
405
+
406
+ <!-- Trusted Origins (if any) -->
407
+ <div v-if="configData.config.server.trustedOrigins?.length" class="config-section">
408
+ <div class="config-header">
409
+ <UIcon name="i-lucide-shield-check" class="size-4" /><span>Trusted Origins</span>
410
+ </div>
411
+ <div class="flex flex-wrap gap-1">
412
+ <UBadge v-for="origin in configData.config.server.trustedOrigins" :key="origin" variant="subtle" color="neutral" size="sm" class="font-mono">
413
+ {{ origin }}
414
+ </UBadge>
415
+ </div>
416
+ </div>
417
+ </template>
418
+ </div>
419
+ </template>
420
+ </UTabs>
421
+ </div>
422
+ </template>
423
+
424
+ <style>
425
+ :root{--background:#fff;--foreground:#0c0a09;--card:#fff;--card-foreground:#0c0a09;--muted-foreground:#78716c;--border:#e7e5e4;--destructive:#ef4444;--tab-active-bg:#eeedec}.dark{--background:#0c0a09;--foreground:#fafaf9;--card:#0c0a09;--card-foreground:#fafaf9;--muted-foreground:#a8a29e;--border:#292524;--destructive:#7f1d1d;--tab-active-bg:#292524}.bg-background{background-color:var(--background)}.text-foreground{color:var(--foreground)}.text-muted-foreground{color:var(--muted-foreground)}.text-destructive{color:var(--destructive)}.border-border{border-color:var(--border)}.header-link{align-items:center;color:var(--muted-foreground);display:flex;font-size:.75rem;padding:.5rem .75rem;position:relative;transition:color .2s}.header-link:hover{color:var(--foreground)}.header-link:after{background:var(--foreground);bottom:0;content:"";height:1px;left:.75rem;position:absolute;right:.75rem;transform:scaleX(0);transform-origin:left;transition:transform .2s ease-out}.header-link:hover:after{transform:scaleX(1)}.config-section{border:1px solid var(--border);padding:.5rem}.config-header{color:var(--muted-foreground);font-size:.8125rem;font-weight:500;gap:.375rem;margin-bottom:.375rem}.config-header,.config-row{align-items:center;display:flex}.config-row{font-size:.75rem;justify-content:space-between;padding:.125rem 0}.config-label{color:var(--muted-foreground)}
426
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,16 @@
1
+ import { defineNuxtPlugin } from "#imports";
2
+ import { useUserSession } from "../composables/useUserSession.js";
3
+ export default defineNuxtPlugin(async (nuxtApp) => {
4
+ const { fetchSession } = useUserSession();
5
+ const safeFetch = async () => {
6
+ try {
7
+ await fetchSession();
8
+ } catch {
9
+ }
10
+ };
11
+ if (!nuxtApp.payload.serverRendered) {
12
+ await safeFetch();
13
+ } else if (nuxtApp.payload.prerenderedAt || nuxtApp.payload.isCached) {
14
+ nuxtApp.hook("app:mounted", safeFetch);
15
+ }
16
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,24 @@
1
+ import { defineNuxtPlugin, useRequestEvent, useRequestHeaders, useState } from "#imports";
2
+ export default defineNuxtPlugin({
3
+ name: "auth:session-init",
4
+ enforce: "pre",
5
+ async setup() {
6
+ const session = useState("auth:session", () => null);
7
+ const user = useState("auth:user", () => null);
8
+ const authReady = useState("auth:ready", () => false);
9
+ const event = useRequestEvent();
10
+ if (event) {
11
+ try {
12
+ const headers = useRequestHeaders(["cookie"]);
13
+ const data = await $fetch("/api/auth/get-session", { headers });
14
+ if (data?.session && data?.user) {
15
+ const { token: _, ...safeSession } = data.session;
16
+ session.value = safeSession;
17
+ user.value = data.user;
18
+ }
19
+ } catch {
20
+ }
21
+ }
22
+ authReady.value = true;
23
+ }
24
+ });
@@ -0,0 +1,44 @@
1
+ import type { BetterAuthOptions } from 'better-auth';
2
+ import type { ClientOptions } from 'better-auth/client';
3
+ import type { CasingOption } from '../schema-generator.js';
4
+ import type { ServerAuthContext } from './types/augment.js';
5
+ export type { ServerAuthContext };
6
+ export interface ClientAuthContext {
7
+ siteUrl: string;
8
+ }
9
+ type ServerAuthConfig = Omit<BetterAuthOptions, 'database' | 'secret' | 'baseURL'>;
10
+ type ClientAuthConfig = Omit<ClientOptions, 'baseURL'> & {
11
+ baseURL?: string;
12
+ };
13
+ export type ServerAuthConfigFn = (ctx: ServerAuthContext) => ServerAuthConfig;
14
+ export type ClientAuthConfigFn = (ctx: ClientAuthContext) => ClientAuthConfig;
15
+ export interface BetterAuthModuleOptions {
16
+ /** Server config path relative to rootDir. Default: 'server/auth.config' */
17
+ serverConfig?: string;
18
+ /** Client config path relative to rootDir. Default: 'app/auth.config' */
19
+ clientConfig?: string;
20
+ redirects?: {
21
+ login?: string;
22
+ guest?: string;
23
+ };
24
+ /** Enable KV secondary storage for sessions. Requires hub.kv: true */
25
+ secondaryStorage?: boolean;
26
+ /** Schema generation options. Must match drizzleAdapter config. */
27
+ schema?: {
28
+ /** Plural table names: user → users. Default: false */
29
+ usePlural?: boolean;
30
+ /** Column/table name casing. Explicit value takes precedence over hub.db.casing. */
31
+ casing?: CasingOption;
32
+ };
33
+ }
34
+ export interface AuthRuntimeConfig {
35
+ redirects: {
36
+ login: string;
37
+ guest: string;
38
+ };
39
+ }
40
+ export interface AuthPrivateRuntimeConfig {
41
+ secondaryStorage: boolean;
42
+ }
43
+ export declare function defineServerAuth<T extends ServerAuthConfig>(config: (ctx: ServerAuthContext) => T): (ctx: ServerAuthContext) => T;
44
+ export declare function defineClientAuth(config: ClientAuthConfigFn): ClientAuthConfigFn;
@@ -0,0 +1,6 @@
1
+ export function defineServerAuth(config) {
2
+ return config;
3
+ }
4
+ export function defineClientAuth(config) {
5
+ return config;
6
+ }
@@ -0,0 +1,8 @@
1
+ import { z } from 'zod';
2
+ export declare const paginationQuerySchema: z.ZodObject<{
3
+ page: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
4
+ limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
5
+ search: z.ZodDefault<z.ZodString>;
6
+ }, z.core.$strip>;
7
+ export type PaginationQuery = z.infer<typeof paginationQuerySchema>;
8
+ export declare function sanitizeSearchPattern(search: string): string;
@@ -0,0 +1,11 @@
1
+ import { z } from "zod";
2
+ export const paginationQuerySchema = z.object({
3
+ page: z.coerce.number().int().min(1).default(1),
4
+ limit: z.coerce.number().int().min(1).max(100).default(20),
5
+ search: z.string().max(100).default("")
6
+ });
7
+ export function sanitizeSearchPattern(search) {
8
+ if (!search)
9
+ return "";
10
+ return `%${search.replace(/[%_\\]/g, "\\$&")}%`;
11
+ }
@@ -0,0 +1,14 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ accounts: never[];
3
+ total: number;
4
+ error: string;
5
+ page?: undefined;
6
+ limit?: undefined;
7
+ } | {
8
+ accounts: any;
9
+ total: any;
10
+ page: number;
11
+ limit: number;
12
+ error?: undefined;
13
+ }>>;
14
+ export default _default;
@@ -0,0 +1,28 @@
1
+ import { defineEventHandler, getQuery } from "h3";
2
+ import { paginationQuerySchema, sanitizeSearchPattern } from "./_schema.js";
3
+ export default defineEventHandler(async (event) => {
4
+ try {
5
+ const { db, schema } = await import("hub:db");
6
+ if (!schema.account)
7
+ return { accounts: [], total: 0, error: "Account table not found" };
8
+ const query = paginationQuerySchema.parse(getQuery(event));
9
+ const { page, limit, search } = query;
10
+ const offset = (page - 1) * limit;
11
+ const { count, like, desc } = await import("drizzle-orm");
12
+ let dbQuery = db.select().from(schema.account);
13
+ let countQuery = db.select({ count: count() }).from(schema.account);
14
+ if (search) {
15
+ const pattern = sanitizeSearchPattern(search);
16
+ dbQuery = dbQuery.where(like(schema.account.providerId, pattern));
17
+ countQuery = countQuery.where(like(schema.account.providerId, pattern));
18
+ }
19
+ const [accounts, totalResult] = await Promise.all([
20
+ dbQuery.orderBy(desc(schema.account.createdAt)).limit(limit).offset(offset),
21
+ countQuery
22
+ ]);
23
+ return { accounts, total: totalResult[0]?.count ?? 0, page, limit };
24
+ } catch (error) {
25
+ console.error("[DevTools] Fetch accounts failed:", error);
26
+ return { accounts: [], total: 0, error: "Failed to fetch accounts" };
27
+ }
28
+ });
@@ -0,0 +1,35 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
+ config: {
3
+ module: {
4
+ redirects: {
5
+ login?: string;
6
+ guest?: string;
7
+ };
8
+ secondaryStorage: boolean;
9
+ useDatabase: boolean;
10
+ };
11
+ server: {
12
+ baseURL: any;
13
+ basePath: any;
14
+ socialProviders: string[];
15
+ plugins: any;
16
+ trustedOrigins: any;
17
+ session: {
18
+ expiresIn: string;
19
+ updateAge: string;
20
+ cookieCache: any;
21
+ };
22
+ emailAndPassword: boolean;
23
+ rateLimit: any;
24
+ advanced: {
25
+ useSecureCookies: any;
26
+ disableCSRFCheck: any;
27
+ };
28
+ };
29
+ };
30
+ error?: undefined;
31
+ } | {
32
+ config: null;
33
+ error: string;
34
+ }>>;
35
+ export default _default;