@frigear-nu/account 0.1.2 → 0.1.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 +21 -9
- package/dist/module.d.mts +3 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +4 -2
- package/dist/runtime/app/components/Account/AccountApiTokenSlideover.d.vue.ts +3 -3
- package/dist/runtime/app/components/Account/AccountApiTokenSlideover.vue +2 -1
- package/dist/runtime/app/components/Account/AccountApiTokenSlideover.vue.d.ts +3 -3
- package/dist/runtime/app/components/Account/AccountChangeEmailSlideover.d.vue.ts +14 -0
- package/dist/runtime/app/components/Account/AccountChangeEmailSlideover.vue +54 -0
- package/dist/runtime/app/components/Account/AccountChangeEmailSlideover.vue.d.ts +14 -0
- package/dist/runtime/app/components/Account/AccountHeader.d.vue.ts +10 -1
- package/dist/runtime/app/components/Account/AccountHeader.vue +6 -3
- package/dist/runtime/app/components/Account/AccountHeader.vue.d.ts +10 -1
- package/dist/runtime/app/components/Account/AccountIdentity.vue +13 -0
- package/dist/runtime/app/components/Account/AccountMenu.vue +13 -3
- package/dist/runtime/app/components/Account/AccountPasskeySlideover.d.vue.ts +10 -0
- package/dist/runtime/app/components/Account/AccountPasskeySlideover.vue +60 -0
- package/dist/runtime/app/components/Account/AccountPasskeySlideover.vue.d.ts +10 -0
- package/dist/runtime/app/composables/useAccount.d.ts +2 -2
- package/dist/runtime/app/composables/useAccount.js +4 -3
- package/dist/runtime/app/layouts/account.d.vue.ts +2 -2
- package/dist/runtime/app/layouts/account.vue +13 -11
- package/dist/runtime/app/layouts/account.vue.d.ts +2 -2
- package/dist/runtime/app/pages/api-tokens.vue +23 -28
- package/dist/runtime/app/pages/documents.vue +22 -7
- package/dist/runtime/app/pages/index.vue +3 -6
- package/dist/runtime/app/pages/profile.vue +50 -89
- package/dist/runtime/app/pages/security.vue +40 -60
- package/dist/runtime/app/utils/api-tokens.d.ts +3 -14
- package/dist/runtime/i18n/lang/da.json +11 -9
- package/dist/runtime/i18n/lang/en.json +11 -9
- package/package.json +11 -11
- package/dist/runtime/app/components/Account/AccountHeaderRight.vue +0 -18
- /package/dist/runtime/app/components/Account/{AccountHeaderRight.d.vue.ts → AccountIdentity.d.vue.ts} +0 -0
- /package/dist/runtime/app/components/Account/{AccountHeaderRight.vue.d.ts → AccountIdentity.vue.d.ts} +0 -0
|
@@ -10,26 +10,41 @@ const documents = computed(() => visibleDocuments(useRuntimeConfig().public.acco
|
|
|
10
10
|
|
|
11
11
|
<template>
|
|
12
12
|
<div class="flex flex-col gap-4">
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
<AccountHeader />
|
|
14
|
+
|
|
15
|
+
<SectionCard flush>
|
|
16
|
+
<ul
|
|
17
|
+
v-if="documents.length"
|
|
18
|
+
class="divide-y divide-default"
|
|
19
|
+
>
|
|
20
|
+
<li
|
|
16
21
|
v-for="document in documents"
|
|
17
22
|
:key="document.id"
|
|
18
|
-
|
|
19
|
-
:icon="document.icon ?? 'i-lucide-file'"
|
|
23
|
+
class="flex items-center gap-3 px-4 py-3"
|
|
20
24
|
>
|
|
25
|
+
<UIcon
|
|
26
|
+
:name="document.icon ?? 'i-lucide-file'"
|
|
27
|
+
class="size-5 shrink-0 text-muted"
|
|
28
|
+
/>
|
|
29
|
+
<p class="min-w-0 flex-1 truncate text-sm font-medium text-highlighted">
|
|
30
|
+
{{ documentLabel(document, locale) }}
|
|
31
|
+
</p>
|
|
21
32
|
<UButton
|
|
22
33
|
download
|
|
23
34
|
external
|
|
24
35
|
:to="document.path"
|
|
36
|
+
color="neutral"
|
|
37
|
+
variant="outline"
|
|
25
38
|
icon="i-lucide-download"
|
|
26
39
|
:label="t('download', 'Download')"
|
|
40
|
+
:aria-label="t('downloadNamed', 'Download {name}', { name: documentLabel(document, locale) })"
|
|
27
41
|
/>
|
|
28
|
-
</
|
|
29
|
-
</
|
|
42
|
+
</li>
|
|
43
|
+
</ul>
|
|
30
44
|
<UEmpty
|
|
31
45
|
v-else
|
|
32
46
|
variant="naked"
|
|
47
|
+
icon="i-lucide-files"
|
|
33
48
|
:title="t('empty', 'No documents for you yet.')"
|
|
34
49
|
/>
|
|
35
50
|
</SectionCard>
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
defineOptions({ name: "AccountIndexPage" });
|
|
3
|
-
definePageMeta({ heading: false });
|
|
4
3
|
const t = useModuleT("account");
|
|
5
4
|
const { user } = useUserSession();
|
|
6
5
|
const { localize } = useLocaleLink();
|
|
@@ -18,10 +17,7 @@ const profile = computed(() => {
|
|
|
18
17
|
|
|
19
18
|
<template>
|
|
20
19
|
<div class="flex flex-col gap-4">
|
|
21
|
-
<
|
|
22
|
-
:title="t('overview.title', 'Account')"
|
|
23
|
-
:description="t('overview.description', 'Your details and everything that belongs to your account.')"
|
|
24
|
-
/>
|
|
20
|
+
<AccountHeader />
|
|
25
21
|
|
|
26
22
|
<SectionCard>
|
|
27
23
|
<div class="flex flex-wrap items-center justify-between gap-4">
|
|
@@ -32,7 +28,7 @@ const profile = computed(() => {
|
|
|
32
28
|
size="lg"
|
|
33
29
|
:ui="{ name: 'truncate', description: 'truncate' }"
|
|
34
30
|
/>
|
|
35
|
-
<
|
|
31
|
+
<AccountIdentity />
|
|
36
32
|
</div>
|
|
37
33
|
</SectionCard>
|
|
38
34
|
|
|
@@ -41,6 +37,7 @@ const profile = computed(() => {
|
|
|
41
37
|
v-if="widgetArea"
|
|
42
38
|
name="account.overview"
|
|
43
39
|
:context="{ user }"
|
|
40
|
+
grid-class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"
|
|
44
41
|
/>
|
|
45
42
|
|
|
46
43
|
<ListCard
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import AccountChangeEmailSlideover from "../components/Account/AccountChangeEmailSlideover.vue";
|
|
3
4
|
defineOptions({ name: "AccountProfilePage" });
|
|
4
5
|
const t = useModuleT("account.profile");
|
|
5
6
|
const toast = useToast();
|
|
6
7
|
const route = useRoute();
|
|
7
8
|
const { user } = useUserSession();
|
|
8
|
-
const { updateProfileDetails
|
|
9
|
+
const { updateProfileDetails } = useAccount();
|
|
9
10
|
const schema = z.object({
|
|
10
11
|
name: z.string().trim().min(1, t("name.required", "Please enter your name.")),
|
|
11
|
-
|
|
12
|
+
// The server's rule: an http(s) URL of at most 2048 characters (empty clears the avatar).
|
|
13
|
+
avatar: z.union([
|
|
14
|
+
z.literal(""),
|
|
15
|
+
z.url({ protocol: /^https?$/, message: t("avatar.invalid", "Enter a valid URL.") }).max(2048, t("avatar.invalid", "Enter a valid URL."))
|
|
16
|
+
]).optional()
|
|
12
17
|
});
|
|
13
18
|
const state = reactive({
|
|
14
19
|
name: user.value?.name ?? "",
|
|
@@ -28,32 +33,32 @@ const dirty = computed(() => state.name !== saved.value.name || state.avatar !==
|
|
|
28
33
|
const profileAction = useAction();
|
|
29
34
|
function onSubmitProfile(event) {
|
|
30
35
|
return profileAction.run(
|
|
31
|
-
() => updateProfileDetails({ name: event.data.name, avatar: event.data.avatar ||
|
|
36
|
+
() => updateProfileDetails({ name: event.data.name, avatar: event.data.avatar || null }),
|
|
32
37
|
{ success: t("saved", "Profile updated"), error: t("saveFailed", "Could not update your profile.") }
|
|
33
38
|
);
|
|
34
39
|
}
|
|
35
|
-
const emailModalOpen = ref(false);
|
|
36
|
-
const newEmail = ref("");
|
|
37
40
|
const emailVerificationSent = ref(false);
|
|
38
41
|
const emailChanged = computed(() => route.query["email-changed"] === "1");
|
|
39
42
|
const emailChangeFailed = computed(() => route.query.error === "email-change");
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
async function
|
|
43
|
-
const result = await
|
|
44
|
-
() => changeEmailAddress({ email: newEmail.value }),
|
|
45
|
-
{ error: t("email.change.error", "Could not change your email address.") }
|
|
46
|
-
);
|
|
43
|
+
const overlay = useOverlay();
|
|
44
|
+
const emailSlideover = overlay.create(AccountChangeEmailSlideover, { destroyOnClose: true });
|
|
45
|
+
async function onChangeEmail() {
|
|
46
|
+
const result = await emailSlideover.open({ email: user.value?.email ?? void 0 });
|
|
47
47
|
if (!result) return;
|
|
48
|
-
emailVerificationSent.value = result.verificationRequired
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
emailVerificationSent.value = result.verificationRequired;
|
|
49
|
+
toast.add({
|
|
50
|
+
title: result.verificationRequired ? t("email.change.sent.title", "Check your inbox") : t("email.change.done", "Your email address was changed."),
|
|
51
|
+
description: result.verificationRequired ? void 0 : user.value?.email ?? void 0,
|
|
52
|
+
color: "success",
|
|
53
|
+
icon: "i-lucide-check"
|
|
54
|
+
});
|
|
52
55
|
}
|
|
53
56
|
</script>
|
|
54
57
|
|
|
55
58
|
<template>
|
|
56
59
|
<div class="flex flex-col gap-4">
|
|
60
|
+
<AccountHeader />
|
|
61
|
+
|
|
57
62
|
<UAlert
|
|
58
63
|
v-if="emailChanged"
|
|
59
64
|
color="success"
|
|
@@ -74,52 +79,51 @@ async function onSubmitEmail() {
|
|
|
74
79
|
/>
|
|
75
80
|
|
|
76
81
|
<SectionCard
|
|
77
|
-
:title="t('title', '
|
|
78
|
-
:description="t('description', '
|
|
82
|
+
:title="t('details.title', 'Name and avatar')"
|
|
83
|
+
:description="t('details.description', 'How you appear to others.')"
|
|
79
84
|
>
|
|
80
85
|
<UForm
|
|
81
86
|
id="account-profile"
|
|
82
87
|
:schema="schema"
|
|
83
88
|
:state="state"
|
|
84
|
-
class="flex flex-col
|
|
89
|
+
class="flex flex-col divide-y divide-default"
|
|
85
90
|
@submit="onSubmitProfile"
|
|
86
91
|
>
|
|
87
|
-
<
|
|
92
|
+
<FormRow
|
|
88
93
|
name="name"
|
|
89
94
|
:label="t('name.label', 'Name')"
|
|
90
95
|
:description="t('name.description', 'Your full name.')"
|
|
91
96
|
required
|
|
92
|
-
class="flex items-start justify-between gap-4 max-sm:flex-col"
|
|
93
97
|
>
|
|
94
98
|
<UInput
|
|
95
99
|
v-model="state.name"
|
|
96
100
|
autocomplete="name"
|
|
101
|
+
class="w-full sm:max-w-sm"
|
|
97
102
|
/>
|
|
98
|
-
</
|
|
99
|
-
<
|
|
100
|
-
<UFormField
|
|
103
|
+
</FormRow>
|
|
104
|
+
<FormRow
|
|
101
105
|
name="avatar"
|
|
102
106
|
:label="t('avatar.label', 'Avatar')"
|
|
103
107
|
:description="t('avatar.description', 'Link to a square image.')"
|
|
104
|
-
class="flex items-start justify-between gap-4 max-sm:flex-col"
|
|
105
108
|
>
|
|
106
|
-
<div class="flex items-center gap-3">
|
|
109
|
+
<div class="flex items-center gap-3 sm:max-w-sm">
|
|
107
110
|
<UAvatar
|
|
108
111
|
:src="state.avatar || void 0"
|
|
109
112
|
:alt="state.name"
|
|
110
|
-
size="
|
|
113
|
+
size="lg"
|
|
111
114
|
/>
|
|
112
115
|
<UInput
|
|
113
116
|
v-model="state.avatar"
|
|
114
117
|
type="url"
|
|
115
118
|
placeholder="https://"
|
|
116
119
|
:aria-label="t('avatar.url', 'Avatar URL')"
|
|
120
|
+
class="min-w-0 flex-1"
|
|
117
121
|
/>
|
|
118
122
|
</div>
|
|
119
|
-
</
|
|
123
|
+
</FormRow>
|
|
120
124
|
</UForm>
|
|
121
125
|
<template #footer>
|
|
122
|
-
<div class="flex justify-end">
|
|
126
|
+
<div class="flex justify-end gap-2">
|
|
123
127
|
<UButton
|
|
124
128
|
type="submit"
|
|
125
129
|
form="account-profile"
|
|
@@ -131,71 +135,28 @@ async function onSubmitEmail() {
|
|
|
131
135
|
</template>
|
|
132
136
|
</SectionCard>
|
|
133
137
|
|
|
134
|
-
<SectionCard
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
<
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
<UInput
|
|
148
|
-
:model-value="user?.email ?? ''"
|
|
149
|
-
type="email"
|
|
150
|
-
readonly
|
|
151
|
-
disabled
|
|
152
|
-
class="w-full sm:max-w-xs"
|
|
153
|
-
:aria-label="t('email.label', 'Email')"
|
|
154
|
-
/>
|
|
155
|
-
</SectionCard>
|
|
156
|
-
|
|
157
|
-
<UModal
|
|
158
|
-
v-model:open="emailModalOpen"
|
|
159
|
-
:title="t('email.change.title', 'Change email')"
|
|
160
|
-
>
|
|
161
|
-
<template #body>
|
|
162
|
-
<UForm
|
|
163
|
-
id="account-change-email"
|
|
164
|
-
:schema="emailSchema"
|
|
165
|
-
:state="{ email: newEmail }"
|
|
166
|
-
class="flex flex-col gap-4"
|
|
167
|
-
@submit="onSubmitEmail"
|
|
168
|
-
>
|
|
169
|
-
<UFormField
|
|
170
|
-
name="email"
|
|
171
|
-
:label="t('email.change.label', 'New e-mail')"
|
|
172
|
-
required
|
|
173
|
-
>
|
|
174
|
-
<UInput
|
|
175
|
-
v-model="newEmail"
|
|
176
|
-
type="email"
|
|
177
|
-
autocomplete="email"
|
|
178
|
-
class="w-full"
|
|
179
|
-
/>
|
|
180
|
-
</UFormField>
|
|
181
|
-
</UForm>
|
|
182
|
-
</template>
|
|
183
|
-
<template #footer>
|
|
184
|
-
<div class="flex w-full justify-end gap-2">
|
|
138
|
+
<SectionCard :title="t('email.label', 'Email')">
|
|
139
|
+
<FormRow
|
|
140
|
+
:label="t('email.address', 'Address')"
|
|
141
|
+
:description="t('email.description', 'Where we send messages and how you sign in.')"
|
|
142
|
+
>
|
|
143
|
+
<div class="flex flex-wrap items-center gap-2 sm:max-w-lg">
|
|
144
|
+
<UInput
|
|
145
|
+
:model-value="user?.email ?? ''"
|
|
146
|
+
type="email"
|
|
147
|
+
readonly
|
|
148
|
+
disabled
|
|
149
|
+
class="min-w-48 flex-1"
|
|
150
|
+
/>
|
|
185
151
|
<UButton
|
|
186
152
|
color="neutral"
|
|
187
153
|
variant="outline"
|
|
188
|
-
|
|
189
|
-
@click="emailModalOpen = false"
|
|
190
|
-
/>
|
|
191
|
-
<UButton
|
|
192
|
-
type="submit"
|
|
193
|
-
form="account-change-email"
|
|
194
|
-
:loading="emailAction.busy.value"
|
|
154
|
+
icon="i-lucide-pencil"
|
|
195
155
|
:label="t('email.change.title', 'Change email')"
|
|
156
|
+
@click="onChangeEmail"
|
|
196
157
|
/>
|
|
197
158
|
</div>
|
|
198
|
-
</
|
|
199
|
-
</
|
|
159
|
+
</FormRow>
|
|
160
|
+
</SectionCard>
|
|
200
161
|
</div>
|
|
201
162
|
</template>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { authApi } from "@frigear-nu/auth/api";
|
|
4
|
+
import AccountPasskeySlideover from "../components/Account/AccountPasskeySlideover.vue";
|
|
3
5
|
defineOptions({ name: "AccountSecurityPage" });
|
|
4
6
|
const t = useModuleT("account.security");
|
|
5
7
|
const toast = useToast();
|
|
@@ -33,34 +35,25 @@ async function onSubmitPassword(event) {
|
|
|
33
35
|
passwordState.newPassword = "";
|
|
34
36
|
passwordState.confirmNewPassword = "";
|
|
35
37
|
}
|
|
36
|
-
const passkeyName = ref("");
|
|
37
|
-
const passkeyPending = ref(false);
|
|
38
38
|
const {
|
|
39
39
|
data: passkeyData,
|
|
40
40
|
error: passkeysError,
|
|
41
41
|
loading: passkeysLoading,
|
|
42
42
|
refresh: loadPasskeys
|
|
43
|
-
} = await useCachedFetch(
|
|
43
|
+
} = await useCachedFetch(authApi.me.passkeys, {
|
|
44
44
|
key: () => `account:passkeys:${user.value?.id ?? "anonymous"}`,
|
|
45
45
|
ttl: 0,
|
|
46
46
|
immediate: webAuthnEnabled,
|
|
47
47
|
watch: false
|
|
48
48
|
});
|
|
49
49
|
const passkeyList = computed(() => passkeyData.value ?? []);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
} catch (e) {
|
|
58
|
-
if (e?.name !== "NotAllowedError") {
|
|
59
|
-
showError(e, t("passkeys.registerError", "Could not register the passkey."));
|
|
60
|
-
}
|
|
61
|
-
} finally {
|
|
62
|
-
passkeyPending.value = false;
|
|
63
|
-
}
|
|
50
|
+
const overlay = useOverlay();
|
|
51
|
+
const passkeySlideover = overlay.create(AccountPasskeySlideover, { destroyOnClose: true });
|
|
52
|
+
async function onAddPasskey() {
|
|
53
|
+
const added = await passkeySlideover.open({ userName: user.value?.email ?? "" });
|
|
54
|
+
if (!added) return;
|
|
55
|
+
toast.add({ title: t("passkeys.registered", "Passkey added."), color: "success", icon: "i-lucide-check" });
|
|
56
|
+
await loadPasskeys();
|
|
64
57
|
}
|
|
65
58
|
const passkeyLabel = (passkey) => passkey.name || t("passkeys.unnamed", "Passkey");
|
|
66
59
|
async function onRemovePasskey(passkey) {
|
|
@@ -86,6 +79,8 @@ const passkeyMenu = (passkey) => [[
|
|
|
86
79
|
|
|
87
80
|
<template>
|
|
88
81
|
<div class="flex flex-col gap-4">
|
|
82
|
+
<AccountHeader />
|
|
83
|
+
|
|
89
84
|
<SectionCard
|
|
90
85
|
:title="t('password.title', 'Password')"
|
|
91
86
|
:description="t('password.description', 'Change your password.')"
|
|
@@ -94,10 +89,10 @@ const passkeyMenu = (passkey) => [[
|
|
|
94
89
|
id="account-change-password"
|
|
95
90
|
:schema="passwordSchema"
|
|
96
91
|
:state="passwordState"
|
|
97
|
-
class="flex flex-col
|
|
92
|
+
class="flex flex-col divide-y divide-default"
|
|
98
93
|
@submit="onSubmitPassword"
|
|
99
94
|
>
|
|
100
|
-
<
|
|
95
|
+
<FormRow
|
|
101
96
|
:label="t('changePassword.current.label', 'Current password')"
|
|
102
97
|
name="currentPassword"
|
|
103
98
|
required
|
|
@@ -108,8 +103,8 @@ const passkeyMenu = (passkey) => [[
|
|
|
108
103
|
autocomplete="current-password"
|
|
109
104
|
class="w-full sm:max-w-sm"
|
|
110
105
|
/>
|
|
111
|
-
</
|
|
112
|
-
<
|
|
106
|
+
</FormRow>
|
|
107
|
+
<FormRow
|
|
113
108
|
:label="t('changePassword.new.label', 'New password')"
|
|
114
109
|
name="newPassword"
|
|
115
110
|
required
|
|
@@ -120,8 +115,8 @@ const passkeyMenu = (passkey) => [[
|
|
|
120
115
|
autocomplete="new-password"
|
|
121
116
|
class="w-full sm:max-w-sm"
|
|
122
117
|
/>
|
|
123
|
-
</
|
|
124
|
-
<
|
|
118
|
+
</FormRow>
|
|
119
|
+
<FormRow
|
|
125
120
|
:label="t('changePassword.confirm.label', 'Confirm new password')"
|
|
126
121
|
name="confirmNewPassword"
|
|
127
122
|
required
|
|
@@ -132,10 +127,10 @@ const passkeyMenu = (passkey) => [[
|
|
|
132
127
|
autocomplete="new-password"
|
|
133
128
|
class="w-full sm:max-w-sm"
|
|
134
129
|
/>
|
|
135
|
-
</
|
|
130
|
+
</FormRow>
|
|
136
131
|
</UForm>
|
|
137
132
|
<template #footer>
|
|
138
|
-
<div class="flex justify-end">
|
|
133
|
+
<div class="flex justify-end gap-2">
|
|
139
134
|
<UButton
|
|
140
135
|
type="submit"
|
|
141
136
|
form="account-change-password"
|
|
@@ -153,6 +148,13 @@ const passkeyMenu = (passkey) => [[
|
|
|
153
148
|
:loading="passkeysLoading"
|
|
154
149
|
flush
|
|
155
150
|
>
|
|
151
|
+
<template #actions>
|
|
152
|
+
<UButton
|
|
153
|
+
icon="i-lucide-plus"
|
|
154
|
+
:label="t('passkeys.add', 'Add passkey')"
|
|
155
|
+
@click="onAddPasskey"
|
|
156
|
+
/>
|
|
157
|
+
</template>
|
|
156
158
|
<ul
|
|
157
159
|
v-if="passkeyList.length"
|
|
158
160
|
class="divide-y divide-default"
|
|
@@ -160,21 +162,19 @@ const passkeyMenu = (passkey) => [[
|
|
|
160
162
|
<li
|
|
161
163
|
v-for="passkey in passkeyList"
|
|
162
164
|
:key="passkey.id"
|
|
163
|
-
class="flex items-center
|
|
165
|
+
class="flex items-center gap-3 px-4 py-3"
|
|
164
166
|
>
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
</p>
|
|
177
|
-
</div>
|
|
167
|
+
<UIcon
|
|
168
|
+
name="i-lucide-key-round"
|
|
169
|
+
class="size-5 shrink-0 text-muted"
|
|
170
|
+
/>
|
|
171
|
+
<div class="min-w-0 flex-1">
|
|
172
|
+
<p class="truncate text-sm font-medium text-highlighted">
|
|
173
|
+
{{ passkeyLabel(passkey) }}
|
|
174
|
+
</p>
|
|
175
|
+
<p class="text-sm text-muted">
|
|
176
|
+
{{ t("passkeys.created", "Added {date}", { date: format.date(passkey.createdAt) }) }}
|
|
177
|
+
</p>
|
|
178
178
|
</div>
|
|
179
179
|
<RowMenu
|
|
180
180
|
:items="passkeyMenu(passkey)"
|
|
@@ -195,28 +195,8 @@ const passkeyMenu = (passkey) => [[
|
|
|
195
195
|
variant="naked"
|
|
196
196
|
icon="i-lucide-key-round"
|
|
197
197
|
:title="t('passkeys.empty', 'You have not added any passkeys yet.')"
|
|
198
|
+
:actions="[{ label: t('passkeys.add', 'Add passkey'), icon: 'i-lucide-plus', onClick: onAddPasskey }]"
|
|
198
199
|
/>
|
|
199
|
-
|
|
200
|
-
<template #footer>
|
|
201
|
-
<form
|
|
202
|
-
class="flex flex-col gap-2 sm:flex-row sm:justify-end"
|
|
203
|
-
@submit.prevent="onRegisterPasskey"
|
|
204
|
-
>
|
|
205
|
-
<UInput
|
|
206
|
-
v-model="passkeyName"
|
|
207
|
-
:placeholder="t('passkeys.name.placeholder', 'Name, e.g. MacBook')"
|
|
208
|
-
:aria-label="t('passkeys.name.label', 'Passkey name')"
|
|
209
|
-
maxlength="100"
|
|
210
|
-
class="w-full sm:max-w-xs"
|
|
211
|
-
/>
|
|
212
|
-
<UButton
|
|
213
|
-
type="submit"
|
|
214
|
-
icon="i-lucide-plus"
|
|
215
|
-
:loading="passkeyPending"
|
|
216
|
-
:label="t('passkeys.register', 'Register a passkey')"
|
|
217
|
-
/>
|
|
218
|
-
</form>
|
|
219
|
-
</template>
|
|
220
200
|
</SectionCard>
|
|
221
201
|
</div>
|
|
222
202
|
</template>
|
|
@@ -1,19 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
name: string;
|
|
4
|
-
prefix: string;
|
|
5
|
-
last4: string;
|
|
6
|
-
scopes: string[];
|
|
7
|
-
expiresAt: string | null;
|
|
8
|
-
lastUsedAt: string | null;
|
|
9
|
-
createdAt: string;
|
|
10
|
-
status: 'ok' | 'revoked' | 'expired';
|
|
11
|
-
}
|
|
1
|
+
import type { AuthCreatedTokenDto, AuthTokenDto } from '@frigear-nu/auth/utils';
|
|
2
|
+
export type ApiToken = AuthTokenDto;
|
|
12
3
|
export interface ApiTokenScope {
|
|
13
4
|
name: string;
|
|
14
5
|
description?: string;
|
|
15
6
|
}
|
|
16
7
|
/** The response to creating a token: the only time its plaintext `token` exists. */
|
|
17
|
-
export type CreatedApiToken =
|
|
18
|
-
token: string;
|
|
19
|
-
};
|
|
8
|
+
export type CreatedApiToken = AuthCreatedTokenDto;
|
|
@@ -39,14 +39,14 @@
|
|
|
39
39
|
"system": "System"
|
|
40
40
|
},
|
|
41
41
|
"overview": {
|
|
42
|
-
"title": "Konto",
|
|
43
|
-
"description": "Dine oplysninger og alt, der hører til din konto.",
|
|
44
42
|
"shortcuts": "Din konto",
|
|
45
43
|
"empty": "Her er ikke noget endnu."
|
|
46
44
|
},
|
|
47
45
|
"profile": {
|
|
48
|
-
"
|
|
49
|
-
|
|
46
|
+
"details": {
|
|
47
|
+
"title": "Navn og avatar",
|
|
48
|
+
"description": "Sådan ser andre dig."
|
|
49
|
+
},
|
|
50
50
|
"name": {
|
|
51
51
|
"label": "Navn",
|
|
52
52
|
"description": "Dit fulde navn.",
|
|
@@ -54,10 +54,12 @@
|
|
|
54
54
|
},
|
|
55
55
|
"email": {
|
|
56
56
|
"label": "E-mail",
|
|
57
|
-
"
|
|
57
|
+
"address": "Adresse",
|
|
58
|
+
"description": "Hvor vi sender beskeder, og hvordan du logger ind.",
|
|
58
59
|
"change": {
|
|
59
60
|
"title": "Skift din e-mail",
|
|
60
61
|
"label": "Ny e-mail",
|
|
62
|
+
"current": "Din adresse er nu {email}.",
|
|
61
63
|
"sent": {
|
|
62
64
|
"title": "Tjek din indbakke",
|
|
63
65
|
"description": "Vi har sendt et link til din nye adresse. Ændringen træder først i kraft når du bekræfter den."
|
|
@@ -76,13 +78,13 @@
|
|
|
76
78
|
},
|
|
77
79
|
"save": "Gem profil",
|
|
78
80
|
"saved": "Profil opdateret",
|
|
79
|
-
"saveFailed": "Kunne ikke opdatere din profil."
|
|
80
|
-
"cancel": "Annuller"
|
|
81
|
+
"saveFailed": "Kunne ikke opdatere din profil."
|
|
81
82
|
},
|
|
82
83
|
"documents": {
|
|
83
84
|
"title": "Dokumenter",
|
|
84
85
|
"description": "Dine dokumenter.",
|
|
85
86
|
"download": "Download",
|
|
87
|
+
"downloadNamed": "Download {name}",
|
|
86
88
|
"empty": "Ingen dokumenter til dig endnu."
|
|
87
89
|
},
|
|
88
90
|
"security": {
|
|
@@ -112,7 +114,7 @@
|
|
|
112
114
|
},
|
|
113
115
|
"passkeys": {
|
|
114
116
|
"title": "Passkeys",
|
|
115
|
-
"
|
|
117
|
+
"add": "Tilføj passkey",
|
|
116
118
|
"description": "Log ind med fingeraftryk, ansigt eller enhedens pinkode i stedet for et password.",
|
|
117
119
|
"unnamed": "Passkey",
|
|
118
120
|
"created": "Tilføjet {date}",
|
|
@@ -124,6 +126,7 @@
|
|
|
124
126
|
"empty": "Du har ikke tilføjet nogen passkeys endnu.",
|
|
125
127
|
"name": {
|
|
126
128
|
"label": "Navn på passkey",
|
|
129
|
+
"description": "Valgfrit. Gør det nemmere at kende dine passkeys fra hinanden.",
|
|
127
130
|
"placeholder": "Navn, fx MacBook"
|
|
128
131
|
},
|
|
129
132
|
"registered": "Passkey tilføjet.",
|
|
@@ -134,7 +137,6 @@
|
|
|
134
137
|
"tryAgain": "Prøv igen"
|
|
135
138
|
}
|
|
136
139
|
},
|
|
137
|
-
"signedInAs": "Logget ind som",
|
|
138
140
|
"apiTokens": {
|
|
139
141
|
"title": "API-nøgler",
|
|
140
142
|
"description": "Opret nøgler til scripts og værktøjer.",
|
|
@@ -39,14 +39,14 @@
|
|
|
39
39
|
"system": "System"
|
|
40
40
|
},
|
|
41
41
|
"overview": {
|
|
42
|
-
"title": "Account",
|
|
43
|
-
"description": "Your details and everything that belongs to your account.",
|
|
44
42
|
"shortcuts": "Your account",
|
|
45
43
|
"empty": "Nothing here yet."
|
|
46
44
|
},
|
|
47
45
|
"profile": {
|
|
48
|
-
"
|
|
49
|
-
|
|
46
|
+
"details": {
|
|
47
|
+
"title": "Name and avatar",
|
|
48
|
+
"description": "How you appear to others."
|
|
49
|
+
},
|
|
50
50
|
"name": {
|
|
51
51
|
"label": "Name",
|
|
52
52
|
"description": "Your full name.",
|
|
@@ -54,10 +54,12 @@
|
|
|
54
54
|
},
|
|
55
55
|
"email": {
|
|
56
56
|
"label": "E-mail",
|
|
57
|
-
"
|
|
57
|
+
"address": "Address",
|
|
58
|
+
"description": "Where we send messages and how you sign in.",
|
|
58
59
|
"change": {
|
|
59
60
|
"title": "Change e-mail",
|
|
60
61
|
"label": "New e-mail",
|
|
62
|
+
"current": "Your address is now {email}.",
|
|
61
63
|
"sent": {
|
|
62
64
|
"title": "Check your inbox",
|
|
63
65
|
"description": "We sent a link to your new address. It takes effect once you confirm it."
|
|
@@ -76,13 +78,13 @@
|
|
|
76
78
|
},
|
|
77
79
|
"save": "Save profile",
|
|
78
80
|
"saved": "Profile updated",
|
|
79
|
-
"saveFailed": "Could not update your profile."
|
|
80
|
-
"cancel": "Cancel"
|
|
81
|
+
"saveFailed": "Could not update your profile."
|
|
81
82
|
},
|
|
82
83
|
"documents": {
|
|
83
84
|
"title": "Documents",
|
|
84
85
|
"description": "Your documents.",
|
|
85
86
|
"download": "Download",
|
|
87
|
+
"downloadNamed": "Download {name}",
|
|
86
88
|
"empty": "No documents for you yet."
|
|
87
89
|
},
|
|
88
90
|
"security": {
|
|
@@ -112,7 +114,7 @@
|
|
|
112
114
|
},
|
|
113
115
|
"passkeys": {
|
|
114
116
|
"title": "Passkeys",
|
|
115
|
-
"
|
|
117
|
+
"add": "Add passkey",
|
|
116
118
|
"description": "Sign in with your fingerprint, face or device PIN instead of a password.",
|
|
117
119
|
"unnamed": "Passkey",
|
|
118
120
|
"created": "Added {date}",
|
|
@@ -124,6 +126,7 @@
|
|
|
124
126
|
"empty": "You have not added any passkeys yet.",
|
|
125
127
|
"name": {
|
|
126
128
|
"label": "Passkey name",
|
|
129
|
+
"description": "Optional. Helps you tell your passkeys apart.",
|
|
127
130
|
"placeholder": "Name, e.g. MacBook"
|
|
128
131
|
},
|
|
129
132
|
"registered": "Passkey added.",
|
|
@@ -134,7 +137,6 @@
|
|
|
134
137
|
"tryAgain": "Try again"
|
|
135
138
|
}
|
|
136
139
|
},
|
|
137
|
-
"signedInAs": "Signed in as",
|
|
138
140
|
"apiTokens": {
|
|
139
141
|
"title": "API tokens",
|
|
140
142
|
"description": "Create tokens for scripts and tools.",
|