@onmax/nuxt-better-auth 0.0.1 → 0.0.2-alpha.1
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.
- package/README.md +17 -170
- package/dist/module.d.mts +20 -2
- package/dist/module.json +1 -1
- package/dist/module.mjs +363 -14
- package/dist/runtime/app/components/BetterAuthState.d.vue.ts +20 -0
- package/dist/runtime/app/components/BetterAuthState.vue +8 -0
- package/dist/runtime/app/components/BetterAuthState.vue.d.ts +20 -0
- package/dist/runtime/app/composables/useUserSession.d.ts +20 -0
- package/dist/runtime/app/composables/useUserSession.js +155 -0
- package/dist/runtime/app/middleware/auth.global.d.ts +13 -0
- package/dist/runtime/app/middleware/auth.global.js +31 -0
- package/dist/runtime/app/pages/__better-auth-devtools.d.vue.ts +3 -0
- package/dist/runtime/app/pages/__better-auth-devtools.vue +426 -0
- package/dist/runtime/app/pages/__better-auth-devtools.vue.d.ts +3 -0
- package/dist/runtime/app/plugins/session.client.d.ts +2 -0
- package/dist/runtime/app/plugins/session.client.js +16 -0
- package/dist/runtime/app/plugins/session.server.d.ts +2 -0
- package/dist/runtime/app/plugins/session.server.js +23 -0
- package/dist/runtime/config.d.ts +36 -0
- package/dist/runtime/config.js +6 -0
- package/dist/runtime/server/api/_better-auth/_schema.d.ts +8 -0
- package/dist/runtime/server/api/_better-auth/_schema.js +11 -0
- package/dist/runtime/server/api/_better-auth/accounts.get.d.ts +14 -0
- package/dist/runtime/server/api/_better-auth/accounts.get.js +28 -0
- package/dist/runtime/server/api/_better-auth/config.get.d.ts +35 -0
- package/dist/runtime/server/api/_better-auth/config.get.js +46 -0
- package/dist/runtime/server/api/_better-auth/sessions.delete.d.ts +4 -0
- package/dist/runtime/server/api/_better-auth/sessions.delete.js +22 -0
- package/dist/runtime/server/api/_better-auth/sessions.get.d.ts +14 -0
- package/dist/runtime/server/api/_better-auth/sessions.get.js +34 -0
- package/dist/runtime/server/api/_better-auth/users.get.d.ts +14 -0
- package/dist/runtime/server/api/_better-auth/users.get.js +34 -0
- package/dist/runtime/server/api/auth/[...all].d.ts +2 -0
- package/dist/runtime/server/api/auth/[...all].js +6 -0
- package/dist/runtime/server/middleware/route-access.d.ts +2 -0
- package/dist/runtime/server/middleware/route-access.js +28 -0
- package/dist/runtime/server/tsconfig.json +3 -0
- package/dist/runtime/server/utils/auth.d.ts +10 -0
- package/dist/runtime/server/utils/auth.js +32 -0
- package/dist/runtime/server/utils/session.d.ts +9 -0
- package/dist/runtime/server/utils/session.js +22 -0
- package/dist/runtime/types/augment.d.ts +42 -0
- package/dist/runtime/types/augment.js +0 -0
- package/dist/runtime/types.d.ts +23 -0
- package/dist/runtime/types.js +0 -0
- package/dist/runtime/utils/match-user.d.ts +2 -0
- package/dist/runtime/utils/match-user.js +13 -0
- package/dist/types.d.mts +8 -10
- package/package.json +35 -10
- package/dist/module.d.cts +0 -2
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AuthUser } from '#nuxt-better-auth';
|
|
2
|
+
export interface SignOutOptions {
|
|
3
|
+
onSuccess?: () => void | Promise<void>;
|
|
4
|
+
}
|
|
5
|
+
export declare function useUserSession(): {
|
|
6
|
+
client: any;
|
|
7
|
+
session: any;
|
|
8
|
+
user: any;
|
|
9
|
+
loggedIn: any;
|
|
10
|
+
ready: any;
|
|
11
|
+
signIn: any;
|
|
12
|
+
signUp: any;
|
|
13
|
+
signOut: (options?: SignOutOptions) => Promise<any>;
|
|
14
|
+
waitForSession: () => Promise<void>;
|
|
15
|
+
fetchSession: (options?: {
|
|
16
|
+
headers?: HeadersInit;
|
|
17
|
+
force?: boolean;
|
|
18
|
+
}) => Promise<void>;
|
|
19
|
+
updateUser: (updates: Partial<AuthUser>) => void;
|
|
20
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { createAppAuthClient } from "#auth/client";
|
|
2
|
+
import { computed, useRequestHeaders, useRequestURL, useRuntimeConfig, useState, watch } from "#imports";
|
|
3
|
+
import { consola } from "consola";
|
|
4
|
+
let _client = null;
|
|
5
|
+
function getClient(baseURL) {
|
|
6
|
+
if (!_client)
|
|
7
|
+
_client = createAppAuthClient(baseURL);
|
|
8
|
+
return _client;
|
|
9
|
+
}
|
|
10
|
+
export function useUserSession() {
|
|
11
|
+
const runtimeConfig = useRuntimeConfig();
|
|
12
|
+
const requestURL = useRequestURL();
|
|
13
|
+
const client = import.meta.client ? getClient(runtimeConfig.public.siteUrl || requestURL.origin) : null;
|
|
14
|
+
const session = useState("auth:session", () => null);
|
|
15
|
+
const user = useState("auth:user", () => null);
|
|
16
|
+
const authReady = useState("auth:ready", () => false);
|
|
17
|
+
const ready = computed(() => authReady.value);
|
|
18
|
+
const loggedIn = computed(() => Boolean(session.value && user.value));
|
|
19
|
+
function clearSession() {
|
|
20
|
+
session.value = null;
|
|
21
|
+
user.value = null;
|
|
22
|
+
}
|
|
23
|
+
function updateUser(updates) {
|
|
24
|
+
if (user.value)
|
|
25
|
+
user.value = { ...user.value, ...updates };
|
|
26
|
+
}
|
|
27
|
+
if (import.meta.client && client) {
|
|
28
|
+
const clientSession = client.useSession();
|
|
29
|
+
watch(
|
|
30
|
+
() => clientSession.value,
|
|
31
|
+
(newSession) => {
|
|
32
|
+
if (newSession?.data?.session && newSession?.data?.user) {
|
|
33
|
+
session.value = newSession.data.session;
|
|
34
|
+
user.value = newSession.data.user;
|
|
35
|
+
} else if (!newSession?.isPending) {
|
|
36
|
+
clearSession();
|
|
37
|
+
}
|
|
38
|
+
if (!authReady.value && !newSession?.isPending)
|
|
39
|
+
authReady.value = true;
|
|
40
|
+
},
|
|
41
|
+
{ immediate: true, deep: true }
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
function waitForSession() {
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
if (loggedIn.value) {
|
|
47
|
+
resolve();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const unwatch = watch(loggedIn, (isLoggedIn) => {
|
|
51
|
+
if (isLoggedIn) {
|
|
52
|
+
unwatch();
|
|
53
|
+
resolve();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
setTimeout(() => {
|
|
57
|
+
unwatch();
|
|
58
|
+
resolve();
|
|
59
|
+
}, 5e3);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function wrapOnSuccess(cb) {
|
|
63
|
+
return async (ctx) => {
|
|
64
|
+
await fetchSession({ force: true });
|
|
65
|
+
await cb(ctx);
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function wrapAuthMethod(method) {
|
|
69
|
+
return (async (...args) => {
|
|
70
|
+
const [data, options] = args;
|
|
71
|
+
if (data?.fetchOptions?.onSuccess) {
|
|
72
|
+
return method({ ...data, fetchOptions: { ...data.fetchOptions, onSuccess: wrapOnSuccess(data.fetchOptions.onSuccess) } }, options);
|
|
73
|
+
}
|
|
74
|
+
if (options?.onSuccess) {
|
|
75
|
+
return method(data, { ...options, onSuccess: wrapOnSuccess(options.onSuccess) });
|
|
76
|
+
}
|
|
77
|
+
return method(data, options);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const signIn = client?.signIn ? new Proxy(client.signIn, {
|
|
81
|
+
get(target, prop) {
|
|
82
|
+
const method = target[prop];
|
|
83
|
+
if (typeof method !== "function")
|
|
84
|
+
return method;
|
|
85
|
+
return wrapAuthMethod((...args) => target[prop](...args));
|
|
86
|
+
}
|
|
87
|
+
}) : new Proxy({}, {
|
|
88
|
+
get: (_, prop) => {
|
|
89
|
+
throw new Error(`signIn.${String(prop)}() can only be called on client-side`);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
const signUp = client?.signUp ? new Proxy(client.signUp, {
|
|
93
|
+
get(target, prop) {
|
|
94
|
+
const method = target[prop];
|
|
95
|
+
if (typeof method !== "function")
|
|
96
|
+
return method;
|
|
97
|
+
return wrapAuthMethod((...args) => target[prop](...args));
|
|
98
|
+
}
|
|
99
|
+
}) : new Proxy({}, {
|
|
100
|
+
get: (_, prop) => {
|
|
101
|
+
throw new Error(`signUp.${String(prop)}() can only be called on client-side`);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
async function fetchSession(options = {}) {
|
|
105
|
+
if (import.meta.server) {
|
|
106
|
+
if (!authReady.value)
|
|
107
|
+
authReady.value = true;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (client) {
|
|
111
|
+
try {
|
|
112
|
+
const headers = options.headers || useRequestHeaders(["cookie"]);
|
|
113
|
+
const fetchOptions = headers ? { headers } : void 0;
|
|
114
|
+
const query = options.force ? { disableCookieCache: true } : void 0;
|
|
115
|
+
const result = await client.getSession({ query }, fetchOptions);
|
|
116
|
+
const data = result.data;
|
|
117
|
+
if (data?.session && data?.user) {
|
|
118
|
+
session.value = data.session;
|
|
119
|
+
user.value = data.user;
|
|
120
|
+
} else {
|
|
121
|
+
clearSession();
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
clearSession();
|
|
125
|
+
if (import.meta.dev)
|
|
126
|
+
consola.error("Failed to fetch auth session:", error);
|
|
127
|
+
} finally {
|
|
128
|
+
if (!authReady.value)
|
|
129
|
+
authReady.value = true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function signOut(options) {
|
|
134
|
+
if (!client)
|
|
135
|
+
throw new Error("signOut can only be called on client-side");
|
|
136
|
+
const response = await client.signOut();
|
|
137
|
+
clearSession();
|
|
138
|
+
if (options?.onSuccess)
|
|
139
|
+
await options.onSuccess();
|
|
140
|
+
return response;
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
client,
|
|
144
|
+
session,
|
|
145
|
+
user,
|
|
146
|
+
loggedIn,
|
|
147
|
+
ready,
|
|
148
|
+
signIn,
|
|
149
|
+
signUp,
|
|
150
|
+
signOut,
|
|
151
|
+
waitForSession,
|
|
152
|
+
fetchSession,
|
|
153
|
+
updateUser
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AuthMeta } from '../../types.js';
|
|
2
|
+
declare module '#app' {
|
|
3
|
+
interface PageMeta {
|
|
4
|
+
auth?: AuthMeta;
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
declare module 'vue-router' {
|
|
8
|
+
interface RouteMeta {
|
|
9
|
+
auth?: AuthMeta;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
declare const _default: any;
|
|
13
|
+
export default _default;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createError, defineNuxtRouteMiddleware, getRouteRules, navigateTo, useRequestHeaders, useRuntimeConfig } from "#imports";
|
|
2
|
+
import { matchesUser } from "../../utils/match-user.js";
|
|
3
|
+
export default defineNuxtRouteMiddleware(async (to) => {
|
|
4
|
+
if (to.meta.auth === void 0) {
|
|
5
|
+
const rules = await getRouteRules({ path: to.path });
|
|
6
|
+
if (rules.auth !== void 0)
|
|
7
|
+
to.meta.auth = rules.auth;
|
|
8
|
+
}
|
|
9
|
+
const auth = to.meta.auth;
|
|
10
|
+
if (auth === void 0 || auth === false)
|
|
11
|
+
return;
|
|
12
|
+
const config = useRuntimeConfig().public.auth;
|
|
13
|
+
const { fetchSession, user, loggedIn, ready } = useUserSession();
|
|
14
|
+
if (!loggedIn.value && !ready.value) {
|
|
15
|
+
const headers = import.meta.server ? useRequestHeaders(["cookie"]) : void 0;
|
|
16
|
+
await fetchSession({ headers });
|
|
17
|
+
}
|
|
18
|
+
const mode = typeof auth === "string" ? auth : auth?.only ?? "user";
|
|
19
|
+
const redirectTo = typeof auth === "object" ? auth.redirectTo : void 0;
|
|
20
|
+
if (mode === "guest") {
|
|
21
|
+
if (loggedIn.value)
|
|
22
|
+
return navigateTo(redirectTo ?? config?.redirects?.guest ?? "/");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (!loggedIn.value)
|
|
26
|
+
return navigateTo(redirectTo ?? config?.redirects?.login ?? "/login");
|
|
27
|
+
if (typeof auth === "object" && auth.user) {
|
|
28
|
+
if (!user.value || !matchesUser(user.value, auth.user))
|
|
29
|
+
throw createError({ statusCode: 403, statusMessage: "Access denied" });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
@@ -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,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;
|