@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,603 @@
|
|
|
1
|
+
<svelte:options runes />
|
|
2
|
+
|
|
3
|
+
<div
|
|
4
|
+
style="width: {width}; {style}"
|
|
5
|
+
use:useActions={use}
|
|
6
|
+
{...exclude(restProps, [
|
|
7
|
+
'successRegisteredMessage$',
|
|
8
|
+
'successLoginMessage$',
|
|
9
|
+
'username$',
|
|
10
|
+
'password$',
|
|
11
|
+
'password2$',
|
|
12
|
+
'name$',
|
|
13
|
+
'email$',
|
|
14
|
+
'phone$',
|
|
15
|
+
'code$',
|
|
16
|
+
'codeMessage$',
|
|
17
|
+
'loginButton$',
|
|
18
|
+
'registerButton$',
|
|
19
|
+
'registerLink$',
|
|
20
|
+
'loginLink$',
|
|
21
|
+
'recoverLink$',
|
|
22
|
+
'recover$',
|
|
23
|
+
'progress$',
|
|
24
|
+
])}
|
|
25
|
+
>
|
|
26
|
+
{#if $clientConfig == null || loading}
|
|
27
|
+
<div style="display: flex; justify-content: center; align-items: center;">
|
|
28
|
+
<CircularProgress
|
|
29
|
+
style="height: 32px; width: 32px;"
|
|
30
|
+
indeterminate
|
|
31
|
+
{...prefixFilter(restProps, 'progress$')}
|
|
32
|
+
/>
|
|
33
|
+
</div>
|
|
34
|
+
{:else if successRegisteredMessage}
|
|
35
|
+
<div {...prefixFilter(restProps, 'successRegisteredMessage$')}>
|
|
36
|
+
{successRegisteredMessage}
|
|
37
|
+
</div>
|
|
38
|
+
{:else if successLoginMessage}
|
|
39
|
+
<div {...prefixFilter(restProps, 'successLoginMessage$')}>
|
|
40
|
+
{successLoginMessage}
|
|
41
|
+
</div>
|
|
42
|
+
{:else if totp}
|
|
43
|
+
<form onsubmit={(e) => e.preventDefault()}>
|
|
44
|
+
<div {...prefixFilter(restProps, 'codeMessage$')}>
|
|
45
|
+
Enter the 6-digit 2FA code from your authenticator app.
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div>
|
|
49
|
+
<Textfield
|
|
50
|
+
bind:value={code}
|
|
51
|
+
label="2FA Code"
|
|
52
|
+
style="width: 100%;"
|
|
53
|
+
input$autocomplete="one-time-code"
|
|
54
|
+
input$name="one-time-code"
|
|
55
|
+
{...prefixFilter(restProps, 'code$')}
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
{#if failureMessage}
|
|
60
|
+
<div class="tilmeld-login-failure">
|
|
61
|
+
{failureMessage}
|
|
62
|
+
</div>
|
|
63
|
+
{/if}
|
|
64
|
+
|
|
65
|
+
<div class="tilmeld-login-buttons">
|
|
66
|
+
<Button
|
|
67
|
+
variant="raised"
|
|
68
|
+
type="submit"
|
|
69
|
+
onclick={login}
|
|
70
|
+
{...prefixFilter(restProps, 'loginButton$')}
|
|
71
|
+
>
|
|
72
|
+
<Label>Log In</Label>
|
|
73
|
+
</Button>
|
|
74
|
+
</div>
|
|
75
|
+
</form>
|
|
76
|
+
{:else}
|
|
77
|
+
<form onsubmit={(e) => e.preventDefault()}>
|
|
78
|
+
<div>
|
|
79
|
+
<Textfield
|
|
80
|
+
bind:value={username}
|
|
81
|
+
bind:this={usernameElem}
|
|
82
|
+
label={$clientConfig.emailUsernames ? 'Email' : 'Username'}
|
|
83
|
+
type={$clientConfig.emailUsernames ? 'email' : 'text'}
|
|
84
|
+
style="width: 100%;"
|
|
85
|
+
helperLine$style="width: 100%;"
|
|
86
|
+
invalid={usernameVerified === false}
|
|
87
|
+
input$autocomplete={$clientConfig.emailUsernames
|
|
88
|
+
? 'email'
|
|
89
|
+
: 'username'}
|
|
90
|
+
input$name={$clientConfig.emailUsernames ? 'email' : 'username'}
|
|
91
|
+
input$autocapitalize="off"
|
|
92
|
+
input$spellcheck="false"
|
|
93
|
+
{...prefixFilter(restProps, 'username$')}
|
|
94
|
+
>
|
|
95
|
+
{#snippet helper()}
|
|
96
|
+
<HelperText persistent>
|
|
97
|
+
{#if mode !== 'login'}
|
|
98
|
+
{usernameVerifiedMessage || ''}
|
|
99
|
+
{/if}
|
|
100
|
+
</HelperText>
|
|
101
|
+
{/snippet}
|
|
102
|
+
</Textfield>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div>
|
|
106
|
+
<Textfield
|
|
107
|
+
bind:value={password}
|
|
108
|
+
label="Password"
|
|
109
|
+
type="password"
|
|
110
|
+
style="width: 100%;"
|
|
111
|
+
input$autocomplete="{$clientConfig.allowRegistration &&
|
|
112
|
+
mode !== 'login'
|
|
113
|
+
? 'new'
|
|
114
|
+
: 'current'}-password"
|
|
115
|
+
input$name="password"
|
|
116
|
+
{...prefixFilter(restProps, 'password$')}
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{#if $clientConfig.allowRegistration && mode !== 'login'}
|
|
121
|
+
<div>
|
|
122
|
+
<Textfield
|
|
123
|
+
bind:value={password2}
|
|
124
|
+
label="Re-enter Password"
|
|
125
|
+
type="password"
|
|
126
|
+
style="width: 100%;"
|
|
127
|
+
input$autocomplete="new-password"
|
|
128
|
+
input$name="password2"
|
|
129
|
+
{...prefixFilter(restProps, 'password2$')}
|
|
130
|
+
/>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{#if $clientConfig.regFields.includes('name')}
|
|
134
|
+
<div>
|
|
135
|
+
<Textfield
|
|
136
|
+
bind:value={name}
|
|
137
|
+
label="Name"
|
|
138
|
+
type="text"
|
|
139
|
+
style="width: 100%;"
|
|
140
|
+
input$autocomplete="name"
|
|
141
|
+
input$name="name"
|
|
142
|
+
{...prefixFilter(restProps, 'name$')}
|
|
143
|
+
/>
|
|
144
|
+
</div>
|
|
145
|
+
{/if}
|
|
146
|
+
|
|
147
|
+
{#if !$clientConfig.emailUsernames && $clientConfig.regFields.includes('email')}
|
|
148
|
+
<div>
|
|
149
|
+
<Textfield
|
|
150
|
+
bind:value={email}
|
|
151
|
+
label="Email"
|
|
152
|
+
type="email"
|
|
153
|
+
style="width: 100%;"
|
|
154
|
+
input$autocomplete="email"
|
|
155
|
+
input$name="email"
|
|
156
|
+
input$autocapitalize="off"
|
|
157
|
+
input$spellcheck="false"
|
|
158
|
+
{...prefixFilter(restProps, 'email$')}
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
{/if}
|
|
162
|
+
|
|
163
|
+
{#if $clientConfig.regFields.includes('phone')}
|
|
164
|
+
<div>
|
|
165
|
+
<Textfield
|
|
166
|
+
bind:value={phone}
|
|
167
|
+
label="Phone Number"
|
|
168
|
+
type="tel"
|
|
169
|
+
style="width: 100%;"
|
|
170
|
+
input$autocomplete="tel"
|
|
171
|
+
input$name="phone"
|
|
172
|
+
{...prefixFilter(restProps, 'phone$')}
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
{/if}
|
|
176
|
+
{/if}
|
|
177
|
+
|
|
178
|
+
{@render additional?.()}
|
|
179
|
+
|
|
180
|
+
{#if failureMessage}
|
|
181
|
+
<div class="tilmeld-login-failure">
|
|
182
|
+
{failureMessage}
|
|
183
|
+
</div>
|
|
184
|
+
{/if}
|
|
185
|
+
|
|
186
|
+
<div class="tilmeld-login-buttons">
|
|
187
|
+
{#if mode === 'login'}
|
|
188
|
+
<Button
|
|
189
|
+
variant="raised"
|
|
190
|
+
type="submit"
|
|
191
|
+
onclick={login}
|
|
192
|
+
{...prefixFilter(restProps, 'loginButton$')}
|
|
193
|
+
>
|
|
194
|
+
<Label>Log In</Label>
|
|
195
|
+
</Button>
|
|
196
|
+
{:else}
|
|
197
|
+
<Button
|
|
198
|
+
variant="raised"
|
|
199
|
+
type="submit"
|
|
200
|
+
onclick={register}
|
|
201
|
+
{...prefixFilter(restProps, 'registerButton$')}
|
|
202
|
+
>
|
|
203
|
+
<Label>Create Account</Label>
|
|
204
|
+
</Button>
|
|
205
|
+
{/if}
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
{#if $clientConfig.allowRegistration && showExistingUserToggle}
|
|
209
|
+
<div class="tilmeld-login-action">
|
|
210
|
+
{#if mode === 'login'}
|
|
211
|
+
<a
|
|
212
|
+
href={'javascript:void(0);'}
|
|
213
|
+
onclick={() => (mode = 'register')}
|
|
214
|
+
{...prefixFilter(restProps, 'registerLink$')}
|
|
215
|
+
>
|
|
216
|
+
Create an account.
|
|
217
|
+
</a>
|
|
218
|
+
{:else}
|
|
219
|
+
<a
|
|
220
|
+
href={'javascript:void(0);'}
|
|
221
|
+
onclick={() => (mode = 'login')}
|
|
222
|
+
{...prefixFilter(restProps, 'loginLink$')}
|
|
223
|
+
>
|
|
224
|
+
Log in to your account.
|
|
225
|
+
</a>
|
|
226
|
+
{/if}
|
|
227
|
+
</div>
|
|
228
|
+
{/if}
|
|
229
|
+
|
|
230
|
+
{#if !hideRecovery && $clientConfig.userFields.includes('email') && $clientConfig.pwRecovery && mode === 'login'}
|
|
231
|
+
<div class="tilmeld-login-action">
|
|
232
|
+
<a
|
|
233
|
+
href={'javascript:void(0);'}
|
|
234
|
+
onclick={() => (recoverOpen = true)}
|
|
235
|
+
{...prefixFilter(restProps, 'recoverLink$')}
|
|
236
|
+
>
|
|
237
|
+
I can't access my account.
|
|
238
|
+
</a>
|
|
239
|
+
</div>
|
|
240
|
+
<Recover
|
|
241
|
+
{User}
|
|
242
|
+
bind:open={recoverOpen}
|
|
243
|
+
bind:clientConfig
|
|
244
|
+
{...prefixFilter(restProps, 'recover$')}
|
|
245
|
+
/>
|
|
246
|
+
{/if}
|
|
247
|
+
</form>
|
|
248
|
+
{/if}
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
<script lang="ts">
|
|
252
|
+
import type { ComponentProps, Snippet } from 'svelte';
|
|
253
|
+
import { onMount } from 'svelte';
|
|
254
|
+
import type { Writable } from 'svelte/store';
|
|
255
|
+
import { writable } from 'svelte/store';
|
|
256
|
+
import CircularProgress from '@smui/circular-progress';
|
|
257
|
+
import Button, { Label } from '@smui/button';
|
|
258
|
+
import Textfield from '@smui/textfield';
|
|
259
|
+
import HelperText from '@smui/textfield/helper-text';
|
|
260
|
+
import type { ActionArray } from '@smui/common/internal';
|
|
261
|
+
import { exclude, prefixFilter, useActions } from '@smui/common/internal';
|
|
262
|
+
import type { SmuiElementPropMap } from '@smui/common';
|
|
263
|
+
import type {
|
|
264
|
+
User as UserClass,
|
|
265
|
+
ClientConfig,
|
|
266
|
+
UserData,
|
|
267
|
+
CurrentUserData,
|
|
268
|
+
} from '@nymphjs/tilmeld-client';
|
|
269
|
+
import {
|
|
270
|
+
login as loginAction,
|
|
271
|
+
register as registerAction,
|
|
272
|
+
checkUsername as checkUsernameAction,
|
|
273
|
+
NeedTOTPError,
|
|
274
|
+
} from '@nymphjs/tilmeld-client';
|
|
275
|
+
import Recover from './Recover.svelte';
|
|
276
|
+
|
|
277
|
+
type OwnProps = {
|
|
278
|
+
/**
|
|
279
|
+
* An array of Action or [Action, ActionProps] to be applied to the element.
|
|
280
|
+
*/
|
|
281
|
+
use?: ActionArray;
|
|
282
|
+
/**
|
|
283
|
+
* A list of CSS styles.
|
|
284
|
+
*/
|
|
285
|
+
style?: string;
|
|
286
|
+
/**
|
|
287
|
+
* A writable store of the Nymph client config.
|
|
288
|
+
*
|
|
289
|
+
* It will be retrieved from the server if not provided.
|
|
290
|
+
*/
|
|
291
|
+
clientConfig?: Writable<ClientConfig | undefined>;
|
|
292
|
+
/**
|
|
293
|
+
* The User class from Nymph.
|
|
294
|
+
*/
|
|
295
|
+
User: typeof UserClass;
|
|
296
|
+
/**
|
|
297
|
+
* Hide the recovery link that only appears if password recovery is on.
|
|
298
|
+
*/
|
|
299
|
+
hideRecovery?: boolean;
|
|
300
|
+
/**
|
|
301
|
+
* Give focus to the username box (or email box) when the form is ready.
|
|
302
|
+
*/
|
|
303
|
+
autofocus?: boolean;
|
|
304
|
+
/**
|
|
305
|
+
* This determines whether the 'Log In' or 'Sign Up' button is activated
|
|
306
|
+
* and which corresponding form is shown.
|
|
307
|
+
*/
|
|
308
|
+
mode?: 'login' | 'register';
|
|
309
|
+
/**
|
|
310
|
+
* Whether to show the 'Log In'/'Sign Up' switcher buttons.
|
|
311
|
+
*/
|
|
312
|
+
showExistingUserToggle?: boolean;
|
|
313
|
+
/**
|
|
314
|
+
* The width of the form.
|
|
315
|
+
*/
|
|
316
|
+
width?: string;
|
|
317
|
+
/**
|
|
318
|
+
* User provided. You can bind to it if you need to.
|
|
319
|
+
*/
|
|
320
|
+
username?: string;
|
|
321
|
+
/**
|
|
322
|
+
* User provided. You can bind to it if you need to.
|
|
323
|
+
*/
|
|
324
|
+
password?: string;
|
|
325
|
+
/**
|
|
326
|
+
* User provided. You can bind to it if you need to.
|
|
327
|
+
*/
|
|
328
|
+
password2?: string;
|
|
329
|
+
/**
|
|
330
|
+
* User provided. You can bind to it if you need to.
|
|
331
|
+
*/
|
|
332
|
+
name?: string;
|
|
333
|
+
/**
|
|
334
|
+
* User provided. You can bind to it if you need to.
|
|
335
|
+
*/
|
|
336
|
+
email?: string;
|
|
337
|
+
/**
|
|
338
|
+
* User provided. You can bind to it if you need to.
|
|
339
|
+
*/
|
|
340
|
+
phone?: string;
|
|
341
|
+
/**
|
|
342
|
+
* User provided. You can bind to it if you need to.
|
|
343
|
+
*/
|
|
344
|
+
code?: string;
|
|
345
|
+
/**
|
|
346
|
+
* Provide this for anything else that should go up to the server.
|
|
347
|
+
*
|
|
348
|
+
* You can look for it in the User events.
|
|
349
|
+
*/
|
|
350
|
+
additionalData?: { [k: string]: any };
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* A spot for additional content.
|
|
354
|
+
*/
|
|
355
|
+
additional?: Snippet;
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* A callback to be called when a user is registered.
|
|
359
|
+
*/
|
|
360
|
+
onregister?: (details: {
|
|
361
|
+
user: UserClass & UserData & CurrentUserData;
|
|
362
|
+
}) => void;
|
|
363
|
+
/**
|
|
364
|
+
* A callback to be called when a user is logged in.
|
|
365
|
+
*/
|
|
366
|
+
onlogin?: (details: {
|
|
367
|
+
user: UserClass & UserData & CurrentUserData;
|
|
368
|
+
}) => void;
|
|
369
|
+
};
|
|
370
|
+
let {
|
|
371
|
+
use = [],
|
|
372
|
+
style = '',
|
|
373
|
+
clientConfig = $bindable(writable(false as unknown as undefined)),
|
|
374
|
+
User,
|
|
375
|
+
hideRecovery = false,
|
|
376
|
+
autofocus = true,
|
|
377
|
+
mode = $bindable('login'),
|
|
378
|
+
showExistingUserToggle = true,
|
|
379
|
+
width = '220px',
|
|
380
|
+
username = $bindable(''),
|
|
381
|
+
password = $bindable(''),
|
|
382
|
+
password2 = $bindable(''),
|
|
383
|
+
name = $bindable(''),
|
|
384
|
+
email = $bindable(''),
|
|
385
|
+
phone = $bindable(''),
|
|
386
|
+
code = $bindable(''),
|
|
387
|
+
additionalData,
|
|
388
|
+
additional,
|
|
389
|
+
onregister,
|
|
390
|
+
onlogin,
|
|
391
|
+
...restProps
|
|
392
|
+
}: OwnProps & {
|
|
393
|
+
[
|
|
394
|
+
k in keyof SmuiElementPropMap['div'] as `successRegisteredMessage\$${k}`
|
|
395
|
+
]?: SmuiElementPropMap['div'][k];
|
|
396
|
+
} & {
|
|
397
|
+
[
|
|
398
|
+
k in keyof SmuiElementPropMap['div'] as `successLoginMessage\$${k}`
|
|
399
|
+
]?: SmuiElementPropMap['div'][k];
|
|
400
|
+
} & {
|
|
401
|
+
[
|
|
402
|
+
k in keyof ComponentProps<typeof Textfield> as `username\$${k}`
|
|
403
|
+
]?: ComponentProps<typeof Textfield>[k];
|
|
404
|
+
} & {
|
|
405
|
+
[
|
|
406
|
+
k in keyof ComponentProps<typeof Textfield> as `password\$${k}`
|
|
407
|
+
]?: ComponentProps<typeof Textfield>[k];
|
|
408
|
+
} & {
|
|
409
|
+
[
|
|
410
|
+
k in keyof ComponentProps<typeof Textfield> as `password2\$${k}`
|
|
411
|
+
]?: ComponentProps<typeof Textfield>[k];
|
|
412
|
+
} & {
|
|
413
|
+
[
|
|
414
|
+
k in keyof ComponentProps<typeof Textfield> as `name\$${k}`
|
|
415
|
+
]?: ComponentProps<typeof Textfield>[k];
|
|
416
|
+
} & {
|
|
417
|
+
[
|
|
418
|
+
k in keyof ComponentProps<typeof Textfield> as `email\$${k}`
|
|
419
|
+
]?: ComponentProps<typeof Textfield>[k];
|
|
420
|
+
} & {
|
|
421
|
+
[
|
|
422
|
+
k in keyof ComponentProps<typeof Textfield> as `phone\$${k}`
|
|
423
|
+
]?: ComponentProps<typeof Textfield>[k];
|
|
424
|
+
} & {
|
|
425
|
+
[
|
|
426
|
+
k in keyof ComponentProps<typeof Textfield> as `code\$${k}`
|
|
427
|
+
]?: ComponentProps<typeof Textfield>[k];
|
|
428
|
+
} & {
|
|
429
|
+
[
|
|
430
|
+
k in keyof SmuiElementPropMap['div'] as `codeMessage\$${k}`
|
|
431
|
+
]?: SmuiElementPropMap['div'][k];
|
|
432
|
+
} & {
|
|
433
|
+
[
|
|
434
|
+
k in keyof ComponentProps<typeof CircularProgress> as `progress\$${k}`
|
|
435
|
+
]?: ComponentProps<typeof CircularProgress>[k];
|
|
436
|
+
} & {
|
|
437
|
+
[
|
|
438
|
+
k in keyof ComponentProps<
|
|
439
|
+
typeof Button<undefined, 'button'>
|
|
440
|
+
> as `loginButton\$${k}`
|
|
441
|
+
]?: ComponentProps<typeof Button<undefined, 'button'>>[k];
|
|
442
|
+
} & {
|
|
443
|
+
[
|
|
444
|
+
k in keyof ComponentProps<
|
|
445
|
+
typeof Button<undefined, 'button'>
|
|
446
|
+
> as `registerButton\$${k}`
|
|
447
|
+
]?: ComponentProps<typeof Button<undefined, 'button'>>[k];
|
|
448
|
+
} & {
|
|
449
|
+
[
|
|
450
|
+
k in keyof SmuiElementPropMap['a'] as `registerLink\$${k}`
|
|
451
|
+
]?: SmuiElementPropMap['a'][k];
|
|
452
|
+
} & {
|
|
453
|
+
[
|
|
454
|
+
k in keyof SmuiElementPropMap['a'] as `loginLink\$${k}`
|
|
455
|
+
]?: SmuiElementPropMap['a'][k];
|
|
456
|
+
} & {
|
|
457
|
+
[
|
|
458
|
+
k in keyof SmuiElementPropMap['a'] as `recoverLink\$${k}`
|
|
459
|
+
]?: SmuiElementPropMap['a'][k];
|
|
460
|
+
} & {
|
|
461
|
+
[
|
|
462
|
+
k in keyof ComponentProps<typeof Recover> as `recover\$${k}`
|
|
463
|
+
]?: ComponentProps<typeof Recover>[k];
|
|
464
|
+
} = $props();
|
|
465
|
+
|
|
466
|
+
let usernameElem: Textfield | undefined = $state();
|
|
467
|
+
let successLoginMessage: string | undefined = $state();
|
|
468
|
+
let successRegisteredMessage: string | undefined = $state();
|
|
469
|
+
let failureMessage: string | undefined = $state();
|
|
470
|
+
let usernameTimer: number | undefined = undefined;
|
|
471
|
+
let usernameVerified: boolean | undefined = $state();
|
|
472
|
+
let usernameVerifiedMessage: string | undefined = $state();
|
|
473
|
+
let totp = $state(false);
|
|
474
|
+
let loading = $state(false);
|
|
475
|
+
let recoverOpen = $state(false);
|
|
476
|
+
|
|
477
|
+
const nameFirst = $derived(name?.match(/^(.*?)(?: ([^ ]+))?$/)?.[1] ?? '');
|
|
478
|
+
const nameLast = $derived(name?.match(/^(.*?)(?: ([^ ]+))?$/)?.[2] ?? '');
|
|
479
|
+
|
|
480
|
+
let _previousMode = mode;
|
|
481
|
+
$effect(() => {
|
|
482
|
+
if (mode !== _previousMode) {
|
|
483
|
+
failureMessage = '';
|
|
484
|
+
checkUsername(username);
|
|
485
|
+
_previousMode = mode;
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
$effect(() => {
|
|
490
|
+
checkUsername(username);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
onMount(async () => {
|
|
494
|
+
if ($clientConfig === (false as unknown as undefined)) {
|
|
495
|
+
$clientConfig = undefined;
|
|
496
|
+
$clientConfig = await User.getClientConfig();
|
|
497
|
+
}
|
|
498
|
+
if (!$clientConfig.allowRegistration) {
|
|
499
|
+
mode = 'login';
|
|
500
|
+
}
|
|
501
|
+
if (autofocus && usernameElem) {
|
|
502
|
+
usernameElem.focus();
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
async function login() {
|
|
507
|
+
successLoginMessage = undefined;
|
|
508
|
+
failureMessage = undefined;
|
|
509
|
+
loading = true;
|
|
510
|
+
try {
|
|
511
|
+
const data = await loginAction(
|
|
512
|
+
User,
|
|
513
|
+
username,
|
|
514
|
+
password,
|
|
515
|
+
code,
|
|
516
|
+
additionalData,
|
|
517
|
+
);
|
|
518
|
+
successLoginMessage = data.message;
|
|
519
|
+
if (data.user) {
|
|
520
|
+
onlogin?.({ user: data.user });
|
|
521
|
+
}
|
|
522
|
+
} catch (e: any) {
|
|
523
|
+
if (e instanceof NeedTOTPError) {
|
|
524
|
+
totp = true;
|
|
525
|
+
} else {
|
|
526
|
+
failureMessage = e?.message;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
loading = false;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function register() {
|
|
533
|
+
successRegisteredMessage = undefined;
|
|
534
|
+
successLoginMessage = undefined;
|
|
535
|
+
failureMessage = undefined;
|
|
536
|
+
loading = true;
|
|
537
|
+
try {
|
|
538
|
+
const data = await registerAction(User, {
|
|
539
|
+
username,
|
|
540
|
+
usernameVerified: !!usernameVerified,
|
|
541
|
+
password,
|
|
542
|
+
password2,
|
|
543
|
+
email,
|
|
544
|
+
nameFirst,
|
|
545
|
+
nameLast,
|
|
546
|
+
phone,
|
|
547
|
+
...(additionalData ? { additionalData } : {}),
|
|
548
|
+
});
|
|
549
|
+
successRegisteredMessage = data.message;
|
|
550
|
+
if (data.user) {
|
|
551
|
+
onregister?.({ user: data.user });
|
|
552
|
+
}
|
|
553
|
+
if (data.loggedin) {
|
|
554
|
+
successLoginMessage = data.message;
|
|
555
|
+
if (data.user) {
|
|
556
|
+
onlogin?.({ user: data.user });
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
} catch (e: any) {
|
|
560
|
+
failureMessage = e?.message;
|
|
561
|
+
}
|
|
562
|
+
loading = false;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function checkUsername(newValue: string) {
|
|
566
|
+
usernameVerified = undefined;
|
|
567
|
+
usernameVerifiedMessage = undefined;
|
|
568
|
+
if (usernameTimer) {
|
|
569
|
+
clearTimeout(usernameTimer);
|
|
570
|
+
usernameTimer = undefined;
|
|
571
|
+
}
|
|
572
|
+
if (newValue === '' || mode === 'login') {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
usernameTimer = setTimeout(async () => {
|
|
576
|
+
try {
|
|
577
|
+
const data = await checkUsernameAction(User, newValue);
|
|
578
|
+
usernameVerified = true;
|
|
579
|
+
usernameVerifiedMessage = data.message;
|
|
580
|
+
} catch (e: any) {
|
|
581
|
+
usernameVerified = false;
|
|
582
|
+
usernameVerifiedMessage = e?.message;
|
|
583
|
+
}
|
|
584
|
+
}, 400);
|
|
585
|
+
}
|
|
586
|
+
</script>
|
|
587
|
+
|
|
588
|
+
<style>
|
|
589
|
+
.tilmeld-login-buttons {
|
|
590
|
+
display: flex;
|
|
591
|
+
flex-direction: row;
|
|
592
|
+
justify-content: start;
|
|
593
|
+
align-items: center;
|
|
594
|
+
margin-top: 1em;
|
|
595
|
+
}
|
|
596
|
+
.tilmeld-login-action {
|
|
597
|
+
margin-top: 1em;
|
|
598
|
+
}
|
|
599
|
+
.tilmeld-login-failure {
|
|
600
|
+
margin-top: 1em;
|
|
601
|
+
color: var(--mdc-theme-error, #f00);
|
|
602
|
+
}
|
|
603
|
+
</style>
|