@nymphjs/tilmeld-components 1.0.0-beta.13 → 1.0.0-beta.130

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,544 @@
1
+ <svelte:options runes />
2
+
3
+ {#if $user != null}
4
+ <Dialog
5
+ {use}
6
+ bind:open
7
+ aria-labelledby="tilmeld-two-factor-title"
8
+ aria-describedby="tilmeld-two-factor-content"
9
+ surface$class="tilmeld-two-factor-dialog-surface"
10
+ {...exclude(restProps, [
11
+ 'password$',
12
+ 'code$',
13
+ 'closeButton$',
14
+ 'setupButton$',
15
+ 'removeButton$',
16
+ 'progress$',
17
+ ])}
18
+ >
19
+ <!-- Title cannot contain leading whitespace due to mdc-typography-baseline-top() -->
20
+ <Title id="tilmeld-two-factor-title">{title}</Title>
21
+ {#if hasTOTPSecret === false}
22
+ <Content id="tilmeld-two-factor-content">
23
+ {#if totpSecret == null}
24
+ <p>
25
+ 2 Factor Authentication (2FA) adds an additional layer of security.
26
+ With 2FA, no one can log in without your authenticator device, even
27
+ with your password.
28
+ </p>
29
+
30
+ <p>
31
+ You'll need an authenticator app that supports TOTP codes. Some
32
+ popular apps are linked here.
33
+ </p>
34
+
35
+ <h6 style="margin-top: 0;">Twilio Authy</h6>
36
+
37
+ <ul>
38
+ <li>
39
+ <a
40
+ href="https://play.google.com/store/apps/details?id=com.authy.authy"
41
+ target="_blank"
42
+ rel="noopener noreferrer">Google Play Store</a
43
+ >
44
+ </li>
45
+ <li>
46
+ <a
47
+ href="https://apps.apple.com/us/app/twilio-authy/id494168017"
48
+ target="_blank"
49
+ rel="noopener noreferrer">Apple App Store</a
50
+ >
51
+ </li>
52
+ <li>
53
+ <a
54
+ href="https://authy.com/download/"
55
+ target="_blank"
56
+ rel="noopener noreferrer">Desktop App</a
57
+ >
58
+ </li>
59
+ </ul>
60
+
61
+ <h6 style="margin-top: 0;">Google Authenticator</h6>
62
+
63
+ <ul>
64
+ <li>
65
+ <a
66
+ href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2"
67
+ target="_blank"
68
+ rel="noopener noreferrer">Google Play Store</a
69
+ >
70
+ </li>
71
+ <li>
72
+ <a
73
+ href="https://apps.apple.com/us/app/google-authenticator/id388497605"
74
+ target="_blank"
75
+ rel="noopener noreferrer">Apple App Store</a
76
+ >
77
+ </li>
78
+ </ul>
79
+
80
+ <h6 style="margin-top: 0;">Microsoft Authenticator</h6>
81
+
82
+ <ul>
83
+ <li>
84
+ <a
85
+ href="https://play.google.com/store/apps/details?id=com.azure.authenticator"
86
+ target="_blank"
87
+ rel="noopener noreferrer">Google Play Store</a
88
+ >
89
+ </li>
90
+ <li>
91
+ <a
92
+ href="https://apps.apple.com/us/app/microsoft-authenticator/id983156458"
93
+ target="_blank"
94
+ rel="noopener noreferrer">Apple App Store</a
95
+ >
96
+ </li>
97
+ </ul>
98
+
99
+ <p>
100
+ Once your authenticator app is ready, continue to generate a QR
101
+ code.
102
+ </p>
103
+ {:else}
104
+ <p>Scan this QR code with your authenticator app:</p>
105
+
106
+ <div style="text-align: center;">
107
+ <img alt="TOTP Set Up QR Code" src={totpSecret.qrcode} />
108
+ </div>
109
+
110
+ <p>Or you can enter this code manually into your app:</p>
111
+
112
+ <div style="text-align: center;">
113
+ <code style="word-break: break-all;">{totpSecret.secret}</code>
114
+ </div>
115
+
116
+ <p>Enter your password to allow this account change:</p>
117
+
118
+ <div>
119
+ <Textfield
120
+ bind:value={password}
121
+ label="Password"
122
+ type="password"
123
+ style="width: 100%;"
124
+ input$autocomplete="current-password"
125
+ {...prefixFilter(restProps, 'password$')}
126
+ />
127
+ </div>
128
+
129
+ <p>
130
+ Enter a code generated by your app to confirm the account is
131
+ configured correctly:
132
+ </p>
133
+
134
+ <div>
135
+ <Textfield
136
+ bind:value={code}
137
+ label="2FA Code"
138
+ style="width: 100%;"
139
+ input$maxlength={6}
140
+ input$minlength={6}
141
+ {...prefixFilter(restProps, 'code$')}
142
+ />
143
+ </div>
144
+ {/if}
145
+
146
+ {#if failureMessage}
147
+ <div class="tilmeld-two-factor-failure">
148
+ {failureMessage}
149
+ </div>
150
+ {/if}
151
+
152
+ {#if loading}
153
+ <div class="tilmeld-two-factor-loading">
154
+ <CircularProgress
155
+ style="height: 24px; width: 24px;"
156
+ indeterminate
157
+ {...prefixFilter(restProps, 'progress$')}
158
+ />
159
+ </div>
160
+ {/if}
161
+ </Content>
162
+ <Actions>
163
+ <Button disabled={loading} {...prefixFilter(restProps, 'closeButton$')}>
164
+ <Label>Close</Label>
165
+ </Button>
166
+ {#if totpSecret == null}
167
+ <Button
168
+ onclick={preventDefault(stopPropagation(beginSetup))}
169
+ disabled={loading}
170
+ {...prefixFilter(restProps, 'setupButton$')}
171
+ >
172
+ <Label>Set Up 2FA</Label>
173
+ </Button>
174
+ {:else}
175
+ <Button
176
+ onclick={preventDefault(stopPropagation(enableTOTP))}
177
+ disabled={loading}
178
+ {...prefixFilter(restProps, 'setupButton$')}
179
+ >
180
+ <Label>Enable 2FA</Label>
181
+ </Button>
182
+ {/if}
183
+ </Actions>
184
+ {:else if hasTOTPSecret === true}
185
+ <Content id="tilmeld-two-factor-content">
186
+ <p>
187
+ 2 Factor Authentication (2FA) is enabled on your account. If you'd
188
+ like to disable it, you can provide your password and a code generated
189
+ by your Authenticator app. You can re-enable it by generating a new QR
190
+ code at any time.
191
+ </p>
192
+
193
+ <div>
194
+ <Textfield
195
+ bind:value={password}
196
+ label="Password"
197
+ type="password"
198
+ style="width: 100%;"
199
+ input$autocomplete="current-password"
200
+ {...prefixFilter(restProps, 'password$')}
201
+ />
202
+ </div>
203
+
204
+ <div>
205
+ <Textfield
206
+ bind:value={code}
207
+ label="2FA Code"
208
+ style="width: 100%;"
209
+ input$maxlength={6}
210
+ input$minlength={6}
211
+ {...prefixFilter(restProps, 'code$')}
212
+ />
213
+ </div>
214
+
215
+ {@render additional?.()}
216
+
217
+ {#if failureMessage}
218
+ <div class="tilmeld-two-factor-failure">
219
+ {failureMessage}
220
+ </div>
221
+ {/if}
222
+
223
+ {#if loading}
224
+ <div class="tilmeld-two-factor-loading">
225
+ <CircularProgress
226
+ style="height: 24px; width: 24px;"
227
+ indeterminate
228
+ {...prefixFilter(restProps, 'progress$')}
229
+ />
230
+ </div>
231
+ {/if}
232
+ </Content>
233
+ <Actions>
234
+ <Button disabled={loading} {...prefixFilter(restProps, 'closeButton$')}>
235
+ <Label>Close</Label>
236
+ </Button>
237
+ <Button
238
+ onclick={preventDefault(stopPropagation(disableTOTP))}
239
+ disabled={loading}
240
+ {...prefixFilter(restProps, 'removeButton$')}
241
+ >
242
+ <Label>Disable 2FA</Label>
243
+ </Button>
244
+ </Actions>
245
+ {:else}
246
+ <div class="tilmeld-two-factor-loading">
247
+ <CircularProgress
248
+ style="height: 24px; width: 24px;"
249
+ indeterminate
250
+ {...prefixFilter(restProps, 'progress$')}
251
+ />
252
+ </div>
253
+ {/if}
254
+ </Dialog>
255
+ {/if}
256
+
257
+ <script lang="ts">
258
+ import type { ComponentProps, Snippet } from 'svelte';
259
+ import { onMount, onDestroy } from 'svelte';
260
+ import type { Writable } from 'svelte/store';
261
+ import { writable } from 'svelte/store';
262
+ import CircularProgress from '@smui/circular-progress';
263
+ import Dialog, { Title, Content, Actions } from '@smui/dialog';
264
+ import Textfield from '@smui/textfield';
265
+ import Button, { Label } from '@smui/button';
266
+ import type { ActionArray } from '@smui/common/internal';
267
+ import { exclude, prefixFilter } from '@smui/common/internal';
268
+ import { preventDefault, stopPropagation } from '@smui/common/events';
269
+ import type { CurrentUserData } from '@nymphjs/tilmeld-client';
270
+ import type { User as UserClass } from '@nymphjs/tilmeld-client';
271
+
272
+ type OwnProps = {
273
+ /**
274
+ * An array of Action or [Action, ActionProps] to be applied to the element.
275
+ */
276
+ use?: ActionArray;
277
+ /**
278
+ * Whether the dialog is open.
279
+ */
280
+ open?: boolean;
281
+ /**
282
+ * The title of the dialog.
283
+ */
284
+ title?: string;
285
+ /**
286
+ * The User class from Nymph.
287
+ */
288
+ User: typeof UserClass;
289
+ /**
290
+ * A writable store of the current user.
291
+ *
292
+ * It will be retrieved from the server if not provided.
293
+ */
294
+ user?: Writable<(UserClass & CurrentUserData) | null | undefined>;
295
+ /**
296
+ * Server provided. You can bind to it if you need to.
297
+ */
298
+ hasTOTPSecret?: boolean | null;
299
+ /**
300
+ * Server provided. You can bind to it if you need to.
301
+ */
302
+ totpSecret?: {
303
+ uri: string;
304
+ qrcode: string;
305
+ secret: string;
306
+ } | null;
307
+ /**
308
+ * User provided. You can bind to it if you need to.
309
+ */
310
+ password?: string;
311
+ /**
312
+ * User provided. You can bind to it if you need to.
313
+ */
314
+ code?: string;
315
+
316
+ /**
317
+ * A spot for additional content.
318
+ */
319
+ additional?: Snippet;
320
+ };
321
+ let {
322
+ use = [],
323
+ open = $bindable(false),
324
+ title = 'Manage Two Factor Authentication',
325
+ User,
326
+ user = $bindable(writable(false as unknown as undefined)),
327
+ hasTOTPSecret = $bindable(null),
328
+ totpSecret = $bindable(null),
329
+ password = $bindable(''),
330
+ code = $bindable(''),
331
+ additional,
332
+ ...restProps
333
+ }: OwnProps & {
334
+ [
335
+ k in keyof ComponentProps<typeof Textfield> as `password\$${k}`
336
+ ]?: ComponentProps<typeof Textfield>[k];
337
+ } & {
338
+ [
339
+ k in keyof ComponentProps<typeof Textfield> as `code\$${k}`
340
+ ]?: ComponentProps<typeof Textfield>[k];
341
+ } & {
342
+ [
343
+ k in keyof ComponentProps<typeof CircularProgress> as `progress\$${k}`
344
+ ]?: ComponentProps<typeof CircularProgress>[k];
345
+ } & {
346
+ [
347
+ k in keyof ComponentProps<
348
+ typeof Button<undefined, 'button'>
349
+ > as `closeButton\$${k}`
350
+ ]?: ComponentProps<typeof Button<undefined, 'button'>>[k];
351
+ } & {
352
+ [
353
+ k in keyof ComponentProps<
354
+ typeof Button<undefined, 'button'>
355
+ > as `setupButton\$${k}`
356
+ ]?: ComponentProps<typeof Button<undefined, 'button'>>[k];
357
+ } & {
358
+ [
359
+ k in keyof ComponentProps<
360
+ typeof Button<undefined, 'button'>
361
+ > as `removeButton\$${k}`
362
+ ]?: ComponentProps<typeof Button<undefined, 'button'>>[k];
363
+ } = $props();
364
+
365
+ let loading = $state(false);
366
+ let failureMessage: string | undefined = $state();
367
+
368
+ $effect(() => {
369
+ if (!open) {
370
+ loading = false;
371
+ failureMessage = undefined;
372
+ totpSecret = null;
373
+ password = '';
374
+ code = '';
375
+ }
376
+ });
377
+
378
+ const onLogin = (currentUser: UserClass & CurrentUserData) => {
379
+ $user = currentUser;
380
+ hasTOTPSecret = null;
381
+ $user.$hasTOTPSecret().then(
382
+ (value) => {
383
+ hasTOTPSecret = value;
384
+ failureMessage = undefined;
385
+ },
386
+ (e) => {
387
+ hasTOTPSecret = null;
388
+ failureMessage = e?.message;
389
+ },
390
+ );
391
+ };
392
+ const onLogout = () => {
393
+ $user = null;
394
+ hasTOTPSecret = null;
395
+ };
396
+
397
+ onMount(async () => {
398
+ User.on('login', onLogin);
399
+ User.on('logout', onLogout);
400
+ if ($user === (false as unknown as undefined)) {
401
+ $user = undefined;
402
+ $user = await User.current();
403
+ }
404
+ if ($user) {
405
+ try {
406
+ hasTOTPSecret = await $user.$hasTOTPSecret();
407
+ failureMessage = undefined;
408
+ } catch (e: any) {
409
+ hasTOTPSecret = null;
410
+ failureMessage = e?.message;
411
+ }
412
+ } else {
413
+ hasTOTPSecret = null;
414
+ }
415
+ });
416
+
417
+ onDestroy(() => {
418
+ User.off('login', onLogin);
419
+ User.off('logout', onLogout);
420
+ });
421
+
422
+ async function beginSetup() {
423
+ loading = true;
424
+
425
+ if ($user == null) {
426
+ failureMessage = 'You must be logged in.';
427
+ loading = false;
428
+ return;
429
+ }
430
+
431
+ try {
432
+ totpSecret = await $user.$getNewTOTPSecret();
433
+ } catch (e: any) {
434
+ failureMessage = e?.message;
435
+ }
436
+ loading = false;
437
+ }
438
+
439
+ async function enableTOTP() {
440
+ if (password === '') {
441
+ failureMessage = 'You need to enter your current password';
442
+ return;
443
+ }
444
+
445
+ if (code === '') {
446
+ failureMessage = 'You need to enter a generated code';
447
+ return;
448
+ }
449
+
450
+ if (totpSecret == null) {
451
+ failureMessage = 'TOTP secret is not set';
452
+ return;
453
+ }
454
+
455
+ failureMessage = undefined;
456
+ loading = true;
457
+
458
+ // Get the current user again, in case their data has changed.
459
+ $user = await User.current();
460
+
461
+ if ($user == null) {
462
+ failureMessage = 'You must be logged in.';
463
+ loading = false;
464
+ return;
465
+ }
466
+
467
+ try {
468
+ // Save the user's TOTP secret.
469
+ const data = await $user.$saveTOTPSecret({
470
+ password,
471
+ secret: totpSecret.secret,
472
+ code,
473
+ });
474
+
475
+ if (!data.result) {
476
+ failureMessage = data.message;
477
+ } else {
478
+ open = false;
479
+ hasTOTPSecret = true;
480
+ }
481
+ } catch (e: any) {
482
+ failureMessage = e?.message;
483
+ }
484
+ loading = false;
485
+ }
486
+
487
+ async function disableTOTP() {
488
+ if (password === '') {
489
+ failureMessage = 'You need to enter your current password';
490
+ return;
491
+ }
492
+
493
+ if (code === '') {
494
+ failureMessage = 'You need to enter a generated code';
495
+ return;
496
+ }
497
+
498
+ failureMessage = undefined;
499
+ loading = true;
500
+
501
+ // Get the current user again, in case their data has changed.
502
+ $user = await User.current();
503
+
504
+ if ($user == null) {
505
+ failureMessage = 'You must be logged in.';
506
+ loading = false;
507
+ return;
508
+ }
509
+
510
+ try {
511
+ // Remove the user's TOTP secret.
512
+ const data = await $user.$removeTOTPSecret({
513
+ password,
514
+ code,
515
+ });
516
+
517
+ if (!data.result) {
518
+ failureMessage = data.message;
519
+ } else {
520
+ open = false;
521
+ hasTOTPSecret = false;
522
+ }
523
+ } catch (e: any) {
524
+ failureMessage = e?.message;
525
+ }
526
+ loading = false;
527
+ }
528
+ </script>
529
+
530
+ <style>
531
+ :global(.mdc-dialog .mdc-dialog__surface.tilmeld-two-factor-dialog-surface) {
532
+ width: 400px;
533
+ max-width: calc(100vw - 32px);
534
+ }
535
+ .tilmeld-two-factor-failure {
536
+ margin-top: 1em;
537
+ color: var(--mdc-theme-error, #f00);
538
+ }
539
+ .tilmeld-two-factor-loading {
540
+ display: flex;
541
+ justify-content: center;
542
+ align-items: center;
543
+ }
544
+ </style>
@@ -0,0 +1,7 @@
1
+ import Account from './Account.svelte';
2
+ import ChangePassword from './ChangePassword.svelte';
3
+ import Login from './Login.svelte';
4
+ import Recover from './Recover.svelte';
5
+ import RevokeTokens from './RevokeTokens.svelte';
6
+ import TwoFactor from './TwoFactor.svelte';
7
+ export { Account, ChangePassword, Login, Recover, RevokeTokens, TwoFactor };
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ import Account from './Account.svelte';
2
+ import ChangePassword from './ChangePassword.svelte';
3
+ import Login from './Login.svelte';
4
+ import Recover from './Recover.svelte';
5
+ import RevokeTokens from './RevokeTokens.svelte';
6
+ import TwoFactor from './TwoFactor.svelte';
7
+ export { Account, ChangePassword, Login, Recover, RevokeTokens, TwoFactor };
8
+ //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,kBAAkB,CAAC;AACvC,OAAO,cAAc,MAAM,yBAAyB,CAAC;AACrD,OAAO,KAAK,MAAM,gBAAgB,CAAC;AACnC,OAAO,OAAO,MAAM,kBAAkB,CAAC;AAEvC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,kBAAkB,CAAC;AACvC,OAAO,cAAc,MAAM,yBAAyB,CAAC;AACrD,OAAO,KAAK,MAAM,gBAAgB,CAAC;AACnC,OAAO,OAAO,MAAM,kBAAkB,CAAC;AACvC,OAAO,YAAY,MAAM,uBAAuB,CAAC;AACjD,OAAO,SAAS,MAAM,oBAAoB,CAAC;AAE3C,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC"}
package/jest.config.js ADDED
@@ -0,0 +1,32 @@
1
+ const jestConfig = {
2
+ extensionsToTreatAsEsm: ['.ts', '.tsx'],
3
+ testMatch: [
4
+ '<rootDir>/src/**/__tests__/**/*.{js,ts,jsx,tsx,mjs,mts}',
5
+ '<rootDir>/src/**/?(*.)(spec|test).{js,ts,jsx,tsx,mjs,mts}',
6
+ ],
7
+ transform: {
8
+ '^.+\\.(ts|tsx|mts)$': [
9
+ '@swc/jest',
10
+ {
11
+ jsc: {
12
+ parser: {
13
+ syntax: 'typescript',
14
+ tsx: true,
15
+ decorators: true,
16
+ },
17
+ target: 'es2024',
18
+ },
19
+ module: {
20
+ type: 'commonjs',
21
+ },
22
+ },
23
+ ],
24
+ },
25
+ transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs)$'],
26
+ moduleDirectories: ['node_modules', 'src'],
27
+ moduleNameMapper: {
28
+ '^(\\.|\\.\\.)\\/(.+)\\.js': '$1/$2',
29
+ },
30
+ };
31
+
32
+ export default jestConfig;