@atlasauth/svelte 0.7.0 → 0.9.0

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.
@@ -0,0 +1,38 @@
1
+ <script>import { createEventDispatcher } from "svelte";
2
+ import { derived } from "svelte/store";
3
+ import { actorFromClaims } from "@atlasauth/js";
4
+ import { useAtlas } from "../context";
5
+ import { classFor } from "../appearance";
6
+ import { translate } from "../i18n";
7
+ const atlas = useAtlas();
8
+ const dispatch = createEventDispatcher();
9
+ const t = (key, vars) => translate(atlas.catalog, key, vars);
10
+ const state = derived([atlas.claims, atlas.session, atlas.user], ([$claims, $session, $user]) => {
11
+ const actor = actorFromClaims($claims) ?? $session?.actor ?? null;
12
+ const name = [$user?.first_name, $user?.last_name].filter(Boolean).join(" ") || $user?.username || $user?.email_addresses?.find((e) => e.primary)?.email_address || $user?.id || "";
13
+ return { impersonating: actor !== null, canReturn: actor !== null && Boolean($session?.actor_session_id), name };
14
+ });
15
+ let busy = false;
16
+ async function end() {
17
+ busy = true;
18
+ try {
19
+ dispatch("ended", await atlas.endImpersonation());
20
+ } finally {
21
+ busy = false;
22
+ }
23
+ }
24
+ </script>
25
+
26
+ {#if $state.impersonating}
27
+ <div class={classFor('card', atlas.appearance, 'atlas-alert atlas-impersonation-banner')} role="status">
28
+ <span class={classFor('headerSubtitle', atlas.appearance, 'atlas-text')}>{t('impersonation.banner', { name: $state.name })}</span>
29
+ <button
30
+ type="button"
31
+ class={classFor('formButtonPrimary', atlas.appearance, 'atlas-button')}
32
+ disabled={busy}
33
+ on:click={() => void end()}
34
+ >
35
+ {$state.canReturn ? t('impersonation.returnToAdmin') : t('impersonation.end')}
36
+ </button>
37
+ </div>
38
+ {/if}
@@ -0,0 +1,19 @@
1
+ import { SvelteComponent } from "svelte";
2
+ import { type EndImpersonationResult } from '@atlasauth/js';
3
+ declare const __propDef: {
4
+ props: Record<string, never>;
5
+ events: {
6
+ ended: CustomEvent<EndImpersonationResult>;
7
+ } & {
8
+ [evt: string]: CustomEvent<any>;
9
+ };
10
+ slots: {};
11
+ exports?: {} | undefined;
12
+ bindings?: string | undefined;
13
+ };
14
+ export type ImpersonationBannerProps = typeof __propDef.props;
15
+ export type ImpersonationBannerEvents = typeof __propDef.events;
16
+ export type ImpersonationBannerSlots = typeof __propDef.slots;
17
+ export default class ImpersonationBanner extends SvelteComponent<ImpersonationBannerProps, ImpersonationBannerEvents, ImpersonationBannerSlots> {
18
+ }
19
+ export {};
@@ -0,0 +1,45 @@
1
+ <script>import { describeAuditAction } from "@atlasauth/js";
2
+ import { useAtlas } from "../context";
3
+ import { getOrganization, getOrganizationAuditLog } from "../accessors";
4
+ import { classFor } from "../appearance";
5
+ export let pageSize = void 0;
6
+ export let action = void 0;
7
+ export let labels = {};
8
+ export let formatDate = (ms) => new Date(ms).toLocaleString();
9
+ const atlas = useAtlas();
10
+ const org = getOrganization();
11
+ const log = getOrganizationAuditLog({ pageSize, action });
12
+ const cls = (key, base) => classFor(key, atlas.appearance, base);
13
+ </script>
14
+
15
+ {#if $org.organization && !$log.forbidden}
16
+ <div class={cls('card', 'atlas-card')} data-atlas="organization-audit-log">
17
+ <h2 class={cls('headerTitle', 'atlas-title')}>{labels.title ?? 'Audit log'}</h2>
18
+ {#if $log.entries.length === 0 && !$log.loading}
19
+ <p class={cls('headerSubtitle', 'atlas-subtitle')}>{labels.empty ?? 'No activity recorded yet.'}</p>
20
+ {:else}
21
+ <ul class={cls('list', 'atlas-audit-list')}>
22
+ {#each $log.entries as entry (entry.id)}
23
+ <li class={cls('listItem', 'atlas-audit-item')} data-action={entry.action}>
24
+ <strong>{describeAuditAction(entry.action)}</strong>
25
+ <span class="atlas-audit-meta">
26
+ · {formatDate(entry.created_at)}{entry.actor.id
27
+ ? ` · ${labels.actor ?? 'by'} ${entry.actor.type} ${entry.actor.id}`
28
+ : ''}
29
+ </span>
30
+ </li>
31
+ {/each}
32
+ </ul>
33
+ {/if}
34
+ {#if $log.hasMore}
35
+ <button
36
+ type="button"
37
+ class={cls('formButtonSecondary', 'atlas-button-secondary')}
38
+ disabled={$log.loading}
39
+ on:click={() => void log.loadMore()}
40
+ >
41
+ {labels.loadMore ?? 'Load more'}
42
+ </button>
43
+ {/if}
44
+ </div>
45
+ {/if}
@@ -0,0 +1,31 @@
1
+ import { SvelteComponent } from "svelte";
2
+ declare const __propDef: {
3
+ props: {
4
+ /**
5
+ * `<OrganizationAuditLog/>` — the active organization's audit trail for its
6
+ * admins, next to `<OrganizationProfile/>`. Renders nothing for a member who
7
+ * may not read it (the server is the authority; the component just follows
8
+ * it). Labels are props so an app can localise them.
9
+ */ pageSize?: number | undefined;
10
+ action?: string | undefined;
11
+ labels?: {
12
+ title?: string;
13
+ empty?: string;
14
+ loadMore?: string;
15
+ actor?: string;
16
+ };
17
+ formatDate?: (ms: number) => string;
18
+ };
19
+ events: {
20
+ [evt: string]: CustomEvent<any>;
21
+ };
22
+ slots: {};
23
+ exports?: {} | undefined;
24
+ bindings?: string | undefined;
25
+ };
26
+ export type OrganizationAuditLogProps = typeof __propDef.props;
27
+ export type OrganizationAuditLogEvents = typeof __propDef.events;
28
+ export type OrganizationAuditLogSlots = typeof __propDef.slots;
29
+ export default class OrganizationAuditLog extends SvelteComponent<OrganizationAuditLogProps, OrganizationAuditLogEvents, OrganizationAuditLogSlots> {
30
+ }
31
+ export {};
@@ -1,22 +1,318 @@
1
- <script>import { useAtlas } from "../context";
2
- import { getOrganization } from "../accessors";
1
+ <script>import { createEventDispatcher, onDestroy } from "svelte";
2
+ import {
3
+ OrganizationProfileController,
4
+ localizedErrorMessage,
5
+ reauthenticate
6
+ } from "@atlasauth/js";
7
+ import { useAtlas } from "../context";
8
+ import { getAuth, getOrganization } from "../accessors";
3
9
  import { classFor } from "../appearance";
4
10
  import { translate } from "../i18n";
5
- import Protect from "./Protect.svelte";
11
+ import { FapiClient } from "../fapi";
12
+ import { ATLAS_SVELTE_SDK } from "../version";
6
13
  const atlas = useAtlas();
7
14
  const org = getOrganization();
8
- const t = (key) => translate(atlas.catalog, key);
9
- $: cardClass = classFor("card", atlas.appearance, "atlas-card");
15
+ const auth = getAuth();
16
+ const dispatch = createEventDispatcher();
17
+ const t = (key, vars) => translate(atlas.catalog, key, vars);
18
+ const cls = (key, base) => classFor(key, atlas.appearance, base);
19
+ const errorText = (err) => localizedErrorMessage(err, atlas.errorCatalog ?? atlas.catalog, atlas.catalog);
20
+ const client = new FapiClient({
21
+ publishableKey: atlas.publishableKey,
22
+ baseUrl: atlas.frontendApi,
23
+ locale: atlas.locale,
24
+ sdk: ATLAS_SVELTE_SDK
25
+ });
26
+ let controller = null;
27
+ let state = null;
28
+ let unsubscribe = null;
29
+ let currentId = null;
30
+ let tab = "members";
31
+ let editingRole = null;
32
+ let roleKey = "";
33
+ let roleName = "";
34
+ let rolePerms = [];
35
+ let email = "";
36
+ let inviteRole = "org:member";
37
+ let confirm = null;
38
+ let needsPassword = false;
39
+ let password = "";
40
+ $: canManage = $auth.has({ permission: "org:sys_memberships:manage" });
41
+ $: isAdmin = $org.membership?.role === "org:admin";
42
+ $: roles = state && state.roles.length > 0 ? state.roles : [{ key: "org:member", name: "org:member" }];
43
+ $: errorMessage = state?.error && !state.error.needsReverification ? errorText(state.error) : null;
44
+ $: tabs = ["members", ...canManage ? ["invitations", "roles"] : [], "settings"];
45
+ $: customRoles = state ? state.roles.filter((r) => Boolean(r.organization_id)) : [];
46
+ $: {
47
+ const id = $org.organization?.id ?? null;
48
+ if (id !== currentId) {
49
+ currentId = id;
50
+ unsubscribe?.();
51
+ controller = null;
52
+ state = null;
53
+ if (id) {
54
+ controller = new OrganizationProfileController(client, id, {
55
+ canManage,
56
+ onChange: async () => {
57
+ await atlas.reload();
58
+ }
59
+ });
60
+ unsubscribe = controller.subscribe((next) => state = next);
61
+ state = controller.getState();
62
+ void controller.load();
63
+ }
64
+ }
65
+ }
66
+ onDestroy(() => unsubscribe?.());
67
+ const memberName = (m) => [m.user?.first_name, m.user?.last_name].filter(Boolean).join(" ") || t("organization.unnamedMember");
68
+ async function sendInvite() {
69
+ const address = email.trim();
70
+ if (controller && address && await controller.invite(address, inviteRole)) email = "";
71
+ }
72
+ async function leave() {
73
+ if (controller && await controller.leave()) dispatch("exit", "left");
74
+ confirm = null;
75
+ }
76
+ async function remove() {
77
+ if (!controller) return;
78
+ if (needsPassword) {
79
+ try {
80
+ await reauthenticate(
81
+ { strategy: "password", password },
82
+ { api: atlas.frontendApi, publishableKey: atlas.publishableKey, locale: atlas.locale }
83
+ );
84
+ } catch {
85
+ return;
86
+ }
87
+ password = "";
88
+ needsPassword = false;
89
+ }
90
+ if (await controller.deleteOrganization()) {
91
+ dispatch("exit", "deleted");
92
+ confirm = null;
93
+ return;
94
+ }
95
+ const err = controller.getState().error;
96
+ if (err?.needsReverification) needsPassword = true;
97
+ }
98
+ function resetRoleForm() {
99
+ editingRole = null;
100
+ roleKey = "";
101
+ roleName = "";
102
+ rolePerms = [];
103
+ }
104
+ function editRole(role) {
105
+ editingRole = role.id ?? null;
106
+ roleKey = role.key;
107
+ roleName = role.name;
108
+ rolePerms = [...role.permissions ?? []];
109
+ }
110
+ function togglePermission(key, on) {
111
+ rolePerms = on ? [...rolePerms, key] : rolePerms.filter((k) => k !== key);
112
+ }
113
+ async function saveRole() {
114
+ if (!controller) return;
115
+ const out = editingRole ? await controller.updateRole(editingRole, { name: roleName.trim(), permissions: rolePerms }) : await controller.createRole({ key: roleKey.trim(), name: roleName.trim(), permissions: rolePerms });
116
+ if (out) resetRoleForm();
117
+ }
118
+ function cancel() {
119
+ confirm = null;
120
+ needsPassword = false;
121
+ password = "";
122
+ }
10
123
  </script>
11
124
 
12
- {#if $org.organization}
13
- <div class={cardClass}>
125
+ {#if $org.organization && controller && state}
126
+ <div class={cls('card', 'atlas-card')} data-atlas="organization-profile">
14
127
  <h1>{$org.organization.name}</h1>
15
128
  <p>{t('organization.roleLabel')}: {$org.membership?.role}</p>
16
- <!-- Management controls are gated on the ACTIVE org role, matching the
17
- server's §9.2 rule rather than duplicating a looser one. -->
18
- <Protect permission="org:sys_memberships:manage">
19
- <button>{t('organization.invite')}</button>
20
- </Protect>
129
+
130
+ <div class={cls('menu', 'atlas-menu')} role="tablist">
131
+ {#each tabs as key}
132
+ <button
133
+ type="button"
134
+ role="tab"
135
+ aria-selected={tab === key}
136
+ class={cls('menuItem', 'atlas-menu-item')}
137
+ data-atlas={`organization-tab-${key}`}
138
+ on:click={() => (tab = key)}
139
+ >
140
+ {t(`organization.${key}`)}
141
+ </button>
142
+ {/each}
143
+ </div>
144
+
145
+ {#if errorMessage}
146
+ <p class={cls('formFieldError', 'atlas-error')} role="alert">{errorMessage}</p>
147
+ {/if}
148
+
149
+ {#if tab === 'members'}
150
+ <ul class={'atlas-list'} data-atlas="organization-members">
151
+ {#each state.members as m (m.id)}
152
+ <li data-atlas="organization-member">
153
+ <span>{memberName(m)}{m.user?.id === $auth.userId ? ` ${t('organization.you')}` : ''}</span>
154
+ {#if canManage && m.user && m.user.id !== $auth.userId}
155
+ <select
156
+ aria-label={t('organization.roleLabel')}
157
+ class={cls('formFieldInput', 'atlas-input')}
158
+ value={m.role}
159
+ disabled={state.busy === `role:${m.user.id}`}
160
+ on:change={(e) => m.user && controller?.changeRole(m.user.id, e.currentTarget.value)}
161
+ >
162
+ {#each roles as r}<option value={r.key}>{r.name}</option>{/each}
163
+ </select>
164
+ <button
165
+ type="button"
166
+ class={cls('formButtonSecondary', 'atlas-button-secondary')}
167
+ disabled={state.busy === `remove:${m.user.id}`}
168
+ on:click={() => m.user && controller?.removeMember(m.user.id)}
169
+ >
170
+ {t('organization.remove')}
171
+ </button>
172
+ {:else}
173
+ <span> · {m.role}</span>
174
+ {/if}
175
+ </li>
176
+ {/each}
177
+ </ul>
178
+ {/if}
179
+
180
+ {#if tab === 'invitations' && canManage}
181
+ <div data-atlas="organization-invitations">
182
+ <form on:submit|preventDefault={sendInvite}>
183
+ <label class={cls('formField', 'atlas-field')}>
184
+ <span class={cls('formFieldLabel', 'atlas-field-label')}>{t('organization.emailLabel')}</span>
185
+ <input type="email" class={cls('formFieldInput', 'atlas-input')} bind:value={email} />
186
+ </label>
187
+ <label class={cls('formField', 'atlas-field')}>
188
+ <span class={cls('formFieldLabel', 'atlas-field-label')}>{t('organization.roleLabel')}</span>
189
+ <select class={cls('formFieldInput', 'atlas-input')} bind:value={inviteRole}>
190
+ {#each roles as r}<option value={r.key}>{r.name}</option>{/each}
191
+ </select>
192
+ </label>
193
+ <button type="submit" class={cls('formButtonPrimary', 'atlas-button')} disabled={state.busy === 'invite' || !email.trim()}>
194
+ {t('organization.sendInvite')}
195
+ </button>
196
+ </form>
197
+ {#if state.invitations.length === 0}
198
+ <p class={'atlas-text'}>{t('organization.noInvitations')}</p>
199
+ {:else}
200
+ <ul class={'atlas-list'}>
201
+ {#each state.invitations as inv (inv.id)}
202
+ <li data-atlas="organization-invitation">
203
+ <span>{inv.email_address} · {inv.role ?? ''}</span>
204
+ <button type="button" class={cls('formButtonSecondary', 'atlas-button-secondary')} on:click={() => controller?.resendInvitation(inv.id)}>
205
+ {t('organization.resend')}
206
+ </button>
207
+ <button type="button" class={cls('formButtonSecondary', 'atlas-button-secondary')} on:click={() => controller?.revokeInvitation(inv.id)}>
208
+ {t('organization.revoke')}
209
+ </button>
210
+ </li>
211
+ {/each}
212
+ </ul>
213
+ {/if}
214
+ </div>
215
+ {/if}
216
+
217
+ {#if tab === 'roles' && canManage}
218
+ <div data-atlas="organization-roles">
219
+ {#if customRoles.length === 0}
220
+ <p class={'atlas-text'}>{t('organization.noCustomRoles')}</p>
221
+ {:else}
222
+ <ul class={'atlas-list'}>
223
+ {#each customRoles as role (role.id ?? role.key)}
224
+ <li data-atlas="organization-role">
225
+ <span>{role.name} · {role.key}</span>
226
+ <button type="button" class={cls('formButtonSecondary', 'atlas-button-secondary')} on:click={() => editRole(role)}>
227
+ {t('organization.editRole')}
228
+ </button>
229
+ <button
230
+ type="button"
231
+ class={cls('formButtonSecondary', 'atlas-button-secondary')}
232
+ disabled={!role.id || state.busy === `role:delete:${role.id}`}
233
+ on:click={() => role.id && controller?.deleteRole(role.id)}
234
+ >
235
+ {t('organization.deleteRole')}
236
+ </button>
237
+ </li>
238
+ {/each}
239
+ </ul>
240
+ {/if}
241
+ <form data-atlas="organization-role-form" on:submit|preventDefault={saveRole}>
242
+ {#if !editingRole}
243
+ <label class={cls('formField', 'atlas-field')}>
244
+ <span class={cls('formFieldLabel', 'atlas-field-label')}>{t('organization.roleKey')}</span>
245
+ <input name="role_key" class={cls('formFieldInput', 'atlas-input')} bind:value={roleKey} />
246
+ </label>
247
+ {/if}
248
+ <label class={cls('formField', 'atlas-field')}>
249
+ <span class={cls('formFieldLabel', 'atlas-field-label')}>{t('organization.roleName')}</span>
250
+ <input name="role_name" class={cls('formFieldInput', 'atlas-input')} bind:value={roleName} />
251
+ </label>
252
+ <fieldset class={cls('formField', 'atlas-field')}>
253
+ <legend class={cls('formFieldLabel', 'atlas-field-label')}>{t('organization.rolePermissions')}</legend>
254
+ {#each state.permissions as p (p.key)}
255
+ <label data-atlas="organization-role-permission">
256
+ <input
257
+ type="checkbox"
258
+ value={p.key}
259
+ checked={rolePerms.includes(p.key)}
260
+ on:change={(e) => togglePermission(p.key, e.currentTarget.checked)}
261
+ />
262
+ {p.name}
263
+ </label>
264
+ {/each}
265
+ </fieldset>
266
+ <button
267
+ type="submit"
268
+ class={cls('formButtonPrimary', 'atlas-button')}
269
+ disabled={!roleName.trim() || (!editingRole && !roleKey.trim()) || Boolean(state.busy?.startsWith('role:'))}
270
+ >
271
+ {editingRole ? t('organization.saveRole') : t('organization.createRole')}
272
+ </button>
273
+ {#if editingRole}
274
+ <button type="button" class={cls('formButtonSecondary', 'atlas-button-secondary')} on:click={resetRoleForm}>
275
+ {t('organization.cancel')}
276
+ </button>
277
+ {/if}
278
+ </form>
279
+ </div>
280
+ {/if}
281
+
282
+ {#if tab === 'settings'}
283
+ <div data-atlas="organization-settings">
284
+ {#if confirm}
285
+ <p class={'atlas-text'}>
286
+ {t(confirm === 'leave' ? 'organization.leaveConfirm' : 'organization.deleteConfirm', { name: $org.organization.name })}
287
+ </p>
288
+ {#if needsPassword}
289
+ <label class={cls('formField', 'atlas-field')}>
290
+ <span class={cls('formFieldLabel', 'atlas-field-label')}>{t('organization.passwordPrompt')}</span>
291
+ <input type="password" autocomplete="current-password" class={cls('formFieldInput', 'atlas-input')} bind:value={password} />
292
+ </label>
293
+ {/if}
294
+ <button
295
+ type="button"
296
+ class={cls('formButtonPrimary', 'atlas-button')}
297
+ disabled={state.busy === 'leave' || state.busy === 'delete'}
298
+ on:click={() => (confirm === 'leave' ? leave() : remove())}
299
+ >
300
+ {t('organization.confirm')}
301
+ </button>
302
+ <button type="button" class={cls('formButtonSecondary', 'atlas-button-secondary')} on:click={cancel}>
303
+ {t('organization.cancel')}
304
+ </button>
305
+ {:else}
306
+ <button type="button" class={cls('formButtonSecondary', 'atlas-button-secondary')} on:click={() => (confirm = 'leave')}>
307
+ {t('organization.leave')}
308
+ </button>
309
+ {#if isAdmin}
310
+ <button type="button" class={cls('formButtonSecondary', 'atlas-button-secondary')} on:click={() => (confirm = 'delete')}>
311
+ {t('organization.delete')}
312
+ </button>
313
+ {/if}
314
+ {/if}
315
+ </div>
316
+ {/if}
21
317
  </div>
22
318
  {/if}
@@ -2,6 +2,8 @@ import { SvelteComponent } from "svelte";
2
2
  declare const __propDef: {
3
3
  props: Record<string, never>;
4
4
  events: {
5
+ exit: CustomEvent<"left" | "deleted">;
6
+ } & {
5
7
  [evt: string]: CustomEvent<any>;
6
8
  };
7
9
  slots: {};
@@ -0,0 +1,23 @@
1
+ <script>import { localizedErrorMessage } from "@atlasauth/js";
2
+ import { useAtlas } from "../context";
3
+ import { classFor } from "../appearance";
4
+ import { translate } from "../i18n";
5
+ export let busy = false;
6
+ export let errors = [];
7
+ export let onCreate;
8
+ const atlas = useAtlas();
9
+ const cls = (key, base) => classFor(key, atlas.appearance, base);
10
+ const t = (key) => translate(atlas.catalog, key);
11
+ const errorText = (error) => localizedErrorMessage(error, atlas.errorCatalog ?? atlas.catalog, atlas.catalog);
12
+ </script>
13
+
14
+ <div class={cls('card', 'atlas-card')} data-atlas-passkey-sign-up="">
15
+ <h1 class={cls('headerTitle', 'atlas-title')}>{t('signUp.title')}</h1>
16
+ {#each errors as error (error.code)}
17
+ <p class={cls('formFieldError', 'atlas-error')} role="alert">{errorText(error)}</p>
18
+ {/each}
19
+ <p class="atlas-text">{t('signUp.passkeyPrompt')}</p>
20
+ <button class={cls('formButtonPrimary', 'atlas-button')} type="button" disabled={busy} on:click={() => onCreate()}>
21
+ {t('signUp.passkeyCreate')}
22
+ </button>
23
+ </div>
@@ -0,0 +1,24 @@
1
+ import { SvelteComponent } from "svelte";
2
+ import { type FieldError } from '@atlasauth/js';
3
+ declare const __propDef: {
4
+ props: {
5
+ /**
6
+ * The passkey-only sign-up's last step: create the passkey. The server
7
+ * creates the account only when this registration verifies.
8
+ */ busy?: boolean;
9
+ errors?: FieldError[];
10
+ onCreate: () => void;
11
+ };
12
+ events: {
13
+ [evt: string]: CustomEvent<any>;
14
+ };
15
+ slots: {};
16
+ exports?: {} | undefined;
17
+ bindings?: string | undefined;
18
+ };
19
+ export type PasskeySignUpStepProps = typeof __propDef.props;
20
+ export type PasskeySignUpStepEvents = typeof __propDef.events;
21
+ export type PasskeySignUpStepSlots = typeof __propDef.slots;
22
+ export default class PasskeySignUpStep extends SvelteComponent<PasskeySignUpStepProps, PasskeySignUpStepEvents, PasskeySignUpStepSlots> {
23
+ }
24
+ export {};
@@ -0,0 +1,55 @@
1
+ <script>import { canDownloadRecoveryCodes, copyRecoveryCodes, downloadRecoveryCodes } from "@atlasauth/js";
2
+ import { useAtlas } from "../context";
3
+ import { classFor } from "../appearance";
4
+ import { translate } from "../i18n";
5
+ export let codes;
6
+ export let busy = false;
7
+ export let onSaved;
8
+ const atlas = useAtlas();
9
+ const cls = (key, base) => classFor(key, atlas.appearance, base);
10
+ const t = (key) => translate(atlas.catalog, key);
11
+ const canCopy = typeof navigator !== "undefined" && !!navigator.clipboard?.writeText;
12
+ const canDownload = canDownloadRecoveryCodes();
13
+ let copied = false;
14
+ $: if (codes) copied = false;
15
+ async function copy() {
16
+ copied = await copyRecoveryCodes(codes);
17
+ }
18
+ </script>
19
+
20
+ <div class={cls('card', 'atlas-card')} data-atlas="recovery-codes">
21
+ <h1 class={cls('headerTitle', 'atlas-title')}>{t('mfa.recoveryCodesTitle')}</h1>
22
+ <p class="atlas-text">{t('mfa.recoveryCodesHelp')}</p>
23
+ <ul
24
+ class="atlas-recovery-codes"
25
+ data-atlas="recovery-codes-list"
26
+ style="list-style:none;padding:0;margin:8px 0 12px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;text-align:center"
27
+ >
28
+ {#each codes as code (code)}
29
+ <li><code>{code}</code></li>
30
+ {/each}
31
+ </ul>
32
+ <div class="atlas-alternatives">
33
+ {#if canCopy}
34
+ <button class={cls('formButtonSecondary', 'atlas-button-secondary')} type="button" data-atlas="recovery-codes-copy" on:click={() => void copy()}>
35
+ {t('mfa.recoveryCodesCopy')}
36
+ </button>
37
+ {/if}
38
+ {#if canDownload}
39
+ <button
40
+ class={cls('formButtonSecondary', 'atlas-button-secondary')}
41
+ type="button"
42
+ data-atlas="recovery-codes-download"
43
+ on:click={() => downloadRecoveryCodes(codes)}
44
+ >
45
+ {t('mfa.recoveryCodesDownload')}
46
+ </button>
47
+ {/if}
48
+ </div>
49
+ {#if copied}
50
+ <p class="atlas-text" aria-live="polite" data-atlas="recovery-codes-copied">{t('mfa.recoveryCodesCopied')}</p>
51
+ {/if}
52
+ <button class={cls('formButtonPrimary', 'atlas-button')} type="button" data-atlas="recovery-codes-saved" disabled={busy} on:click={onSaved}>
53
+ {t('mfa.recoveryCodesSaved')}
54
+ </button>
55
+ </div>
@@ -0,0 +1,25 @@
1
+ import { SvelteComponent } from "svelte";
2
+ declare const __propDef: {
3
+ props: {
4
+ /**
5
+ * The recovery codes a mid-sign-in MFA enrolment came with, shown once before
6
+ * the sign-in finishes: copy them, download them as a .txt file, then
7
+ * "I've saved my codes" (`onSaved`) exchanges the ticket. The codes live only
8
+ * in the flow's memory and are forgotten once saved.
9
+ */ codes: string[];
10
+ busy?: boolean;
11
+ onSaved: () => void;
12
+ };
13
+ events: {
14
+ [evt: string]: CustomEvent<any>;
15
+ };
16
+ slots: {};
17
+ exports?: {} | undefined;
18
+ bindings?: string | undefined;
19
+ };
20
+ export type RecoveryCodesProps = typeof __propDef.props;
21
+ export type RecoveryCodesEvents = typeof __propDef.events;
22
+ export type RecoveryCodesSlots = typeof __propDef.slots;
23
+ export default class RecoveryCodes extends SvelteComponent<RecoveryCodesProps, RecoveryCodesEvents, RecoveryCodesSlots> {
24
+ }
25
+ export {};