@frigear-nu/account 0.0.10 → 0.1.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 +7 -4
- package/dist/module.d.mts +5 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +32 -26
- package/dist/runtime/app/components/Account/AccountApiTokenSlideover.d.vue.ts +11 -0
- package/dist/runtime/app/components/Account/AccountApiTokenSlideover.vue +95 -0
- package/dist/runtime/app/components/Account/AccountApiTokenSlideover.vue.d.ts +11 -0
- package/dist/runtime/app/components/Account/AccountHeader.vue +14 -34
- package/dist/runtime/app/components/Account/AccountMenu.d.vue.ts +10 -1
- package/dist/runtime/app/components/Account/AccountMenu.vue +100 -51
- package/dist/runtime/app/components/Account/AccountMenu.vue.d.ts +10 -1
- package/dist/runtime/app/composables/index.d.ts +2 -0
- package/dist/runtime/app/composables/index.js +1 -0
- package/dist/runtime/app/composables/useAccountNav.d.ts +20 -0
- package/dist/runtime/app/composables/useAccountNav.js +18 -0
- package/dist/runtime/app/layouts/account.d.vue.ts +13 -0
- package/dist/runtime/app/layouts/account.vue +54 -0
- package/dist/runtime/app/layouts/account.vue.d.ts +13 -0
- package/dist/runtime/app/pages/api-tokens.vue +193 -261
- package/dist/runtime/app/pages/documents.vue +25 -26
- package/dist/runtime/app/pages/index.vue +73 -17
- package/dist/runtime/app/pages/profile.vue +152 -169
- package/dist/runtime/app/pages/security.vue +156 -151
- package/dist/runtime/app/utils/api-tokens.d.ts +19 -0
- package/dist/runtime/app/utils/api-tokens.js +0 -0
- package/dist/runtime/i18n/lang/da.json +41 -9
- package/dist/runtime/i18n/lang/en.json +41 -9
- package/dist/runtime/menus.d.ts +1 -1
- package/dist/runtime/menus.js +10 -0
- package/package.json +21 -15
- package/dist/runtime/app/pages/layout.d.vue.ts +0 -3
- package/dist/runtime/app/pages/layout.vue +0 -10
- package/dist/runtime/app/pages/layout.vue.d.ts +0 -3
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
defineOptions({ name: "AccountLayout" });
|
|
3
|
+
const siteLayout = "default";
|
|
4
|
+
const t = useModuleT("account");
|
|
5
|
+
const { user } = useUserSession();
|
|
6
|
+
const { links } = await useAccountNav();
|
|
7
|
+
const profile = computed(() => {
|
|
8
|
+
const value = user.value;
|
|
9
|
+
const name = value?.name || value?.email || t("title", "Account");
|
|
10
|
+
return {
|
|
11
|
+
name,
|
|
12
|
+
email: value?.name ? value.email ?? void 0 : void 0,
|
|
13
|
+
avatar: { src: value?.avatar || void 0, alt: name }
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<NuxtLayout :name="siteLayout">
|
|
20
|
+
<UContainer>
|
|
21
|
+
<UPage>
|
|
22
|
+
<template #left>
|
|
23
|
+
<UPageAside>
|
|
24
|
+
<UUser
|
|
25
|
+
v-if="user"
|
|
26
|
+
:name="profile.name"
|
|
27
|
+
:description="profile.email"
|
|
28
|
+
:avatar="profile.avatar"
|
|
29
|
+
class="mb-4 px-2.5"
|
|
30
|
+
:ui="{ name: 'truncate', description: 'truncate' }"
|
|
31
|
+
/>
|
|
32
|
+
<UNavigationMenu
|
|
33
|
+
:items="links"
|
|
34
|
+
orientation="vertical"
|
|
35
|
+
highlight
|
|
36
|
+
:aria-label="t('nav.label', 'Account navigation')"
|
|
37
|
+
/>
|
|
38
|
+
</UPageAside>
|
|
39
|
+
</template>
|
|
40
|
+
|
|
41
|
+
<div class="flex min-w-0 flex-col gap-6 py-8">
|
|
42
|
+
<PageTabs
|
|
43
|
+
v-if="links.length"
|
|
44
|
+
class="lg:hidden"
|
|
45
|
+
:items="links"
|
|
46
|
+
:aria-label="t('nav.label', 'Account navigation')"
|
|
47
|
+
/>
|
|
48
|
+
<AccountHeader />
|
|
49
|
+
<slot />
|
|
50
|
+
</div>
|
|
51
|
+
</UPage>
|
|
52
|
+
</UContainer>
|
|
53
|
+
</NuxtLayout>
|
|
54
|
+
</template>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
declare var __VLS_47: {};
|
|
2
|
+
type __VLS_Slots = {} & {
|
|
3
|
+
default?: (props: typeof __VLS_47) => any;
|
|
4
|
+
};
|
|
5
|
+
declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
6
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
7
|
+
declare const _default: typeof __VLS_export;
|
|
8
|
+
export default _default;
|
|
9
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
10
|
+
new (): {
|
|
11
|
+
$slots: S;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
@@ -1,72 +1,53 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import
|
|
2
|
+
import AccountApiTokenSlideover from "../components/Account/AccountApiTokenSlideover.vue";
|
|
3
3
|
defineOptions({ name: "AccountApiTokensPage" });
|
|
4
4
|
const t = useModuleT("account.apiTokens");
|
|
5
5
|
const toast = useToast();
|
|
6
|
-
const
|
|
6
|
+
const showError = useErrorToast();
|
|
7
|
+
const confirm = useConfirmDialog();
|
|
8
|
+
const format = useFormat();
|
|
9
|
+
const { copy } = useCopyToClipboard();
|
|
10
|
+
const overlay = useOverlay();
|
|
11
|
+
const createSlideover = overlay.create(AccountApiTokenSlideover, { destroyOnClose: true });
|
|
12
|
+
const { data: tokens, refresh, status } = await useFetch("/api/auth/tokens", { key: "account-api-tokens", default: () => [] });
|
|
7
13
|
const { data: scopes } = await useFetch("/api/auth/scopes", { key: "account-api-token-scopes", default: () => [] });
|
|
8
|
-
const scopeItems = computed(() => (scopes.value ?? []).map((scope) => ({
|
|
9
|
-
label: scope.name === "*" ? t("allScopes", "Full access") : scope.name,
|
|
10
|
-
description: scope.description,
|
|
11
|
-
value: scope.name
|
|
12
|
-
})));
|
|
13
|
-
const expiryItems = computed(() => [
|
|
14
|
-
{ label: t("expiry.30", "30 days"), value: 30 },
|
|
15
|
-
{ label: t("expiry.90", "90 days"), value: 90 },
|
|
16
|
-
{ label: t("expiry.365", "1 year"), value: 365 },
|
|
17
|
-
{ label: t("expiry.never", "No expiry"), value: 0 }
|
|
18
|
-
]);
|
|
19
|
-
const schema = z.object({
|
|
20
|
-
name: z.string().trim().min(1, t("name.required", "Give the token a name.")).max(80),
|
|
21
|
-
scopes: z.array(z.string()).min(1, t("scopes.required", "Pick at least one scope.")),
|
|
22
|
-
expiresInDays: z.number()
|
|
23
|
-
});
|
|
24
|
-
const state = reactive({ name: "", scopes: [], expiresInDays: 90 });
|
|
25
|
-
const creating = ref(false);
|
|
26
|
-
const revokingId = ref(null);
|
|
27
14
|
const revealed = ref(null);
|
|
28
|
-
function
|
|
29
|
-
const
|
|
30
|
-
|
|
15
|
+
async function onCreate() {
|
|
16
|
+
const created = await createSlideover.open({ scopes: scopes.value ?? [] });
|
|
17
|
+
if (!created) return;
|
|
18
|
+
revealed.value = { name: created.name, token: created.token };
|
|
19
|
+
toast.add({ title: t("created", "Token created"), description: created.name, color: "success", icon: "i-lucide-check" });
|
|
20
|
+
await refresh();
|
|
31
21
|
}
|
|
32
|
-
|
|
33
|
-
creating.value = true;
|
|
34
|
-
try {
|
|
35
|
-
const created = await $fetch("/api/auth/tokens", {
|
|
36
|
-
method: "POST",
|
|
37
|
-
body: { name: event.data.name, scopes: event.data.scopes, expiresInDays: event.data.expiresInDays || null }
|
|
38
|
-
});
|
|
39
|
-
revealed.value = { name: created.name, token: created.token };
|
|
40
|
-
state.name = "";
|
|
41
|
-
state.scopes = [];
|
|
42
|
-
await refresh();
|
|
43
|
-
} catch (e) {
|
|
44
|
-
toast.add({ title: errorMessage(e, t("createError", "Could not create the token.")), color: "error" });
|
|
45
|
-
} finally {
|
|
46
|
-
creating.value = false;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
async function copyText(text, success) {
|
|
50
|
-
try {
|
|
51
|
-
await navigator.clipboard.writeText(text);
|
|
52
|
-
toast.add({ title: success, color: "success" });
|
|
53
|
-
} catch {
|
|
54
|
-
toast.add({ title: t("copyError", "Could not copy. Select the token and copy it manually."), color: "warning" });
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
const copyToken = () => revealed.value && copyText(revealed.value.token, t("copied", "Token copied."));
|
|
22
|
+
const copyToken = () => revealed.value && copy(revealed.value.token, t("copied", "Token copied."));
|
|
58
23
|
async function onRevoke(token) {
|
|
59
|
-
|
|
24
|
+
const confirmed = await confirm({
|
|
25
|
+
title: t("revokeConfirm.title", "Revoke {name}?", { name: token.name }),
|
|
26
|
+
description: t("revokeConfirm.description", "Scripts and tools using this token stop working immediately. This cannot be undone."),
|
|
27
|
+
confirmText: t("revoke", "Revoke token"),
|
|
28
|
+
destructive: true
|
|
29
|
+
});
|
|
30
|
+
if (!confirmed) return;
|
|
60
31
|
try {
|
|
61
32
|
await $fetch(`/api/auth/tokens/${token.id}`, { method: "DELETE" });
|
|
62
|
-
toast.add({ title: t("revoked", "Token revoked."), color: "success" });
|
|
33
|
+
toast.add({ title: t("revoked", "Token revoked."), description: token.name, color: "success", icon: "i-lucide-check" });
|
|
63
34
|
await refresh();
|
|
64
35
|
} catch (e) {
|
|
65
|
-
|
|
66
|
-
} finally {
|
|
67
|
-
revokingId.value = null;
|
|
36
|
+
showError(e, t("revokeError", "Could not revoke the token."));
|
|
68
37
|
}
|
|
69
38
|
}
|
|
39
|
+
const tokenMenu = (token) => token.status === "revoked" ? [] : [[{ label: t("revoke", "Revoke token"), icon: "i-lucide-ban", color: "error", onSelect: () => onRevoke(token) }]];
|
|
40
|
+
const columns = computed(() => [
|
|
41
|
+
{ accessorKey: "name", header: t("list.name", "Name") },
|
|
42
|
+
{ accessorKey: "scopes", header: t("scopes.label", "Scopes") },
|
|
43
|
+
{ accessorKey: "createdAt", header: t("list.createdColumn", "Created") },
|
|
44
|
+
{ accessorKey: "lastUsedAt", header: t("list.lastUsedColumn", "Last used") },
|
|
45
|
+
{ accessorKey: "expiresAt", header: t("list.expiresColumn", "Expires") },
|
|
46
|
+
{ id: "actions", meta: { class: { td: "w-px text-right" } } }
|
|
47
|
+
]);
|
|
48
|
+
function tokenStatusColor(status2) {
|
|
49
|
+
return statusColor(status2, { ok: "success", expired: "warning" });
|
|
50
|
+
}
|
|
70
51
|
const mcpEnabled = useRuntimeConfig().public.users?.mcp === true;
|
|
71
52
|
const activeConnect = ref("claude");
|
|
72
53
|
const mcpUrl = `${useRequestURL().origin}/mcp`;
|
|
@@ -85,224 +66,175 @@ const connectTabs = computed(() => [
|
|
|
85
66
|
label: "ChatGPT",
|
|
86
67
|
value: "chatgpt",
|
|
87
68
|
steps: t("connect.chatgpt.steps", "ChatGPT: open Settings \u2192 Connectors \u2192 Advanced, turn on Developer mode, then create a connector with the URL below. Choose OAuth and sign in when asked. No token needed."),
|
|
88
|
-
code: mcpUrl
|
|
69
|
+
code: mcpUrl,
|
|
70
|
+
extraSteps: void 0,
|
|
71
|
+
extraCode: void 0
|
|
89
72
|
}
|
|
90
73
|
]);
|
|
91
|
-
const
|
|
92
|
-
function formatDate(value) {
|
|
93
|
-
if (!value) return "";
|
|
94
|
-
const date = new Date(value);
|
|
95
|
-
return Number.isNaN(date.getTime()) ? "" : dateFormatter.format(date);
|
|
96
|
-
}
|
|
97
|
-
function statusColor(status) {
|
|
98
|
-
return status === "ok" ? "success" : status === "expired" ? "warning" : "neutral";
|
|
99
|
-
}
|
|
74
|
+
const activeTab = computed(() => connectTabs.value.find((tab) => tab.value === activeConnect.value) ?? connectTabs.value[0]);
|
|
100
75
|
</script>
|
|
101
76
|
|
|
102
77
|
<template>
|
|
103
|
-
<
|
|
104
|
-
<
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
78
|
+
<div class="flex flex-col gap-4">
|
|
79
|
+
<UAlert
|
|
80
|
+
v-if="revealed"
|
|
81
|
+
color="success"
|
|
82
|
+
variant="subtle"
|
|
83
|
+
icon="i-lucide-key-round"
|
|
84
|
+
:title="t('reveal.title', 'Copy your new token now')"
|
|
85
|
+
:description="t('reveal.description', 'This is the only time it is shown. If you lose it, create a new one.')"
|
|
86
|
+
>
|
|
87
|
+
<template #actions>
|
|
88
|
+
<UInput
|
|
89
|
+
:model-value="revealed.token"
|
|
90
|
+
readonly
|
|
91
|
+
class="w-full sm:w-96"
|
|
92
|
+
:aria-label="t('reveal.label', 'New token')"
|
|
93
|
+
@focus="$event.target.select()"
|
|
94
|
+
/>
|
|
95
|
+
<UButton
|
|
96
|
+
icon="i-lucide-copy"
|
|
97
|
+
:label="t('copy', 'Copy token')"
|
|
98
|
+
@click="copyToken"
|
|
99
|
+
/>
|
|
100
|
+
<UButton
|
|
101
|
+
color="neutral"
|
|
102
|
+
variant="ghost"
|
|
103
|
+
:label="t('reveal.done', 'Done')"
|
|
104
|
+
@click="revealed = null"
|
|
105
|
+
/>
|
|
106
|
+
</template>
|
|
107
|
+
</UAlert>
|
|
108
|
+
|
|
109
|
+
<TableCard
|
|
110
|
+
:loading="status === 'pending' && !tokens?.length"
|
|
111
|
+
:refreshing="status === 'pending' && !!tokens?.length"
|
|
112
|
+
>
|
|
113
|
+
<template #header>
|
|
114
|
+
<div class="min-w-0">
|
|
115
|
+
<h2 class="text-sm font-semibold text-highlighted">
|
|
116
|
+
{{ t("list.title", "Your tokens") }}
|
|
117
|
+
</h2>
|
|
118
|
+
<p class="text-sm text-muted">
|
|
119
|
+
{{ t("list.description", "Tokens let scripts and tools such as MCP clients act as you. Revoke one to stop it working immediately.") }}
|
|
120
|
+
</p>
|
|
121
|
+
</div>
|
|
122
|
+
<UButton
|
|
123
|
+
icon="i-lucide-plus"
|
|
124
|
+
:label="t('create.submit', 'Create API token')"
|
|
125
|
+
@click="onCreate"
|
|
126
|
+
/>
|
|
127
|
+
</template>
|
|
128
|
+
|
|
129
|
+
<UTable
|
|
130
|
+
:data="tokens ?? []"
|
|
131
|
+
:columns="columns"
|
|
132
|
+
:ui="tableUi({ tr: '' })"
|
|
133
|
+
>
|
|
134
|
+
<template #name-cell="{ row }">
|
|
135
|
+
<TableIdentity
|
|
136
|
+
:title="row.original.name"
|
|
137
|
+
:description="`${row.original.prefix}\u2026${row.original.last4}`"
|
|
118
138
|
>
|
|
119
|
-
<template #
|
|
120
|
-
<
|
|
121
|
-
:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
:
|
|
125
|
-
@focus="$event.target.select()"
|
|
126
|
-
/>
|
|
127
|
-
<UButton
|
|
128
|
-
icon="i-lucide-copy"
|
|
129
|
-
:label="t('copy', 'Copy')"
|
|
130
|
-
@click="copyToken"
|
|
131
|
-
/>
|
|
132
|
-
<UButton
|
|
133
|
-
color="neutral"
|
|
134
|
-
variant="ghost"
|
|
135
|
-
:label="t('reveal.done', 'Done')"
|
|
136
|
-
@click="revealed = null"
|
|
139
|
+
<template #badges>
|
|
140
|
+
<UBadge
|
|
141
|
+
:color="tokenStatusColor(row.original.status)"
|
|
142
|
+
variant="subtle"
|
|
143
|
+
size="sm"
|
|
144
|
+
:label="t(`status.${row.original.status}`, row.original.status)"
|
|
137
145
|
/>
|
|
138
146
|
</template>
|
|
139
|
-
</
|
|
147
|
+
</TableIdentity>
|
|
148
|
+
</template>
|
|
149
|
+
<template #scopes-cell="{ row }">
|
|
150
|
+
<span class="text-muted">{{ row.original.scopes.join(", ") }}</span>
|
|
151
|
+
</template>
|
|
152
|
+
<template #createdAt-cell="{ row }">
|
|
153
|
+
<span class="tabular-nums">{{ format.date(row.original.createdAt) }}</span>
|
|
154
|
+
</template>
|
|
155
|
+
<template #lastUsedAt-cell="{ row }">
|
|
156
|
+
<span
|
|
157
|
+
v-if="row.original.lastUsedAt"
|
|
158
|
+
class="tabular-nums"
|
|
159
|
+
>{{ format.date(row.original.lastUsedAt) }}</span>
|
|
160
|
+
<span
|
|
161
|
+
v-else
|
|
162
|
+
class="text-dimmed"
|
|
163
|
+
>{{ t("list.neverUsed", "Never") }}</span>
|
|
164
|
+
</template>
|
|
165
|
+
<template #expiresAt-cell="{ row }">
|
|
166
|
+
<span
|
|
167
|
+
v-if="row.original.expiresAt"
|
|
168
|
+
class="tabular-nums"
|
|
169
|
+
>{{ format.date(row.original.expiresAt) }}</span>
|
|
170
|
+
<span
|
|
171
|
+
v-else
|
|
172
|
+
class="text-dimmed"
|
|
173
|
+
>{{ t("expiry.never", "No expiry") }}</span>
|
|
174
|
+
</template>
|
|
175
|
+
<template #actions-cell="{ row }">
|
|
176
|
+
<RowMenu
|
|
177
|
+
v-if="row.original.status !== 'revoked'"
|
|
178
|
+
:items="tokenMenu(row.original)"
|
|
179
|
+
:subject="row.original.name"
|
|
180
|
+
/>
|
|
181
|
+
</template>
|
|
182
|
+
<template #empty>
|
|
183
|
+
<UEmpty
|
|
184
|
+
variant="naked"
|
|
185
|
+
icon="i-lucide-key-round"
|
|
186
|
+
:title="t('list.empty', 'You have not created any tokens yet.')"
|
|
187
|
+
:actions="[{ label: t('create.submit', 'Create API token'), icon: 'i-lucide-plus', onClick: onCreate }]"
|
|
188
|
+
/>
|
|
189
|
+
</template>
|
|
190
|
+
</UTable>
|
|
191
|
+
</TableCard>
|
|
140
192
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
/>
|
|
158
|
-
</UFormField>
|
|
159
|
-
<UFormField
|
|
160
|
-
:label="t('scopes.label', 'Scopes')"
|
|
161
|
-
name="scopes"
|
|
162
|
-
required
|
|
163
|
-
>
|
|
164
|
-
<USelectMenu
|
|
165
|
-
v-model="state.scopes"
|
|
166
|
-
:items="scopeItems"
|
|
167
|
-
value-key="value"
|
|
168
|
-
multiple
|
|
169
|
-
:placeholder="t('scopes.placeholder', 'Choose what the token may do')"
|
|
170
|
-
class="w-full"
|
|
171
|
-
/>
|
|
172
|
-
</UFormField>
|
|
173
|
-
<UFormField
|
|
174
|
-
:label="t('expiry.label', 'Expires')"
|
|
175
|
-
name="expiresInDays"
|
|
176
|
-
>
|
|
177
|
-
<USelect
|
|
178
|
-
v-model="state.expiresInDays"
|
|
179
|
-
:items="expiryItems"
|
|
180
|
-
value-key="value"
|
|
181
|
-
class="w-full sm:max-w-xs"
|
|
182
|
-
/>
|
|
183
|
-
</UFormField>
|
|
193
|
+
<SectionCard
|
|
194
|
+
v-if="mcpEnabled"
|
|
195
|
+
:title="t('connect.title', 'Connect to MCP')"
|
|
196
|
+
:description="t('connect.description', 'Use your account from an AI assistant. Your API token is only filled into the commands right after you create it.')"
|
|
197
|
+
>
|
|
198
|
+
<div class="flex min-w-0 flex-col gap-3">
|
|
199
|
+
<PageTabs
|
|
200
|
+
v-model="activeConnect"
|
|
201
|
+
:items="connectTabs.map((tab) => ({ label: tab.label, value: tab.value }))"
|
|
202
|
+
/>
|
|
203
|
+
<template v-if="activeTab">
|
|
204
|
+
<p class="text-sm text-muted">
|
|
205
|
+
{{ activeTab.steps }}
|
|
206
|
+
</p>
|
|
207
|
+
<div class="relative min-w-0">
|
|
208
|
+
<pre class="max-w-full whitespace-pre-wrap break-all rounded-md bg-elevated p-3 pr-12 text-sm"><code>{{ activeTab.code }}</code></pre>
|
|
184
209
|
<UButton
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
210
|
+
class="absolute right-2 top-2"
|
|
211
|
+
color="neutral"
|
|
212
|
+
variant="ghost"
|
|
213
|
+
size="xs"
|
|
214
|
+
icon="i-lucide-copy"
|
|
215
|
+
:aria-label="t('copySnippet', 'Copy command')"
|
|
216
|
+
@click="copy(activeTab.code, t('copiedSnippet', 'Copied.'))"
|
|
188
217
|
/>
|
|
189
|
-
</
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
variant="subtle"
|
|
197
|
-
>
|
|
198
|
-
<UTabs
|
|
199
|
-
v-model="activeConnect"
|
|
200
|
-
:items="connectTabs"
|
|
201
|
-
:content="false"
|
|
202
|
-
class="w-full"
|
|
203
|
-
/>
|
|
204
|
-
<template
|
|
205
|
-
v-for="tab in connectTabs"
|
|
206
|
-
:key="tab.value"
|
|
207
|
-
>
|
|
208
|
-
<div
|
|
209
|
-
v-if="tab.value === activeConnect"
|
|
210
|
-
class="min-w-0 max-w-full space-y-3"
|
|
211
|
-
>
|
|
212
|
-
<p class="text-sm text-muted">
|
|
213
|
-
{{ tab.steps }}
|
|
214
|
-
</p>
|
|
215
|
-
<div class="relative min-w-0">
|
|
216
|
-
<pre class="max-w-full whitespace-pre-wrap break-all rounded-md bg-elevated p-3 pr-12 text-sm"><code>{{ tab.code }}</code></pre>
|
|
217
|
-
<UButton
|
|
218
|
-
class="absolute right-2 top-2"
|
|
219
|
-
color="neutral"
|
|
220
|
-
variant="ghost"
|
|
221
|
-
size="xs"
|
|
222
|
-
icon="i-lucide-copy"
|
|
223
|
-
:aria-label="t('copy', 'Copy')"
|
|
224
|
-
@click="copyText(tab.code, t('copiedSnippet', 'Copied.'))"
|
|
225
|
-
/>
|
|
226
|
-
</div>
|
|
227
|
-
<template v-if="tab.extraCode">
|
|
228
|
-
<p class="text-sm text-muted">
|
|
229
|
-
{{ tab.extraSteps }}
|
|
230
|
-
</p>
|
|
231
|
-
<div class="relative min-w-0">
|
|
232
|
-
<pre class="max-w-full whitespace-pre-wrap break-all rounded-md bg-elevated p-3 pr-12 text-sm"><code>{{ tab.extraCode }}</code></pre>
|
|
233
|
-
<UButton
|
|
234
|
-
class="absolute right-2 top-2"
|
|
235
|
-
color="neutral"
|
|
236
|
-
variant="ghost"
|
|
237
|
-
size="xs"
|
|
238
|
-
icon="i-lucide-copy"
|
|
239
|
-
:aria-label="t('copy', 'Copy')"
|
|
240
|
-
@click="copyText(tab.extraCode ?? '', t('copiedSnippet', 'Copied.'))"
|
|
241
|
-
/>
|
|
242
|
-
</div>
|
|
243
|
-
</template>
|
|
244
|
-
</div>
|
|
245
|
-
</template>
|
|
246
|
-
</UPageCard>
|
|
247
|
-
|
|
248
|
-
<UPageCard
|
|
249
|
-
:title="t('list.title', 'Your tokens')"
|
|
250
|
-
:description="t('list.description', 'Revoke a token to stop it working immediately.')"
|
|
251
|
-
variant="subtle"
|
|
252
|
-
>
|
|
253
|
-
<ul
|
|
254
|
-
v-if="tokens?.length"
|
|
255
|
-
class="divide-y divide-default"
|
|
256
|
-
>
|
|
257
|
-
<li
|
|
258
|
-
v-for="token in tokens"
|
|
259
|
-
:key="token.id"
|
|
260
|
-
class="flex items-center justify-between gap-3 py-3"
|
|
261
|
-
>
|
|
262
|
-
<div class="min-w-0">
|
|
263
|
-
<p class="flex items-center gap-2 truncate font-medium text-highlighted">
|
|
264
|
-
{{ token.name }}
|
|
265
|
-
<UBadge
|
|
266
|
-
:color="statusColor(token.status)"
|
|
267
|
-
variant="subtle"
|
|
268
|
-
:label="t(`status.${token.status}`, token.status)"
|
|
269
|
-
/>
|
|
270
|
-
</p>
|
|
271
|
-
<p class="font-mono text-sm text-muted">
|
|
272
|
-
{{ token.prefix }}…{{ token.last4 }}
|
|
273
|
-
</p>
|
|
274
|
-
<p class="text-sm text-muted">
|
|
275
|
-
{{ token.scopes.join(", ") }}
|
|
276
|
-
</p>
|
|
277
|
-
<p class="text-sm text-muted">
|
|
278
|
-
{{ t("list.created", "Created {date}", { date: formatDate(token.createdAt) }) }}
|
|
279
|
-
<template v-if="token.lastUsedAt">
|
|
280
|
-
· {{ t("list.lastUsed", "Last used {date}", { date: formatDate(token.lastUsedAt) }) }}
|
|
281
|
-
</template>
|
|
282
|
-
<template v-if="token.expiresAt">
|
|
283
|
-
· {{ t("list.expires", "Expires {date}", { date: formatDate(token.expiresAt) }) }}
|
|
284
|
-
</template>
|
|
285
|
-
</p>
|
|
286
|
-
</div>
|
|
218
|
+
</div>
|
|
219
|
+
<template v-if="activeTab.extraCode">
|
|
220
|
+
<p class="text-sm text-muted">
|
|
221
|
+
{{ activeTab.extraSteps }}
|
|
222
|
+
</p>
|
|
223
|
+
<div class="relative min-w-0">
|
|
224
|
+
<pre class="max-w-full whitespace-pre-wrap break-all rounded-md bg-elevated p-3 pr-12 text-sm"><code>{{ activeTab.extraCode }}</code></pre>
|
|
287
225
|
<UButton
|
|
288
|
-
|
|
289
|
-
color="
|
|
226
|
+
class="absolute right-2 top-2"
|
|
227
|
+
color="neutral"
|
|
290
228
|
variant="ghost"
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
:aria-label="t('
|
|
294
|
-
@click="
|
|
229
|
+
size="xs"
|
|
230
|
+
icon="i-lucide-copy"
|
|
231
|
+
:aria-label="t('copySnippet', 'Copy command')"
|
|
232
|
+
@click="copy(activeTab.extraCode, t('copiedSnippet', 'Copied.'))"
|
|
295
233
|
/>
|
|
296
|
-
</
|
|
297
|
-
</
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
{{ t("list.empty", "You have not created any tokens yet.") }}
|
|
303
|
-
</p>
|
|
304
|
-
</UPageCard>
|
|
305
|
-
</UPageBody>
|
|
306
|
-
</UPage>
|
|
307
|
-
</UContainer>
|
|
234
|
+
</div>
|
|
235
|
+
</template>
|
|
236
|
+
</template>
|
|
237
|
+
</div>
|
|
238
|
+
</SectionCard>
|
|
239
|
+
</div>
|
|
308
240
|
</template>
|
|
@@ -9,30 +9,29 @@ const documents = computed(() => visibleDocuments(useRuntimeConfig().public.acco
|
|
|
9
9
|
</script>
|
|
10
10
|
|
|
11
11
|
<template>
|
|
12
|
-
<
|
|
13
|
-
<
|
|
14
|
-
<
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
</UContainer>
|
|
12
|
+
<div class="flex flex-col gap-4">
|
|
13
|
+
<SectionCard>
|
|
14
|
+
<UPageGrid v-if="documents.length">
|
|
15
|
+
<UPageCard
|
|
16
|
+
v-for="document in documents"
|
|
17
|
+
:key="document.id"
|
|
18
|
+
:title="documentLabel(document, locale)"
|
|
19
|
+
:icon="document.icon ?? 'i-lucide-file'"
|
|
20
|
+
>
|
|
21
|
+
<UButton
|
|
22
|
+
download
|
|
23
|
+
external
|
|
24
|
+
:to="document.path"
|
|
25
|
+
icon="i-lucide-download"
|
|
26
|
+
:label="t('download', 'Download')"
|
|
27
|
+
/>
|
|
28
|
+
</UPageCard>
|
|
29
|
+
</UPageGrid>
|
|
30
|
+
<UEmpty
|
|
31
|
+
v-else
|
|
32
|
+
variant="naked"
|
|
33
|
+
:title="t('empty', 'No documents for you yet.')"
|
|
34
|
+
/>
|
|
35
|
+
</SectionCard>
|
|
36
|
+
</div>
|
|
38
37
|
</template>
|