@nymphjs/tilmeld-components 1.0.0-beta.13 → 1.0.0-beta.131
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 +494 -0
- package/README.md +6 -4
- package/dist/Account.svelte +516 -0
- package/dist/ChangePassword.svelte +309 -0
- package/dist/Login.svelte +603 -0
- package/dist/Recover.svelte +463 -0
- package/dist/RevokeTokens.svelte +236 -0
- package/dist/TwoFactor.svelte +544 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8 -0
- package/{lib → dist}/index.js.map +1 -1
- package/jest.config.js +32 -0
- package/package.json +54 -43
- package/src/Account.svelte +280 -113
- package/src/ChangePassword.svelte +147 -51
- package/src/Login.svelte +328 -115
- package/src/Recover.svelte +180 -78
- package/src/RevokeTokens.svelte +236 -0
- package/src/TwoFactor.svelte +544 -0
- package/src/index.d.ts +3 -1
- package/src/index.ts +3 -1
- package/tsconfig.json +8 -4
- package/typedoc.json +4 -0
- package/dist/index.cjs +0 -3
- package/dist/index.cjs.LICENSE.txt +0 -45
- package/dist/index.cjs.map +0 -1
- package/jest.config.cjs +0 -6
- package/lib/Account.svelte +0 -333
- package/lib/Account.svelte.map +0 -1
- package/lib/ChangePassword.svelte +0 -188
- package/lib/ChangePassword.svelte.map +0 -1
- package/lib/Login.svelte +0 -353
- package/lib/Login.svelte.map +0 -1
- package/lib/Recover.svelte +0 -339
- package/lib/Recover.svelte.map +0 -1
- package/lib/index.d.ts +0 -5
- package/lib/index.js +0 -6
- package/webpack.config.cjs +0 -35
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
<svelte:options runes />
|
|
2
|
+
|
|
3
|
+
{#if $clientConfig != null && $clientConfig.pwRecovery}
|
|
4
|
+
<Dialog
|
|
5
|
+
{use}
|
|
6
|
+
bind:open
|
|
7
|
+
aria-labelledby="tilmeld-recovery-title"
|
|
8
|
+
aria-describedby="tilmeld-recovery-content"
|
|
9
|
+
surface$class="tilmeld-recover-dialog-surface"
|
|
10
|
+
{...exclude(restProps, [
|
|
11
|
+
'recoveryTypePassword$',
|
|
12
|
+
'recoveryTypeUsername$',
|
|
13
|
+
'account$',
|
|
14
|
+
'alreadyGotCodeLink$',
|
|
15
|
+
'codeSentMessage$',
|
|
16
|
+
'recoveryCode$',
|
|
17
|
+
'password$',
|
|
18
|
+
'password2$',
|
|
19
|
+
'needCodeLink$',
|
|
20
|
+
'sendCodeButton$',
|
|
21
|
+
'resetPasswordButton$',
|
|
22
|
+
'progress$',
|
|
23
|
+
])}
|
|
24
|
+
>
|
|
25
|
+
<!-- Title cannot contain leading whitespace due to mdc-typography-baseline-top() -->
|
|
26
|
+
<Title id="tilmeld-recovery-title">{title}</Title>
|
|
27
|
+
<Content id="tilmeld-recovery-content">
|
|
28
|
+
{#if successRecoveredMessage}
|
|
29
|
+
{successRecoveredMessage}
|
|
30
|
+
{:else}
|
|
31
|
+
{#if !hasSentSecret}
|
|
32
|
+
{#if !$clientConfig.emailUsernames}
|
|
33
|
+
<div>
|
|
34
|
+
<FormField style="margin-right: 1em;">
|
|
35
|
+
<Radio
|
|
36
|
+
bind:group={recoveryType}
|
|
37
|
+
value="password"
|
|
38
|
+
{...prefixFilter(restProps, 'recoveryTypePassword$')}
|
|
39
|
+
/>
|
|
40
|
+
{#snippet label()}
|
|
41
|
+
I don't know my password.
|
|
42
|
+
{/snippet}
|
|
43
|
+
</FormField>
|
|
44
|
+
<FormField style="margin-right: 1em;">
|
|
45
|
+
<Radio
|
|
46
|
+
bind:group={recoveryType}
|
|
47
|
+
value="username"
|
|
48
|
+
{...prefixFilter(restProps, 'recoveryTypeUsername$')}
|
|
49
|
+
/>
|
|
50
|
+
{#snippet label()}
|
|
51
|
+
I don't know my username.
|
|
52
|
+
{/snippet}
|
|
53
|
+
</FormField>
|
|
54
|
+
</div>
|
|
55
|
+
{/if}
|
|
56
|
+
|
|
57
|
+
<div>
|
|
58
|
+
{#if recoveryType === 'password'}
|
|
59
|
+
<p>
|
|
60
|
+
To reset your password, type the {$clientConfig.emailUsernames
|
|
61
|
+
? 'email'
|
|
62
|
+
: 'username'}
|
|
63
|
+
you use to sign in below.
|
|
64
|
+
</p>
|
|
65
|
+
{/if}
|
|
66
|
+
{#if recoveryType === 'username'}
|
|
67
|
+
<p>
|
|
68
|
+
To get your username, type your email as you entered it when
|
|
69
|
+
creating your account.
|
|
70
|
+
</p>
|
|
71
|
+
{/if}
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div>
|
|
75
|
+
<Textfield
|
|
76
|
+
bind:this={accountElem}
|
|
77
|
+
bind:value={account}
|
|
78
|
+
label={$clientConfig.emailUsernames || recoveryType === 'username'
|
|
79
|
+
? 'Email Address'
|
|
80
|
+
: 'Username'}
|
|
81
|
+
type={$clientConfig.emailUsernames || recoveryType === 'username'
|
|
82
|
+
? 'email'
|
|
83
|
+
: 'text'}
|
|
84
|
+
input$autocomplete={$clientConfig.emailUsernames ||
|
|
85
|
+
recoveryType === 'username'
|
|
86
|
+
? 'email'
|
|
87
|
+
: 'username'}
|
|
88
|
+
input$autocapitalize="off"
|
|
89
|
+
input$spellcheck="false"
|
|
90
|
+
{...prefixFilter(restProps, 'account$')}
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
{#if recoveryType === 'password'}
|
|
95
|
+
<div class="tilmeld-recover-action">
|
|
96
|
+
<a
|
|
97
|
+
href={'javascript:void(0);'}
|
|
98
|
+
onclick={() => (hasSentSecret = 1)}
|
|
99
|
+
{...prefixFilter(restProps, 'alreadyGotCodeLink$')}
|
|
100
|
+
>
|
|
101
|
+
Already Got a Code?
|
|
102
|
+
</a>
|
|
103
|
+
</div>
|
|
104
|
+
{/if}
|
|
105
|
+
{:else}
|
|
106
|
+
<div>
|
|
107
|
+
<p {...prefixFilter(restProps, 'codeSentMessage$')}>
|
|
108
|
+
A code has been sent to you by email. Enter that code here, and a
|
|
109
|
+
new password for your account.
|
|
110
|
+
</p>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
{#if hasSentSecret === 1}
|
|
114
|
+
<div>
|
|
115
|
+
<Textfield
|
|
116
|
+
bind:this={accountElem}
|
|
117
|
+
bind:value={account}
|
|
118
|
+
label={$clientConfig.emailUsernames
|
|
119
|
+
? 'Email Address'
|
|
120
|
+
: 'Username'}
|
|
121
|
+
type={$clientConfig.emailUsernames ? 'email' : 'text'}
|
|
122
|
+
input$autocomplete={$clientConfig.emailUsernames
|
|
123
|
+
? 'email'
|
|
124
|
+
: 'username'}
|
|
125
|
+
input$autocapitalize="off"
|
|
126
|
+
input$spellcheck="false"
|
|
127
|
+
{...prefixFilter(restProps, 'account$')}
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
{/if}
|
|
131
|
+
|
|
132
|
+
<div>
|
|
133
|
+
<Textfield
|
|
134
|
+
bind:value={secret}
|
|
135
|
+
label="Recovery Code"
|
|
136
|
+
type="text"
|
|
137
|
+
input$autocomplete="one-time-code"
|
|
138
|
+
{...prefixFilter(restProps, 'recoveryCode$')}
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<div>
|
|
143
|
+
<Textfield
|
|
144
|
+
bind:value={password}
|
|
145
|
+
label="Password"
|
|
146
|
+
type="password"
|
|
147
|
+
input$autocomplete="new-password"
|
|
148
|
+
{...prefixFilter(restProps, 'password$')}
|
|
149
|
+
/>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div>
|
|
153
|
+
<Textfield
|
|
154
|
+
bind:value={password2}
|
|
155
|
+
label="Re-enter Password"
|
|
156
|
+
type="password"
|
|
157
|
+
input$autocomplete="new-password"
|
|
158
|
+
{...prefixFilter(restProps, 'password2$')}
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div class="tilmeld-recover-action">
|
|
163
|
+
<a
|
|
164
|
+
href={'javascript:void(0);'}
|
|
165
|
+
onclick={() => (hasSentSecret = false)}
|
|
166
|
+
{...prefixFilter(restProps, 'needCodeLink$')}
|
|
167
|
+
>
|
|
168
|
+
Need a New Code?
|
|
169
|
+
</a>
|
|
170
|
+
</div>
|
|
171
|
+
{/if}
|
|
172
|
+
|
|
173
|
+
{#if failureMessage}
|
|
174
|
+
<div class="tilmeld-recover-failure">
|
|
175
|
+
{failureMessage}
|
|
176
|
+
</div>
|
|
177
|
+
{/if}
|
|
178
|
+
|
|
179
|
+
{#if loading}
|
|
180
|
+
<div class="tilmeld-recover-loading">
|
|
181
|
+
<CircularProgress
|
|
182
|
+
style="height: 24px; width: 24px;"
|
|
183
|
+
indeterminate
|
|
184
|
+
{...prefixFilter(restProps, 'progress$')}
|
|
185
|
+
/>
|
|
186
|
+
</div>
|
|
187
|
+
{/if}
|
|
188
|
+
{/if}
|
|
189
|
+
</Content>
|
|
190
|
+
<Actions>
|
|
191
|
+
<Button onclick={() => (open = false)} disabled={loading}>
|
|
192
|
+
<Label>{successRecoveredMessage ? 'Close' : 'Cancel'}</Label>
|
|
193
|
+
</Button>
|
|
194
|
+
{#if !successRecoveredMessage}
|
|
195
|
+
{#if !hasSentSecret}
|
|
196
|
+
<Button
|
|
197
|
+
onclick={preventDefault(stopPropagation(sendRecovery))}
|
|
198
|
+
disabled={loading}
|
|
199
|
+
{...prefixFilter(restProps, 'sendCodeButton$')}
|
|
200
|
+
>
|
|
201
|
+
<Label>Send Recovery</Label>
|
|
202
|
+
</Button>
|
|
203
|
+
{:else}
|
|
204
|
+
<Button
|
|
205
|
+
onclick={preventDefault(stopPropagation(recover))}
|
|
206
|
+
disabled={loading}
|
|
207
|
+
{...prefixFilter(restProps, 'resetPasswordButton$')}
|
|
208
|
+
>
|
|
209
|
+
<Label>Reset Password</Label>
|
|
210
|
+
</Button>
|
|
211
|
+
{/if}
|
|
212
|
+
{/if}
|
|
213
|
+
</Actions>
|
|
214
|
+
</Dialog>
|
|
215
|
+
{/if}
|
|
216
|
+
|
|
217
|
+
<script lang="ts">
|
|
218
|
+
import type { ComponentProps } from 'svelte';
|
|
219
|
+
import { onMount } from 'svelte';
|
|
220
|
+
import type { Writable } from 'svelte/store';
|
|
221
|
+
import { writable } from 'svelte/store';
|
|
222
|
+
import CircularProgress from '@smui/circular-progress';
|
|
223
|
+
import Dialog, { Title, Content, Actions } from '@smui/dialog';
|
|
224
|
+
import Textfield from '@smui/textfield';
|
|
225
|
+
import Button, { Label } from '@smui/button';
|
|
226
|
+
import FormField from '@smui/form-field';
|
|
227
|
+
import Radio from '@smui/radio';
|
|
228
|
+
import type { ActionArray } from '@smui/common/internal';
|
|
229
|
+
import { exclude, prefixFilter } from '@smui/common/internal';
|
|
230
|
+
import { preventDefault, stopPropagation } from '@smui/common/events';
|
|
231
|
+
import type { SmuiElementPropMap } from '@smui/common';
|
|
232
|
+
import type {
|
|
233
|
+
User as UserClass,
|
|
234
|
+
ClientConfig,
|
|
235
|
+
} from '@nymphjs/tilmeld-client';
|
|
236
|
+
|
|
237
|
+
type OwnProps = {
|
|
238
|
+
/**
|
|
239
|
+
* An array of Action or [Action, ActionProps] to be applied to the element.
|
|
240
|
+
*/
|
|
241
|
+
use?: ActionArray;
|
|
242
|
+
/**
|
|
243
|
+
* Whether the dialog is open.
|
|
244
|
+
*/
|
|
245
|
+
open?: boolean;
|
|
246
|
+
/**
|
|
247
|
+
* The title of the dialog.
|
|
248
|
+
*/
|
|
249
|
+
title?: string;
|
|
250
|
+
/**
|
|
251
|
+
* A writable store of the Nymph client config.
|
|
252
|
+
*
|
|
253
|
+
* It will be retrieved from the server if not provided.
|
|
254
|
+
*/
|
|
255
|
+
clientConfig?: Writable<ClientConfig | undefined>;
|
|
256
|
+
/**
|
|
257
|
+
* The User class from Nymph.
|
|
258
|
+
*/
|
|
259
|
+
User: typeof UserClass;
|
|
260
|
+
/**
|
|
261
|
+
* Give focus to the account box when the form is ready.
|
|
262
|
+
*/
|
|
263
|
+
autofocus?: boolean;
|
|
264
|
+
/**
|
|
265
|
+
* The type of recovery being requested.
|
|
266
|
+
*
|
|
267
|
+
* User provided. You can bind to it if you need to.
|
|
268
|
+
*/
|
|
269
|
+
recoveryType?: 'username' | 'password';
|
|
270
|
+
/**
|
|
271
|
+
* User provided. You can bind to it if you need to.
|
|
272
|
+
*/
|
|
273
|
+
account?: string;
|
|
274
|
+
/**
|
|
275
|
+
* User provided. You can bind to it if you need to.
|
|
276
|
+
*/
|
|
277
|
+
secret?: string;
|
|
278
|
+
/**
|
|
279
|
+
* User provided. You can bind to it if you need to.
|
|
280
|
+
*/
|
|
281
|
+
password?: string;
|
|
282
|
+
/**
|
|
283
|
+
* User provided. You can bind to it if you need to.
|
|
284
|
+
*/
|
|
285
|
+
password2?: string;
|
|
286
|
+
};
|
|
287
|
+
let {
|
|
288
|
+
use = [],
|
|
289
|
+
open = $bindable(false),
|
|
290
|
+
title = 'Recover Your Account',
|
|
291
|
+
clientConfig = $bindable(writable(false as unknown as undefined)),
|
|
292
|
+
User,
|
|
293
|
+
autofocus = true,
|
|
294
|
+
recoveryType = $bindable('password'),
|
|
295
|
+
account = $bindable(''),
|
|
296
|
+
secret = $bindable(''),
|
|
297
|
+
password = $bindable(''),
|
|
298
|
+
password2 = $bindable(''),
|
|
299
|
+
...restProps
|
|
300
|
+
}: OwnProps & {
|
|
301
|
+
[
|
|
302
|
+
k in keyof ComponentProps<typeof Radio> as `recoveryTypePassword\$${k}`
|
|
303
|
+
]?: ComponentProps<typeof Radio>[k];
|
|
304
|
+
} & {
|
|
305
|
+
[
|
|
306
|
+
k in keyof ComponentProps<typeof Radio> as `recoveryTypeUsername\$${k}`
|
|
307
|
+
]?: ComponentProps<typeof Radio>[k];
|
|
308
|
+
} & {
|
|
309
|
+
[
|
|
310
|
+
k in keyof ComponentProps<typeof Textfield> as `account\$${k}`
|
|
311
|
+
]?: ComponentProps<typeof Textfield>[k];
|
|
312
|
+
} & {
|
|
313
|
+
[
|
|
314
|
+
k in keyof SmuiElementPropMap['a'] as `alreadyGotCodeLink\$${k}`
|
|
315
|
+
]?: SmuiElementPropMap['a'][k];
|
|
316
|
+
} & {
|
|
317
|
+
[
|
|
318
|
+
k in keyof SmuiElementPropMap['p'] as `codeSentMessage\$${k}`
|
|
319
|
+
]?: SmuiElementPropMap['p'][k];
|
|
320
|
+
} & {
|
|
321
|
+
[
|
|
322
|
+
k in keyof ComponentProps<typeof Textfield> as `recoveryCode\$${k}`
|
|
323
|
+
]?: ComponentProps<typeof Textfield>[k];
|
|
324
|
+
} & {
|
|
325
|
+
[
|
|
326
|
+
k in keyof ComponentProps<typeof Textfield> as `password\$${k}`
|
|
327
|
+
]?: ComponentProps<typeof Textfield>[k];
|
|
328
|
+
} & {
|
|
329
|
+
[
|
|
330
|
+
k in keyof ComponentProps<typeof Textfield> as `password2\$${k}`
|
|
331
|
+
]?: ComponentProps<typeof Textfield>[k];
|
|
332
|
+
} & {
|
|
333
|
+
[
|
|
334
|
+
k in keyof SmuiElementPropMap['a'] as `needCodeLink\$${k}`
|
|
335
|
+
]?: SmuiElementPropMap['a'][k];
|
|
336
|
+
} & {
|
|
337
|
+
[
|
|
338
|
+
k in keyof ComponentProps<typeof CircularProgress> as `progress\$${k}`
|
|
339
|
+
]?: ComponentProps<typeof CircularProgress>[k];
|
|
340
|
+
} & {
|
|
341
|
+
[
|
|
342
|
+
k in keyof ComponentProps<
|
|
343
|
+
typeof Button<undefined, 'button'>
|
|
344
|
+
> as `sendCodeButton\$${k}`
|
|
345
|
+
]?: ComponentProps<typeof Button<undefined, 'button'>>[k];
|
|
346
|
+
} & {
|
|
347
|
+
[
|
|
348
|
+
k in keyof ComponentProps<
|
|
349
|
+
typeof Button<undefined, 'button'>
|
|
350
|
+
> as `resetPasswordButton\$${k}`
|
|
351
|
+
]?: ComponentProps<typeof Button<undefined, 'button'>>[k];
|
|
352
|
+
} = $props();
|
|
353
|
+
|
|
354
|
+
let loading = $state(false);
|
|
355
|
+
let hasSentSecret: number | boolean = $state(false);
|
|
356
|
+
let accountElem: Textfield | undefined = $state();
|
|
357
|
+
let failureMessage: string | undefined = $state();
|
|
358
|
+
let successRecoveredMessage: string | undefined = $state();
|
|
359
|
+
|
|
360
|
+
$effect(() => {
|
|
361
|
+
if (open && autofocus && accountElem) {
|
|
362
|
+
accountElem.focus();
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
onMount(async () => {
|
|
367
|
+
if ($clientConfig === (false as unknown as undefined)) {
|
|
368
|
+
$clientConfig = undefined;
|
|
369
|
+
$clientConfig = await User.getClientConfig();
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
async function sendRecovery() {
|
|
374
|
+
if (account === '') {
|
|
375
|
+
failureMessage =
|
|
376
|
+
'You need to enter ' +
|
|
377
|
+
($clientConfig?.emailUsernames || recoveryType === 'username'
|
|
378
|
+
? 'an email address'
|
|
379
|
+
: 'a username') +
|
|
380
|
+
'.';
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
failureMessage = undefined;
|
|
385
|
+
loading = true;
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const data = await User.sendRecovery({
|
|
389
|
+
recoveryType,
|
|
390
|
+
account,
|
|
391
|
+
});
|
|
392
|
+
if (!data.result) {
|
|
393
|
+
failureMessage = data.message;
|
|
394
|
+
} else {
|
|
395
|
+
if (recoveryType === 'username') {
|
|
396
|
+
successRecoveredMessage = data.message;
|
|
397
|
+
} else if (recoveryType === 'password') {
|
|
398
|
+
hasSentSecret = true;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} catch (e: any) {
|
|
402
|
+
failureMessage = e?.message;
|
|
403
|
+
}
|
|
404
|
+
loading = false;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function recover() {
|
|
408
|
+
if (account === '') {
|
|
409
|
+
failureMessage =
|
|
410
|
+
'You need to enter ' +
|
|
411
|
+
($clientConfig?.emailUsernames || recoveryType === 'username'
|
|
412
|
+
? 'an email address'
|
|
413
|
+
: 'a username') +
|
|
414
|
+
'.';
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (password !== password2) {
|
|
418
|
+
failureMessage = 'Your passwords do not match.';
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (password === '') {
|
|
422
|
+
failureMessage = 'You need to enter a password.';
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
failureMessage = undefined;
|
|
427
|
+
loading = true;
|
|
428
|
+
try {
|
|
429
|
+
const data = await User.recover({
|
|
430
|
+
username: account,
|
|
431
|
+
secret,
|
|
432
|
+
password,
|
|
433
|
+
});
|
|
434
|
+
if (!data.result) {
|
|
435
|
+
failureMessage = data.message;
|
|
436
|
+
} else {
|
|
437
|
+
successRecoveredMessage = data.message;
|
|
438
|
+
}
|
|
439
|
+
} catch (e: any) {
|
|
440
|
+
failureMessage = e?.message;
|
|
441
|
+
}
|
|
442
|
+
loading = false;
|
|
443
|
+
}
|
|
444
|
+
</script>
|
|
445
|
+
|
|
446
|
+
<style>
|
|
447
|
+
:global(.mdc-dialog .mdc-dialog__surface.tilmeld-recover-dialog-surface) {
|
|
448
|
+
width: 540px;
|
|
449
|
+
max-width: calc(100vw - 32px);
|
|
450
|
+
}
|
|
451
|
+
.tilmeld-recover-action {
|
|
452
|
+
margin-top: 1em;
|
|
453
|
+
}
|
|
454
|
+
.tilmeld-recover-failure {
|
|
455
|
+
margin-top: 1em;
|
|
456
|
+
color: var(--mdc-theme-error, #f00);
|
|
457
|
+
}
|
|
458
|
+
.tilmeld-recover-loading {
|
|
459
|
+
display: flex;
|
|
460
|
+
justify-content: center;
|
|
461
|
+
align-items: center;
|
|
462
|
+
}
|
|
463
|
+
</style>
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
<svelte:options runes />
|
|
2
|
+
|
|
3
|
+
{#if $user != null}
|
|
4
|
+
<Dialog
|
|
5
|
+
{use}
|
|
6
|
+
bind:open
|
|
7
|
+
aria-labelledby="tilmeld-revoke-tokens-title"
|
|
8
|
+
aria-describedby="tilmeld-revoke-tokens-content"
|
|
9
|
+
surface$class="tilmeld-revoke-tokens-dialog-surface"
|
|
10
|
+
{...exclude(restProps, [
|
|
11
|
+
'password$',
|
|
12
|
+
'closeButton$',
|
|
13
|
+
'saveButton$',
|
|
14
|
+
'progress$',
|
|
15
|
+
])}
|
|
16
|
+
>
|
|
17
|
+
<!-- Title cannot contain leading whitespace due to mdc-typography-baseline-top() -->
|
|
18
|
+
<Title id="tilmeld-revoke-tokens-title">{title}</Title>
|
|
19
|
+
<Content id="tilmeld-revoke-tokens-content">
|
|
20
|
+
<div>
|
|
21
|
+
{description}
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div style="margin-top: 1em;">
|
|
25
|
+
<Textfield
|
|
26
|
+
bind:value={password}
|
|
27
|
+
label="Password"
|
|
28
|
+
type="password"
|
|
29
|
+
style="width: 100%;"
|
|
30
|
+
input$autocomplete="current-password"
|
|
31
|
+
{...prefixFilter(restProps, 'password$')}
|
|
32
|
+
/>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
{@render additional?.()}
|
|
36
|
+
|
|
37
|
+
{#if failureMessage}
|
|
38
|
+
<div class="tilmeld-revoke-tokens-failure">
|
|
39
|
+
{failureMessage}
|
|
40
|
+
</div>
|
|
41
|
+
{/if}
|
|
42
|
+
|
|
43
|
+
{#if loading}
|
|
44
|
+
<div class="tilmeld-revoke-tokens-loading">
|
|
45
|
+
<CircularProgress
|
|
46
|
+
style="height: 24px; width: 24px;"
|
|
47
|
+
indeterminate
|
|
48
|
+
{...prefixFilter(restProps, 'progress$')}
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
{/if}
|
|
52
|
+
</Content>
|
|
53
|
+
<Actions>
|
|
54
|
+
<Button disabled={loading} {...prefixFilter(restProps, 'closeButton$')}>
|
|
55
|
+
<Label>Close</Label>
|
|
56
|
+
</Button>
|
|
57
|
+
<Button
|
|
58
|
+
onclick={preventDefault(stopPropagation(revokeTokens))}
|
|
59
|
+
disabled={loading}
|
|
60
|
+
{...prefixFilter(restProps, 'saveButton$')}
|
|
61
|
+
>
|
|
62
|
+
<Label>Log Out Other Sessions</Label>
|
|
63
|
+
</Button>
|
|
64
|
+
</Actions>
|
|
65
|
+
</Dialog>
|
|
66
|
+
{/if}
|
|
67
|
+
|
|
68
|
+
<script lang="ts">
|
|
69
|
+
import type { ComponentProps, Snippet } from 'svelte';
|
|
70
|
+
import { onMount, onDestroy } from 'svelte';
|
|
71
|
+
import type { Writable } from 'svelte/store';
|
|
72
|
+
import { writable } from 'svelte/store';
|
|
73
|
+
import CircularProgress from '@smui/circular-progress';
|
|
74
|
+
import Dialog, { Title, Content, Actions } from '@smui/dialog';
|
|
75
|
+
import Textfield from '@smui/textfield';
|
|
76
|
+
import Button, { Label } from '@smui/button';
|
|
77
|
+
import type { ActionArray } from '@smui/common/internal';
|
|
78
|
+
import { exclude, prefixFilter } from '@smui/common/internal';
|
|
79
|
+
import { preventDefault, stopPropagation } from '@smui/common/events';
|
|
80
|
+
import type { CurrentUserData } from '@nymphjs/tilmeld-client';
|
|
81
|
+
import type { User as UserClass } from '@nymphjs/tilmeld-client';
|
|
82
|
+
|
|
83
|
+
type OwnProps = {
|
|
84
|
+
/**
|
|
85
|
+
* An array of Action or [Action, ActionProps] to be applied to the element.
|
|
86
|
+
*/
|
|
87
|
+
use?: ActionArray;
|
|
88
|
+
/**
|
|
89
|
+
* Whether the dialog is open.
|
|
90
|
+
*/
|
|
91
|
+
open?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* The title of the dialog.
|
|
94
|
+
*/
|
|
95
|
+
title?: string;
|
|
96
|
+
/**
|
|
97
|
+
* The description of the action given to the user.
|
|
98
|
+
*/
|
|
99
|
+
description?: string;
|
|
100
|
+
/**
|
|
101
|
+
* The User class from Nymph.
|
|
102
|
+
*/
|
|
103
|
+
User: typeof UserClass;
|
|
104
|
+
/**
|
|
105
|
+
* A writable store of the current user.
|
|
106
|
+
*
|
|
107
|
+
* It will be retrieved from the server if not provided.
|
|
108
|
+
*/
|
|
109
|
+
user?: Writable<(UserClass & CurrentUserData) | null | undefined>;
|
|
110
|
+
/**
|
|
111
|
+
* User provided. You can bind to it if you need to.
|
|
112
|
+
*/
|
|
113
|
+
password?: string;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* A spot for additional content.
|
|
117
|
+
*/
|
|
118
|
+
additional?: Snippet;
|
|
119
|
+
};
|
|
120
|
+
let {
|
|
121
|
+
use = [],
|
|
122
|
+
open = $bindable(false),
|
|
123
|
+
title = 'Log Out of Other Sessions',
|
|
124
|
+
description = 'This will log you out of all other sessions. You can use this if you think your account has been compromised. You will need to log in again on your other devices.',
|
|
125
|
+
User,
|
|
126
|
+
user = $bindable(writable(false as unknown as undefined)),
|
|
127
|
+
password = $bindable(''),
|
|
128
|
+
additional,
|
|
129
|
+
...restProps
|
|
130
|
+
}: OwnProps & {
|
|
131
|
+
[
|
|
132
|
+
k in keyof ComponentProps<typeof Textfield> as `password\$${k}`
|
|
133
|
+
]?: ComponentProps<typeof Textfield>[k];
|
|
134
|
+
} & {
|
|
135
|
+
[
|
|
136
|
+
k in keyof ComponentProps<typeof CircularProgress> as `progress\$${k}`
|
|
137
|
+
]?: ComponentProps<typeof CircularProgress>[k];
|
|
138
|
+
} & {
|
|
139
|
+
[
|
|
140
|
+
k in keyof ComponentProps<
|
|
141
|
+
typeof Button<undefined, 'button'>
|
|
142
|
+
> as `closeButton\$${k}`
|
|
143
|
+
]?: ComponentProps<typeof Button<undefined, 'button'>>[k];
|
|
144
|
+
} & {
|
|
145
|
+
[
|
|
146
|
+
k in keyof ComponentProps<
|
|
147
|
+
typeof Button<undefined, 'button'>
|
|
148
|
+
> as `saveButton\$${k}`
|
|
149
|
+
]?: ComponentProps<typeof Button<undefined, 'button'>>[k];
|
|
150
|
+
} = $props();
|
|
151
|
+
|
|
152
|
+
let loading = $state(false);
|
|
153
|
+
let failureMessage: string | undefined = $state();
|
|
154
|
+
|
|
155
|
+
$effect(() => {
|
|
156
|
+
if (!open) {
|
|
157
|
+
loading = false;
|
|
158
|
+
failureMessage = undefined;
|
|
159
|
+
password = '';
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const onLogin = (currentUser: UserClass & CurrentUserData) => {
|
|
164
|
+
$user = currentUser;
|
|
165
|
+
};
|
|
166
|
+
const onLogout = () => {
|
|
167
|
+
$user = null;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
onMount(async () => {
|
|
171
|
+
User.on('login', onLogin);
|
|
172
|
+
User.on('logout', onLogout);
|
|
173
|
+
if ($user === (false as unknown as undefined)) {
|
|
174
|
+
$user = undefined;
|
|
175
|
+
$user = await User.current();
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
onDestroy(() => {
|
|
180
|
+
User.off('login', onLogin);
|
|
181
|
+
User.off('logout', onLogout);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
async function revokeTokens() {
|
|
185
|
+
if (password === '') {
|
|
186
|
+
failureMessage = 'You need to enter your password';
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
failureMessage = undefined;
|
|
191
|
+
loading = true;
|
|
192
|
+
|
|
193
|
+
// Get the current user again, in case their data has changed.
|
|
194
|
+
$user = await User.current();
|
|
195
|
+
|
|
196
|
+
if ($user == null) {
|
|
197
|
+
failureMessage = 'You must be logged in.';
|
|
198
|
+
loading = false;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
// Revoke the current tokens.
|
|
204
|
+
const data = await $user.$revokeCurrentTokens({
|
|
205
|
+
password,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (!data.result) {
|
|
209
|
+
failureMessage = data.message;
|
|
210
|
+
} else {
|
|
211
|
+
open = false;
|
|
212
|
+
}
|
|
213
|
+
} catch (e: any) {
|
|
214
|
+
failureMessage = e?.message;
|
|
215
|
+
}
|
|
216
|
+
loading = false;
|
|
217
|
+
}
|
|
218
|
+
</script>
|
|
219
|
+
|
|
220
|
+
<style>
|
|
221
|
+
:global(
|
|
222
|
+
.mdc-dialog .mdc-dialog__surface.tilmeld-revoke-tokens-dialog-surface
|
|
223
|
+
) {
|
|
224
|
+
width: 360px;
|
|
225
|
+
max-width: calc(100vw - 32px);
|
|
226
|
+
}
|
|
227
|
+
.tilmeld-revoke-tokens-failure {
|
|
228
|
+
margin-top: 1em;
|
|
229
|
+
color: var(--mdc-theme-error, #f00);
|
|
230
|
+
}
|
|
231
|
+
.tilmeld-revoke-tokens-loading {
|
|
232
|
+
display: flex;
|
|
233
|
+
justify-content: center;
|
|
234
|
+
align-items: center;
|
|
235
|
+
}
|
|
236
|
+
</style>
|