@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.
- package/CHANGELOG.md +490 -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
package/src/Recover.svelte
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
<svelte:options runes />
|
|
2
|
+
|
|
3
|
+
{#if $clientConfig != null && $clientConfig.pwRecovery}
|
|
2
4
|
<Dialog
|
|
3
|
-
use
|
|
5
|
+
{use}
|
|
4
6
|
bind:open
|
|
5
7
|
aria-labelledby="tilmeld-recovery-title"
|
|
6
8
|
aria-describedby="tilmeld-recovery-content"
|
|
7
9
|
surface$class="tilmeld-recover-dialog-surface"
|
|
8
|
-
{...exclude(
|
|
10
|
+
{...exclude(restProps, [
|
|
9
11
|
'recoveryTypePassword$',
|
|
10
12
|
'recoveryTypeUsername$',
|
|
11
13
|
'account$',
|
|
@@ -27,23 +29,27 @@
|
|
|
27
29
|
{successRecoveredMessage}
|
|
28
30
|
{:else}
|
|
29
31
|
{#if !hasSentSecret}
|
|
30
|
-
{#if
|
|
32
|
+
{#if !$clientConfig.emailUsernames}
|
|
31
33
|
<div>
|
|
32
34
|
<FormField style="margin-right: 1em;">
|
|
33
35
|
<Radio
|
|
34
36
|
bind:group={recoveryType}
|
|
35
37
|
value="password"
|
|
36
|
-
{...prefixFilter(
|
|
38
|
+
{...prefixFilter(restProps, 'recoveryTypePassword$')}
|
|
37
39
|
/>
|
|
38
|
-
|
|
40
|
+
{#snippet label()}
|
|
41
|
+
I don't know my password.
|
|
42
|
+
{/snippet}
|
|
39
43
|
</FormField>
|
|
40
44
|
<FormField style="margin-right: 1em;">
|
|
41
45
|
<Radio
|
|
42
46
|
bind:group={recoveryType}
|
|
43
47
|
value="username"
|
|
44
|
-
{...prefixFilter(
|
|
48
|
+
{...prefixFilter(restProps, 'recoveryTypeUsername$')}
|
|
45
49
|
/>
|
|
46
|
-
|
|
50
|
+
{#snippet label()}
|
|
51
|
+
I don't know my username.
|
|
52
|
+
{/snippet}
|
|
47
53
|
</FormField>
|
|
48
54
|
</div>
|
|
49
55
|
{/if}
|
|
@@ -51,7 +57,7 @@
|
|
|
51
57
|
<div>
|
|
52
58
|
{#if recoveryType === 'password'}
|
|
53
59
|
<p>
|
|
54
|
-
To reset your password, type the {clientConfig.emailUsernames
|
|
60
|
+
To reset your password, type the {$clientConfig.emailUsernames
|
|
55
61
|
? 'email'
|
|
56
62
|
: 'username'}
|
|
57
63
|
you use to sign in below.
|
|
@@ -69,28 +75,28 @@
|
|
|
69
75
|
<Textfield
|
|
70
76
|
bind:this={accountElem}
|
|
71
77
|
bind:value={account}
|
|
72
|
-
label={clientConfig.emailUsernames || recoveryType === 'username'
|
|
78
|
+
label={$clientConfig.emailUsernames || recoveryType === 'username'
|
|
73
79
|
? 'Email Address'
|
|
74
80
|
: 'Username'}
|
|
75
|
-
type={clientConfig.emailUsernames || recoveryType === 'username'
|
|
81
|
+
type={$clientConfig.emailUsernames || recoveryType === 'username'
|
|
76
82
|
? 'email'
|
|
77
83
|
: 'text'}
|
|
78
|
-
input$autocomplete={clientConfig.emailUsernames ||
|
|
84
|
+
input$autocomplete={$clientConfig.emailUsernames ||
|
|
79
85
|
recoveryType === 'username'
|
|
80
86
|
? 'email'
|
|
81
87
|
: 'username'}
|
|
82
88
|
input$autocapitalize="off"
|
|
83
89
|
input$spellcheck="false"
|
|
84
|
-
{...prefixFilter(
|
|
90
|
+
{...prefixFilter(restProps, 'account$')}
|
|
85
91
|
/>
|
|
86
92
|
</div>
|
|
87
93
|
|
|
88
94
|
{#if recoveryType === 'password'}
|
|
89
95
|
<div class="tilmeld-recover-action">
|
|
90
96
|
<a
|
|
91
|
-
href=
|
|
92
|
-
|
|
93
|
-
{...prefixFilter(
|
|
97
|
+
href={'javascript:void(0);'}
|
|
98
|
+
onclick={() => (hasSentSecret = 1)}
|
|
99
|
+
{...prefixFilter(restProps, 'alreadyGotCodeLink$')}
|
|
94
100
|
>
|
|
95
101
|
Already Got a Code?
|
|
96
102
|
</a>
|
|
@@ -98,7 +104,7 @@
|
|
|
98
104
|
{/if}
|
|
99
105
|
{:else}
|
|
100
106
|
<div>
|
|
101
|
-
<p {...prefixFilter(
|
|
107
|
+
<p {...prefixFilter(restProps, 'codeSentMessage$')}>
|
|
102
108
|
A code has been sent to you by email. Enter that code here, and a
|
|
103
109
|
new password for your account.
|
|
104
110
|
</p>
|
|
@@ -109,16 +115,16 @@
|
|
|
109
115
|
<Textfield
|
|
110
116
|
bind:this={accountElem}
|
|
111
117
|
bind:value={account}
|
|
112
|
-
label={clientConfig.emailUsernames
|
|
118
|
+
label={$clientConfig.emailUsernames
|
|
113
119
|
? 'Email Address'
|
|
114
120
|
: 'Username'}
|
|
115
|
-
type={clientConfig.emailUsernames ? 'email' : 'text'}
|
|
116
|
-
input$autocomplete={clientConfig.emailUsernames
|
|
121
|
+
type={$clientConfig.emailUsernames ? 'email' : 'text'}
|
|
122
|
+
input$autocomplete={$clientConfig.emailUsernames
|
|
117
123
|
? 'email'
|
|
118
124
|
: 'username'}
|
|
119
125
|
input$autocapitalize="off"
|
|
120
126
|
input$spellcheck="false"
|
|
121
|
-
{...prefixFilter(
|
|
127
|
+
{...prefixFilter(restProps, 'account$')}
|
|
122
128
|
/>
|
|
123
129
|
</div>
|
|
124
130
|
{/if}
|
|
@@ -129,7 +135,7 @@
|
|
|
129
135
|
label="Recovery Code"
|
|
130
136
|
type="text"
|
|
131
137
|
input$autocomplete="one-time-code"
|
|
132
|
-
{...prefixFilter(
|
|
138
|
+
{...prefixFilter(restProps, 'recoveryCode$')}
|
|
133
139
|
/>
|
|
134
140
|
</div>
|
|
135
141
|
|
|
@@ -139,7 +145,7 @@
|
|
|
139
145
|
label="Password"
|
|
140
146
|
type="password"
|
|
141
147
|
input$autocomplete="new-password"
|
|
142
|
-
{...prefixFilter(
|
|
148
|
+
{...prefixFilter(restProps, 'password$')}
|
|
143
149
|
/>
|
|
144
150
|
</div>
|
|
145
151
|
|
|
@@ -149,15 +155,15 @@
|
|
|
149
155
|
label="Re-enter Password"
|
|
150
156
|
type="password"
|
|
151
157
|
input$autocomplete="new-password"
|
|
152
|
-
{...prefixFilter(
|
|
158
|
+
{...prefixFilter(restProps, 'password2$')}
|
|
153
159
|
/>
|
|
154
160
|
</div>
|
|
155
161
|
|
|
156
162
|
<div class="tilmeld-recover-action">
|
|
157
163
|
<a
|
|
158
|
-
href=
|
|
159
|
-
|
|
160
|
-
{...prefixFilter(
|
|
164
|
+
href={'javascript:void(0);'}
|
|
165
|
+
onclick={() => (hasSentSecret = false)}
|
|
166
|
+
{...prefixFilter(restProps, 'needCodeLink$')}
|
|
161
167
|
>
|
|
162
168
|
Need a New Code?
|
|
163
169
|
</a>
|
|
@@ -170,35 +176,35 @@
|
|
|
170
176
|
</div>
|
|
171
177
|
{/if}
|
|
172
178
|
|
|
173
|
-
{#if
|
|
179
|
+
{#if loading}
|
|
174
180
|
<div class="tilmeld-recover-loading">
|
|
175
181
|
<CircularProgress
|
|
176
182
|
style="height: 24px; width: 24px;"
|
|
177
183
|
indeterminate
|
|
178
|
-
{...prefixFilter(
|
|
184
|
+
{...prefixFilter(restProps, 'progress$')}
|
|
179
185
|
/>
|
|
180
186
|
</div>
|
|
181
187
|
{/if}
|
|
182
188
|
{/if}
|
|
183
189
|
</Content>
|
|
184
190
|
<Actions>
|
|
185
|
-
<Button
|
|
191
|
+
<Button onclick={() => (open = false)} disabled={loading}>
|
|
186
192
|
<Label>{successRecoveredMessage ? 'Close' : 'Cancel'}</Label>
|
|
187
193
|
</Button>
|
|
188
194
|
{#if !successRecoveredMessage}
|
|
189
195
|
{#if !hasSentSecret}
|
|
190
196
|
<Button
|
|
191
|
-
|
|
192
|
-
disabled={
|
|
193
|
-
{...prefixFilter(
|
|
197
|
+
onclick={preventDefault(stopPropagation(sendRecovery))}
|
|
198
|
+
disabled={loading}
|
|
199
|
+
{...prefixFilter(restProps, 'sendCodeButton$')}
|
|
194
200
|
>
|
|
195
201
|
<Label>Send Recovery</Label>
|
|
196
202
|
</Button>
|
|
197
203
|
{:else}
|
|
198
204
|
<Button
|
|
199
|
-
|
|
200
|
-
disabled={
|
|
201
|
-
{...prefixFilter(
|
|
205
|
+
onclick={preventDefault(stopPropagation(recover))}
|
|
206
|
+
disabled={loading}
|
|
207
|
+
{...prefixFilter(restProps, 'resetPasswordButton$')}
|
|
202
208
|
>
|
|
203
209
|
<Label>Reset Password</Label>
|
|
204
210
|
</Button>
|
|
@@ -209,8 +215,10 @@
|
|
|
209
215
|
{/if}
|
|
210
216
|
|
|
211
217
|
<script lang="ts">
|
|
218
|
+
import type { ComponentProps } from 'svelte';
|
|
212
219
|
import { onMount } from 'svelte';
|
|
213
|
-
import {
|
|
220
|
+
import type { Writable } from 'svelte/store';
|
|
221
|
+
import { writable } from 'svelte/store';
|
|
214
222
|
import CircularProgress from '@smui/circular-progress';
|
|
215
223
|
import Dialog, { Title, Content, Actions } from '@smui/dialog';
|
|
216
224
|
import Textfield from '@smui/textfield';
|
|
@@ -218,53 +226,147 @@
|
|
|
218
226
|
import FormField from '@smui/form-field';
|
|
219
227
|
import Radio from '@smui/radio';
|
|
220
228
|
import type { ActionArray } from '@smui/common/internal';
|
|
221
|
-
import {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
prefixFilter,
|
|
225
|
-
} 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';
|
|
226
232
|
import type {
|
|
227
233
|
User as UserClass,
|
|
228
234
|
ClientConfig,
|
|
229
235
|
} from '@nymphjs/tilmeld-client';
|
|
230
236
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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();
|
|
243
353
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
export let password = '';
|
|
250
|
-
/** User provided. You can bind to it if you need to. */
|
|
251
|
-
export let password2 = '';
|
|
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();
|
|
252
359
|
|
|
253
|
-
|
|
254
|
-
let hasSentSecret: number | boolean = false;
|
|
255
|
-
let accountElem: Textfield;
|
|
256
|
-
let failureMessage: string | undefined = undefined;
|
|
257
|
-
let successRecoveredMessage: string | undefined = undefined;
|
|
258
|
-
|
|
259
|
-
$: {
|
|
360
|
+
$effect(() => {
|
|
260
361
|
if (open && autofocus && accountElem) {
|
|
261
362
|
accountElem.focus();
|
|
262
363
|
}
|
|
263
|
-
}
|
|
364
|
+
});
|
|
264
365
|
|
|
265
366
|
onMount(async () => {
|
|
266
|
-
if (clientConfig === undefined) {
|
|
267
|
-
clientConfig =
|
|
367
|
+
if ($clientConfig === (false as unknown as undefined)) {
|
|
368
|
+
$clientConfig = undefined;
|
|
369
|
+
$clientConfig = await User.getClientConfig();
|
|
268
370
|
}
|
|
269
371
|
});
|
|
270
372
|
|
|
@@ -272,7 +374,7 @@
|
|
|
272
374
|
if (account === '') {
|
|
273
375
|
failureMessage =
|
|
274
376
|
'You need to enter ' +
|
|
275
|
-
(clientConfig?.emailUsernames || recoveryType === 'username'
|
|
377
|
+
($clientConfig?.emailUsernames || recoveryType === 'username'
|
|
276
378
|
? 'an email address'
|
|
277
379
|
: 'a username') +
|
|
278
380
|
'.';
|
|
@@ -280,7 +382,7 @@
|
|
|
280
382
|
}
|
|
281
383
|
|
|
282
384
|
failureMessage = undefined;
|
|
283
|
-
|
|
385
|
+
loading = true;
|
|
284
386
|
|
|
285
387
|
try {
|
|
286
388
|
const data = await User.sendRecovery({
|
|
@@ -299,14 +401,14 @@
|
|
|
299
401
|
} catch (e: any) {
|
|
300
402
|
failureMessage = e?.message;
|
|
301
403
|
}
|
|
302
|
-
|
|
404
|
+
loading = false;
|
|
303
405
|
}
|
|
304
406
|
|
|
305
407
|
async function recover() {
|
|
306
408
|
if (account === '') {
|
|
307
409
|
failureMessage =
|
|
308
410
|
'You need to enter ' +
|
|
309
|
-
(clientConfig?.emailUsernames || recoveryType === 'username'
|
|
411
|
+
($clientConfig?.emailUsernames || recoveryType === 'username'
|
|
310
412
|
? 'an email address'
|
|
311
413
|
: 'a username') +
|
|
312
414
|
'.';
|
|
@@ -322,7 +424,7 @@
|
|
|
322
424
|
}
|
|
323
425
|
|
|
324
426
|
failureMessage = undefined;
|
|
325
|
-
|
|
427
|
+
loading = true;
|
|
326
428
|
try {
|
|
327
429
|
const data = await User.recover({
|
|
328
430
|
username: account,
|
|
@@ -337,7 +439,7 @@
|
|
|
337
439
|
} catch (e: any) {
|
|
338
440
|
failureMessage = e?.message;
|
|
339
441
|
}
|
|
340
|
-
|
|
442
|
+
loading = false;
|
|
341
443
|
}
|
|
342
444
|
</script>
|
|
343
445
|
|
|
@@ -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>
|