@nymphjs/tilmeld-setup 1.0.0-beta.11 → 1.0.0-beta.111
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 +442 -0
- package/README.md +6 -5
- package/app/index.ts +9 -3
- package/app/src/App.svelte +143 -71
- package/app/src/nymph.ts +1 -1
- package/app/src/routes/GroupEdit.svelte +638 -0
- package/app/src/routes/Groups.svelte +201 -0
- package/app/src/{Intro.svelte → routes/Intro.svelte} +14 -1
- package/app/src/routes/NotFound.svelte +9 -0
- package/app/src/routes/UserEdit.svelte +1202 -0
- package/app/src/routes/Users.svelte +201 -0
- package/dist/app/index.css +105 -0
- package/dist/app/index.d.ts +1 -0
- package/dist/app/index.js +41288 -3
- package/dist/app/smui.css +1 -29
- package/dist/app/src/nymph.d.ts +6 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +19 -18
- package/dist/index.js.map +1 -1
- package/jest.config.js +11 -2
- package/package.json +58 -54
- package/rollup.config.mjs +36 -0
- package/src/index.ts +28 -21
- package/src/type/locutus.d.ts +3 -0
- package/static/index.html +4 -1
- package/test.mjs +13 -12
- package/tsconfig.json +11 -1
- package/tsconfig.server.json +5 -3
- package/typedoc.json +5 -0
- package/app/src/GroupEdit.svelte +0 -570
- package/app/src/Groups.svelte +0 -159
- package/app/src/UserEdit.svelte +0 -902
- package/app/src/Users.svelte +0 -159
- package/dist/app/index.js.LICENSE.txt +0 -114
- package/dist/app/index.js.map +0 -1
- package/webpack.config.js +0 -38
package/app/src/UserEdit.svelte
DELETED
|
@@ -1,902 +0,0 @@
|
|
|
1
|
-
<div style="display: flex; align-items: center; padding: 12px;">
|
|
2
|
-
<IconButton title="Back" on:click={() => dispatch('leave')}>
|
|
3
|
-
<Icon component={Svg} viewBox="0 0 24 24">
|
|
4
|
-
<path fill="currentColor" d={mdiArrowLeft} />
|
|
5
|
-
</Icon>
|
|
6
|
-
</IconButton>
|
|
7
|
-
<h2 style="margin: 0px 12px 0px;" class="mdc-typography--headline5">
|
|
8
|
-
Editing {entity.guid
|
|
9
|
-
? entity.$is(user)
|
|
10
|
-
? 'Yourself'
|
|
11
|
-
: entity.name
|
|
12
|
-
: 'New User'}
|
|
13
|
-
</h2>
|
|
14
|
-
</div>
|
|
15
|
-
{#if entity != null}
|
|
16
|
-
{#if clientConfig == null || user == null}
|
|
17
|
-
<section style="padding-top: 0;">
|
|
18
|
-
<div style="display: flex; justify-content: center; align-items: center;">
|
|
19
|
-
<CircularProgress style="height: 45px; width: 45px;" indeterminate />
|
|
20
|
-
</div>
|
|
21
|
-
</section>
|
|
22
|
-
{:else}
|
|
23
|
-
<TabBar
|
|
24
|
-
tabs={['General', 'Groups', 'Abilities', 'Security']}
|
|
25
|
-
let:tab
|
|
26
|
-
bind:active={activeTab}
|
|
27
|
-
>
|
|
28
|
-
<Tab {tab}>
|
|
29
|
-
<Label>{tab}</Label>
|
|
30
|
-
</Tab>
|
|
31
|
-
</TabBar>
|
|
32
|
-
|
|
33
|
-
<section>
|
|
34
|
-
{#if activeTab === 'General'}
|
|
35
|
-
<LayoutGrid style="padding: 0;">
|
|
36
|
-
<LayoutCell span={4}>
|
|
37
|
-
<div class="mdc-typography--headline6">GUID</div>
|
|
38
|
-
<code>{entity.guid}</code>
|
|
39
|
-
</LayoutCell>
|
|
40
|
-
<LayoutCell span={4}>
|
|
41
|
-
<FormField>
|
|
42
|
-
<Checkbox bind:checked={entity.enabled} />
|
|
43
|
-
<span slot="label">Enabled (Able to log in)</span>
|
|
44
|
-
</FormField>
|
|
45
|
-
</LayoutCell>
|
|
46
|
-
<LayoutCell span={4} style="text-align: end;">
|
|
47
|
-
<a href="https://en.gravatar.com/" target="_blank" rel="noreferrer">
|
|
48
|
-
<img src={avatar} alt="Avatar" title="Avatar by Gravatar" />
|
|
49
|
-
</a>
|
|
50
|
-
</LayoutCell>
|
|
51
|
-
{#if !clientConfig.emailUsernames}
|
|
52
|
-
<LayoutCell span={6}>
|
|
53
|
-
<Textfield
|
|
54
|
-
bind:value={entity.username}
|
|
55
|
-
label="Username"
|
|
56
|
-
type="text"
|
|
57
|
-
style="width: 100%;"
|
|
58
|
-
helperLine$style="width: 100%;"
|
|
59
|
-
invalid={usernameVerified === false}
|
|
60
|
-
input$autocomplete="off"
|
|
61
|
-
input$autocapitalize="off"
|
|
62
|
-
input$spellcheck="false"
|
|
63
|
-
>
|
|
64
|
-
<HelperText persistent slot="helper">
|
|
65
|
-
{usernameVerifiedMessage ?? ''}
|
|
66
|
-
</HelperText>
|
|
67
|
-
</Textfield>
|
|
68
|
-
</LayoutCell>
|
|
69
|
-
{/if}
|
|
70
|
-
<LayoutCell span={clientConfig.emailUsernames ? 12 : 6}>
|
|
71
|
-
<Textfield
|
|
72
|
-
bind:value={entity.email}
|
|
73
|
-
label="Email"
|
|
74
|
-
type="email"
|
|
75
|
-
style="width: 100%;"
|
|
76
|
-
helperLine$style="width: 100%;"
|
|
77
|
-
invalid={emailVerified === false}
|
|
78
|
-
input$autocomplete="off"
|
|
79
|
-
input$autocapitalize="off"
|
|
80
|
-
input$spellcheck="false"
|
|
81
|
-
>
|
|
82
|
-
<HelperText persistent slot="helper">
|
|
83
|
-
{emailVerifiedMessage ?? ''}
|
|
84
|
-
</HelperText>
|
|
85
|
-
</Textfield>
|
|
86
|
-
</LayoutCell>
|
|
87
|
-
<LayoutCell span={4}>
|
|
88
|
-
<Textfield
|
|
89
|
-
bind:value={entity.nameFirst}
|
|
90
|
-
label="First Name"
|
|
91
|
-
type="text"
|
|
92
|
-
style="width: 100%;"
|
|
93
|
-
input$autocomplete="off"
|
|
94
|
-
/>
|
|
95
|
-
</LayoutCell>
|
|
96
|
-
<LayoutCell span={4}>
|
|
97
|
-
<Textfield
|
|
98
|
-
bind:value={entity.nameMiddle}
|
|
99
|
-
label="Middle Name"
|
|
100
|
-
type="text"
|
|
101
|
-
style="width: 100%;"
|
|
102
|
-
input$autocomplete="off"
|
|
103
|
-
/>
|
|
104
|
-
</LayoutCell>
|
|
105
|
-
<LayoutCell span={4}>
|
|
106
|
-
<Textfield
|
|
107
|
-
bind:value={entity.nameLast}
|
|
108
|
-
label="Last Name"
|
|
109
|
-
type="text"
|
|
110
|
-
style="width: 100%;"
|
|
111
|
-
input$autocomplete="off"
|
|
112
|
-
/>
|
|
113
|
-
</LayoutCell>
|
|
114
|
-
<LayoutCell span={8}>
|
|
115
|
-
<Textfield
|
|
116
|
-
bind:value={entity.avatar}
|
|
117
|
-
label="Avatar"
|
|
118
|
-
type="text"
|
|
119
|
-
style="width: 100%;"
|
|
120
|
-
input$autocomplete="off"
|
|
121
|
-
/>
|
|
122
|
-
</LayoutCell>
|
|
123
|
-
<LayoutCell span={4}>
|
|
124
|
-
<Textfield
|
|
125
|
-
bind:value={entity.phone}
|
|
126
|
-
label="Phone"
|
|
127
|
-
type="tel"
|
|
128
|
-
style="width: 100%;"
|
|
129
|
-
input$autocomplete="off"
|
|
130
|
-
/>
|
|
131
|
-
</LayoutCell>
|
|
132
|
-
<LayoutCell span={6}>
|
|
133
|
-
<Textfield
|
|
134
|
-
bind:value={entity.passwordTemp}
|
|
135
|
-
label={`${entity.guid ? 'Update ' : ''}Password`}
|
|
136
|
-
type="password"
|
|
137
|
-
style="width: 100%;"
|
|
138
|
-
input$autocomplete="off"
|
|
139
|
-
/>
|
|
140
|
-
</LayoutCell>
|
|
141
|
-
<LayoutCell span={6}>
|
|
142
|
-
<Textfield
|
|
143
|
-
bind:value={passwordVerify}
|
|
144
|
-
label="Repeat Password"
|
|
145
|
-
type="password"
|
|
146
|
-
style="width: 100%;"
|
|
147
|
-
invalid={passwordVerified === false}
|
|
148
|
-
input$autocomplete="off"
|
|
149
|
-
on:blur={doVerifyPassword}
|
|
150
|
-
/>
|
|
151
|
-
</LayoutCell>
|
|
152
|
-
</LayoutGrid>
|
|
153
|
-
{/if}
|
|
154
|
-
|
|
155
|
-
{#if activeTab === 'Groups'}
|
|
156
|
-
<h5 style="margin-top: 0;">Primary Group</h5>
|
|
157
|
-
|
|
158
|
-
<Paper
|
|
159
|
-
style="display: flex; justify-content: space-between; align-items: center;"
|
|
160
|
-
>
|
|
161
|
-
{#if !entity.group}
|
|
162
|
-
No primary group
|
|
163
|
-
{:else}
|
|
164
|
-
<span
|
|
165
|
-
>{entity.group.name + ' (' + entity.group.groupname + ')'}</span
|
|
166
|
-
>
|
|
167
|
-
|
|
168
|
-
<IconButton
|
|
169
|
-
on:click={() => {
|
|
170
|
-
delete entity.group;
|
|
171
|
-
entity = entity;
|
|
172
|
-
}}
|
|
173
|
-
>
|
|
174
|
-
<Icon component={Svg} viewBox="0 0 24 24">
|
|
175
|
-
<path fill="currentColor" d={mdiMinus} />
|
|
176
|
-
</Icon>
|
|
177
|
-
</IconButton>
|
|
178
|
-
{/if}
|
|
179
|
-
</Paper>
|
|
180
|
-
|
|
181
|
-
<h6>Change Primary Group</h6>
|
|
182
|
-
|
|
183
|
-
<div class="solo-search-container solo-container">
|
|
184
|
-
<Paper class="solo-paper" elevation={1}>
|
|
185
|
-
<Icon class="solo-icon" component={Svg} viewBox="0 0 24 24">
|
|
186
|
-
<path fill="currentColor" d={mdiMagnify} />
|
|
187
|
-
</Icon>
|
|
188
|
-
<Input
|
|
189
|
-
bind:value={primaryGroupSearch}
|
|
190
|
-
on:keydown={primaryGroupSearchKeyDown}
|
|
191
|
-
placeholder="Primary Group Search"
|
|
192
|
-
class="solo-input"
|
|
193
|
-
/>
|
|
194
|
-
</Paper>
|
|
195
|
-
<IconButton
|
|
196
|
-
on:click={searchPrimaryGroups}
|
|
197
|
-
disabled={primaryGroupSearch === ''}
|
|
198
|
-
class="solo-fab"
|
|
199
|
-
title="Search"
|
|
200
|
-
>
|
|
201
|
-
<Icon component={Svg} viewBox="0 0 24 24">
|
|
202
|
-
<path fill="currentColor" d={mdiArrowRight} />
|
|
203
|
-
</Icon>
|
|
204
|
-
</IconButton>
|
|
205
|
-
</div>
|
|
206
|
-
|
|
207
|
-
{#if primaryGroupsSearching}
|
|
208
|
-
<div
|
|
209
|
-
style="display: flex; justify-content: center; align-items: center;"
|
|
210
|
-
>
|
|
211
|
-
<CircularProgress
|
|
212
|
-
style="height: 32px; width: 32px;"
|
|
213
|
-
indeterminate
|
|
214
|
-
/>
|
|
215
|
-
</div>
|
|
216
|
-
{:else if primaryGroups != null}
|
|
217
|
-
<DataTable table$aria-label="Primary group list" style="width: 100%;">
|
|
218
|
-
<Head>
|
|
219
|
-
<Row>
|
|
220
|
-
{#if !clientConfig.emailUsernames}
|
|
221
|
-
<Cell>Groupname</Cell>
|
|
222
|
-
{/if}
|
|
223
|
-
<Cell>Name</Cell>
|
|
224
|
-
<Cell>Email</Cell>
|
|
225
|
-
<Cell>Enabled</Cell>
|
|
226
|
-
</Row>
|
|
227
|
-
</Head>
|
|
228
|
-
<Body>
|
|
229
|
-
{#each primaryGroups as curEntity (curEntity.guid)}
|
|
230
|
-
<Row
|
|
231
|
-
on:click={() => (entity.group = curEntity)}
|
|
232
|
-
style="cursor: pointer;"
|
|
233
|
-
>
|
|
234
|
-
{#if !clientConfig.emailUsernames}
|
|
235
|
-
<Cell>{curEntity.groupname}</Cell>
|
|
236
|
-
{/if}
|
|
237
|
-
<Cell>{curEntity.name}</Cell>
|
|
238
|
-
<Cell>{curEntity.email}</Cell>
|
|
239
|
-
<Cell>{curEntity.enabled ? 'Yes' : 'No'}</Cell>
|
|
240
|
-
</Row>
|
|
241
|
-
{/each}
|
|
242
|
-
</Body>
|
|
243
|
-
</DataTable>
|
|
244
|
-
{/if}
|
|
245
|
-
|
|
246
|
-
<h5>Secondary Groups</h5>
|
|
247
|
-
|
|
248
|
-
<DataTable
|
|
249
|
-
table$aria-label="Current secondary groups"
|
|
250
|
-
style="width: 100%;"
|
|
251
|
-
>
|
|
252
|
-
<Head>
|
|
253
|
-
<Row>
|
|
254
|
-
{#if !clientConfig.emailUsernames}
|
|
255
|
-
<Cell>Groupname</Cell>
|
|
256
|
-
{/if}
|
|
257
|
-
<Cell>Name</Cell>
|
|
258
|
-
<Cell>Email</Cell>
|
|
259
|
-
<Cell>Enabled</Cell>
|
|
260
|
-
<Cell>Remove</Cell>
|
|
261
|
-
</Row>
|
|
262
|
-
</Head>
|
|
263
|
-
<Body>
|
|
264
|
-
{#if entity.groups}
|
|
265
|
-
{#each entity.groups as curEntity, index (curEntity.guid)}
|
|
266
|
-
<Row>
|
|
267
|
-
{#if !clientConfig.emailUsernames}
|
|
268
|
-
<Cell>{curEntity.groupname}</Cell>
|
|
269
|
-
{/if}
|
|
270
|
-
<Cell>{curEntity.name}</Cell>
|
|
271
|
-
<Cell>{curEntity.email}</Cell>
|
|
272
|
-
<Cell>{curEntity.enabled ? 'Yes' : 'No'}</Cell>
|
|
273
|
-
<Cell>
|
|
274
|
-
<IconButton
|
|
275
|
-
on:click={() => {
|
|
276
|
-
entity.groups?.splice(index, 1);
|
|
277
|
-
entity = entity;
|
|
278
|
-
}}
|
|
279
|
-
>
|
|
280
|
-
<Icon component={Svg} viewBox="0 0 24 24">
|
|
281
|
-
<path fill="currentColor" d={mdiMinus} />
|
|
282
|
-
</Icon>
|
|
283
|
-
</IconButton>
|
|
284
|
-
</Cell>
|
|
285
|
-
</Row>
|
|
286
|
-
{:else}
|
|
287
|
-
<Row>
|
|
288
|
-
<Cell colspan={clientConfig.emailUsernames ? 4 : 5}>
|
|
289
|
-
No secondary groups
|
|
290
|
-
</Cell>
|
|
291
|
-
</Row>
|
|
292
|
-
{/each}
|
|
293
|
-
{/if}
|
|
294
|
-
</Body>
|
|
295
|
-
</DataTable>
|
|
296
|
-
|
|
297
|
-
<h6>Add Secondary Groups</h6>
|
|
298
|
-
|
|
299
|
-
<div class="solo-search-container solo-container">
|
|
300
|
-
<Paper class="solo-paper" elevation={1}>
|
|
301
|
-
<Icon class="solo-icon" component={Svg} viewBox="0 0 24 24">
|
|
302
|
-
<path fill="currentColor" d={mdiMagnify} />
|
|
303
|
-
</Icon>
|
|
304
|
-
<Input
|
|
305
|
-
bind:value={secondaryGroupSearch}
|
|
306
|
-
on:keydown={secondaryGroupSearchKeyDown}
|
|
307
|
-
placeholder="Secondary Group Search"
|
|
308
|
-
class="solo-input"
|
|
309
|
-
/>
|
|
310
|
-
</Paper>
|
|
311
|
-
<IconButton
|
|
312
|
-
on:click={searchSecondaryGroups}
|
|
313
|
-
disabled={secondaryGroupSearch === ''}
|
|
314
|
-
class="solo-fab"
|
|
315
|
-
title="Search"
|
|
316
|
-
>
|
|
317
|
-
<Icon component={Svg} viewBox="0 0 24 24">
|
|
318
|
-
<path fill="currentColor" d={mdiArrowRight} />
|
|
319
|
-
</Icon>
|
|
320
|
-
</IconButton>
|
|
321
|
-
</div>
|
|
322
|
-
|
|
323
|
-
{#if secondaryGroupsSearching}
|
|
324
|
-
<div
|
|
325
|
-
style="display: flex; justify-content: center; align-items: center;"
|
|
326
|
-
>
|
|
327
|
-
<CircularProgress
|
|
328
|
-
style="height: 32px; width: 32px;"
|
|
329
|
-
indeterminate
|
|
330
|
-
/>
|
|
331
|
-
</div>
|
|
332
|
-
{:else if secondaryGroups != null}
|
|
333
|
-
<DataTable
|
|
334
|
-
table$aria-label="Secondary group list"
|
|
335
|
-
style="width: 100%;"
|
|
336
|
-
>
|
|
337
|
-
<Head>
|
|
338
|
-
<Row>
|
|
339
|
-
{#if !clientConfig.emailUsernames}
|
|
340
|
-
<Cell>Groupname</Cell>
|
|
341
|
-
{/if}
|
|
342
|
-
<Cell>Name</Cell>
|
|
343
|
-
<Cell>Email</Cell>
|
|
344
|
-
<Cell>Enabled</Cell>
|
|
345
|
-
</Row>
|
|
346
|
-
</Head>
|
|
347
|
-
<Body>
|
|
348
|
-
{#each secondaryGroups as curEntity, index (curEntity.guid)}
|
|
349
|
-
<Row
|
|
350
|
-
on:click={() => {
|
|
351
|
-
entity.groups?.push(curEntity);
|
|
352
|
-
secondaryGroups?.splice(index, 1);
|
|
353
|
-
entity = entity;
|
|
354
|
-
}}
|
|
355
|
-
style="cursor: pointer;"
|
|
356
|
-
>
|
|
357
|
-
{#if !clientConfig.emailUsernames}
|
|
358
|
-
<Cell>{curEntity.groupname}</Cell>
|
|
359
|
-
{/if}
|
|
360
|
-
<Cell>{curEntity.name}</Cell>
|
|
361
|
-
<Cell>{curEntity.email}</Cell>
|
|
362
|
-
<Cell>{curEntity.enabled ? 'Yes' : 'No'}</Cell>
|
|
363
|
-
</Row>
|
|
364
|
-
{/each}
|
|
365
|
-
</Body>
|
|
366
|
-
</DataTable>
|
|
367
|
-
{/if}
|
|
368
|
-
{/if}
|
|
369
|
-
|
|
370
|
-
{#if activeTab === 'Abilities'}
|
|
371
|
-
<h5 style="margin-top: 0;">Abilities</h5>
|
|
372
|
-
|
|
373
|
-
<List nonInteractive>
|
|
374
|
-
{#if entity.abilities}
|
|
375
|
-
{#each entity.abilities as ability, index (ability)}
|
|
376
|
-
<Item>
|
|
377
|
-
<Text>
|
|
378
|
-
{ability}
|
|
379
|
-
</Text>
|
|
380
|
-
<Meta>
|
|
381
|
-
<IconButton
|
|
382
|
-
on:click={() => {
|
|
383
|
-
entity.abilities?.splice(index, 1);
|
|
384
|
-
entity = entity;
|
|
385
|
-
}}
|
|
386
|
-
>
|
|
387
|
-
<Icon component={Svg} viewBox="0 0 24 24">
|
|
388
|
-
<path fill="currentColor" d={mdiMinus} />
|
|
389
|
-
</Icon>
|
|
390
|
-
</IconButton>
|
|
391
|
-
</Meta>
|
|
392
|
-
</Item>
|
|
393
|
-
{:else}
|
|
394
|
-
<Item>
|
|
395
|
-
<Text>No abilities</Text>
|
|
396
|
-
</Item>
|
|
397
|
-
{/each}
|
|
398
|
-
{/if}
|
|
399
|
-
</List>
|
|
400
|
-
|
|
401
|
-
<h6>Add Ability</h6>
|
|
402
|
-
|
|
403
|
-
<div style="display: flex; align-items: center; flex-wrap: wrap;">
|
|
404
|
-
<Textfield
|
|
405
|
-
bind:value={ability}
|
|
406
|
-
label="Ability"
|
|
407
|
-
type="text"
|
|
408
|
-
style="width: 250px; max-width: 100%;"
|
|
409
|
-
on:keydown={abilityKeyDown}
|
|
410
|
-
/>
|
|
411
|
-
<IconButton on:click={addAbility}>
|
|
412
|
-
<Icon component={Svg} viewBox="0 0 24 24">
|
|
413
|
-
<path fill="currentColor" d={mdiPlus} />
|
|
414
|
-
</Icon>
|
|
415
|
-
</IconButton>
|
|
416
|
-
<Button
|
|
417
|
-
on:click={addTilmeldAdminAbility}
|
|
418
|
-
title="Tilmeld Admins have the ability to modify, create, and delete users and groups, and grant and revoke abilities."
|
|
419
|
-
>
|
|
420
|
-
<Label>Tilmeld Admin</Label>
|
|
421
|
-
</Button>
|
|
422
|
-
{#if sysAdmin}
|
|
423
|
-
<Button
|
|
424
|
-
on:click={addSystemAdminAbility}
|
|
425
|
-
title="System Admins have all abilities. Gatekeeper checks always return true."
|
|
426
|
-
>
|
|
427
|
-
<Label>System Admin</Label>
|
|
428
|
-
</Button>
|
|
429
|
-
{/if}
|
|
430
|
-
</div>
|
|
431
|
-
|
|
432
|
-
<h6>Inherit Abilities</h6>
|
|
433
|
-
|
|
434
|
-
<div>
|
|
435
|
-
<FormField>
|
|
436
|
-
<Checkbox bind:checked={entity.inheritAbilities} />
|
|
437
|
-
<span slot="label"
|
|
438
|
-
>Additionally, inherit the abilities of the group(s) this user
|
|
439
|
-
belongs to.</span
|
|
440
|
-
>
|
|
441
|
-
</FormField>
|
|
442
|
-
</div>
|
|
443
|
-
{/if}
|
|
444
|
-
|
|
445
|
-
{#if activeTab === 'Security'}
|
|
446
|
-
<LayoutGrid style="padding: 0;">
|
|
447
|
-
<LayoutCell span={12}>
|
|
448
|
-
The email verification secret is the code emailed to the user to
|
|
449
|
-
verify their address when they first sign up.
|
|
450
|
-
</LayoutCell>
|
|
451
|
-
<LayoutCell span={12}>
|
|
452
|
-
<Textfield
|
|
453
|
-
bind:value={entity.secret}
|
|
454
|
-
label="Email Verification Secret"
|
|
455
|
-
type="text"
|
|
456
|
-
style="width: 100%;"
|
|
457
|
-
input$autocomplete="off"
|
|
458
|
-
/>
|
|
459
|
-
</LayoutCell>
|
|
460
|
-
<LayoutCell span={12}>
|
|
461
|
-
The account recovery secret is the code emailed to the user to allow
|
|
462
|
-
them to change their password and recover their account. The date is
|
|
463
|
-
used to determine if the code has expired.
|
|
464
|
-
</LayoutCell>
|
|
465
|
-
<LayoutCell span={6}>
|
|
466
|
-
<Textfield
|
|
467
|
-
bind:value={entity.recoverSecret}
|
|
468
|
-
label="Account Recovery Secret"
|
|
469
|
-
type="text"
|
|
470
|
-
style="width: 100%;"
|
|
471
|
-
input$autocomplete="off"
|
|
472
|
-
/>
|
|
473
|
-
</LayoutCell>
|
|
474
|
-
<LayoutCell span={6}>
|
|
475
|
-
<Textfield
|
|
476
|
-
bind:value={entity.recoverSecretDate}
|
|
477
|
-
label="Account Recovery Date (Timestamp)"
|
|
478
|
-
type="number"
|
|
479
|
-
style="width: 100%;"
|
|
480
|
-
input$autocomplete="off"
|
|
481
|
-
/>
|
|
482
|
-
</LayoutCell>
|
|
483
|
-
<LayoutCell span={12}>
|
|
484
|
-
An email change uses all of the following properties. The email
|
|
485
|
-
change date is used to rate limit email changes and to allow the
|
|
486
|
-
user to cancel the change within the rate limit time. The new secret
|
|
487
|
-
is emailed to the new address, and when the user clicks the link,
|
|
488
|
-
that email address is set for their account. The cancel secret is
|
|
489
|
-
emailed to the old address and will reset the user's email to the
|
|
490
|
-
cancel address if the link is clicked in time.
|
|
491
|
-
</LayoutCell>
|
|
492
|
-
<LayoutCell span={12}>
|
|
493
|
-
<Textfield
|
|
494
|
-
bind:value={entity.emailChangeDate}
|
|
495
|
-
label="Email Change Date (Timestamp)"
|
|
496
|
-
type="number"
|
|
497
|
-
style="width: 100%;"
|
|
498
|
-
input$autocomplete="off"
|
|
499
|
-
/>
|
|
500
|
-
</LayoutCell>
|
|
501
|
-
<LayoutCell span={6}>
|
|
502
|
-
<Textfield
|
|
503
|
-
bind:value={entity.newEmailSecret}
|
|
504
|
-
label="New Email Verification Secret"
|
|
505
|
-
type="text"
|
|
506
|
-
style="width: 100%;"
|
|
507
|
-
input$autocomplete="off"
|
|
508
|
-
/>
|
|
509
|
-
</LayoutCell>
|
|
510
|
-
<LayoutCell span={6}>
|
|
511
|
-
<Textfield
|
|
512
|
-
bind:value={entity.newEmailAddress}
|
|
513
|
-
label="New Email Address"
|
|
514
|
-
type="email"
|
|
515
|
-
style="width: 100%;"
|
|
516
|
-
input$autocomplete="off"
|
|
517
|
-
/>
|
|
518
|
-
</LayoutCell>
|
|
519
|
-
<LayoutCell span={6}>
|
|
520
|
-
<Textfield
|
|
521
|
-
bind:value={entity.cancelEmailSecret}
|
|
522
|
-
label="Cancel Email Verification Secret"
|
|
523
|
-
type="text"
|
|
524
|
-
style="width: 100%;"
|
|
525
|
-
input$autocomplete="off"
|
|
526
|
-
/>
|
|
527
|
-
</LayoutCell>
|
|
528
|
-
<LayoutCell span={6}>
|
|
529
|
-
<Textfield
|
|
530
|
-
bind:value={entity.cancelEmailAddress}
|
|
531
|
-
label="Cancel Email Address"
|
|
532
|
-
type="email"
|
|
533
|
-
style="width: 100%;"
|
|
534
|
-
input$autocomplete="off"
|
|
535
|
-
/>
|
|
536
|
-
</LayoutCell>
|
|
537
|
-
</LayoutGrid>
|
|
538
|
-
{/if}
|
|
539
|
-
|
|
540
|
-
{#if failureMessage}
|
|
541
|
-
<div class="tilmeld-failure">
|
|
542
|
-
{failureMessage}
|
|
543
|
-
</div>
|
|
544
|
-
{/if}
|
|
545
|
-
|
|
546
|
-
<div style="margin-top: 36px;">
|
|
547
|
-
<Button variant="raised" on:click={saveEntity} disabled={saving}>
|
|
548
|
-
<Label>Save User</Label>
|
|
549
|
-
</Button>
|
|
550
|
-
{#if entity.guid}
|
|
551
|
-
<Button on:click={deleteEntity} disabled={saving}>
|
|
552
|
-
<Label>Delete</Label>
|
|
553
|
-
</Button>
|
|
554
|
-
{/if}
|
|
555
|
-
{#if success}
|
|
556
|
-
<span>Successfully saved!</span>
|
|
557
|
-
{/if}
|
|
558
|
-
</div>
|
|
559
|
-
</section>
|
|
560
|
-
{/if}
|
|
561
|
-
{/if}
|
|
562
|
-
|
|
563
|
-
<script lang="ts">
|
|
564
|
-
import { createEventDispatcher, onMount } from 'svelte';
|
|
565
|
-
import type {
|
|
566
|
-
AdminGroupData,
|
|
567
|
-
AdminUserData,
|
|
568
|
-
ClientConfig,
|
|
569
|
-
CurrentUserData,
|
|
570
|
-
} from '@nymphjs/tilmeld-client';
|
|
571
|
-
import type {
|
|
572
|
-
Group as GroupClass,
|
|
573
|
-
User as UserClass,
|
|
574
|
-
} from '@nymphjs/tilmeld-client';
|
|
575
|
-
import queryParser from '@nymphjs/query-parser';
|
|
576
|
-
import {
|
|
577
|
-
mdiArrowLeft,
|
|
578
|
-
mdiArrowRight,
|
|
579
|
-
mdiMagnify,
|
|
580
|
-
mdiMinus,
|
|
581
|
-
mdiPlus,
|
|
582
|
-
} from '@mdi/js';
|
|
583
|
-
import CircularProgress from '@smui/circular-progress';
|
|
584
|
-
import Tab from '@smui/tab';
|
|
585
|
-
import TabBar from '@smui/tab-bar';
|
|
586
|
-
import LayoutGrid, { Cell as LayoutCell } from '@smui/layout-grid';
|
|
587
|
-
import FormField from '@smui/form-field';
|
|
588
|
-
import Checkbox from '@smui/checkbox';
|
|
589
|
-
import List, { Item, Text, Meta } from '@smui/list';
|
|
590
|
-
import Paper from '@smui/paper';
|
|
591
|
-
import DataTable, { Head, Body, Row, Cell } from '@smui/data-table';
|
|
592
|
-
import Textfield, { Input } from '@smui/textfield';
|
|
593
|
-
import HelperText from '@smui/textfield/helper-text';
|
|
594
|
-
import IconButton from '@smui/icon-button';
|
|
595
|
-
import Button from '@smui/button';
|
|
596
|
-
import { Icon, Label, Svg } from '@smui/common';
|
|
597
|
-
|
|
598
|
-
import { User, Group } from './nymph';
|
|
599
|
-
|
|
600
|
-
const dispatch = createEventDispatcher();
|
|
601
|
-
|
|
602
|
-
export let entity: UserClass & AdminUserData;
|
|
603
|
-
|
|
604
|
-
let clientConfig: ClientConfig | undefined = undefined;
|
|
605
|
-
let user: (UserClass & CurrentUserData) | undefined = undefined;
|
|
606
|
-
let sysAdmin = false;
|
|
607
|
-
let activeTab: 'General' | 'Groups' | 'Abilities' | 'Security' = 'General';
|
|
608
|
-
let primaryGroupSearch = '';
|
|
609
|
-
let secondaryGroupSearch = '';
|
|
610
|
-
let ability = '';
|
|
611
|
-
let avatar = 'https://secure.gravatar.com/avatar/?d=mm&s=40';
|
|
612
|
-
let failureMessage: string | undefined = undefined;
|
|
613
|
-
let passwordVerify = '';
|
|
614
|
-
let passwordVerified: boolean | undefined = undefined;
|
|
615
|
-
let usernameTimer: NodeJS.Timeout | undefined = undefined;
|
|
616
|
-
let usernameVerified: boolean | undefined = undefined;
|
|
617
|
-
let usernameVerifiedMessage: string | undefined = undefined;
|
|
618
|
-
let emailTimer: NodeJS.Timeout | undefined = undefined;
|
|
619
|
-
let emailVerified: boolean | undefined = undefined;
|
|
620
|
-
let emailVerifiedMessage: string | undefined = undefined;
|
|
621
|
-
let saving = false;
|
|
622
|
-
let success: boolean | undefined = undefined;
|
|
623
|
-
|
|
624
|
-
onMount(async () => {
|
|
625
|
-
user = (await User.current()) ?? undefined;
|
|
626
|
-
sysAdmin = (await user?.$gatekeeper('system/admin')) ?? false;
|
|
627
|
-
});
|
|
628
|
-
onMount(async () => {
|
|
629
|
-
clientConfig = await User.getClientConfig();
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
readyEntity();
|
|
633
|
-
function readyEntity() {
|
|
634
|
-
// Make sure all fields are defined.
|
|
635
|
-
if (entity.enabled == null) {
|
|
636
|
-
entity.enabled = false;
|
|
637
|
-
}
|
|
638
|
-
if (entity.username == null) {
|
|
639
|
-
entity.username = '';
|
|
640
|
-
}
|
|
641
|
-
if (entity.email == null) {
|
|
642
|
-
entity.email = '';
|
|
643
|
-
}
|
|
644
|
-
if (entity.nameFirst == null) {
|
|
645
|
-
entity.nameFirst = '';
|
|
646
|
-
}
|
|
647
|
-
if (entity.nameMiddle == null) {
|
|
648
|
-
entity.nameMiddle = '';
|
|
649
|
-
}
|
|
650
|
-
if (entity.nameLast == null) {
|
|
651
|
-
entity.nameLast = '';
|
|
652
|
-
}
|
|
653
|
-
if (entity.avatar == null) {
|
|
654
|
-
entity.avatar = '';
|
|
655
|
-
}
|
|
656
|
-
if (entity.phone == null) {
|
|
657
|
-
entity.phone = '';
|
|
658
|
-
}
|
|
659
|
-
if (entity.passwordTemp == null) {
|
|
660
|
-
entity.passwordTemp = '';
|
|
661
|
-
}
|
|
662
|
-
if (entity.inheritAbilities == null) {
|
|
663
|
-
entity.inheritAbilities = false;
|
|
664
|
-
}
|
|
665
|
-
if (entity.secret == null) {
|
|
666
|
-
entity.secret = '';
|
|
667
|
-
}
|
|
668
|
-
if (entity.emailChangeDate == null) {
|
|
669
|
-
entity.emailChangeDate = 0;
|
|
670
|
-
}
|
|
671
|
-
if (entity.newEmailSecret == null) {
|
|
672
|
-
entity.newEmailSecret = '';
|
|
673
|
-
}
|
|
674
|
-
if (entity.newEmailAddress == null) {
|
|
675
|
-
entity.newEmailAddress = '';
|
|
676
|
-
}
|
|
677
|
-
if (entity.cancelEmailSecret == null) {
|
|
678
|
-
entity.cancelEmailSecret = '';
|
|
679
|
-
}
|
|
680
|
-
if (entity.cancelEmailAddress == null) {
|
|
681
|
-
entity.cancelEmailAddress = '';
|
|
682
|
-
}
|
|
683
|
-
if (entity.recoverSecret == null) {
|
|
684
|
-
entity.recoverSecret = '';
|
|
685
|
-
}
|
|
686
|
-
if (entity.recoverSecretDate == null) {
|
|
687
|
-
entity.recoverSecretDate = 0;
|
|
688
|
-
}
|
|
689
|
-
entity.$getAvatar().then((value) => {
|
|
690
|
-
avatar = value;
|
|
691
|
-
});
|
|
692
|
-
entity.$readyAll(1).then(() => {
|
|
693
|
-
entity = entity;
|
|
694
|
-
});
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
let primaryGroupsSearching = false;
|
|
698
|
-
let primaryGroups: (GroupClass & AdminGroupData)[] | undefined = undefined;
|
|
699
|
-
async function searchPrimaryGroups() {
|
|
700
|
-
primaryGroupsSearching = true;
|
|
701
|
-
failureMessage = undefined;
|
|
702
|
-
if (primaryGroupSearch.trim() == '') {
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
try {
|
|
706
|
-
const [options, ...selectors] = queryParser({
|
|
707
|
-
query: primaryGroupSearch,
|
|
708
|
-
entityClass: Group,
|
|
709
|
-
defaultFields: ['groupname', 'name', 'email'],
|
|
710
|
-
qrefMap: {
|
|
711
|
-
User: {
|
|
712
|
-
class: User,
|
|
713
|
-
defaultFields: ['username', 'name', 'email'],
|
|
714
|
-
},
|
|
715
|
-
Group: {
|
|
716
|
-
class: Group,
|
|
717
|
-
defaultFields: ['groupname', 'name', 'email'],
|
|
718
|
-
},
|
|
719
|
-
},
|
|
720
|
-
});
|
|
721
|
-
primaryGroups = (await Group.getPrimaryGroups(options, selectors)).filter(
|
|
722
|
-
(group) => {
|
|
723
|
-
return !group.$is(entity.group);
|
|
724
|
-
}
|
|
725
|
-
);
|
|
726
|
-
} catch (e: any) {
|
|
727
|
-
failureMessage = e?.message;
|
|
728
|
-
}
|
|
729
|
-
primaryGroupsSearching = false;
|
|
730
|
-
}
|
|
731
|
-
function primaryGroupSearchKeyDown(event: CustomEvent | KeyboardEvent) {
|
|
732
|
-
event = event as KeyboardEvent;
|
|
733
|
-
if (event.key === 'Enter') searchPrimaryGroups();
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
let secondaryGroupsSearching = false;
|
|
737
|
-
let secondaryGroups: (GroupClass & AdminGroupData)[] | undefined = undefined;
|
|
738
|
-
async function searchSecondaryGroups() {
|
|
739
|
-
secondaryGroupsSearching = true;
|
|
740
|
-
failureMessage = undefined;
|
|
741
|
-
if (secondaryGroupSearch.trim() == '') {
|
|
742
|
-
return;
|
|
743
|
-
}
|
|
744
|
-
try {
|
|
745
|
-
const [options, ...selectors] = queryParser({
|
|
746
|
-
query: secondaryGroupSearch,
|
|
747
|
-
entityClass: Group,
|
|
748
|
-
defaultFields: ['groupname', 'name', 'email'],
|
|
749
|
-
qrefMap: {
|
|
750
|
-
User: {
|
|
751
|
-
class: User,
|
|
752
|
-
defaultFields: ['username', 'name', 'email'],
|
|
753
|
-
},
|
|
754
|
-
Group: {
|
|
755
|
-
class: Group,
|
|
756
|
-
defaultFields: ['groupname', 'name', 'email'],
|
|
757
|
-
},
|
|
758
|
-
},
|
|
759
|
-
});
|
|
760
|
-
secondaryGroups = (
|
|
761
|
-
await Group.getSecondaryGroups(options, selectors)
|
|
762
|
-
).filter((group) => {
|
|
763
|
-
return !group.$inArray(entity.groups ?? []);
|
|
764
|
-
});
|
|
765
|
-
} catch (e: any) {
|
|
766
|
-
failureMessage = e?.message;
|
|
767
|
-
}
|
|
768
|
-
secondaryGroupsSearching = false;
|
|
769
|
-
}
|
|
770
|
-
function secondaryGroupSearchKeyDown(event: CustomEvent | KeyboardEvent) {
|
|
771
|
-
event = event as KeyboardEvent;
|
|
772
|
-
if (event.key === 'Enter') searchSecondaryGroups();
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
let oldUsername = entity.username;
|
|
776
|
-
$: if (entity.username !== oldUsername) {
|
|
777
|
-
if (usernameTimer) {
|
|
778
|
-
clearTimeout(usernameTimer);
|
|
779
|
-
}
|
|
780
|
-
usernameTimer = setTimeout(async () => {
|
|
781
|
-
if (entity.username === '') {
|
|
782
|
-
usernameVerified = undefined;
|
|
783
|
-
usernameVerifiedMessage = undefined;
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
786
|
-
try {
|
|
787
|
-
const data = await entity.$checkUsername();
|
|
788
|
-
usernameVerified = data.result;
|
|
789
|
-
usernameVerifiedMessage = data.message;
|
|
790
|
-
} catch (e: any) {
|
|
791
|
-
usernameVerified = false;
|
|
792
|
-
usernameVerifiedMessage = e?.message;
|
|
793
|
-
}
|
|
794
|
-
}, 400);
|
|
795
|
-
oldUsername = entity.username;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
let oldEmail = entity.email;
|
|
799
|
-
$: if (entity.email !== oldEmail) {
|
|
800
|
-
if (emailTimer) {
|
|
801
|
-
clearTimeout(emailTimer);
|
|
802
|
-
}
|
|
803
|
-
emailTimer = setTimeout(async () => {
|
|
804
|
-
if (entity.email === '') {
|
|
805
|
-
emailVerified = undefined;
|
|
806
|
-
emailVerifiedMessage = undefined;
|
|
807
|
-
return;
|
|
808
|
-
}
|
|
809
|
-
try {
|
|
810
|
-
const data = await entity.$checkEmail();
|
|
811
|
-
emailVerified = data.result;
|
|
812
|
-
emailVerifiedMessage = data.message;
|
|
813
|
-
} catch (e: any) {
|
|
814
|
-
emailVerified = false;
|
|
815
|
-
emailVerifiedMessage = e?.message;
|
|
816
|
-
}
|
|
817
|
-
}, 400);
|
|
818
|
-
oldEmail = entity.email;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
function doVerifyPassword() {
|
|
822
|
-
if (
|
|
823
|
-
(entity.passwordTemp == null || entity.passwordTemp === '') &&
|
|
824
|
-
passwordVerify === ''
|
|
825
|
-
) {
|
|
826
|
-
passwordVerified = undefined;
|
|
827
|
-
}
|
|
828
|
-
passwordVerified = entity.passwordTemp === passwordVerify;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
function addAbility() {
|
|
832
|
-
if (ability === '') {
|
|
833
|
-
return;
|
|
834
|
-
}
|
|
835
|
-
entity.abilities?.push(ability);
|
|
836
|
-
ability = '';
|
|
837
|
-
entity = entity;
|
|
838
|
-
}
|
|
839
|
-
function abilityKeyDown(event: CustomEvent | KeyboardEvent) {
|
|
840
|
-
event = event as KeyboardEvent;
|
|
841
|
-
if (event.key === 'Enter') addAbility();
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
function addTilmeldAdminAbility() {
|
|
845
|
-
if (entity.abilities?.indexOf('tilmeld/admin') === -1) {
|
|
846
|
-
entity.abilities?.push('tilmeld/admin');
|
|
847
|
-
entity = entity;
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
function addSystemAdminAbility() {
|
|
852
|
-
if (entity.abilities?.indexOf('system/admin') === -1) {
|
|
853
|
-
entity.abilities?.push('system/admin');
|
|
854
|
-
entity = entity;
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
async function saveEntity() {
|
|
859
|
-
if (
|
|
860
|
-
(entity.passwordTemp != null || entity.passwordTemp !== '') &&
|
|
861
|
-
entity.passwordTemp !== passwordVerify
|
|
862
|
-
) {
|
|
863
|
-
failureMessage = "Passwords don't match!";
|
|
864
|
-
return;
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
saving = true;
|
|
868
|
-
failureMessage = undefined;
|
|
869
|
-
try {
|
|
870
|
-
if (await entity.$save()) {
|
|
871
|
-
success = true;
|
|
872
|
-
setTimeout(() => {
|
|
873
|
-
success = undefined;
|
|
874
|
-
}, 1000);
|
|
875
|
-
readyEntity();
|
|
876
|
-
} else {
|
|
877
|
-
failureMessage = 'Error saving user.';
|
|
878
|
-
}
|
|
879
|
-
} catch (e: any) {
|
|
880
|
-
console.log('error:', e);
|
|
881
|
-
failureMessage = e?.message;
|
|
882
|
-
}
|
|
883
|
-
saving = false;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
async function deleteEntity() {
|
|
887
|
-
failureMessage = undefined;
|
|
888
|
-
if (confirm('Are you sure you want to delete this?')) {
|
|
889
|
-
saving = true;
|
|
890
|
-
try {
|
|
891
|
-
if (await entity.$delete()) {
|
|
892
|
-
dispatch('leave');
|
|
893
|
-
} else {
|
|
894
|
-
failureMessage = 'An error occurred.';
|
|
895
|
-
}
|
|
896
|
-
} catch (e: any) {
|
|
897
|
-
failureMessage = e?.message;
|
|
898
|
-
}
|
|
899
|
-
saving = false;
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
</script>
|