@onmax/nuxt-better-auth 0.0.2 → 0.0.4
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/dist/module.json +1 -1
- package/dist/module.mjs +31 -15
- package/dist/runtime/app/composables/runWithSessionRefresh.d.ts +1 -0
- package/dist/runtime/app/composables/runWithSessionRefresh.js +12 -0
- package/dist/runtime/app/internal/auth-action-handles.d.ts +3 -0
- package/dist/runtime/app/internal/auth-action-handles.js +2 -2
- package/dist/runtime/app/internal/wrap-auth-method.d.ts +3 -0
- package/dist/runtime/app/internal/wrap-auth-method.js +7 -4
- package/dist/runtime/app/middleware/auth.global.js +3 -0
- package/dist/runtime/app/pages/__better-auth-devtools.vue +293 -333
- package/dist/runtime/composables.d.ts +1 -0
- package/dist/runtime/composables.js +1 -0
- package/dist/runtime/internal/auth-route-rules.d.ts +1 -0
- package/dist/runtime/internal/auth-route-rules.js +24 -0
- package/dist/runtime/server/middleware/route-access.js +2 -1
- package/dist/runtime/server/utils/session.d.ts +1 -0
- package/dist/runtime/server/utils/session.js +40 -1
- package/package.json +5 -3
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, statSync, writeFileSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { getLayerDirectories, updateTemplates, addServerImportsDir, addServerImports, addServerScanDir, addServerHandler, addImportsDir, addPlugin, addComponentsDir,
|
|
3
|
+
import { getLayerDirectories, updateTemplates, addServerImportsDir, addServerImports, addServerScanDir, addServerHandler, addImportsDir, addPlugin, addComponentsDir, extendPages, addTemplate, hasNuxtModule, addTypeTemplate, defineNuxtModule, createResolver } from '@nuxt/kit';
|
|
4
4
|
import { consola as consola$1 } from 'consola';
|
|
5
5
|
import { isAbsolute, join, relative, dirname } from 'pathe';
|
|
6
6
|
import { defu } from 'defu';
|
|
@@ -10,7 +10,7 @@ import { randomBytes } from 'node:crypto';
|
|
|
10
10
|
import { isCI, isTest } from 'std-env';
|
|
11
11
|
export { defineClientAuth, defineServerAuth } from '../dist/runtime/config.js';
|
|
12
12
|
|
|
13
|
-
const version = "0.0.
|
|
13
|
+
const version = "0.0.4";
|
|
14
14
|
|
|
15
15
|
const CONFIG_EXTENSIONS = [".ts", ".js"];
|
|
16
16
|
const CONFIG_EXTENSION_RE = /\.(?:ts|js)$/;
|
|
@@ -224,8 +224,6 @@ async function registerDevtools(input) {
|
|
|
224
224
|
const isProduction = process.env.NODE_ENV === "production" || !nuxt.options.dev;
|
|
225
225
|
if (isProduction || clientOnly)
|
|
226
226
|
return;
|
|
227
|
-
if (!hasNuxtModule("@nuxt/ui"))
|
|
228
|
-
await installModule("@nuxt/ui");
|
|
229
227
|
setupDevTools(nuxt);
|
|
230
228
|
addServerHandler({ route: "/api/_better-auth/config", method: "get", handler: resolve("./runtime/server/api/_better-auth/config.get") });
|
|
231
229
|
if (hasHubDb) {
|
|
@@ -238,7 +236,7 @@ async function registerDevtools(input) {
|
|
|
238
236
|
handlers.forEach((handler) => addServerHandler(handler));
|
|
239
237
|
}
|
|
240
238
|
extendPages((pages) => {
|
|
241
|
-
pages.push({ name: "better-auth-devtools", path: "/__better-auth-devtools", file: resolve("./runtime/app/pages/__better-auth-devtools.vue") });
|
|
239
|
+
pages.push({ name: "better-auth-devtools", path: "/__better-auth-devtools", file: resolve("./runtime/app/pages/__better-auth-devtools.vue"), meta: { layout: false } });
|
|
242
240
|
});
|
|
243
241
|
}
|
|
244
242
|
function registerRouteRulesMetaHook(nuxt) {
|
|
@@ -295,7 +293,15 @@ async function generateDrizzleSchema(authOptions, dialect, schemaOptions) {
|
|
|
295
293
|
}
|
|
296
294
|
return result.code;
|
|
297
295
|
}
|
|
298
|
-
|
|
296
|
+
function loadLocalEnv(rootDir) {
|
|
297
|
+
if (!rootDir)
|
|
298
|
+
return;
|
|
299
|
+
const envPath = join(rootDir, ".env.local");
|
|
300
|
+
if (!existsSync(envPath))
|
|
301
|
+
return;
|
|
302
|
+
process.loadEnvFile(envPath);
|
|
303
|
+
}
|
|
304
|
+
async function loadUserAuthConfig(configPath, throwOnError = false, alias, runtimeConfig = {}, rootDir) {
|
|
299
305
|
const { createJiti } = await import('jiti');
|
|
300
306
|
const { defineServerAuth: runtimeDefineServerAuth } = await import('../dist/runtime/config.js');
|
|
301
307
|
const jiti = createJiti(import.meta.url, { interopDefault: true, moduleCache: false, alias });
|
|
@@ -309,10 +315,11 @@ async function loadUserAuthConfig(configPath, throwOnError = false, alias) {
|
|
|
309
315
|
}
|
|
310
316
|
schemaGlobals.__nuxtBetterAuthDefineServerAuth._count++;
|
|
311
317
|
try {
|
|
318
|
+
loadLocalEnv(rootDir);
|
|
312
319
|
const mod = await jiti.import(configPath);
|
|
313
320
|
const configFn = mod.default;
|
|
314
321
|
if (typeof configFn === "function") {
|
|
315
|
-
return configFn({ runtimeConfig
|
|
322
|
+
return configFn({ runtimeConfig, db: null });
|
|
316
323
|
}
|
|
317
324
|
consola$1.warn("[@onmax/nuxt-better-auth] auth.config.ts does not export default. Expected: export default defineServerAuth(...)");
|
|
318
325
|
if (throwOnError) {
|
|
@@ -388,7 +395,7 @@ async function loadAuthOptions(context) {
|
|
|
388
395
|
const alias = Object.fromEntries(
|
|
389
396
|
Object.entries(context.nuxt.options.alias).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
|
|
390
397
|
);
|
|
391
|
-
const userConfig = await loadUserAuthConfig(configFile, isProduction, alias);
|
|
398
|
+
const userConfig = await loadUserAuthConfig(configFile, isProduction, alias, context.nuxt.options.runtimeConfig, context.nuxt.options.rootDir);
|
|
392
399
|
const extendedConfig = {};
|
|
393
400
|
await context.nuxt.callHook("better-auth:config:extend", extendedConfig);
|
|
394
401
|
const plugins = [...userConfig.plugins || [], ...extendedConfig.plugins || []];
|
|
@@ -730,11 +737,23 @@ export { db }`;
|
|
|
730
737
|
export const db = undefined`;
|
|
731
738
|
}
|
|
732
739
|
function buildSchemaExportCode(hasHubDb, hubDialect) {
|
|
733
|
-
if (!hasHubDb)
|
|
734
|
-
return
|
|
740
|
+
if (!hasHubDb) {
|
|
741
|
+
return `export const user = undefined
|
|
742
|
+
export const session = undefined
|
|
743
|
+
export const account = undefined
|
|
744
|
+
export const verification = undefined
|
|
745
|
+
export const schema = undefined
|
|
746
|
+
`;
|
|
747
|
+
}
|
|
735
748
|
return `export * from './schema.${hubDialect}.mjs'
|
|
736
|
-
import * as
|
|
737
|
-
|
|
749
|
+
import * as generatedSchema from './schema.${hubDialect}.mjs'
|
|
750
|
+
|
|
751
|
+
const getGeneratedTable = name => generatedSchema[name] ?? generatedSchema[\`\${name}s\`]
|
|
752
|
+
export const user = getGeneratedTable('user')
|
|
753
|
+
export const session = getGeneratedTable('session')
|
|
754
|
+
export const account = getGeneratedTable('account')
|
|
755
|
+
export const verification = getGeneratedTable('verification')
|
|
756
|
+
export const schema = { ...generatedSchema, user, session, account, verification }
|
|
738
757
|
`;
|
|
739
758
|
}
|
|
740
759
|
function buildAuthRouteRulesCode(authRouteRules) {
|
|
@@ -1176,7 +1195,6 @@ declare module 'nitropack' {
|
|
|
1176
1195
|
interface NitroRouteConfig {
|
|
1177
1196
|
auth?: import('${runtimeTypesPath}').AuthMeta
|
|
1178
1197
|
}
|
|
1179
|
-
interface InternalApi extends _GeneratedAuthInternalApi {}
|
|
1180
1198
|
}
|
|
1181
1199
|
declare module 'nitropack/types' {
|
|
1182
1200
|
interface NitroRouteRules {
|
|
@@ -1185,7 +1203,6 @@ declare module 'nitropack/types' {
|
|
|
1185
1203
|
interface NitroRouteConfig {
|
|
1186
1204
|
auth?: import('${runtimeTypesPath}').AuthMeta
|
|
1187
1205
|
}
|
|
1188
|
-
interface InternalApi extends _GeneratedAuthInternalApi {}
|
|
1189
1206
|
}
|
|
1190
1207
|
declare module 'nitro/types' {
|
|
1191
1208
|
interface NitroRouteRules {
|
|
@@ -1194,7 +1211,6 @@ declare module 'nitro/types' {
|
|
|
1194
1211
|
interface NitroRouteConfig {
|
|
1195
1212
|
auth?: import('${runtimeTypesPath}').AuthMeta
|
|
1196
1213
|
}
|
|
1197
|
-
interface InternalApi extends _GeneratedAuthInternalApi {}
|
|
1198
1214
|
}
|
|
1199
1215
|
export {}
|
|
1200
1216
|
`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runWithSessionRefresh<TResult>(runner: () => Promise<TResult>): Promise<TResult>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { isAuthActionErrorResult } from "../internal/auth-action-handles.js";
|
|
2
|
+
import { refreshSessionAfterAuthAction } from "../internal/wrap-auth-method.js";
|
|
3
|
+
import { useUserSession } from "./useUserSession.js";
|
|
4
|
+
export async function runWithSessionRefresh(runner) {
|
|
5
|
+
if (typeof runner !== "function")
|
|
6
|
+
throw new TypeError("runWithSessionRefresh(runner) requires an async function");
|
|
7
|
+
const auth = useUserSession();
|
|
8
|
+
const result = await runner();
|
|
9
|
+
if (!isAuthActionErrorResult(result))
|
|
10
|
+
await refreshSessionAfterAuthAction(auth.fetchSession, auth.loggedIn, auth.waitForSession);
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
@@ -14,5 +14,8 @@ export type ActionHandleFor<T> = T extends (...args: infer A) => Promise<infer R
|
|
|
14
14
|
export type ActionHandleMap<T> = {
|
|
15
15
|
[K in keyof T]: ActionHandleFor<T[K]>;
|
|
16
16
|
};
|
|
17
|
+
export declare function isAuthActionErrorResult(value: unknown): value is {
|
|
18
|
+
error: unknown;
|
|
19
|
+
};
|
|
17
20
|
export declare function createActionHandle<TArgs extends unknown[], TResult>(getMethod: () => (...args: TArgs) => Promise<TResult>, options?: CreateActionHandleOptions): UserAuthActionHandle<TArgs, TResult>;
|
|
18
21
|
export declare function createActionHandles<T extends object>(getTarget: () => T, targetName: string): ActionHandleMap<T>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ref } from "#imports";
|
|
2
2
|
import { normalizeAuthActionError } from "./auth-action-error.js";
|
|
3
3
|
import { isRecord } from "./utils.js";
|
|
4
|
-
function
|
|
4
|
+
export function isAuthActionErrorResult(value) {
|
|
5
5
|
if (!isRecord(value))
|
|
6
6
|
return false;
|
|
7
7
|
if (!("error" in value))
|
|
@@ -36,7 +36,7 @@ export function createActionHandle(getMethod, options = {}) {
|
|
|
36
36
|
error.value = null;
|
|
37
37
|
try {
|
|
38
38
|
const result = await getMethod()(...args);
|
|
39
|
-
if (
|
|
39
|
+
if (isAuthActionErrorResult(result)) {
|
|
40
40
|
const normalizedError = normalizeAuthActionError(result.error);
|
|
41
41
|
if (callId === latestCallId) {
|
|
42
42
|
status.value = "error";
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { ComputedRef } from 'vue';
|
|
2
|
+
export declare function refreshSessionAfterAuthAction(fetchSession: (options?: {
|
|
3
|
+
force?: boolean;
|
|
4
|
+
}) => Promise<void>, loggedIn: ComputedRef<boolean>, waitForSession: () => Promise<void>): Promise<void>;
|
|
2
5
|
export declare function wrapOnSuccess(fetchSession: (options?: {
|
|
3
6
|
force?: boolean;
|
|
4
7
|
}) => Promise<void>, loggedIn: ComputedRef<boolean>, waitForSession: () => Promise<void>, cb: (ctx: unknown) => void | Promise<void>): (ctx: unknown) => Promise<void>;
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { nextTick } from "#imports";
|
|
2
2
|
import { isRecord } from "./utils.js";
|
|
3
|
+
export async function refreshSessionAfterAuthAction(fetchSession, loggedIn, waitForSession) {
|
|
4
|
+
await fetchSession({ force: true });
|
|
5
|
+
if (!loggedIn.value)
|
|
6
|
+
await waitForSession();
|
|
7
|
+
await nextTick();
|
|
8
|
+
}
|
|
3
9
|
export function wrapOnSuccess(fetchSession, loggedIn, waitForSession, cb) {
|
|
4
10
|
return async (ctx) => {
|
|
5
|
-
await fetchSession
|
|
6
|
-
if (!loggedIn.value)
|
|
7
|
-
await waitForSession();
|
|
8
|
-
await nextTick();
|
|
11
|
+
await refreshSessionAfterAuthAction(fetchSession, loggedIn, waitForSession);
|
|
9
12
|
await cb(ctx);
|
|
10
13
|
};
|
|
11
14
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { defu } from "defu";
|
|
2
2
|
import { createRouter, toRouteMatcher } from "radix3";
|
|
3
3
|
import { createError, defineNuxtRouteMiddleware, getRouteRules, navigateTo, useNuxtApp, useRequestHeaders, useRuntimeConfig } from "#imports";
|
|
4
|
+
import { shouldSkipAuthRouteRules } from "../../internal/auth-route-rules.js";
|
|
4
5
|
import { matchesUser } from "../../utils/match-user.js";
|
|
5
6
|
import { useUserSession } from "../composables/useUserSession.js";
|
|
6
7
|
let authRouteRulesPromise = null;
|
|
7
8
|
let routeRulesMatcherPromise = null;
|
|
8
9
|
export default defineNuxtRouteMiddleware(async (to) => {
|
|
10
|
+
if (shouldSkipAuthRouteRules(to.path))
|
|
11
|
+
return;
|
|
9
12
|
const nuxtApp = useNuxtApp();
|
|
10
13
|
if (to.meta.auth === void 0) {
|
|
11
14
|
const routeRulesMatcher = await getRouteRulesMatcher();
|
|
@@ -1,420 +1,380 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { useFetch, useRuntimeConfig } from "#imports";
|
|
2
3
|
import { useDevtoolsClient } from "@nuxt/devtools-kit/iframe-client";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
const
|
|
4
|
+
import { computed, ref, watch, watchEffect } from "vue";
|
|
5
|
+
const pageSize = 20;
|
|
6
|
+
const tabLabels = {
|
|
7
|
+
sessions: "Sessions",
|
|
8
|
+
users: "Users",
|
|
9
|
+
accounts: "Accounts",
|
|
10
|
+
config: "Config"
|
|
11
|
+
};
|
|
6
12
|
const devtoolsClient = useDevtoolsClient();
|
|
7
13
|
const runtimeConfig = useRuntimeConfig();
|
|
8
14
|
const hasDb = computed(() => runtimeConfig.public.auth?.useDatabase ?? false);
|
|
9
15
|
const isDark = computed(() => devtoolsClient.value?.host?.app?.colorMode?.value === "dark");
|
|
10
16
|
watchEffect(() => {
|
|
11
|
-
if (import.meta.client)
|
|
17
|
+
if (import.meta.client)
|
|
12
18
|
document.documentElement.classList.toggle("dark", isDark.value);
|
|
13
|
-
|
|
19
|
+
});
|
|
20
|
+
const activeTab = ref(hasDb.value ? "sessions" : "config");
|
|
21
|
+
const tabs = computed(() => hasDb.value ? ["sessions", "users", "accounts", "config"] : ["config"]);
|
|
22
|
+
watch(hasDb, (enabled) => {
|
|
23
|
+
if (!enabled)
|
|
24
|
+
activeTab.value = "config";
|
|
14
25
|
});
|
|
15
26
|
const sessionsPage = ref(1);
|
|
16
27
|
const usersPage = ref(1);
|
|
17
28
|
const accountsPage = ref(1);
|
|
18
29
|
const deleteConfirm = ref(null);
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const usersSearch = refDebounced(usersSearchRaw, 300);
|
|
24
|
-
const accountsSearch = refDebounced(accountsSearchRaw, 300);
|
|
30
|
+
const notice = ref("");
|
|
31
|
+
const sessionsSearch = ref("");
|
|
32
|
+
const usersSearch = ref("");
|
|
33
|
+
const accountsSearch = ref("");
|
|
25
34
|
watch(sessionsSearch, () => sessionsPage.value = 1);
|
|
26
35
|
watch(usersSearch, () => usersPage.value = 1);
|
|
27
36
|
watch(accountsSearch, () => accountsPage.value = 1);
|
|
28
|
-
const sessionsQuery = computed(() => ({ page: sessionsPage.value, limit:
|
|
29
|
-
const usersQuery = computed(() => ({ page: usersPage.value, limit:
|
|
30
|
-
const accountsQuery = computed(() => ({ page: accountsPage.value, limit:
|
|
37
|
+
const sessionsQuery = computed(() => ({ page: sessionsPage.value, limit: pageSize, search: sessionsSearch.value }));
|
|
38
|
+
const usersQuery = computed(() => ({ page: usersPage.value, limit: pageSize, search: usersSearch.value }));
|
|
39
|
+
const accountsQuery = computed(() => ({ page: accountsPage.value, limit: pageSize, search: accountsSearch.value }));
|
|
31
40
|
const { data: sessionsData, refresh: refreshSessions } = await useFetch("/api/_better-auth/sessions", { query: sessionsQuery, immediate: hasDb.value });
|
|
32
41
|
const { data: usersData, refresh: refreshUsers } = await useFetch("/api/_better-auth/users", { query: usersQuery, immediate: hasDb.value });
|
|
33
42
|
const { data: accountsData, refresh: refreshAccounts } = await useFetch("/api/_better-auth/accounts", { query: accountsQuery, immediate: hasDb.value });
|
|
34
43
|
const { data: configData } = await useFetch("/api/_better-auth/config");
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
const sessions = computed(() => sessionsData.value?.sessions ?? []);
|
|
45
|
+
const users = computed(() => usersData.value?.users ?? []);
|
|
46
|
+
const accounts = computed(() => accountsData.value?.accounts ?? []);
|
|
47
|
+
let noticeTimer;
|
|
48
|
+
function showNotice(message) {
|
|
49
|
+
notice.value = message;
|
|
50
|
+
if (noticeTimer)
|
|
51
|
+
clearTimeout(noticeTimer);
|
|
52
|
+
noticeTimer = setTimeout(() => notice.value = "", 2200);
|
|
53
|
+
}
|
|
44
54
|
function isExpired(date) {
|
|
45
|
-
|
|
46
|
-
return false;
|
|
47
|
-
return new Date(date) < /* @__PURE__ */ new Date();
|
|
55
|
+
return date ? new Date(date) < /* @__PURE__ */ new Date() : false;
|
|
48
56
|
}
|
|
49
57
|
function formatDate(date) {
|
|
50
|
-
|
|
51
|
-
return "-";
|
|
52
|
-
return new Date(date).toLocaleString();
|
|
58
|
+
return date ? new Date(date).toLocaleString() : "-";
|
|
53
59
|
}
|
|
54
|
-
function truncate(str, len =
|
|
60
|
+
function truncate(str, len = 14) {
|
|
55
61
|
if (!str)
|
|
56
62
|
return "-";
|
|
57
63
|
if (str.length <= len)
|
|
58
64
|
return str;
|
|
59
|
-
const half = Math.floor((len -
|
|
60
|
-
return `${str.slice(0, half)}
|
|
65
|
+
const half = Math.floor((len - 3) / 2);
|
|
66
|
+
return `${str.slice(0, half)}...${str.slice(-half)}`;
|
|
67
|
+
}
|
|
68
|
+
function totalPages(total) {
|
|
69
|
+
return Math.max(1, Math.ceil((total ?? 0) / pageSize));
|
|
70
|
+
}
|
|
71
|
+
function pageBack(page) {
|
|
72
|
+
page.value = Math.max(1, page.value - 1);
|
|
73
|
+
}
|
|
74
|
+
function pageNext(page, total) {
|
|
75
|
+
page.value = Math.min(totalPages(total), page.value + 1);
|
|
61
76
|
}
|
|
62
77
|
async function copyToClipboard(text, label = "Value") {
|
|
63
78
|
try {
|
|
64
79
|
await navigator.clipboard.writeText(text);
|
|
65
|
-
|
|
80
|
+
showNotice(`${label} copied`);
|
|
66
81
|
} catch {
|
|
67
|
-
|
|
82
|
+
showNotice("Copy failed");
|
|
68
83
|
}
|
|
69
84
|
}
|
|
70
85
|
function generateConfigMarkdown() {
|
|
71
86
|
const config = configData.value?.config;
|
|
72
87
|
if (!config)
|
|
73
88
|
return "";
|
|
74
|
-
const moduleJson = JSON.stringify(config.module, null, 2);
|
|
75
|
-
const serverJson = JSON.stringify(config.server, null, 2);
|
|
76
89
|
return `## Module Config (\`nuxt.config.ts\`)
|
|
77
90
|
|
|
78
91
|
\`\`\`json
|
|
79
|
-
${
|
|
92
|
+
${JSON.stringify(config.module ?? {}, null, 2)}
|
|
80
93
|
\`\`\`
|
|
81
94
|
|
|
82
95
|
## Server Config (\`server/auth.config.ts\`)
|
|
83
96
|
|
|
84
97
|
\`\`\`json
|
|
85
|
-
${
|
|
98
|
+
${JSON.stringify(config.server ?? {}, null, 2)}
|
|
86
99
|
\`\`\`
|
|
87
100
|
`;
|
|
88
101
|
}
|
|
102
|
+
function json(value) {
|
|
103
|
+
return JSON.stringify(value ?? {}, null, 2);
|
|
104
|
+
}
|
|
89
105
|
async function deleteSession(id) {
|
|
90
106
|
try {
|
|
91
|
-
await
|
|
92
|
-
|
|
107
|
+
const response = await fetch("/api/_better-auth/sessions", {
|
|
108
|
+
method: "DELETE",
|
|
109
|
+
headers: { "content-type": "application/json" },
|
|
110
|
+
body: JSON.stringify({ id })
|
|
111
|
+
});
|
|
112
|
+
if (!response.ok)
|
|
113
|
+
throw new Error("Delete failed");
|
|
93
114
|
deleteConfirm.value = null;
|
|
94
|
-
|
|
115
|
+
showNotice("Session deleted");
|
|
116
|
+
await refreshSessions();
|
|
95
117
|
} catch {
|
|
96
|
-
|
|
118
|
+
showNotice("Failed to delete session");
|
|
97
119
|
}
|
|
98
120
|
}
|
|
99
|
-
|
|
100
|
-
|
|
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") }]];
|
|
121
|
+
async function deleteConfirmedSession() {
|
|
122
|
+
if (deleteConfirm.value)
|
|
123
|
+
await deleteSession(deleteConfirm.value);
|
|
173
124
|
}
|
|
174
125
|
</script>
|
|
175
126
|
|
|
176
127
|
<template>
|
|
177
|
-
<div class="
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0H15V15H30V30H15V45H0V30V15V0ZM45 30V15H30V0H45H60V15V30V45H45H30V30H45Z" class="fill-current" />
|
|
128
|
+
<div class="devtools-shell">
|
|
129
|
+
<header class="devtools-header">
|
|
130
|
+
<div class="brand">
|
|
131
|
+
<svg width="60" height="45" viewBox="0 0 60 45" fill="none" class="brand-mark" xmlns="http://www.w3.org/2000/svg">
|
|
132
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0H15V15H30V30H15V45H0V30V15V0ZM45 30V15H30V0H45H60V15V30V45H45H30V30H45Z" fill="currentColor" />
|
|
183
133
|
</svg>
|
|
184
|
-
<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>
|
|
134
|
+
<span>Better Auth DevTools</span>
|
|
191
135
|
</div>
|
|
136
|
+
<nav class="links">
|
|
137
|
+
<a href="https://www.better-auth.com/docs" target="_blank" rel="noreferrer">Docs</a>
|
|
138
|
+
<a href="https://github.com/onmax/nuxt-better-auth" target="_blank" rel="noreferrer">GitHub</a>
|
|
139
|
+
</nav>
|
|
192
140
|
</header>
|
|
193
141
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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) }]" />
|
|
142
|
+
<nav class="tabs" aria-label="Devtools sections">
|
|
143
|
+
<button
|
|
144
|
+
v-for="tab in tabs"
|
|
145
|
+
:key="tab"
|
|
146
|
+
type="button"
|
|
147
|
+
:class="{ active: activeTab === tab }"
|
|
148
|
+
@click="activeTab = tab"
|
|
149
|
+
>
|
|
150
|
+
{{ tabLabels[tab] }}
|
|
151
|
+
</button>
|
|
152
|
+
</nav>
|
|
210
153
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
154
|
+
<p v-if="notice" class="notice">
|
|
155
|
+
{{ notice }}
|
|
156
|
+
</p>
|
|
214
157
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
</
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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>
|
|
158
|
+
<main class="panel">
|
|
159
|
+
<section v-if="activeTab === 'sessions'" class="section">
|
|
160
|
+
<div class="toolbar">
|
|
161
|
+
<input v-model="sessionsSearch" type="search" placeholder="Search by user ID or IP...">
|
|
162
|
+
<div class="toolbar-meta">
|
|
163
|
+
<span>{{ sessionsData?.total ?? 0 }} sessions</span>
|
|
164
|
+
<button type="button" @click="() => refreshSessions()">
|
|
165
|
+
Refresh
|
|
166
|
+
</button>
|
|
240
167
|
</div>
|
|
168
|
+
</div>
|
|
241
169
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
</template>
|
|
252
|
-
</UTable>
|
|
170
|
+
<div v-if="deleteConfirm" class="confirm">
|
|
171
|
+
<span>Delete this session?</span>
|
|
172
|
+
<button type="button" @click="deleteConfirm = null">
|
|
173
|
+
Cancel
|
|
174
|
+
</button>
|
|
175
|
+
<button type="button" class="danger" @click="deleteConfirmedSession">
|
|
176
|
+
Delete
|
|
177
|
+
</button>
|
|
178
|
+
</div>
|
|
253
179
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
180
|
+
<p v-if="sessionsData?.error" class="error">
|
|
181
|
+
{{ sessionsData.error }}
|
|
182
|
+
</p>
|
|
183
|
+
<table v-else-if="sessions.length">
|
|
184
|
+
<thead>
|
|
185
|
+
<tr>
|
|
186
|
+
<th>ID</th>
|
|
187
|
+
<th>User</th>
|
|
188
|
+
<th>User Agent</th>
|
|
189
|
+
<th>Status</th>
|
|
190
|
+
<th>Created</th>
|
|
191
|
+
<th />
|
|
192
|
+
</tr>
|
|
193
|
+
</thead>
|
|
194
|
+
<tbody>
|
|
195
|
+
<tr v-for="session in sessions" :key="session.id">
|
|
196
|
+
<td><code>{{ truncate(session.id) }}</code></td>
|
|
197
|
+
<td>
|
|
198
|
+
<code>{{ truncate(session.userId) }}</code>
|
|
199
|
+
<small>{{ session.ipAddress || "No IP" }}</small>
|
|
200
|
+
</td>
|
|
201
|
+
<td>{{ truncate(session.userAgent, 32) }}</td>
|
|
202
|
+
<td>
|
|
203
|
+
<span class="badge" :class="isExpired(session.expiresAt) ? 'bad' : 'good'">
|
|
204
|
+
{{ isExpired(session.expiresAt) ? "Expired" : "Active" }}
|
|
205
|
+
</span>
|
|
206
|
+
</td>
|
|
207
|
+
<td>{{ formatDate(session.createdAt) }}</td>
|
|
208
|
+
<td class="actions">
|
|
209
|
+
<button type="button" @click="copyToClipboard(session.id, 'Session ID')">
|
|
210
|
+
Copy
|
|
211
|
+
</button>
|
|
212
|
+
<button type="button" class="danger" @click="deleteConfirm = session.id">
|
|
213
|
+
Delete
|
|
214
|
+
</button>
|
|
215
|
+
</td>
|
|
216
|
+
</tr>
|
|
217
|
+
</tbody>
|
|
218
|
+
</table>
|
|
219
|
+
<p v-else class="empty">
|
|
220
|
+
No sessions found
|
|
221
|
+
</p>
|
|
257
222
|
|
|
258
|
-
|
|
223
|
+
<div v-if="(sessionsData?.total ?? 0) > pageSize" class="pagination">
|
|
224
|
+
<button type="button" :disabled="sessionsPage === 1" @click="pageBack(sessionsPage)">
|
|
225
|
+
Previous
|
|
226
|
+
</button>
|
|
227
|
+
<span>Page {{ sessionsPage }} of {{ totalPages(sessionsData?.total) }}</span>
|
|
228
|
+
<button type="button" :disabled="sessionsPage >= totalPages(sessionsData?.total)" @click="pageNext(sessionsPage, sessionsData?.total)">
|
|
229
|
+
Next
|
|
230
|
+
</button>
|
|
259
231
|
</div>
|
|
260
|
-
</
|
|
232
|
+
</section>
|
|
261
233
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
<div class="
|
|
266
|
-
<
|
|
267
|
-
<
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
</div>
|
|
234
|
+
<section v-else-if="activeTab === 'users'" class="section">
|
|
235
|
+
<div class="toolbar">
|
|
236
|
+
<input v-model="usersSearch" type="search" placeholder="Search by name or email...">
|
|
237
|
+
<div class="toolbar-meta">
|
|
238
|
+
<span>{{ usersData?.total ?? 0 }} users</span>
|
|
239
|
+
<button type="button" @click="() => refreshUsers()">
|
|
240
|
+
Refresh
|
|
241
|
+
</button>
|
|
271
242
|
</div>
|
|
243
|
+
</div>
|
|
272
244
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
<
|
|
278
|
-
<
|
|
279
|
-
<
|
|
280
|
-
|
|
281
|
-
</
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
245
|
+
<p v-if="usersData?.error" class="error">
|
|
246
|
+
{{ usersData.error }}
|
|
247
|
+
</p>
|
|
248
|
+
<table v-else-if="users.length">
|
|
249
|
+
<thead>
|
|
250
|
+
<tr>
|
|
251
|
+
<th>ID</th>
|
|
252
|
+
<th>User</th>
|
|
253
|
+
<th>Verified</th>
|
|
254
|
+
<th>Created</th>
|
|
255
|
+
<th />
|
|
256
|
+
</tr>
|
|
257
|
+
</thead>
|
|
258
|
+
<tbody>
|
|
259
|
+
<tr v-for="user in users" :key="user.id">
|
|
260
|
+
<td><code>{{ truncate(user.id) }}</code></td>
|
|
261
|
+
<td>
|
|
262
|
+
<strong>{{ user.name || "Unnamed" }}</strong>
|
|
263
|
+
<small>{{ user.email }}</small>
|
|
264
|
+
</td>
|
|
265
|
+
<td>
|
|
266
|
+
<span class="badge" :class="user.emailVerified ? 'good' : ''">
|
|
267
|
+
{{ user.emailVerified ? "Yes" : "No" }}
|
|
268
|
+
</span>
|
|
269
|
+
</td>
|
|
270
|
+
<td>{{ formatDate(user.createdAt) }}</td>
|
|
271
|
+
<td class="actions">
|
|
272
|
+
<button type="button" @click="copyToClipboard(user.id, 'User ID')">
|
|
273
|
+
Copy ID
|
|
274
|
+
</button>
|
|
275
|
+
<button type="button" @click="copyToClipboard(user.email, 'Email')">
|
|
276
|
+
Copy Email
|
|
277
|
+
</button>
|
|
278
|
+
</td>
|
|
279
|
+
</tr>
|
|
280
|
+
</tbody>
|
|
281
|
+
</table>
|
|
282
|
+
<p v-else class="empty">
|
|
283
|
+
No users found
|
|
284
|
+
</p>
|
|
288
285
|
|
|
289
|
-
|
|
286
|
+
<div v-if="(usersData?.total ?? 0) > pageSize" class="pagination">
|
|
287
|
+
<button type="button" :disabled="usersPage === 1" @click="pageBack(usersPage)">
|
|
288
|
+
Previous
|
|
289
|
+
</button>
|
|
290
|
+
<span>Page {{ usersPage }} of {{ totalPages(usersData?.total) }}</span>
|
|
291
|
+
<button type="button" :disabled="usersPage >= totalPages(usersData?.total)" @click="pageNext(usersPage, usersData?.total)">
|
|
292
|
+
Next
|
|
293
|
+
</button>
|
|
290
294
|
</div>
|
|
291
|
-
</
|
|
295
|
+
</section>
|
|
292
296
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
<div class="
|
|
297
|
-
<
|
|
298
|
-
|
|
299
|
-
|
|
297
|
+
<section v-else-if="activeTab === 'accounts'" class="section">
|
|
298
|
+
<div class="toolbar">
|
|
299
|
+
<input v-model="accountsSearch" type="search" placeholder="Search by provider...">
|
|
300
|
+
<div class="toolbar-meta">
|
|
301
|
+
<span>{{ accountsData?.total ?? 0 }} accounts</span>
|
|
302
|
+
<button type="button" @click="() => refreshAccounts()">
|
|
303
|
+
Refresh
|
|
304
|
+
</button>
|
|
300
305
|
</div>
|
|
306
|
+
</div>
|
|
301
307
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
<
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
<
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
</
|
|
320
|
-
<
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
<
|
|
325
|
-
|
|
326
|
-
</
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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>
|
|
308
|
+
<p v-if="accountsData?.error" class="error">
|
|
309
|
+
{{ accountsData.error }}
|
|
310
|
+
</p>
|
|
311
|
+
<table v-else-if="accounts.length">
|
|
312
|
+
<thead>
|
|
313
|
+
<tr>
|
|
314
|
+
<th>Provider</th>
|
|
315
|
+
<th>Account</th>
|
|
316
|
+
<th>User ID</th>
|
|
317
|
+
<th>Created</th>
|
|
318
|
+
<th />
|
|
319
|
+
</tr>
|
|
320
|
+
</thead>
|
|
321
|
+
<tbody>
|
|
322
|
+
<tr v-for="account in accounts" :key="account.id">
|
|
323
|
+
<td class="capitalize">
|
|
324
|
+
{{ account.providerId }}
|
|
325
|
+
</td>
|
|
326
|
+
<td><code>{{ truncate(account.accountId, 18) }}</code></td>
|
|
327
|
+
<td><code>{{ truncate(account.userId) }}</code></td>
|
|
328
|
+
<td>{{ formatDate(account.createdAt) }}</td>
|
|
329
|
+
<td class="actions">
|
|
330
|
+
<button type="button" @click="copyToClipboard(account.id, 'Account ID')">
|
|
331
|
+
Copy
|
|
332
|
+
</button>
|
|
333
|
+
</td>
|
|
334
|
+
</tr>
|
|
335
|
+
</tbody>
|
|
336
|
+
</table>
|
|
337
|
+
<p v-else class="empty">
|
|
338
|
+
No accounts found
|
|
339
|
+
</p>
|
|
351
340
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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">DB</span><UBadge :color="configData.config.module?.databaseProvider === 'none' ? 'neutral' : 'success'" variant="subtle" size="sm">
|
|
378
|
-
{{ configData.config.module?.databaseProvider === "nuxthub" ? "Hub" : "Off" }}
|
|
379
|
-
</UBadge>
|
|
380
|
-
</div>
|
|
381
|
-
<div class="config-row">
|
|
382
|
-
<span class="config-label">KV</span><UBadge :color="configData.config.module?.hubSecondaryStorage ? 'success' : 'neutral'" variant="subtle" size="sm">
|
|
383
|
-
{{ configData.config.module?.hubSecondaryStorage ? configData.config.module.hubSecondaryStorage === "custom" ? "Custom" : "On" : "Off" }}
|
|
384
|
-
</UBadge>
|
|
385
|
-
</div>
|
|
386
|
-
</div>
|
|
387
|
-
<div class="config-section">
|
|
388
|
-
<div class="config-header">
|
|
389
|
-
<UIcon name="i-lucide-puzzle" class="size-4" /><span>Plugins</span>
|
|
390
|
-
</div>
|
|
391
|
-
<div class="flex flex-wrap gap-1">
|
|
392
|
-
<UBadge v-for="plugin in configData.config.server.plugins" :key="plugin" variant="subtle" color="neutral" size="sm">
|
|
393
|
-
{{ plugin }}
|
|
394
|
-
</UBadge>
|
|
395
|
-
<span v-if="!configData.config.server.plugins?.length" class="text-muted-foreground text-sm">None</span>
|
|
396
|
-
</div>
|
|
397
|
-
</div>
|
|
398
|
-
</div>
|
|
341
|
+
<div v-if="(accountsData?.total ?? 0) > pageSize" class="pagination">
|
|
342
|
+
<button type="button" :disabled="accountsPage === 1" @click="pageBack(accountsPage)">
|
|
343
|
+
Previous
|
|
344
|
+
</button>
|
|
345
|
+
<span>Page {{ accountsPage }} of {{ totalPages(accountsData?.total) }}</span>
|
|
346
|
+
<button type="button" :disabled="accountsPage >= totalPages(accountsData?.total)" @click="pageNext(accountsPage, accountsData?.total)">
|
|
347
|
+
Next
|
|
348
|
+
</button>
|
|
349
|
+
</div>
|
|
350
|
+
</section>
|
|
399
351
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
<UBadge v-for="origin in configData.config.server.trustedOrigins" :key="origin" variant="subtle" color="neutral" size="sm" class="font-mono">
|
|
407
|
-
{{ origin }}
|
|
408
|
-
</UBadge>
|
|
409
|
-
</div>
|
|
410
|
-
</div>
|
|
411
|
-
</template>
|
|
352
|
+
<section v-else class="section config-grid">
|
|
353
|
+
<div class="toolbar">
|
|
354
|
+
<span />
|
|
355
|
+
<button type="button" @click="copyToClipboard(generateConfigMarkdown(), 'Config')">
|
|
356
|
+
Copy config
|
|
357
|
+
</button>
|
|
412
358
|
</div>
|
|
413
|
-
|
|
414
|
-
|
|
359
|
+
|
|
360
|
+
<p v-if="configData?.error" class="error">
|
|
361
|
+
{{ configData.error }}
|
|
362
|
+
</p>
|
|
363
|
+
<template v-else>
|
|
364
|
+
<article>
|
|
365
|
+
<h2>Module Config</h2>
|
|
366
|
+
<pre>{{ json(configData?.config?.module) }}</pre>
|
|
367
|
+
</article>
|
|
368
|
+
<article>
|
|
369
|
+
<h2>Server Config</h2>
|
|
370
|
+
<pre>{{ json(configData?.config?.server) }}</pre>
|
|
371
|
+
</article>
|
|
372
|
+
</template>
|
|
373
|
+
</section>
|
|
374
|
+
</main>
|
|
415
375
|
</div>
|
|
416
376
|
</template>
|
|
417
377
|
|
|
418
378
|
<style>
|
|
419
|
-
:root{--
|
|
379
|
+
:root{--ba-bg:#fff;--ba-fg:#1f1f1f;--ba-muted:#707070;--ba-border:#e5e5e5;--ba-soft:#f7f7f7;--ba-hover:#efefef;--ba-good:#047857;--ba-bad:#b91c1c}.dark{--ba-bg:#111;--ba-fg:#f4f4f5;--ba-muted:#a1a1aa;--ba-border:#2d2d2d;--ba-soft:#191919;--ba-hover:#242424;--ba-good:#34d399;--ba-bad:#f87171}*{box-sizing:border-box}body{margin:0}button,input{font:inherit}.devtools-shell{background:var(--ba-bg);color:var(--ba-fg);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:13px;min-height:100vh}.devtools-header{border-bottom:1px solid var(--ba-border);gap:16px;justify-content:space-between;min-height:48px;padding:0 16px}.actions,.brand,.confirm,.devtools-header,.links,.pagination,.toolbar,.toolbar-meta{align-items:center;display:flex}.brand{font-weight:600;gap:10px}.brand-mark{height:auto;width:24px}.links a{color:var(--ba-muted);padding:8px 10px;text-decoration:none}.links a:hover{color:var(--ba-fg)}.tabs{border-bottom:1px solid var(--ba-border);display:flex;gap:4px;padding:8px 12px 0}.actions button,.confirm button,.pagination button,.tabs button,.toolbar button{background:transparent;border:1px solid var(--ba-border);color:var(--ba-fg);cursor:pointer;min-height:30px;padding:5px 10px}.tabs button{border-bottom:0}.tabs button.active,button:hover:not(:disabled){background:var(--ba-hover)}button:disabled{color:var(--ba-muted);cursor:default}button.danger{color:var(--ba-bad)}.panel{padding:16px}.section{display:grid;gap:14px}.toolbar{gap:12px;justify-content:space-between}.actions,.confirm,.pagination,.toolbar-meta{gap:8px}input{background:var(--ba-bg);border:1px solid var(--ba-border);color:var(--ba-fg);padding:7px 10px;width:min(340px,100%)}table{border:1px solid var(--ba-border);border-collapse:collapse;width:100%}td,th{border-bottom:1px solid var(--ba-border);padding:9px 10px;text-align:left;vertical-align:top}th{background:var(--ba-soft);font-weight:500}td small,th{color:var(--ba-muted)}td small{display:block;margin-top:3px}code,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,monospace}.badge{background:var(--ba-soft);border:1px solid var(--ba-border);color:var(--ba-muted);display:inline-flex;padding:2px 8px}.badge.good{color:var(--ba-good)}.badge.bad,.error{color:var(--ba-bad)}.confirm,.empty,.notice{background:var(--ba-soft);border:1px solid var(--ba-border);padding:10px 12px}.empty,.notice{color:var(--ba-muted)}.config-grid{grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}.config-grid .error,.config-grid .toolbar{grid-column:1/-1}article{background:var(--ba-soft);border:1px solid var(--ba-border)}h2{border-bottom:1px solid var(--ba-border);font-size:13px;padding:10px 12px}h2,pre{margin:0}pre{font-size:12px;line-height:1.5;overflow:auto;padding:12px}.capitalize{text-transform:capitalize}@media (max-width:720px){.devtools-header,.toolbar,.toolbar-meta{align-items:stretch;flex-direction:column}.actions,.tabs{flex-wrap:wrap}table{display:block;overflow-x:auto}}
|
|
420
380
|
</style>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { runWithSessionRefresh } from './app/composables/runWithSessionRefresh.js';
|
|
1
2
|
export { useAction } from './app/composables/useAction.js';
|
|
2
3
|
export { useAuthAsyncData } from './app/composables/useAuthAsyncData.js';
|
|
3
4
|
export { useAuthClient } from './app/composables/useAuthClient.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { runWithSessionRefresh } from "./app/composables/runWithSessionRefresh.js";
|
|
1
2
|
export { useAction } from "./app/composables/useAction.js";
|
|
2
3
|
export { useAuthAsyncData } from "./app/composables/useAuthAsyncData.js";
|
|
3
4
|
export { useAuthClient } from "./app/composables/useAuthClient.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function shouldSkipAuthRouteRules(path: string): boolean;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const internalRouteRuleAuthPaths = /* @__PURE__ */ new Set([
|
|
2
|
+
"/_ipx",
|
|
3
|
+
"/_nuxt",
|
|
4
|
+
"/__better-auth-devtools",
|
|
5
|
+
"/__nuxt_devtools__",
|
|
6
|
+
"/__nuxt_error",
|
|
7
|
+
"/__nuxt_vite_node__",
|
|
8
|
+
"/api/_better-auth",
|
|
9
|
+
"/api/_nuxt_icon",
|
|
10
|
+
"/api/auth"
|
|
11
|
+
]);
|
|
12
|
+
const internalRouteRuleAuthPrefixes = [
|
|
13
|
+
"/_ipx/",
|
|
14
|
+
"/_nuxt/",
|
|
15
|
+
"/__nuxt_devtools__/",
|
|
16
|
+
"/__nuxt_vite_node__/",
|
|
17
|
+
"/api/_better-auth/",
|
|
18
|
+
"/api/_nuxt_icon/",
|
|
19
|
+
"/api/auth/"
|
|
20
|
+
];
|
|
21
|
+
export function shouldSkipAuthRouteRules(path) {
|
|
22
|
+
const pathname = path.split(/[?#]/, 1)[0] || "/";
|
|
23
|
+
return internalRouteRuleAuthPaths.has(pathname) || internalRouteRuleAuthPrefixes.some((prefix) => pathname.startsWith(prefix));
|
|
24
|
+
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { createError, defineEventHandler, getRequestURL } from "h3";
|
|
2
2
|
import { getRouteRules } from "#imports";
|
|
3
|
+
import { shouldSkipAuthRouteRules } from "../../internal/auth-route-rules.js";
|
|
3
4
|
import { matchesUser } from "../../utils/match-user.js";
|
|
4
5
|
import { getUserSession, requireUserSession } from "../utils/session.js";
|
|
5
6
|
export default defineEventHandler(async (event) => {
|
|
6
7
|
const path = getRequestURL(event).pathname;
|
|
7
8
|
if (!path.startsWith("/api/"))
|
|
8
9
|
return;
|
|
9
|
-
if (path
|
|
10
|
+
if (shouldSkipAuthRouteRules(path))
|
|
10
11
|
return;
|
|
11
12
|
const rules = getRouteRules(event);
|
|
12
13
|
if (!rules.auth)
|
|
@@ -2,6 +2,7 @@ import type { H3Event } from 'h3';
|
|
|
2
2
|
import type { AppSession, AuthSession, RequireSessionOptions } from '#nuxt-better-auth';
|
|
3
3
|
export declare function getRequestSession(event: H3Event): Promise<AppSession | null>;
|
|
4
4
|
export declare function getUserSession(event: H3Event): Promise<AppSession | null>;
|
|
5
|
+
export declare function refreshSessionCookieCache(event: H3Event): Promise<AppSession | null>;
|
|
5
6
|
export declare function setSessionCookie(event: H3Event, token: string): Promise<void>;
|
|
6
7
|
export declare function createSession(event: H3Event, userId: string): Promise<AuthSession>;
|
|
7
8
|
export declare function requireUserSession(event: H3Event, options?: RequireSessionOptions): Promise<AppSession>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createError } from "h3";
|
|
1
|
+
import { createError, splitCookiesString } from "h3";
|
|
2
2
|
import { matchesUser } from "../../utils/match-user.js";
|
|
3
3
|
import { serverAuth } from "./auth.js";
|
|
4
4
|
const requestSessionLoadKey = Symbol.for("nuxt-better-auth.requestSessionLoad");
|
|
@@ -23,6 +23,14 @@ function loadSession(event) {
|
|
|
23
23
|
const auth = serverAuth(event);
|
|
24
24
|
return auth.api.getSession({ headers: getRequestHeaders(event) });
|
|
25
25
|
}
|
|
26
|
+
function loadFreshSession(event) {
|
|
27
|
+
const auth = serverAuth(event);
|
|
28
|
+
return auth.api.getSession({
|
|
29
|
+
headers: getRequestHeaders(event),
|
|
30
|
+
query: { disableCookieCache: true },
|
|
31
|
+
returnHeaders: true
|
|
32
|
+
});
|
|
33
|
+
}
|
|
26
34
|
function getServerAuthContext(event) {
|
|
27
35
|
const auth = serverAuth(event);
|
|
28
36
|
return auth.$context;
|
|
@@ -108,6 +116,18 @@ function appendCookieHeader(event, header) {
|
|
|
108
116
|
const responseHeaders = event.response?.headers;
|
|
109
117
|
responseHeaders?.append("set-cookie", header);
|
|
110
118
|
}
|
|
119
|
+
function getSetCookieHeaders(headers) {
|
|
120
|
+
const getSetCookie = headers.getSetCookie;
|
|
121
|
+
const cookies = getSetCookie?.call(headers);
|
|
122
|
+
if (cookies?.length)
|
|
123
|
+
return cookies.flatMap((cookie) => splitCookiesString(cookie));
|
|
124
|
+
const header = headers.get("set-cookie");
|
|
125
|
+
return header ? splitCookiesString(header) : [];
|
|
126
|
+
}
|
|
127
|
+
function appendSetCookieHeaders(event, headers) {
|
|
128
|
+
for (const header of getSetCookieHeaders(headers))
|
|
129
|
+
appendCookieHeader(event, header);
|
|
130
|
+
}
|
|
111
131
|
function parseRequestCookies(cookieHeader) {
|
|
112
132
|
const cookies = /* @__PURE__ */ new Map();
|
|
113
133
|
if (!cookieHeader)
|
|
@@ -192,6 +212,25 @@ export async function getUserSession(event) {
|
|
|
192
212
|
return inFlight;
|
|
193
213
|
return loadSession(event);
|
|
194
214
|
}
|
|
215
|
+
export async function refreshSessionCookieCache(event) {
|
|
216
|
+
const context = getRequestSessionContext(event);
|
|
217
|
+
const inFlight = context[requestSessionLoadKey];
|
|
218
|
+
if (inFlight)
|
|
219
|
+
await inFlight.catch(() => void 0);
|
|
220
|
+
delete context.requestSession;
|
|
221
|
+
const load = loadFreshSession(event).then(({ headers, response }) => {
|
|
222
|
+
appendSetCookieHeaders(event, headers);
|
|
223
|
+
context.requestSession = response;
|
|
224
|
+
return response;
|
|
225
|
+
});
|
|
226
|
+
context[requestSessionLoadKey] = load;
|
|
227
|
+
try {
|
|
228
|
+
return await load;
|
|
229
|
+
} finally {
|
|
230
|
+
if (context[requestSessionLoadKey] === load)
|
|
231
|
+
delete context[requestSessionLoadKey];
|
|
232
|
+
}
|
|
233
|
+
}
|
|
195
234
|
export async function setSessionCookie(event, token) {
|
|
196
235
|
const context = await getServerAuthContext(event);
|
|
197
236
|
const sessionCookie = await serializeSignedCookie(
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onmax/nuxt-better-auth",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.4",
|
|
5
5
|
"packageManager": "pnpm@11.1.0",
|
|
6
6
|
"description": "Nuxt module for Better Auth integration with NuxtHub, route protection, session management, and role-based access",
|
|
7
7
|
"author": "onmax",
|
|
@@ -44,11 +44,13 @@
|
|
|
44
44
|
],
|
|
45
45
|
"scripts": {
|
|
46
46
|
"prepack": "nuxt-module-build build",
|
|
47
|
+
"build": "pnpm build:docs",
|
|
47
48
|
"dev": "pnpm dev:prepare && nuxi dev playground",
|
|
48
49
|
"dev:build": "nuxi build playground",
|
|
49
50
|
"dev:prepare": "nuxt-module-build build --stub && nuxi prepare playground",
|
|
50
51
|
"dev:docs": "nuxi dev docs",
|
|
51
52
|
"build:docs": "nuxi build docs",
|
|
53
|
+
"deploy": "wrangler deploy",
|
|
52
54
|
"release": "bumpp --push --no-push-all",
|
|
53
55
|
"lint": "eslint .",
|
|
54
56
|
"lint:fix": "eslint . --fix",
|
|
@@ -70,7 +72,6 @@
|
|
|
70
72
|
"dependencies": {
|
|
71
73
|
"@better-auth/cli": "1.5.0-beta.13",
|
|
72
74
|
"@nuxt/kit": "^4.3.1",
|
|
73
|
-
"@nuxt/ui": "^4.5.0",
|
|
74
75
|
"defu": "^6.1.4",
|
|
75
76
|
"h3": "^1.15.11",
|
|
76
77
|
"jiti": "^2.6.1",
|
|
@@ -93,7 +94,7 @@
|
|
|
93
94
|
"@types/better-sqlite3": "^7.6.13",
|
|
94
95
|
"@types/node": "latest",
|
|
95
96
|
"better-auth": "1.5.5",
|
|
96
|
-
"better-call": "1.3.
|
|
97
|
+
"better-call": "1.3.5",
|
|
97
98
|
"better-sqlite3": "^12.6.2",
|
|
98
99
|
"bumpp": "^11.0.0",
|
|
99
100
|
"changelogen": "^0.6.2",
|
|
@@ -113,6 +114,7 @@
|
|
|
113
114
|
"vitest-package-exports": "^1.2.0",
|
|
114
115
|
"vue": "3.5.29",
|
|
115
116
|
"vue-tsc": "^3.2.7",
|
|
117
|
+
"wrangler": "4.72.0",
|
|
116
118
|
"yaml": "^2.8.3"
|
|
117
119
|
}
|
|
118
120
|
}
|