@onmax/nuxt-better-auth 0.0.2-alpha.9 → 0.0.3
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 +57 -17
- package/dist/module.d.mts +28 -1
- package/dist/module.json +3 -3
- package/dist/module.mjs +1258 -351
- package/dist/runtime/app/components/BetterAuthState.d.vue.ts +4 -4
- package/dist/runtime/app/components/BetterAuthState.vue +1 -0
- package/dist/runtime/app/components/BetterAuthState.vue.d.ts +4 -4
- package/dist/runtime/app/composables/runWithSessionRefresh.d.ts +1 -0
- package/dist/runtime/app/composables/runWithSessionRefresh.js +12 -0
- package/dist/runtime/app/composables/useAction.d.ts +2 -0
- package/dist/runtime/app/composables/useAction.js +6 -0
- package/dist/runtime/app/composables/useAuthAsyncData.d.ts +6 -0
- package/dist/runtime/app/composables/useAuthAsyncData.js +16 -0
- package/dist/runtime/app/composables/useAuthClient.d.ts +9 -0
- package/dist/runtime/app/composables/useAuthClient.js +34 -0
- package/dist/runtime/app/composables/useAuthClientAction.d.ts +3 -0
- package/dist/runtime/app/composables/useAuthClientAction.js +15 -0
- package/dist/runtime/app/composables/useAuthRequestFetch.d.ts +11 -0
- package/dist/runtime/app/composables/useAuthRequestFetch.js +4 -0
- package/dist/runtime/app/composables/useSignIn.d.ts +16 -0
- package/dist/runtime/app/composables/useSignIn.js +8 -0
- package/dist/runtime/app/composables/useSignUp.d.ts +5 -0
- package/dist/runtime/app/composables/useSignUp.js +8 -0
- package/dist/runtime/app/composables/useUserSession.d.ts +6 -5
- package/dist/runtime/app/composables/useUserSession.js +189 -100
- package/dist/runtime/app/composables/useUserSessionState.d.ts +3 -0
- package/dist/runtime/app/composables/useUserSessionState.js +4 -0
- package/dist/runtime/app/internal/auth-action-error.d.ts +3 -0
- package/dist/runtime/app/internal/auth-action-error.js +33 -0
- package/dist/runtime/app/internal/auth-action-handles.d.ts +21 -0
- package/dist/runtime/app/internal/auth-action-handles.js +105 -0
- package/dist/runtime/app/internal/redirect-helpers.d.ts +4 -0
- package/dist/runtime/app/internal/redirect-helpers.js +37 -0
- package/dist/runtime/app/internal/session-fetch.d.ts +12 -0
- package/dist/runtime/app/internal/session-fetch.js +56 -0
- package/dist/runtime/app/internal/utils.d.ts +1 -0
- package/dist/runtime/app/internal/utils.js +3 -0
- package/dist/runtime/app/internal/vue-safe-auth-proxy.d.ts +3 -0
- package/dist/runtime/app/internal/vue-safe-auth-proxy.js +68 -0
- package/dist/runtime/app/internal/wrap-auth-method.d.ts +18 -0
- package/dist/runtime/app/internal/wrap-auth-method.js +69 -0
- package/dist/runtime/app/middleware/auth.global.js +80 -15
- package/dist/runtime/app/pages/__better-auth-devtools.vue +293 -339
- package/dist/runtime/composables.d.ts +12 -0
- package/dist/runtime/composables.js +10 -0
- package/dist/runtime/config.d.ts +69 -15
- package/dist/runtime/config.js +9 -2
- 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/api/_better-auth/_schema.d.ts +1 -5
- package/dist/runtime/server/api/_better-auth/_schema.js +2 -1
- package/dist/runtime/server/api/_better-auth/accounts.get.d.ts +2 -2
- package/dist/runtime/server/api/_better-auth/accounts.get.js +3 -2
- package/dist/runtime/server/api/_better-auth/config.get.d.ts +9 -3
- package/dist/runtime/server/api/_better-auth/config.get.js +18 -6
- package/dist/runtime/server/api/_better-auth/sessions.delete.js +3 -2
- package/dist/runtime/server/api/_better-auth/sessions.get.d.ts +5 -3
- package/dist/runtime/server/api/_better-auth/sessions.get.js +3 -2
- package/dist/runtime/server/api/_better-auth/users.get.d.ts +2 -2
- package/dist/runtime/server/api/_better-auth/users.get.js +3 -2
- package/dist/runtime/server/api/auth/[...all].js +1 -1
- package/dist/runtime/server/middleware/route-access.js +4 -2
- package/dist/runtime/server/tsconfig.json +9 -1
- package/dist/runtime/server/utils/auth.d.ts +16 -8
- package/dist/runtime/server/utils/auth.js +229 -23
- package/dist/runtime/server/utils/custom-secondary-storage.d.ts +6 -0
- package/dist/runtime/server/utils/custom-secondary-storage.js +8 -0
- package/dist/runtime/server/utils/session.d.ts +7 -8
- package/dist/runtime/server/utils/session.js +256 -5
- package/dist/runtime/server/virtual-modules.d.ts +27 -0
- package/dist/runtime/types/augment.d.ts +18 -4
- package/dist/runtime/types.d.ts +12 -13
- package/dist/types.d.mts +1 -1
- package/package.json +53 -47
|
@@ -1,426 +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">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>
|
|
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>
|
|
405
351
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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>
|
|
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>
|
|
418
358
|
</div>
|
|
419
|
-
|
|
420
|
-
|
|
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>
|
|
421
375
|
</div>
|
|
422
376
|
</template>
|
|
423
377
|
|
|
424
378
|
<style>
|
|
425
|
-
: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}}
|
|
426
380
|
</style>
|