@frigear-nu/account 0.0.6 → 0.0.8
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 +20 -6
- package/dist/module.d.mts +31 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +41 -36
- package/dist/runtime/app/components/Account/AccountHeader.d.vue.ts +3 -0
- package/dist/runtime/app/components/Account/AccountHeader.vue +59 -0
- package/dist/runtime/app/components/Account/AccountHeader.vue.d.ts +3 -0
- package/dist/runtime/app/components/Account/AccountMenu.vue +7 -2
- package/dist/runtime/app/components/Account/IdentificationCode.d.vue.ts +3 -0
- package/dist/runtime/app/components/Account/IdentificationCode.vue +82 -0
- package/dist/runtime/app/components/Account/IdentificationCode.vue.d.ts +3 -0
- package/dist/runtime/app/components/Account/IdentificationCodeDialog.d.vue.ts +20 -0
- package/dist/runtime/app/components/Account/IdentificationCodeDialog.vue +16 -0
- package/dist/runtime/app/components/Account/IdentificationCodeDialog.vue.d.ts +20 -0
- package/dist/runtime/app/composables/useAccount.js +3 -3
- package/dist/runtime/app/pages/api-tokens.d.vue.ts +3 -0
- package/dist/runtime/app/pages/api-tokens.vue +308 -0
- package/dist/runtime/app/pages/api-tokens.vue.d.ts +3 -0
- package/dist/runtime/app/pages/documents.d.vue.ts +3 -0
- package/dist/runtime/app/pages/documents.vue +38 -0
- package/dist/runtime/app/pages/documents.vue.d.ts +3 -0
- package/dist/runtime/app/pages/id-code.d.vue.ts +3 -0
- package/dist/runtime/app/pages/id-code.vue +13 -0
- package/dist/runtime/app/pages/id-code.vue.d.ts +3 -0
- package/dist/runtime/app/pages/layout.d.vue.ts +3 -0
- package/dist/runtime/app/pages/layout.vue +10 -0
- package/dist/runtime/app/pages/layout.vue.d.ts +3 -0
- package/dist/runtime/app/pages/profile.vue +188 -46
- package/dist/runtime/app/pages/security.vue +180 -60
- package/dist/runtime/app/utils/documents.d.ts +5 -0
- package/dist/runtime/app/utils/documents.js +7 -0
- package/dist/runtime/i18n/lang/da.json +138 -14
- package/dist/runtime/i18n/lang/en.json +138 -14
- package/dist/runtime/menus.js +42 -0
- package/dist/runtime/server/routes/account/id-code.get.js +20 -0
- package/package.json +25 -11
- package/dist/runtime/server/db/schema.sqlite.d.ts +0 -24
- package/dist/runtime/server/db/schema.sqlite.js +0 -4
- package/dist/runtime/server/routes/account/change-email.post.js +0 -20
- package/dist/runtime/server/routes/account/change-password.post.d.ts +0 -2
- package/dist/runtime/server/routes/account/change-password.post.js +0 -26
- package/dist/runtime/server/routes/account/profile.post.d.ts +0 -2
- package/dist/runtime/server/routes/account/profile.post.js +0 -17
- /package/dist/runtime/server/routes/account/{change-email.post.d.ts → id-code.get.d.ts} +0 -0
|
@@ -1,90 +1,210 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
defineOptions({ name: "AccountSecurityPage" });
|
|
3
4
|
const t = useModuleT("account.security");
|
|
5
|
+
const toast = useToast();
|
|
4
6
|
const { user } = useUserSession();
|
|
5
|
-
const { changePassword, pending
|
|
6
|
-
const webAuthnEnabled = useRuntimeConfig().public.
|
|
7
|
-
const passkeys = webAuthnEnabled ?
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
7
|
+
const { changePassword, pending } = useAccount();
|
|
8
|
+
const webAuthnEnabled = useRuntimeConfig().public.authentication?.webAuthn === true;
|
|
9
|
+
const passkeys = webAuthnEnabled ? usePasskeys() : void 0;
|
|
10
|
+
function errorMessage(e, fallback) {
|
|
11
|
+
const err = e;
|
|
12
|
+
return err?.data?.statusMessage ?? err?.statusMessage ?? fallback;
|
|
13
|
+
}
|
|
14
|
+
const passwordSchema = z.object({
|
|
15
|
+
currentPassword: z.string().min(1, t("changePassword.current.required", "Enter your current password.")),
|
|
16
|
+
newPassword: z.string().min(8, t("changePassword.new.min", "Use at least 8 characters.")),
|
|
17
|
+
confirmNewPassword: z.string()
|
|
18
|
+
}).refine((data) => data.newPassword === data.confirmNewPassword, {
|
|
19
|
+
message: t("changePassword.confirm.mismatch", "The passwords do not match."),
|
|
20
|
+
path: ["confirmNewPassword"]
|
|
21
|
+
});
|
|
22
|
+
const passwordState = reactive({ currentPassword: "", newPassword: "", confirmNewPassword: "" });
|
|
23
|
+
async function onSubmitPassword(event) {
|
|
24
|
+
try {
|
|
25
|
+
await changePassword(event.data);
|
|
26
|
+
passwordState.currentPassword = "";
|
|
27
|
+
passwordState.newPassword = "";
|
|
28
|
+
passwordState.confirmNewPassword = "";
|
|
29
|
+
toast.add({ title: t("changePassword.success", "Your password was changed."), color: "success" });
|
|
30
|
+
} catch (e) {
|
|
31
|
+
toast.add({ title: errorMessage(e, t("changePassword.error", "Could not change your password.")), color: "error" });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const passkeyList = ref([]);
|
|
35
|
+
const passkeyName = ref("");
|
|
11
36
|
const passkeyPending = ref(false);
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
37
|
+
const passkeysLoading = ref(false);
|
|
38
|
+
const removingId = ref(null);
|
|
39
|
+
async function loadPasskeys() {
|
|
40
|
+
if (!passkeys) return;
|
|
41
|
+
passkeysLoading.value = true;
|
|
42
|
+
try {
|
|
43
|
+
passkeyList.value = await passkeys.listPasskeys();
|
|
44
|
+
} catch (e) {
|
|
45
|
+
toast.add({ title: errorMessage(e, t("passkeys.loadError", "Could not load your passkeys.")), color: "error" });
|
|
46
|
+
} finally {
|
|
47
|
+
passkeysLoading.value = false;
|
|
48
|
+
}
|
|
21
49
|
}
|
|
22
50
|
async function onRegisterPasskey() {
|
|
23
51
|
passkeyPending.value = true;
|
|
24
52
|
try {
|
|
25
|
-
await passkeys?.registerPasskey(user.value?.email ?? "");
|
|
53
|
+
await passkeys?.registerPasskey(user.value?.email ?? "", passkeyName.value);
|
|
54
|
+
passkeyName.value = "";
|
|
55
|
+
toast.add({ title: t("passkeys.registered", "Passkey added."), color: "success" });
|
|
56
|
+
await loadPasskeys();
|
|
57
|
+
} catch (e) {
|
|
58
|
+
if (e?.name !== "NotAllowedError") {
|
|
59
|
+
toast.add({ title: errorMessage(e, t("passkeys.registerError", "Could not register the passkey.")), color: "error" });
|
|
60
|
+
}
|
|
26
61
|
} finally {
|
|
27
62
|
passkeyPending.value = false;
|
|
28
63
|
}
|
|
29
64
|
}
|
|
65
|
+
async function onRemovePasskey(id) {
|
|
66
|
+
removingId.value = id;
|
|
67
|
+
try {
|
|
68
|
+
await passkeys?.removePasskey(id);
|
|
69
|
+
toast.add({ title: t("passkeys.removed", "Passkey removed."), color: "success" });
|
|
70
|
+
await loadPasskeys();
|
|
71
|
+
} catch (e) {
|
|
72
|
+
toast.add({ title: errorMessage(e, t("passkeys.removeError", "Could not remove the passkey.")), color: "error" });
|
|
73
|
+
} finally {
|
|
74
|
+
removingId.value = null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const dateFormatter = new Intl.DateTimeFormat(void 0, { dateStyle: "medium" });
|
|
78
|
+
function formatDate(value) {
|
|
79
|
+
const date = new Date(value);
|
|
80
|
+
return Number.isNaN(date.getTime()) ? "" : dateFormatter.format(date);
|
|
81
|
+
}
|
|
82
|
+
onMounted(loadPasskeys);
|
|
30
83
|
</script>
|
|
31
84
|
|
|
32
85
|
<template>
|
|
33
86
|
<UContainer>
|
|
34
87
|
<UPage>
|
|
35
88
|
<UPageBody>
|
|
36
|
-
<UPageCard
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
<UFormField
|
|
47
|
-
:label="t('changePassword.new.label', 'New password')"
|
|
48
|
-
name="newPassword"
|
|
49
|
-
>
|
|
50
|
-
<UInput
|
|
51
|
-
v-model="newPassword"
|
|
52
|
-
type="password"
|
|
53
|
-
/>
|
|
54
|
-
</UFormField>
|
|
55
|
-
<UFormField
|
|
56
|
-
:label="t('changePassword.confirm.label', 'Confirm new password')"
|
|
57
|
-
name="confirmNewPassword"
|
|
89
|
+
<UPageCard
|
|
90
|
+
:title="t('password.title', 'Password')"
|
|
91
|
+
:description="t('password.description', 'Change your password.')"
|
|
92
|
+
variant="subtle"
|
|
93
|
+
>
|
|
94
|
+
<UForm
|
|
95
|
+
:schema="passwordSchema"
|
|
96
|
+
:state="passwordState"
|
|
97
|
+
class="space-y-4"
|
|
98
|
+
@submit="onSubmitPassword"
|
|
58
99
|
>
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
100
|
+
<UFormField
|
|
101
|
+
:label="t('changePassword.current.label', 'Current password')"
|
|
102
|
+
name="currentPassword"
|
|
103
|
+
required
|
|
104
|
+
>
|
|
105
|
+
<UInput
|
|
106
|
+
v-model="passwordState.currentPassword"
|
|
107
|
+
type="password"
|
|
108
|
+
autocomplete="current-password"
|
|
109
|
+
class="w-full"
|
|
110
|
+
/>
|
|
111
|
+
</UFormField>
|
|
112
|
+
<UFormField
|
|
113
|
+
:label="t('changePassword.new.label', 'New password')"
|
|
114
|
+
name="newPassword"
|
|
115
|
+
required
|
|
116
|
+
>
|
|
117
|
+
<UInput
|
|
118
|
+
v-model="passwordState.newPassword"
|
|
119
|
+
type="password"
|
|
120
|
+
autocomplete="new-password"
|
|
121
|
+
class="w-full"
|
|
122
|
+
/>
|
|
123
|
+
</UFormField>
|
|
124
|
+
<UFormField
|
|
125
|
+
:label="t('changePassword.confirm.label', 'Confirm new password')"
|
|
126
|
+
name="confirmNewPassword"
|
|
127
|
+
required
|
|
128
|
+
>
|
|
129
|
+
<UInput
|
|
130
|
+
v-model="passwordState.confirmNewPassword"
|
|
131
|
+
type="password"
|
|
132
|
+
autocomplete="new-password"
|
|
133
|
+
class="w-full"
|
|
134
|
+
/>
|
|
135
|
+
</UFormField>
|
|
136
|
+
<UButton
|
|
137
|
+
type="submit"
|
|
138
|
+
:loading="pending"
|
|
139
|
+
:label="t('changePassword.submit', 'Change password')"
|
|
62
140
|
/>
|
|
63
|
-
</
|
|
64
|
-
<UButton
|
|
65
|
-
class="mt-4"
|
|
66
|
-
:loading="pending"
|
|
67
|
-
:label="t('changePassword.submit', 'Change password')"
|
|
68
|
-
@click="onSubmitPassword"
|
|
69
|
-
/>
|
|
141
|
+
</UForm>
|
|
70
142
|
</UPageCard>
|
|
143
|
+
|
|
71
144
|
<UPageCard
|
|
72
145
|
v-if="passkeys?.isEnabled.value"
|
|
73
|
-
class="mt-4"
|
|
74
146
|
:title="t('passkeys.title', 'Passkeys')"
|
|
147
|
+
:description="t('passkeys.description', 'Sign in with your fingerprint, face or device PIN instead of a password.')"
|
|
148
|
+
variant="subtle"
|
|
75
149
|
>
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
150
|
+
<ul
|
|
151
|
+
v-if="passkeyList.length"
|
|
152
|
+
class="divide-y divide-default"
|
|
153
|
+
>
|
|
154
|
+
<li
|
|
155
|
+
v-for="passkey in passkeyList"
|
|
156
|
+
:key="passkey.id"
|
|
157
|
+
class="flex items-center justify-between gap-3 py-3"
|
|
158
|
+
>
|
|
159
|
+
<div class="flex min-w-0 items-center gap-3">
|
|
160
|
+
<UIcon
|
|
161
|
+
name="i-lucide-key-round"
|
|
162
|
+
class="size-5 shrink-0 text-muted"
|
|
163
|
+
/>
|
|
164
|
+
<div class="min-w-0">
|
|
165
|
+
<p class="truncate font-medium text-highlighted">
|
|
166
|
+
{{ passkey.name || t("passkeys.unnamed", "Passkey") }}
|
|
167
|
+
</p>
|
|
168
|
+
<p class="text-sm text-muted">
|
|
169
|
+
{{ t("passkeys.created", "Added {date}", { date: formatDate(passkey.createdAt) }) }}
|
|
170
|
+
</p>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
<UButton
|
|
174
|
+
color="error"
|
|
175
|
+
variant="ghost"
|
|
176
|
+
icon="i-lucide-trash-2"
|
|
177
|
+
:loading="removingId === passkey.id"
|
|
178
|
+
:aria-label="t('passkeys.remove', 'Remove passkey')"
|
|
179
|
+
@click="onRemovePasskey(passkey.id)"
|
|
180
|
+
/>
|
|
181
|
+
</li>
|
|
182
|
+
</ul>
|
|
183
|
+
<p
|
|
184
|
+
v-else-if="!passkeysLoading"
|
|
185
|
+
class="text-sm text-muted"
|
|
186
|
+
>
|
|
187
|
+
{{ t("passkeys.empty", "You have not added any passkeys yet.") }}
|
|
188
|
+
</p>
|
|
189
|
+
|
|
190
|
+
<form
|
|
191
|
+
class="flex flex-col gap-2 sm:flex-row"
|
|
192
|
+
@submit.prevent="onRegisterPasskey"
|
|
193
|
+
>
|
|
194
|
+
<UInput
|
|
195
|
+
v-model="passkeyName"
|
|
196
|
+
:placeholder="t('passkeys.name.placeholder', 'Name, e.g. MacBook')"
|
|
197
|
+
:aria-label="t('passkeys.name.label', 'Passkey name')"
|
|
198
|
+
maxlength="100"
|
|
199
|
+
class="w-full sm:max-w-xs"
|
|
200
|
+
/>
|
|
201
|
+
<UButton
|
|
202
|
+
type="submit"
|
|
203
|
+
:loading="passkeyPending"
|
|
204
|
+
:label="t('passkeys.register', 'Register a passkey')"
|
|
205
|
+
/>
|
|
206
|
+
</form>
|
|
81
207
|
</UPageCard>
|
|
82
|
-
<UAlert
|
|
83
|
-
v-if="error"
|
|
84
|
-
class="mt-4"
|
|
85
|
-
color="error"
|
|
86
|
-
:title="error"
|
|
87
|
-
/>
|
|
88
208
|
</UPageBody>
|
|
89
209
|
</UPage>
|
|
90
210
|
</UContainer>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AccountDocument } from '../../../types.js';
|
|
2
|
+
/** Label in `locale`, falling back to `en`, then the first available value. */
|
|
3
|
+
export declare function documentLabel(document: AccountDocument, locale: string): string;
|
|
4
|
+
/** The documents a user with `role` may see: everything without `roles`, otherwise only matching roles. */
|
|
5
|
+
export declare function visibleDocuments(documents: AccountDocument[], role: string | undefined): AccountDocument[];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function documentLabel(document, locale) {
|
|
2
|
+
if (typeof document.label === "string") return document.label;
|
|
3
|
+
return document.label[locale] ?? document.label.en ?? Object.values(document.label)[0] ?? document.id;
|
|
4
|
+
}
|
|
5
|
+
export function visibleDocuments(documents, role) {
|
|
6
|
+
return documents.filter((document) => !document.roles?.length || !!role && document.roles.includes(role));
|
|
7
|
+
}
|
|
@@ -2,38 +2,76 @@
|
|
|
2
2
|
"account": {
|
|
3
3
|
"title": "Konto",
|
|
4
4
|
"menu": {
|
|
5
|
-
"label": "Konto Menu"
|
|
5
|
+
"label": "Konto Menu",
|
|
6
|
+
"account": {
|
|
7
|
+
"label": "Konto",
|
|
8
|
+
"description": "Din konto."
|
|
9
|
+
},
|
|
10
|
+
"profile": {
|
|
11
|
+
"label": "Profil",
|
|
12
|
+
"description": "Administrer dine profiloplysninger og e-mailadresse."
|
|
13
|
+
},
|
|
14
|
+
"security": {
|
|
15
|
+
"label": "Sikkerhed",
|
|
16
|
+
"description": "Skift din adgangskode og administrer adgangsnøgler."
|
|
17
|
+
},
|
|
18
|
+
"documents": {
|
|
19
|
+
"label": "Dokumenter",
|
|
20
|
+
"description": "Håndbøger og andre filer til dig."
|
|
21
|
+
},
|
|
22
|
+
"apiTokens": {
|
|
23
|
+
"label": "API-nøgler",
|
|
24
|
+
"description": "Opret nøgler til scripts og værktøjer."
|
|
25
|
+
}
|
|
6
26
|
},
|
|
7
27
|
"identificationCode": {
|
|
8
28
|
"title": "Frigear ID",
|
|
9
|
-
"description": "Tryk her for at vise dit ID."
|
|
29
|
+
"description": "Tryk her for at vise dit ID.",
|
|
30
|
+
"expiresIn": "Fornyes om {time}s",
|
|
31
|
+
"refresh": "Opdater",
|
|
32
|
+
"role": "Rolle",
|
|
33
|
+
"error": "Kunne ikke oprette dit Frigear ID."
|
|
10
34
|
},
|
|
11
35
|
"profile": {
|
|
12
36
|
"title": "Profil",
|
|
13
37
|
"description": "Skift dine brugeroplysninger.",
|
|
14
38
|
"name": {
|
|
15
39
|
"label": "Navn",
|
|
16
|
-
"description": "Dit fulde navn."
|
|
40
|
+
"description": "Dit fulde navn.",
|
|
41
|
+
"required": "Indtast dit navn."
|
|
17
42
|
},
|
|
18
43
|
"email": {
|
|
19
44
|
"label": "E-mail",
|
|
20
45
|
"description": "Din awesome e-mail.",
|
|
21
46
|
"change": {
|
|
22
47
|
"title": "Skift din e-mail",
|
|
23
|
-
"label": "Ny e-mail"
|
|
24
|
-
|
|
48
|
+
"label": "Ny e-mail",
|
|
49
|
+
"sent": {
|
|
50
|
+
"title": "Tjek din indbakke",
|
|
51
|
+
"description": "Vi har sendt et link til din nye adresse. Ændringen træder først i kraft når du bekræfter den."
|
|
52
|
+
},
|
|
53
|
+
"done": "Din e-mailadresse er ændret.",
|
|
54
|
+
"failed": "Bekræftelseslinket er ugyldigt eller udløbet.",
|
|
55
|
+
"error": "Kunne ikke ændre din e-mailadresse."
|
|
56
|
+
},
|
|
57
|
+
"invalid": "Indtast en gyldig e-mailadresse."
|
|
25
58
|
},
|
|
26
59
|
"avatar": {
|
|
27
60
|
"label": "Avatar",
|
|
28
|
-
"description": "
|
|
29
|
-
"url": "Avatar URL"
|
|
61
|
+
"description": "Link til et kvadratisk billede.",
|
|
62
|
+
"url": "Avatar URL",
|
|
63
|
+
"invalid": "Indtast en gyldig URL."
|
|
30
64
|
},
|
|
31
|
-
"save": "Gem profil"
|
|
65
|
+
"save": "Gem profil",
|
|
66
|
+
"saved": "Profil opdateret",
|
|
67
|
+
"saveFailed": "Kunne ikke opdatere din profil.",
|
|
68
|
+
"cancel": "Annuller"
|
|
32
69
|
},
|
|
33
70
|
"documents": {
|
|
34
71
|
"title": "Dokumenter",
|
|
35
72
|
"description": "Dine dokumenter.",
|
|
36
|
-
"download": "Download"
|
|
73
|
+
"download": "Download",
|
|
74
|
+
"empty": "Ingen dokumenter til dig endnu."
|
|
37
75
|
},
|
|
38
76
|
"security": {
|
|
39
77
|
"title": "Sikkerhed",
|
|
@@ -46,19 +84,105 @@
|
|
|
46
84
|
"title": "Skift password",
|
|
47
85
|
"submit": "Skift password",
|
|
48
86
|
"new": {
|
|
49
|
-
"label": "Nyt password"
|
|
87
|
+
"label": "Nyt password",
|
|
88
|
+
"min": "Brug mindst 8 tegn."
|
|
50
89
|
},
|
|
51
90
|
"confirm": {
|
|
52
|
-
"label": "Bekræft nyt password"
|
|
91
|
+
"label": "Bekræft nyt password",
|
|
92
|
+
"mismatch": "Passwords er ikke ens."
|
|
53
93
|
},
|
|
54
94
|
"current": {
|
|
55
|
-
"label": "Nuværende password"
|
|
56
|
-
|
|
95
|
+
"label": "Nuværende password",
|
|
96
|
+
"required": "Indtast dit nuværende password."
|
|
97
|
+
},
|
|
98
|
+
"success": "Dit password er skiftet.",
|
|
99
|
+
"error": "Kunne ikke skifte dit password."
|
|
57
100
|
},
|
|
58
101
|
"passkeys": {
|
|
59
102
|
"title": "Passkeys",
|
|
60
|
-
"register": "Registrér en passkey"
|
|
103
|
+
"register": "Registrér en passkey",
|
|
104
|
+
"description": "Log ind med fingeraftryk, ansigt eller enhedens pinkode i stedet for et password.",
|
|
105
|
+
"unnamed": "Passkey",
|
|
106
|
+
"created": "Tilføjet {date}",
|
|
107
|
+
"remove": "Fjern passkey",
|
|
108
|
+
"empty": "Du har ikke tilføjet nogen passkeys endnu.",
|
|
109
|
+
"name": {
|
|
110
|
+
"label": "Navn på passkey",
|
|
111
|
+
"placeholder": "Navn, fx MacBook"
|
|
112
|
+
},
|
|
113
|
+
"registered": "Passkey tilføjet.",
|
|
114
|
+
"registerError": "Kunne ikke registrere passkey.",
|
|
115
|
+
"removed": "Passkey fjernet.",
|
|
116
|
+
"removeError": "Kunne ikke fjerne passkey.",
|
|
117
|
+
"loadError": "Kunne ikke hente dine passkeys."
|
|
61
118
|
}
|
|
119
|
+
},
|
|
120
|
+
"backTo": "Tilbage til konto",
|
|
121
|
+
"signedInAs": "Logget ind som",
|
|
122
|
+
"apiTokens": {
|
|
123
|
+
"title": "API-nøgler",
|
|
124
|
+
"description": "Opret nøgler til scripts og værktøjer.",
|
|
125
|
+
"allScopes": "Fuld adgang",
|
|
126
|
+
"copy": "Kopiér",
|
|
127
|
+
"copied": "Nøglen er kopieret.",
|
|
128
|
+
"copiedSnippet": "Kopieret.",
|
|
129
|
+
"copyError": "Kunne ikke kopiere. Markér nøglen og kopiér den manuelt.",
|
|
130
|
+
"create": {
|
|
131
|
+
"title": "Opret en API-nøgle",
|
|
132
|
+
"description": "Nøgler lader scripts og værktøjer som MCP-klienter handle på dine vegne. Giv hver nøgle kun de rettigheder, den har brug for.",
|
|
133
|
+
"submit": "Opret nøgle"
|
|
134
|
+
},
|
|
135
|
+
"createError": "Kunne ikke oprette nøglen.",
|
|
136
|
+
"name": {
|
|
137
|
+
"label": "Navn",
|
|
138
|
+
"placeholder": "fx Min MCP-klient",
|
|
139
|
+
"required": "Giv nøglen et navn."
|
|
140
|
+
},
|
|
141
|
+
"scopes": {
|
|
142
|
+
"label": "Rettigheder",
|
|
143
|
+
"placeholder": "Vælg hvad nøglen må",
|
|
144
|
+
"required": "Vælg mindst én rettighed."
|
|
145
|
+
},
|
|
146
|
+
"expiry": {
|
|
147
|
+
"label": "Udløber",
|
|
148
|
+
"30": "30 dage",
|
|
149
|
+
"90": "90 dage",
|
|
150
|
+
"365": "1 år",
|
|
151
|
+
"never": "Udløber aldrig"
|
|
152
|
+
},
|
|
153
|
+
"reveal": {
|
|
154
|
+
"title": "Kopiér din nye nøgle nu",
|
|
155
|
+
"description": "Det er eneste gang, den vises. Mister du den, skal du oprette en ny.",
|
|
156
|
+
"label": "Ny nøgle",
|
|
157
|
+
"done": "Færdig"
|
|
158
|
+
},
|
|
159
|
+
"connect": {
|
|
160
|
+
"title": "Forbind til MCP",
|
|
161
|
+
"description": "Brug din konto fra en AI-assistent. Din API-nøgle indsættes kun i kommandoerne lige efter, du har oprettet den.",
|
|
162
|
+
"claude": {
|
|
163
|
+
"steps": "Claude.ai og Claude Desktop: åbn Indstillinger → Connectors → Tilføj brugerdefineret connector, indsæt URL'en nedenfor og log ind, når du bliver bedt om det. Ingen nøgle nødvendig.",
|
|
164
|
+
"cli": "Claude Code (bruger en API-nøgle):"
|
|
165
|
+
},
|
|
166
|
+
"chatgpt": {
|
|
167
|
+
"steps": "ChatGPT: åbn Indstillinger → Connectors → Avanceret, slå udviklertilstand til, og opret en connector med URL'en nedenfor. Vælg OAuth og log ind, når du bliver bedt om det. Ingen nøgle nødvendig."
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
"list": {
|
|
171
|
+
"title": "Dine nøgler",
|
|
172
|
+
"description": "Træk en nøgle tilbage for at stoppe den med det samme.",
|
|
173
|
+
"empty": "Du har ikke oprettet nogen nøgler endnu.",
|
|
174
|
+
"created": "Oprettet {date}",
|
|
175
|
+
"lastUsed": "Sidst brugt {date}",
|
|
176
|
+
"expires": "Udløber {date}"
|
|
177
|
+
},
|
|
178
|
+
"status": {
|
|
179
|
+
"ok": "Aktiv",
|
|
180
|
+
"revoked": "Trukket tilbage",
|
|
181
|
+
"expired": "Udløbet"
|
|
182
|
+
},
|
|
183
|
+
"revoke": "Træk nøgle tilbage",
|
|
184
|
+
"revoked": "Nøglen er trukket tilbage.",
|
|
185
|
+
"revokeError": "Kunne ikke trække nøglen tilbage."
|
|
62
186
|
}
|
|
63
187
|
}
|
|
64
188
|
}
|