@things-factory/auth-ui 10.1.17 → 10.1.18

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.
Files changed (62) hide show
  1. package/dist-client/components/app-binding-detail.d.ts +46 -0
  2. package/dist-client/components/app-binding-detail.js +384 -0
  3. package/dist-client/components/app-binding-detail.js.map +1 -0
  4. package/dist-client/components/appliance-editor.d.ts +37 -0
  5. package/dist-client/components/appliance-editor.js +489 -0
  6. package/dist-client/components/appliance-editor.js.map +1 -0
  7. package/dist-client/components/application-editor.d.ts +31 -0
  8. package/dist-client/components/application-editor.js +491 -0
  9. package/dist-client/components/application-editor.js.map +1 -0
  10. package/dist-client/components/profile-component.js +28 -11
  11. package/dist-client/components/profile-component.js.map +1 -1
  12. package/dist-client/constants/application-type-parity.test.js +62 -0
  13. package/dist-client/constants/application-type-parity.test.js.map +1 -0
  14. package/dist-client/constants/application.d.ts +17 -7
  15. package/dist-client/constants/application.js +18 -8
  16. package/dist-client/constants/application.js.map +1 -1
  17. package/dist-client/pages/app-binding/app-bindings.d.ts +2 -0
  18. package/dist-client/pages/app-binding/app-bindings.js +19 -1
  19. package/dist-client/pages/app-binding/app-bindings.js.map +1 -1
  20. package/dist-client/pages/appliance/home.d.ts +2 -0
  21. package/dist-client/pages/appliance/home.js +21 -3
  22. package/dist-client/pages/appliance/home.js.map +1 -1
  23. package/dist-client/pages/application/applications.d.ts +2 -0
  24. package/dist-client/pages/application/applications.js +21 -3
  25. package/dist-client/pages/application/applications.js.map +1 -1
  26. package/dist-client/pages/secrets-are-not-printed.test.js +21 -3
  27. package/dist-client/pages/secrets-are-not-printed.test.js.map +1 -1
  28. package/dist-client/route.js +0 -15
  29. package/dist-client/route.js.map +1 -1
  30. package/dist-client/tsconfig.tsbuildinfo +1 -1
  31. package/dist-client/utils/form-values.d.ts +29 -0
  32. package/dist-client/utils/form-values.js +45 -0
  33. package/dist-client/utils/form-values.js.map +1 -1
  34. package/dist-client/utils/form-values.test.js +59 -1
  35. package/dist-client/utils/form-values.test.js.map +1 -1
  36. package/dist-client/utils/webauthn-verification.d.ts +56 -0
  37. package/dist-client/utils/webauthn-verification.js +79 -0
  38. package/dist-client/utils/webauthn-verification.js.map +1 -0
  39. package/dist-client/utils/webauthn-verification.test.js +79 -0
  40. package/dist-client/utils/webauthn-verification.test.js.map +1 -0
  41. package/package.json +3 -3
  42. package/things-factory.config.js +0 -5
  43. package/translations/en.json +25 -0
  44. package/translations/ja.json +26 -0
  45. package/translations/ko.json +25 -0
  46. package/translations/ms.json +26 -0
  47. package/translations/zh.json +26 -0
  48. package/dist-client/pages/app-binding/app-binding.d.ts +0 -2
  49. package/dist-client/pages/app-binding/app-binding.js +0 -371
  50. package/dist-client/pages/app-binding/app-binding.js.map +0 -1
  51. package/dist-client/pages/appliance/appliance.d.ts +0 -2
  52. package/dist-client/pages/appliance/appliance.js +0 -390
  53. package/dist-client/pages/appliance/appliance.js.map +0 -1
  54. package/dist-client/pages/appliance/register.js +0 -166
  55. package/dist-client/pages/appliance/register.js.map +0 -1
  56. package/dist-client/pages/application/application.d.ts +0 -2
  57. package/dist-client/pages/application/application.js +0 -384
  58. package/dist-client/pages/application/application.js.map +0 -1
  59. package/dist-client/pages/application/register.js +0 -246
  60. package/dist-client/pages/application/register.js.map +0 -1
  61. /package/dist-client/{pages/appliance/register.d.ts → constants/application-type-parity.test.d.ts} +0 -0
  62. /package/dist-client/{pages/application/register.d.ts → utils/webauthn-verification.test.d.ts} +0 -0
@@ -0,0 +1,491 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import '@material/web/icon/icon.js';
3
+ import '@material/web/button/text-button.js';
4
+ import '@material/web/button/filled-button.js';
5
+ import gql from 'graphql-tag';
6
+ import { css, html, LitElement } from 'lit';
7
+ import { customElement, property, query, state } from 'lit/decorators.js';
8
+ import { client } from '@operato/graphql';
9
+ import { i18next, localize } from '@operato/i18n';
10
+ import { OxPrompt } from '@operato/popup/ox-prompt.js';
11
+ import { CommonHeaderStyles, CommonPopupStyles } from '@operato/styles';
12
+ import './secret-field.js';
13
+ import { APPLICATION_TYPES } from '../constants/application.js';
14
+ import { changedValues, filledValues, missingRequired } from '../utils/form-values.js';
15
+ /**
16
+ * Registering and editing an application, in a popup.
17
+ *
18
+ * ── Why this replaced two pages ─────────────────────────────────────────────
19
+ * Same reason as `appliance-editor`: `application-register` and `application/{id}` were the
20
+ * only routes of their kind left in a package where every other resource is a list with a
21
+ * popup over it, and nothing outside auth-ui linked to either.
22
+ *
23
+ * ── Three defects the old detail page carried ───────────────────────────────
24
+ * 1. **The type could not be changed.** Every option in the dropdown was written as
25
+ * `<option value="${app.type}">`, so all six options submitted the *current* value. The
26
+ * label differed, the value did not.
27
+ * 2. **`generate new secret` did nothing.** The button had no handler, while
28
+ * `generateApplicationSecret` had been on the server the whole time.
29
+ * 3. **Saving with an empty contact email was refused.** The page sent every box, so an
30
+ * untouched `email` went as `''`, and `ApplicationPatch.email` is a GraphQLEmailAddress —
31
+ * `''` is not "no email" to it, it is an invalid one. The screen printed `update fail` to
32
+ * the console.
33
+ *
34
+ * The type list itself had drifted too: the client constant named five types, the server enum
35
+ * declares six. `application-type-parity.test.ts` now fails when they part again.
36
+ */
37
+ /** The one field `NewApplication` declares non-null. The schema's list, not a house rule. */
38
+ const REQUIRED = ['name'];
39
+ /*
40
+ * What a new application is when nobody chooses. A `<select>` always has a value, so leaving
41
+ * this out would not mean "unset" — it would mean whichever option happens to be listed
42
+ * first. `Application.type` declares `default: ApplicationType.OTHERS`, so that is the one to
43
+ * preselect; anything else would quietly disagree with the server about untouched records.
44
+ */
45
+ const DEFAULT_TYPE = 'OTHERS';
46
+ const APPLICATION_FIELDS = `
47
+ id
48
+ name
49
+ description
50
+ email
51
+ url
52
+ icon
53
+ redirectUrl
54
+ authUrl
55
+ accessTokenUrl
56
+ webhook
57
+ availableScopes
58
+ appKey
59
+ appSecret
60
+ type
61
+ `;
62
+ let ApplicationEditor = class ApplicationEditor extends localize(i18next)(LitElement) {
63
+ constructor() {
64
+ super(...arguments);
65
+ this.application = null;
66
+ this.missing = [];
67
+ this.failed = false;
68
+ this.loadFailed = false;
69
+ }
70
+ static { this.styles = [
71
+ CommonHeaderStyles,
72
+ CommonPopupStyles,
73
+ css `
74
+ :host {
75
+ display: flex;
76
+ flex-direction: column;
77
+
78
+ background-color: var(--md-sys-color-surface);
79
+ overflow: auto;
80
+ }
81
+
82
+ form {
83
+ flex: 1;
84
+ padding: var(--popup-padding);
85
+ }
86
+
87
+ fieldset {
88
+ border-radius: var(--border-radius);
89
+ border: var(--border-dim-color);
90
+ margin: 0 0 var(--popup-gap) 0;
91
+ padding: var(--popup-padding);
92
+ }
93
+
94
+ legend {
95
+ padding: var(--legend-padding);
96
+ font-weight: bold;
97
+ color: var(--legend-color);
98
+ }
99
+
100
+ [field-2column] {
101
+ display: grid;
102
+ grid-template-columns: 1fr 1fr;
103
+ grid-gap: var(--popup-gap);
104
+ }
105
+
106
+ [field] {
107
+ display: flex;
108
+ flex-direction: column;
109
+ }
110
+
111
+ [grid-span] {
112
+ grid-column: span 2;
113
+ }
114
+
115
+ label {
116
+ font: var(--label-font);
117
+ color: var(--label-color, var(--md-sys-color-on-surface));
118
+ text-transform: var(--label-text-transform);
119
+ }
120
+
121
+ input,
122
+ select {
123
+ background-color: var(--input-field-background, var(--md-sys-color-surface-container-highest));
124
+ color: var(--input-field-color, var(--md-sys-color-on-surface));
125
+ border: var(--border-dim-color);
126
+ border-radius: var(--border-radius);
127
+ margin: var(--input-margin);
128
+ padding: var(--input-padding);
129
+ font: var(--input-font);
130
+ }
131
+
132
+ input:focus,
133
+ select:focus {
134
+ outline: none;
135
+ }
136
+
137
+ input[readonly] {
138
+ opacity: 0.7;
139
+ }
140
+
141
+ input[invalid] {
142
+ border-color: var(--md-sys-color-error);
143
+ }
144
+
145
+ [field-error] {
146
+ color: var(--md-sys-color-error);
147
+ font-size: var(--popup-font-size-small);
148
+ margin-top: var(--spacing-tiny, 2px);
149
+ }
150
+
151
+ [field] .note {
152
+ margin-top: var(--spacing-tiny, 2px);
153
+ }
154
+
155
+ .buttons .left {
156
+ margin-right: auto;
157
+ }
158
+
159
+ @media screen and (max-width: 480px) {
160
+ [field] {
161
+ grid-column: span 2;
162
+ }
163
+ }
164
+ `
165
+ ]; }
166
+ get editing() {
167
+ return !!this.applicationId;
168
+ }
169
+ /*
170
+ * ⚠ The type dropdown: each option carries its OWN value. The page this replaced wrote the
171
+ * current type as the value of every option, so all six submitted the same thing and the
172
+ * type could not be changed from this screen at all.
173
+ */
174
+ render() {
175
+ const app = this.application || {};
176
+ if (this.editing && this.loadFailed) {
177
+ return html `<div class="failure">${i18next.t('text.the application could not be loaded')}</div>`;
178
+ }
179
+ return html `
180
+ <form @submit=${(e) => e.preventDefault()}>
181
+ <fieldset>
182
+ <legend>${i18next.t('text.application')}</legend>
183
+ <div field-2column>
184
+ <div field grid-span>
185
+ <label for="name">${i18next.t('field.name')}</label>
186
+ <input
187
+ id="name"
188
+ type="text"
189
+ name="name"
190
+ .value=${app.name || ''}
191
+ required
192
+ autofocus
193
+ ?invalid=${this.missing.includes('name')}
194
+ />
195
+ ${this.missing.includes('name')
196
+ ? html `<div field-error>${i18next.t('text.this field is required')}</div>`
197
+ : ''}
198
+ </div>
199
+
200
+ <div field grid-span>
201
+ <label for="description">${i18next.t('field.description')}</label>
202
+ <input id="description" type="text" name="description" .value=${app.description || ''} />
203
+ </div>
204
+
205
+ <!-- type=email so the browser checks the shape the server's scalar will check. -->
206
+ <div field grid-span>
207
+ <label for="email">${i18next.t('field.contact email')}</label>
208
+ <input id="email" type="email" name="email" .value=${app.email || ''} />
209
+ </div>
210
+
211
+ <div field grid-span>
212
+ <label for="type">${i18next.t('field.type')}</label>
213
+ <select id="type" name="type">
214
+ ${APPLICATION_TYPES.map(type => html `<option value=${type} ?selected=${type === (app.type || DEFAULT_TYPE)}>${type}</option>`)}
215
+ </select>
216
+ </div>
217
+
218
+ <div field grid-span>
219
+ <label for="url">${i18next.t('field.application url')}</label>
220
+ <input id="url" type="url" name="url" .value=${app.url || ''} />
221
+ </div>
222
+
223
+ <div field grid-span>
224
+ <label for="redirect-url">${i18next.t('field.redirect url')}</label>
225
+ <input id="redirect-url" type="url" name="redirectUrl" .value=${app.redirectUrl || ''} />
226
+ </div>
227
+
228
+ <div field grid-span>
229
+ <label for="icon">${i18next.t('field.application icon')}</label>
230
+ <input id="icon" type="url" name="icon" .value=${app.icon || ''} />
231
+ </div>
232
+
233
+ <div field grid-span>
234
+ <label for="webhook">${i18next.t('field.webhook')}</label>
235
+ <input id="webhook" type="url" name="webhook" .value=${app.webhook || ''} />
236
+ </div>
237
+ </div>
238
+ </fieldset>
239
+
240
+ ${this.editing ? this.renderEndpoints(app) : ''} ${this.editing ? this.renderCredential(app) : ''}
241
+ ${this.failed ? html `<div class="failure">${i18next.t('text.could not be saved')}</div>` : ''}
242
+ </form>
243
+
244
+ <div class="buttons">
245
+ ${this.editing
246
+ ? html `
247
+ <md-text-button class="left" @click=${this.deleteApplication.bind(this)}>
248
+ ${i18next.t('button.delete')}
249
+ </md-text-button>
250
+ `
251
+ : ''}
252
+ <md-text-button @click=${this.cancel.bind(this)}>${i18next.t('button.cancel')}</md-text-button>
253
+ <md-filled-button @click=${this.save.bind(this)}>
254
+ ${this.editing ? i18next.t('button.update') : i18next.t('button.register')}
255
+ </md-filled-button>
256
+ </div>
257
+ `;
258
+ }
259
+ /*
260
+ * What the server hands back rather than what the person types: the OAuth endpoints and the
261
+ * scopes this application may ask for. Read-only and outside the form's named inputs, so
262
+ * they are never part of a patch.
263
+ */
264
+ renderEndpoints(app) {
265
+ return html `
266
+ <fieldset>
267
+ <legend>${i18next.t('text.endpoints')}</legend>
268
+ <div field-2column>
269
+ <div field grid-span>
270
+ <label for="auth-url">${i18next.t('field.auth url')}</label>
271
+ <input id="auth-url" type="text" .value=${app.authUrl || ''} readonly />
272
+ <div class="note">${i18next.t('text.where an integration asks for an authorization code')}</div>
273
+ </div>
274
+
275
+ <div field grid-span>
276
+ <label for="access-token-url">${i18next.t('field.access token url')}</label>
277
+ <input id="access-token-url" type="text" .value=${app.accessTokenUrl || ''} readonly />
278
+ <div class="note">${i18next.t('text.where that code is exchanged for an access token')}</div>
279
+ </div>
280
+
281
+ <div field grid-span>
282
+ <label for="available-scopes">${i18next.t('field.available scopes')}</label>
283
+ <input id="available-scopes" type="text" .value=${app.availableScopes || ''} readonly />
284
+ </div>
285
+ </div>
286
+ </fieldset>
287
+ `;
288
+ }
289
+ /*
290
+ * appKey stays a plain field: it is the client identifier, meant to be read and quoted.
291
+ * appSecret is the other half — it carries its own @privilege for security:query, and it is
292
+ * the value an integration signs with.
293
+ */
294
+ renderCredential(app) {
295
+ return html `
296
+ <fieldset>
297
+ <legend>${i18next.t('text.credentials')}</legend>
298
+ <div field-2column>
299
+ <div field grid-span>
300
+ <label for="app-key">${i18next.t('field.app key')}</label>
301
+ <input id="app-key" type="text" .value=${app.appKey || ''} readonly />
302
+ </div>
303
+
304
+ <div field grid-span>
305
+ <label for="app-secret">${i18next.t('field.client-secret')}</label>
306
+ <secret-field id="app-secret" .value=${app.appSecret || ''}></secret-field>
307
+ <div class="row">
308
+ <md-text-button @click=${this.generateSecret.bind(this)}>
309
+ ${i18next.t('button.generate new secret')}
310
+ </md-text-button>
311
+ </div>
312
+ </div>
313
+ </div>
314
+ </fieldset>
315
+ `;
316
+ }
317
+ async connectedCallback() {
318
+ super.connectedCallback();
319
+ if (this.editing) {
320
+ await this.fetchApplication();
321
+ }
322
+ }
323
+ async fetchApplication() {
324
+ const response = await client.query({
325
+ query: gql `
326
+ query ($id: String!) {
327
+ application(id: $id) { ${APPLICATION_FIELDS} }
328
+ }
329
+ `,
330
+ variables: { id: this.applicationId }
331
+ });
332
+ /*
333
+ * A refusal arrives in `errors` rather than throwing. An unchecked read would render an
334
+ * empty form that looks like a record with every field blank, and saving it would write
335
+ * those blanks.
336
+ */
337
+ if (response.errors || !response.data?.application) {
338
+ this.loadFailed = true;
339
+ return;
340
+ }
341
+ this.loadFailed = false;
342
+ this.application = response.data.application;
343
+ }
344
+ cancel(e) {
345
+ e.preventDefault();
346
+ this.close();
347
+ }
348
+ async save(e) {
349
+ e.preventDefault();
350
+ const formData = new FormData(this.form);
351
+ const missing = missingRequired(formData, REQUIRED);
352
+ if (missing.length) {
353
+ this.missing = missing;
354
+ return;
355
+ }
356
+ this.missing = [];
357
+ return this.editing ? this.updateApplication(formData) : this.createApplication(formData);
358
+ }
359
+ async createApplication(formData) {
360
+ const response = await client.mutate({
361
+ mutation: gql `
362
+ mutation ($application: NewApplication!) {
363
+ createApplication(application: $application) {
364
+ id
365
+ }
366
+ }
367
+ `,
368
+ variables: { application: filledValues(formData) }
369
+ });
370
+ if (response.errors) {
371
+ this.failed = true;
372
+ return;
373
+ }
374
+ this.saved();
375
+ }
376
+ async updateApplication(formData) {
377
+ const patch = changedValues(formData, this.application);
378
+ if (!Object.keys(patch).length) {
379
+ this.close();
380
+ return;
381
+ }
382
+ const response = await client.mutate({
383
+ mutation: gql `
384
+ mutation ($id: String!, $patch: ApplicationPatch!) {
385
+ updateApplication(id: $id, patch: $patch) { ${APPLICATION_FIELDS} }
386
+ }
387
+ `,
388
+ variables: { id: this.applicationId, patch }
389
+ });
390
+ if (response.errors) {
391
+ this.failed = true;
392
+ return;
393
+ }
394
+ this.saved();
395
+ }
396
+ async generateSecret(e) {
397
+ e.preventDefault();
398
+ const confirmed = await OxPrompt.open({
399
+ type: 'warning',
400
+ title: i18next.t('button.generate new secret'),
401
+ text: i18next.t('text.the current secret stops working immediately'),
402
+ confirmButton: { text: i18next.t('button.confirm') },
403
+ cancelButton: { text: i18next.t('button.cancel') }
404
+ });
405
+ if (!confirmed) {
406
+ return;
407
+ }
408
+ const response = await client.mutate({
409
+ mutation: gql `
410
+ mutation ($id: String!) {
411
+ generateApplicationSecret(id: $id) { ${APPLICATION_FIELDS} }
412
+ }
413
+ `,
414
+ variables: { id: this.applicationId }
415
+ });
416
+ if (response.errors) {
417
+ this.failed = true;
418
+ return;
419
+ }
420
+ /*
421
+ * Stays open on purpose — a fresh secret is shown once and has to be copied before it is
422
+ * out of reach.
423
+ */
424
+ this.failed = false;
425
+ this.application = response.data.generateApplicationSecret;
426
+ this.dispatchEvent(new CustomEvent('application-changed', { bubbles: true, composed: true }));
427
+ }
428
+ async deleteApplication(e) {
429
+ e.preventDefault();
430
+ const confirmed = await OxPrompt.open({
431
+ type: 'warning',
432
+ title: i18next.t('button.delete'),
433
+ text: i18next.t('text.are you sure you want to delete x', { x: this.application?.name || '' }),
434
+ confirmButton: { text: i18next.t('button.delete') },
435
+ cancelButton: { text: i18next.t('button.cancel') }
436
+ });
437
+ if (!confirmed) {
438
+ return;
439
+ }
440
+ const response = await client.mutate({
441
+ mutation: gql `
442
+ mutation ($id: String!) {
443
+ deleteApplication(id: $id)
444
+ }
445
+ `,
446
+ variables: { id: this.applicationId }
447
+ });
448
+ if (response.errors) {
449
+ this.failed = true;
450
+ return;
451
+ }
452
+ this.saved();
453
+ }
454
+ saved() {
455
+ this.failed = false;
456
+ this.dispatchEvent(new CustomEvent('application-changed', { bubbles: true, composed: true }));
457
+ this.close();
458
+ }
459
+ close() {
460
+ this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }));
461
+ }
462
+ };
463
+ __decorate([
464
+ property({ type: String }),
465
+ __metadata("design:type", String)
466
+ ], ApplicationEditor.prototype, "applicationId", void 0);
467
+ __decorate([
468
+ query('form'),
469
+ __metadata("design:type", HTMLFormElement)
470
+ ], ApplicationEditor.prototype, "form", void 0);
471
+ __decorate([
472
+ state(),
473
+ __metadata("design:type", Object)
474
+ ], ApplicationEditor.prototype, "application", void 0);
475
+ __decorate([
476
+ state(),
477
+ __metadata("design:type", Array)
478
+ ], ApplicationEditor.prototype, "missing", void 0);
479
+ __decorate([
480
+ state(),
481
+ __metadata("design:type", Boolean)
482
+ ], ApplicationEditor.prototype, "failed", void 0);
483
+ __decorate([
484
+ state(),
485
+ __metadata("design:type", Boolean)
486
+ ], ApplicationEditor.prototype, "loadFailed", void 0);
487
+ ApplicationEditor = __decorate([
488
+ customElement('application-editor')
489
+ ], ApplicationEditor);
490
+ export { ApplicationEditor };
491
+ //# sourceMappingURL=application-editor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"application-editor.js","sourceRoot":"","sources":["../../client/components/application-editor.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@material/web/button/text-button.js'\nimport '@material/web/button/filled-button.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\n\nimport { client } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { OxPrompt } from '@operato/popup/ox-prompt.js'\nimport { CommonHeaderStyles, CommonPopupStyles } from '@operato/styles'\n\nimport './secret-field.js'\nimport { APPLICATION_TYPES } from '../constants/application.js'\nimport { changedValues, filledValues, missingRequired } from '../utils/form-values.js'\n\n/**\n * Registering and editing an application, in a popup.\n *\n * ── Why this replaced two pages ─────────────────────────────────────────────\n * Same reason as `appliance-editor`: `application-register` and `application/{id}` were the\n * only routes of their kind left in a package where every other resource is a list with a\n * popup over it, and nothing outside auth-ui linked to either.\n *\n * ── Three defects the old detail page carried ───────────────────────────────\n * 1. **The type could not be changed.** Every option in the dropdown was written as\n * `<option value=\"${app.type}\">`, so all six options submitted the *current* value. The\n * label differed, the value did not.\n * 2. **`generate new secret` did nothing.** The button had no handler, while\n * `generateApplicationSecret` had been on the server the whole time.\n * 3. **Saving with an empty contact email was refused.** The page sent every box, so an\n * untouched `email` went as `''`, and `ApplicationPatch.email` is a GraphQLEmailAddress —\n * `''` is not \"no email\" to it, it is an invalid one. The screen printed `update fail` to\n * the console.\n *\n * The type list itself had drifted too: the client constant named five types, the server enum\n * declares six. `application-type-parity.test.ts` now fails when they part again.\n */\n\n/** The one field `NewApplication` declares non-null. The schema's list, not a house rule. */\nconst REQUIRED = ['name']\n\n/*\n * What a new application is when nobody chooses. A `<select>` always has a value, so leaving\n * this out would not mean \"unset\" — it would mean whichever option happens to be listed\n * first. `Application.type` declares `default: ApplicationType.OTHERS`, so that is the one to\n * preselect; anything else would quietly disagree with the server about untouched records.\n */\nconst DEFAULT_TYPE = 'OTHERS'\n\nconst APPLICATION_FIELDS = `\n id\n name\n description\n email\n url\n icon\n redirectUrl\n authUrl\n accessTokenUrl\n webhook\n availableScopes\n appKey\n appSecret\n type\n`\n\n@customElement('application-editor')\nexport class ApplicationEditor extends localize(i18next)(LitElement) {\n static styles = [\n CommonHeaderStyles,\n CommonPopupStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n\n background-color: var(--md-sys-color-surface);\n overflow: auto;\n }\n\n form {\n flex: 1;\n padding: var(--popup-padding);\n }\n\n fieldset {\n border-radius: var(--border-radius);\n border: var(--border-dim-color);\n margin: 0 0 var(--popup-gap) 0;\n padding: var(--popup-padding);\n }\n\n legend {\n padding: var(--legend-padding);\n font-weight: bold;\n color: var(--legend-color);\n }\n\n [field-2column] {\n display: grid;\n grid-template-columns: 1fr 1fr;\n grid-gap: var(--popup-gap);\n }\n\n [field] {\n display: flex;\n flex-direction: column;\n }\n\n [grid-span] {\n grid-column: span 2;\n }\n\n label {\n font: var(--label-font);\n color: var(--label-color, var(--md-sys-color-on-surface));\n text-transform: var(--label-text-transform);\n }\n\n input,\n select {\n background-color: var(--input-field-background, var(--md-sys-color-surface-container-highest));\n color: var(--input-field-color, var(--md-sys-color-on-surface));\n border: var(--border-dim-color);\n border-radius: var(--border-radius);\n margin: var(--input-margin);\n padding: var(--input-padding);\n font: var(--input-font);\n }\n\n input:focus,\n select:focus {\n outline: none;\n }\n\n input[readonly] {\n opacity: 0.7;\n }\n\n input[invalid] {\n border-color: var(--md-sys-color-error);\n }\n\n [field-error] {\n color: var(--md-sys-color-error);\n font-size: var(--popup-font-size-small);\n margin-top: var(--spacing-tiny, 2px);\n }\n\n [field] .note {\n margin-top: var(--spacing-tiny, 2px);\n }\n\n .buttons .left {\n margin-right: auto;\n }\n\n @media screen and (max-width: 480px) {\n [field] {\n grid-column: span 2;\n }\n }\n `\n ]\n\n /** The application to edit. Absent means this popup is registering a new one. */\n @property({ type: String }) applicationId?: string\n\n @query('form') form!: HTMLFormElement\n\n @state() private application: any = null\n\n @state() private missing: string[] = []\n\n @state() private failed: boolean = false\n\n @state() private loadFailed: boolean = false\n\n private get editing(): boolean {\n return !!this.applicationId\n }\n\n /*\n * ⚠ The type dropdown: each option carries its OWN value. The page this replaced wrote the\n * current type as the value of every option, so all six submitted the same thing and the\n * type could not be changed from this screen at all.\n */\n render() {\n const app = this.application || {}\n\n if (this.editing && this.loadFailed) {\n return html`<div class=\"failure\">${i18next.t('text.the application could not be loaded')}</div>`\n }\n\n return html`\n <form @submit=${(e: Event) => e.preventDefault()}>\n <fieldset>\n <legend>${i18next.t('text.application')}</legend>\n <div field-2column>\n <div field grid-span>\n <label for=\"name\">${i18next.t('field.name')}</label>\n <input\n id=\"name\"\n type=\"text\"\n name=\"name\"\n .value=${app.name || ''}\n required\n autofocus\n ?invalid=${this.missing.includes('name')}\n />\n ${\n this.missing.includes('name')\n ? html`<div field-error>${i18next.t('text.this field is required')}</div>`\n : ''\n }\n </div>\n\n <div field grid-span>\n <label for=\"description\">${i18next.t('field.description')}</label>\n <input id=\"description\" type=\"text\" name=\"description\" .value=${app.description || ''} />\n </div>\n\n <!-- type=email so the browser checks the shape the server's scalar will check. -->\n <div field grid-span>\n <label for=\"email\">${i18next.t('field.contact email')}</label>\n <input id=\"email\" type=\"email\" name=\"email\" .value=${app.email || ''} />\n </div>\n\n <div field grid-span>\n <label for=\"type\">${i18next.t('field.type')}</label>\n <select id=\"type\" name=\"type\">\n ${APPLICATION_TYPES.map(\n type => html`<option value=${type} ?selected=${type === (app.type || DEFAULT_TYPE)}>${type}</option>`\n )}\n </select>\n </div>\n\n <div field grid-span>\n <label for=\"url\">${i18next.t('field.application url')}</label>\n <input id=\"url\" type=\"url\" name=\"url\" .value=${app.url || ''} />\n </div>\n\n <div field grid-span>\n <label for=\"redirect-url\">${i18next.t('field.redirect url')}</label>\n <input id=\"redirect-url\" type=\"url\" name=\"redirectUrl\" .value=${app.redirectUrl || ''} />\n </div>\n\n <div field grid-span>\n <label for=\"icon\">${i18next.t('field.application icon')}</label>\n <input id=\"icon\" type=\"url\" name=\"icon\" .value=${app.icon || ''} />\n </div>\n\n <div field grid-span>\n <label for=\"webhook\">${i18next.t('field.webhook')}</label>\n <input id=\"webhook\" type=\"url\" name=\"webhook\" .value=${app.webhook || ''} />\n </div>\n </div>\n </fieldset>\n\n ${this.editing ? this.renderEndpoints(app) : ''} ${this.editing ? this.renderCredential(app) : ''}\n ${this.failed ? html`<div class=\"failure\">${i18next.t('text.could not be saved')}</div>` : ''}\n </form>\n\n <div class=\"buttons\">\n ${\n this.editing\n ? html`\n <md-text-button class=\"left\" @click=${this.deleteApplication.bind(this)}>\n ${i18next.t('button.delete')}\n </md-text-button>\n `\n : ''\n }\n <md-text-button @click=${this.cancel.bind(this)}>${i18next.t('button.cancel')}</md-text-button>\n <md-filled-button @click=${this.save.bind(this)}>\n ${this.editing ? i18next.t('button.update') : i18next.t('button.register')}\n </md-filled-button>\n </div>\n `\n }\n\n /*\n * What the server hands back rather than what the person types: the OAuth endpoints and the\n * scopes this application may ask for. Read-only and outside the form's named inputs, so\n * they are never part of a patch.\n */\n private renderEndpoints(app: any) {\n return html`\n <fieldset>\n <legend>${i18next.t('text.endpoints')}</legend>\n <div field-2column>\n <div field grid-span>\n <label for=\"auth-url\">${i18next.t('field.auth url')}</label>\n <input id=\"auth-url\" type=\"text\" .value=${app.authUrl || ''} readonly />\n <div class=\"note\">${i18next.t('text.where an integration asks for an authorization code')}</div>\n </div>\n\n <div field grid-span>\n <label for=\"access-token-url\">${i18next.t('field.access token url')}</label>\n <input id=\"access-token-url\" type=\"text\" .value=${app.accessTokenUrl || ''} readonly />\n <div class=\"note\">${i18next.t('text.where that code is exchanged for an access token')}</div>\n </div>\n\n <div field grid-span>\n <label for=\"available-scopes\">${i18next.t('field.available scopes')}</label>\n <input id=\"available-scopes\" type=\"text\" .value=${app.availableScopes || ''} readonly />\n </div>\n </div>\n </fieldset>\n `\n }\n\n /*\n * appKey stays a plain field: it is the client identifier, meant to be read and quoted.\n * appSecret is the other half — it carries its own @privilege for security:query, and it is\n * the value an integration signs with.\n */\n private renderCredential(app: any) {\n return html`\n <fieldset>\n <legend>${i18next.t('text.credentials')}</legend>\n <div field-2column>\n <div field grid-span>\n <label for=\"app-key\">${i18next.t('field.app key')}</label>\n <input id=\"app-key\" type=\"text\" .value=${app.appKey || ''} readonly />\n </div>\n\n <div field grid-span>\n <label for=\"app-secret\">${i18next.t('field.client-secret')}</label>\n <secret-field id=\"app-secret\" .value=${app.appSecret || ''}></secret-field>\n <div class=\"row\">\n <md-text-button @click=${this.generateSecret.bind(this)}>\n ${i18next.t('button.generate new secret')}\n </md-text-button>\n </div>\n </div>\n </div>\n </fieldset>\n `\n }\n\n async connectedCallback() {\n super.connectedCallback()\n\n if (this.editing) {\n await this.fetchApplication()\n }\n }\n\n async fetchApplication() {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n application(id: $id) { ${APPLICATION_FIELDS} }\n }\n `,\n variables: { id: this.applicationId }\n })\n\n /*\n * A refusal arrives in `errors` rather than throwing. An unchecked read would render an\n * empty form that looks like a record with every field blank, and saving it would write\n * those blanks.\n */\n if (response.errors || !response.data?.application) {\n this.loadFailed = true\n return\n }\n\n this.loadFailed = false\n this.application = response.data.application\n }\n\n private cancel(e: Event) {\n e.preventDefault()\n this.close()\n }\n\n async save(e: Event) {\n e.preventDefault()\n\n const formData = new FormData(this.form)\n\n const missing = missingRequired(formData, REQUIRED)\n\n if (missing.length) {\n this.missing = missing\n return\n }\n\n this.missing = []\n\n return this.editing ? this.updateApplication(formData) : this.createApplication(formData)\n }\n\n private async createApplication(formData: FormData) {\n const response = await client.mutate({\n mutation: gql`\n mutation ($application: NewApplication!) {\n createApplication(application: $application) {\n id\n }\n }\n `,\n variables: { application: filledValues(formData) }\n })\n\n if (response.errors) {\n this.failed = true\n return\n }\n\n this.saved()\n }\n\n private async updateApplication(formData: FormData) {\n const patch = changedValues(formData, this.application)\n\n if (!Object.keys(patch).length) {\n this.close()\n return\n }\n\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $patch: ApplicationPatch!) {\n updateApplication(id: $id, patch: $patch) { ${APPLICATION_FIELDS} }\n }\n `,\n variables: { id: this.applicationId, patch }\n })\n\n if (response.errors) {\n this.failed = true\n return\n }\n\n this.saved()\n }\n\n private async generateSecret(e: Event) {\n e.preventDefault()\n\n const confirmed = await OxPrompt.open({\n type: 'warning',\n title: i18next.t('button.generate new secret'),\n text: i18next.t('text.the current secret stops working immediately'),\n confirmButton: { text: i18next.t('button.confirm') },\n cancelButton: { text: i18next.t('button.cancel') }\n })\n\n if (!confirmed) {\n return\n }\n\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n generateApplicationSecret(id: $id) { ${APPLICATION_FIELDS} }\n }\n `,\n variables: { id: this.applicationId }\n })\n\n if (response.errors) {\n this.failed = true\n return\n }\n\n /*\n * Stays open on purpose — a fresh secret is shown once and has to be copied before it is\n * out of reach.\n */\n this.failed = false\n this.application = response.data.generateApplicationSecret\n this.dispatchEvent(new CustomEvent('application-changed', { bubbles: true, composed: true }))\n }\n\n private async deleteApplication(e: Event) {\n e.preventDefault()\n\n const confirmed = await OxPrompt.open({\n type: 'warning',\n title: i18next.t('button.delete'),\n text: i18next.t('text.are you sure you want to delete x', { x: this.application?.name || '' }),\n confirmButton: { text: i18next.t('button.delete') },\n cancelButton: { text: i18next.t('button.cancel') }\n })\n\n if (!confirmed) {\n return\n }\n\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n deleteApplication(id: $id)\n }\n `,\n variables: { id: this.applicationId }\n })\n\n if (response.errors) {\n this.failed = true\n return\n }\n\n this.saved()\n }\n\n private saved() {\n this.failed = false\n this.dispatchEvent(new CustomEvent('application-changed', { bubbles: true, composed: true }))\n this.close()\n }\n\n private close() {\n this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }))\n }\n}\n"]}
@@ -13,6 +13,7 @@ import { i18next, localize } from '@operato/i18n';
13
13
  import { notify, openPopup } from '@operato/layout';
14
14
  import { FormFieldStyles } from '@operato/styles';
15
15
  import { auth, getLanguages } from '@things-factory/auth-base/dist-client';
16
+ import { readVerification } from '../utils/webauthn-verification.js';
16
17
  const isAvailableWebauthn = 'PublicKeyCredential' in window;
17
18
  let ProfileComponent = class ProfileComponent extends localize(i18next)(LitElement) {
18
19
  constructor() {
@@ -316,35 +317,51 @@ let ProfileComponent = class ProfileComponent extends localize(i18next)(LitEleme
316
317
  title: i18next.t('label.delete account')
317
318
  });
318
319
  }
320
+ /*
321
+ * ⚠ The three endings here are not interchangeable, and the screen used to give the same one
322
+ * for all of them.
323
+ *
324
+ * registered the server confirmed it
325
+ * refused the server said no — usually this credential is already registered
326
+ * failed we never got an answer: the browser refused, the device was cancelled,
327
+ * the network dropped
328
+ *
329
+ * `await verification.text()` on an already-parsed body threw a TypeError that the catch
330
+ * below swallowed, so **refused arrived as failed** and the refusal message was unreachable
331
+ * code. `readVerification` works out which ending it is without being able to throw; see
332
+ * `utils/webauthn-verification.ts`.
333
+ */
319
334
  async registerUser() {
320
335
  try {
321
336
  const options = await fetch('/auth/register-webauthn/challenge').then(res => res.json());
322
337
  const attResp = await startRegistration(options);
323
- const verification = await fetch('/auth/verify-registration', {
338
+ const verification = await readVerification(await fetch('/auth/verify-registration', {
324
339
  method: 'POST',
325
340
  headers: {
326
341
  'Content-Type': 'application/json'
327
342
  },
328
343
  body: JSON.stringify(attResp)
329
- }).then(res => res.json());
330
- if (verification.verified) {
344
+ }));
345
+ if (verification.outcome === 'registered') {
331
346
  notify({
332
347
  level: 'info',
333
348
  message: i18next.t('text.user credential registered successfully')
334
349
  });
350
+ return;
335
351
  }
336
- else {
337
- console.error(await verification.text());
338
- notify({
339
- level: 'error',
340
- message: i18next.t('error.user credential registeration not allowed')
341
- });
342
- }
352
+ /* The server's own words, in English, for whoever reads the console — not for the screen. */
353
+ console.error('passkey registration refused:', verification.reason);
354
+ notify({
355
+ level: 'error',
356
+ message: i18next.t('error.user credential registration not allowed')
357
+ });
343
358
  }
344
359
  catch (error) {
360
+ /* Never reached the server, or never got past the device. A different thing to say. */
361
+ console.error('passkey registration failed:', error);
345
362
  notify({
346
363
  level: 'error',
347
- message: i18next.t('error.user credential registeration failed')
364
+ message: i18next.t('error.user credential registration failed')
348
365
  });
349
366
  }
350
367
  }