@atlasauth/svelte 0.8.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.
- package/CHANGELOG.md +16 -0
- package/dist/accessors.d.ts +57 -1
- package/dist/accessors.js +99 -2
- package/dist/appearance.d.ts +1 -1
- package/dist/components/AttemptFlow.svelte +181 -4
- package/dist/components/AttemptFlow.svelte.d.ts +27 -1
- package/dist/components/ImpersonationBanner.svelte +38 -0
- package/dist/components/ImpersonationBanner.svelte.d.ts +19 -0
- package/dist/components/OrganizationAuditLog.svelte +45 -0
- package/dist/components/OrganizationAuditLog.svelte.d.ts +31 -0
- package/dist/components/OrganizationProfile.svelte +308 -12
- package/dist/components/OrganizationProfile.svelte.d.ts +2 -0
- package/dist/components/PasskeySignUpStep.svelte +23 -0
- package/dist/components/PasskeySignUpStep.svelte.d.ts +24 -0
- package/dist/components/RecoveryCodes.svelte +55 -0
- package/dist/components/RecoveryCodes.svelte.d.ts +25 -0
- package/dist/components/SignIn.svelte +20 -1
- package/dist/components/SignUp.svelte +17 -2
- package/dist/components/SignUpForm.svelte +41 -16
- package/dist/components/SignUpForm.svelte.d.ts +4 -0
- package/dist/context.d.ts +8 -1
- package/dist/context.js +17 -0
- package/dist/i18n.js +55 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/types.d.ts +6 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -4
|
@@ -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 {
|
|
2
|
-
import {
|
|
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
|
|
11
|
+
import { FapiClient } from "../fapi";
|
|
12
|
+
import { ATLAS_SVELTE_SDK } from "../version";
|
|
6
13
|
const atlas = useAtlas();
|
|
7
14
|
const org = getOrganization();
|
|
8
|
-
const
|
|
9
|
-
|
|
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={
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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}
|
|
@@ -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 {};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
<script>import { onDestroy } from "svelte";
|
|
2
|
-
import { signUp as hostedSignUp } from "@atlasauth/js";
|
|
2
|
+
import { lastUsedMethodLabel, pendingRecoveryCodes, signUp as hostedSignUp } from "@atlasauth/js";
|
|
3
3
|
import { signIn } from "../accessors";
|
|
4
4
|
import { useAtlas } from "../context";
|
|
5
5
|
import { classFor } from "../appearance";
|
|
6
6
|
import { translate } from "../i18n";
|
|
7
7
|
import AttemptFlow from "./AttemptFlow.svelte";
|
|
8
|
+
import RecoveryCodes from "./RecoveryCodes.svelte";
|
|
8
9
|
export let afterUrl = void 0;
|
|
9
10
|
export let signUpUrl = void 0;
|
|
10
11
|
const atlas = useAtlas();
|
|
@@ -26,11 +27,24 @@ $: promptHref = $flowState.attempt?.status !== "complete" ? signUpHref(signUpUrl
|
|
|
26
27
|
const flow = signIn();
|
|
27
28
|
const flowState = flow.state;
|
|
28
29
|
const settings = flow.settings;
|
|
30
|
+
const lastMethod = flow.lastMethod;
|
|
31
|
+
const enrollmentState = flow.enrollmentState;
|
|
29
32
|
onDestroy(() => flow.destroy());
|
|
33
|
+
$: recoveryCodes = pendingRecoveryCodes($flowState);
|
|
34
|
+
$: lastLabel = !$flowState.attempt ? lastUsedMethodLabel($lastMethod, t) : null;
|
|
30
35
|
const drawn = (..._deps) => flow.captcha.needed ? flow.captcha : null;
|
|
31
36
|
$: captcha = drawn($settings, $flowState);
|
|
32
37
|
</script>
|
|
33
38
|
|
|
39
|
+
{#if lastLabel}
|
|
40
|
+
<p class={classFor('badge', atlas.appearance, 'atlas-text atlas-last-used-hint')} data-atlas="last-used" data-atlas-method={$lastMethod}>
|
|
41
|
+
{t('signIn.lastUsed')}: {lastLabel}
|
|
42
|
+
</p>
|
|
43
|
+
{/if}
|
|
44
|
+
|
|
45
|
+
{#if recoveryCodes}
|
|
46
|
+
<RecoveryCodes codes={recoveryCodes} busy={$flowState.busy} onSaved={flow.saveRecoveryCodes} />
|
|
47
|
+
{:else}
|
|
34
48
|
<AttemptFlow
|
|
35
49
|
attempt={$flowState.attempt}
|
|
36
50
|
busy={$flowState.busy}
|
|
@@ -39,9 +53,14 @@ $: captcha = drawn($settings, $flowState);
|
|
|
39
53
|
titleKey="signIn.title"
|
|
40
54
|
onSubmit={flow.submit}
|
|
41
55
|
onPrepare={flow.prepare}
|
|
56
|
+
onEmailSecondFactor={$settings?.account.emailSecondFactor ? flow.sendEmailSecondFactor : null}
|
|
57
|
+
onSmsSecondFactor={$settings && flow.mfaChoices().sms ? flow.sendSmsSecondFactor : null}
|
|
58
|
+
enrollment={flow.enrollment}
|
|
59
|
+
enrollmentState={$enrollmentState}
|
|
42
60
|
emailDelivery={$settings?.emailDelivery ?? null}
|
|
43
61
|
{captcha}
|
|
44
62
|
/>
|
|
63
|
+
{/if}
|
|
45
64
|
|
|
46
65
|
{#if promptHref}
|
|
47
66
|
<p class={classFor('footerAction', atlas.appearance, 'atlas-footer')} data-atlas-sign-up-prompt="">
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script>import { onDestroy } from "svelte";
|
|
2
|
+
import { awaitingPasskey, passkeysSupported } from "@atlasauth/js";
|
|
2
3
|
import { signUp } from "../accessors";
|
|
3
4
|
import AttemptFlow from "./AttemptFlow.svelte";
|
|
5
|
+
import PasskeySignUpStep from "./PasskeySignUpStep.svelte";
|
|
4
6
|
import SignUpForm from "./SignUpForm.svelte";
|
|
5
7
|
export let afterUrl = void 0;
|
|
6
8
|
const flow = signUp();
|
|
@@ -9,9 +11,18 @@ const settings = flow.settings;
|
|
|
9
11
|
onDestroy(() => flow.destroy());
|
|
10
12
|
const drawn = (..._deps) => flow.signUpCaptcha.needed ? flow.signUpCaptcha : null;
|
|
11
13
|
$: captcha = drawn($settings, $flowState);
|
|
14
|
+
let mode = "password";
|
|
15
|
+
$: offer = $settings?.account.passkeySignUp ?? null;
|
|
16
|
+
$: canPasskey = !!offer && passkeysSupported();
|
|
17
|
+
$: usingPasskey = mode === "passkey" && canPasskey;
|
|
18
|
+
function switchMode() {
|
|
19
|
+
mode = usingPasskey ? "password" : "passkey";
|
|
20
|
+
}
|
|
12
21
|
</script>
|
|
13
22
|
|
|
14
|
-
{#if $flowState.attempt}
|
|
23
|
+
{#if $flowState.attempt && awaitingPasskey($flowState.attempt)}
|
|
24
|
+
<PasskeySignUpStep busy={$flowState.busy} errors={$flowState.errors} onCreate={flow.createPasskey} />
|
|
25
|
+
{:else if $flowState.attempt}
|
|
15
26
|
<AttemptFlow
|
|
16
27
|
attempt={$flowState.attempt}
|
|
17
28
|
busy={$flowState.busy}
|
|
@@ -19,6 +30,8 @@ $: captcha = drawn($settings, $flowState);
|
|
|
19
30
|
sessionKept={$flowState.sessionKept}
|
|
20
31
|
titleKey="signUp.title"
|
|
21
32
|
onSubmit={flow.submit}
|
|
33
|
+
emailVerification={$settings?.account.emailVerification ?? 'code'}
|
|
34
|
+
onResend={flow.resendSignUp}
|
|
22
35
|
/>
|
|
23
36
|
{:else}
|
|
24
37
|
<SignUpForm
|
|
@@ -26,6 +39,8 @@ $: captcha = drawn($settings, $flowState);
|
|
|
26
39
|
busy={$flowState.busy}
|
|
27
40
|
errors={$flowState.errors}
|
|
28
41
|
{captcha}
|
|
29
|
-
onSubmit={flow.create}
|
|
42
|
+
onSubmit={usingPasskey ? flow.createWithPasskey : flow.create}
|
|
43
|
+
passkey={usingPasskey && offer ? { email: offer.email } : null}
|
|
44
|
+
onSwitchMode={canPasskey ? switchMode : undefined}
|
|
30
45
|
/>
|
|
31
46
|
{/if}
|