@things-factory/auth-ui 10.1.256 → 10.1.258

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 (83) hide show
  1. package/dist-client/bootstrap.js +2 -2
  2. package/dist-client/bootstrap.js.map +1 -1
  3. package/dist-client/components/access-decision-popup.d.ts +9 -8
  4. package/dist-client/components/access-decision-popup.js +46 -58
  5. package/dist-client/components/access-decision-popup.js.map +1 -1
  6. package/dist-client/components/access-request-popup.d.ts +13 -8
  7. package/dist-client/components/access-request-popup.js +78 -102
  8. package/dist-client/components/access-request-popup.js.map +1 -1
  9. package/dist-client/components/app-binding-detail.d.ts +7 -5
  10. package/dist-client/components/app-binding-detail.js +62 -82
  11. package/dist-client/components/app-binding-detail.js.map +1 -1
  12. package/dist-client/components/appliance-editor.d.ts +10 -9
  13. package/dist-client/components/appliance-editor.js +87 -138
  14. package/dist-client/components/appliance-editor.js.map +1 -1
  15. package/dist-client/components/application-editor.d.ts +7 -5
  16. package/dist-client/components/application-editor.js +58 -92
  17. package/dist-client/components/application-editor.js.map +1 -1
  18. package/dist-client/components/my-login-history.js +18 -40
  19. package/dist-client/components/my-login-history.js.map +1 -1
  20. package/dist-client/components/my-passkeys.d.ts +4 -3
  21. package/dist-client/components/my-passkeys.js +29 -51
  22. package/dist-client/components/my-passkeys.js.map +1 -1
  23. package/dist-client/components/my-roles.d.ts +3 -4
  24. package/dist-client/components/my-roles.js +36 -55
  25. package/dist-client/components/my-roles.js.map +1 -1
  26. package/dist-client/components/partner-role-editor.js +27 -50
  27. package/dist-client/components/partner-role-editor.js.map +1 -1
  28. package/dist-client/components/profile-component.d.ts +2 -2
  29. package/dist-client/components/profile-component.js +18 -26
  30. package/dist-client/components/profile-component.js.map +1 -1
  31. package/dist-client/components/role-template-picker.d.ts +3 -3
  32. package/dist-client/components/role-template-picker.js +28 -42
  33. package/dist-client/components/role-template-picker.js.map +1 -1
  34. package/dist-client/components/user-detail.d.ts +23 -22
  35. package/dist-client/components/user-detail.js +125 -179
  36. package/dist-client/components/user-detail.js.map +1 -1
  37. package/dist-client/components/user-invite.d.ts +8 -6
  38. package/dist-client/components/user-invite.js +106 -141
  39. package/dist-client/components/user-invite.js.map +1 -1
  40. package/dist-client/components/user-popups-standard.test.js +1 -1
  41. package/dist-client/components/user-popups-standard.test.js.map +1 -1
  42. package/dist-client/components/user-role-editor.js +72 -122
  43. package/dist-client/components/user-role-editor.js.map +1 -1
  44. package/dist-client/entity360/person-360.d.ts +8 -11
  45. package/dist-client/entity360/person-360.js +49 -68
  46. package/dist-client/entity360/person-360.js.map +1 -1
  47. package/dist-client/entries/oauth2/oauth2-decision-page.js +13 -25
  48. package/dist-client/entries/oauth2/oauth2-decision-page.js.map +1 -1
  49. package/dist-client/identity-confirmation/identity-confirmation.d.ts +13 -14
  50. package/dist-client/identity-confirmation/identity-confirmation.js +39 -45
  51. package/dist-client/identity-confirmation/identity-confirmation.js.map +1 -1
  52. package/dist-client/pages/audit/audit-page.d.ts +4 -4
  53. package/dist-client/pages/audit/audit-page.js +28 -39
  54. package/dist-client/pages/audit/audit-page.js.map +1 -1
  55. package/dist-client/pages/domain-link/domain-link-management.js +1 -1
  56. package/dist-client/pages/domain-link/domain-link-management.js.map +1 -1
  57. package/dist-client/pages/partner/partner-management.js +30 -68
  58. package/dist-client/pages/partner/partner-management.js.map +1 -1
  59. package/dist-client/pages/role/role-extra-removal.test.d.mts +1 -0
  60. package/dist-client/pages/role/role-extra-removal.test.mjs +73 -0
  61. package/dist-client/pages/role/role-extra-removal.test.mjs.map +1 -0
  62. package/dist-client/pages/role/role-list-page.js +20 -22
  63. package/dist-client/pages/role/role-list-page.js.map +1 -1
  64. package/dist-client/pages/role/role-privilege-page.d.ts +36 -8
  65. package/dist-client/pages/role/role-privilege-page.js +216 -158
  66. package/dist-client/pages/role/role-privilege-page.js.map +1 -1
  67. package/dist-client/pages/role/role-task-review.test.js +20 -16
  68. package/dist-client/pages/role/role-task-review.test.js.map +1 -1
  69. package/dist-client/pages/role/role-template-gap.test.js +8 -3
  70. package/dist-client/pages/role/role-template-gap.test.js.map +1 -1
  71. package/dist-client/tsconfig.tsbuildinfo +1 -1
  72. package/helps/auth/roles.en.md +13 -49
  73. package/helps/auth/roles.ko.md +13 -32
  74. package/package.json +11 -10
  75. package/tests/page-state.test.ts +2 -1
  76. package/tests/password-rule-shown.test.ts +2 -2
  77. package/tests/person-360-sections.test.ts +9 -5
  78. package/translations/en.json +5 -1
  79. package/translations/es.json +5 -1
  80. package/translations/ja.json +5 -1
  81. package/translations/ko.json +5 -1
  82. package/translations/ms.json +5 -1
  83. package/translations/zh.json +5 -1
@@ -6,7 +6,7 @@ import '@material/web/button/filled-button.js';
6
6
  import gql from 'graphql-tag';
7
7
  import { css, html, LitElement } from 'lit';
8
8
  import { customElement, property, query, state } from 'lit/decorators.js';
9
- import { client } from '@operato/graphql';
9
+ import { MutationController, QueryController } from '@operato/graphql';
10
10
  import { i18next, localize } from '@operato/i18n';
11
11
  import { OxPrompt } from '@operato/popup/ox-prompt.js';
12
12
  import { CommonHeaderStyles, CommonPopupStyles, ScrollbarStyles } from '@operato/styles';
@@ -63,10 +63,39 @@ const APPLICATION_FIELDS = `
63
63
  let ApplicationEditor = class ApplicationEditor extends localize(i18next)(LitElement) {
64
64
  constructor() {
65
65
  super(...arguments);
66
- this.application = null;
67
66
  this.missing = [];
68
- this.failed = false;
69
- this.loadFailed = false;
67
+ /** The application as a new secret left it - shown once, so it has to be copied before the popup closes. */
68
+ this.regenerated = null;
69
+ this.read = new QueryController(this, gql `
70
+ query ($id: String!) {
71
+ application(id: $id) { ${APPLICATION_FIELDS} }
72
+ }
73
+ `, { variables: () => (this.applicationId ? { id: this.applicationId } : undefined) });
74
+ /* A failed save: the GraphQL link says why, the form says it was not saved. */
75
+ this.saves = {
76
+ create: new MutationController(this, gql `
77
+ mutation ($application: NewApplication!) {
78
+ createApplication(application: $application) {
79
+ id
80
+ }
81
+ }
82
+ `),
83
+ update: new MutationController(this, gql `
84
+ mutation ($id: String!, $patch: ApplicationPatch!) {
85
+ updateApplication(id: $id, patch: $patch) { ${APPLICATION_FIELDS} }
86
+ }
87
+ `),
88
+ secret: new MutationController(this, gql `
89
+ mutation ($id: String!) {
90
+ generateApplicationSecret(id: $id) { ${APPLICATION_FIELDS} }
91
+ }
92
+ `),
93
+ delete: new MutationController(this, gql `
94
+ mutation ($id: String!) {
95
+ deleteApplication(id: $id)
96
+ }
97
+ `)
98
+ };
70
99
  }
71
100
  /*
72
101
  * ── Where the scrolling happens ─────────────────────────────────────────
@@ -187,6 +216,20 @@ let ApplicationEditor = class ApplicationEditor extends localize(i18next)(LitEle
187
216
  }
188
217
  `
189
218
  ]; }
219
+ get application() {
220
+ return this.regenerated ?? this.read.data?.application ?? null;
221
+ }
222
+ get failed() {
223
+ return Object.values(this.saves).some(save => save.error);
224
+ }
225
+ /*
226
+ * A refusal arrives in `errors` rather than throwing. An unchecked read would render an
227
+ * empty form that looks like a record with every field blank, and saving it would write
228
+ * those blanks.
229
+ */
230
+ get loadFailed() {
231
+ return !!this.read.error || (!!this.read.data && !this.read.data.application);
232
+ }
190
233
  get editing() {
191
234
  return !!this.applicationId;
192
235
  }
@@ -332,33 +375,6 @@ let ApplicationEditor = class ApplicationEditor extends localize(i18next)(LitEle
332
375
  </fieldset>
333
376
  `;
334
377
  }
335
- async connectedCallback() {
336
- super.connectedCallback();
337
- if (this.editing) {
338
- await this.fetchApplication();
339
- }
340
- }
341
- async fetchApplication() {
342
- const response = await client.query({
343
- query: gql `
344
- query ($id: String!) {
345
- application(id: $id) { ${APPLICATION_FIELDS} }
346
- }
347
- `,
348
- variables: { id: this.applicationId }
349
- });
350
- /*
351
- * A refusal arrives in `errors` rather than throwing. An unchecked read would render an
352
- * empty form that looks like a record with every field blank, and saving it would write
353
- * those blanks.
354
- */
355
- if (response.errors || !response.data?.application) {
356
- this.loadFailed = true;
357
- return;
358
- }
359
- this.loadFailed = false;
360
- this.application = response.data.application;
361
- }
362
378
  cancel(e) {
363
379
  e.preventDefault();
364
380
  this.close();
@@ -375,21 +391,9 @@ let ApplicationEditor = class ApplicationEditor extends localize(i18next)(LitEle
375
391
  return this.editing ? this.updateApplication(formData) : this.createApplication(formData);
376
392
  }
377
393
  async createApplication(formData) {
378
- const response = await client.mutate({
379
- mutation: gql `
380
- mutation ($application: NewApplication!) {
381
- createApplication(application: $application) {
382
- id
383
- }
384
- }
385
- `,
386
- variables: { application: filledValues(formData) }
387
- });
388
- if (response.errors) {
389
- this.failed = true;
390
- return;
394
+ if (await this.saves.create.run({ application: filledValues(formData) })) {
395
+ this.saved();
391
396
  }
392
- this.saved();
393
397
  }
394
398
  async updateApplication(formData) {
395
399
  const patch = changedValues(formData, this.application);
@@ -397,19 +401,9 @@ let ApplicationEditor = class ApplicationEditor extends localize(i18next)(LitEle
397
401
  this.close();
398
402
  return;
399
403
  }
400
- const response = await client.mutate({
401
- mutation: gql `
402
- mutation ($id: String!, $patch: ApplicationPatch!) {
403
- updateApplication(id: $id, patch: $patch) { ${APPLICATION_FIELDS} }
404
+ if (await this.saves.update.run({ id: this.applicationId, patch })) {
405
+ this.saved();
404
406
  }
405
- `,
406
- variables: { id: this.applicationId, patch }
407
- });
408
- if (response.errors) {
409
- this.failed = true;
410
- return;
411
- }
412
- this.saved();
413
407
  }
414
408
  async generateSecret(e) {
415
409
  e.preventDefault();
@@ -423,24 +417,15 @@ let ApplicationEditor = class ApplicationEditor extends localize(i18next)(LitEle
423
417
  if (!confirmed) {
424
418
  return;
425
419
  }
426
- const response = await client.mutate({
427
- mutation: gql `
428
- mutation ($id: String!) {
429
- generateApplicationSecret(id: $id) { ${APPLICATION_FIELDS} }
430
- }
431
- `,
432
- variables: { id: this.applicationId }
433
- });
434
- if (response.errors) {
435
- this.failed = true;
420
+ const generated = await this.saves.secret.run({ id: this.applicationId });
421
+ if (!generated) {
436
422
  return;
437
423
  }
438
424
  /*
439
425
  * Stays open on purpose — a fresh secret is shown once and has to be copied before it is
440
426
  * out of reach.
441
427
  */
442
- this.failed = false;
443
- this.application = response.data.generateApplicationSecret;
428
+ this.regenerated = generated.generateApplicationSecret;
444
429
  this.dispatchEvent(new CustomEvent('application-changed', { bubbles: true, composed: true }));
445
430
  }
446
431
  async deleteApplication(e) {
@@ -455,22 +440,11 @@ let ApplicationEditor = class ApplicationEditor extends localize(i18next)(LitEle
455
440
  if (!confirmed) {
456
441
  return;
457
442
  }
458
- const response = await client.mutate({
459
- mutation: gql `
460
- mutation ($id: String!) {
461
- deleteApplication(id: $id)
462
- }
463
- `,
464
- variables: { id: this.applicationId }
465
- });
466
- if (response.errors) {
467
- this.failed = true;
468
- return;
443
+ if (await this.saves.delete.run({ id: this.applicationId })) {
444
+ this.saved();
469
445
  }
470
- this.saved();
471
446
  }
472
447
  saved() {
473
- this.failed = false;
474
448
  this.dispatchEvent(new CustomEvent('application-changed', { bubbles: true, composed: true }));
475
449
  this.close();
476
450
  }
@@ -486,22 +460,14 @@ __decorate([
486
460
  query('form'),
487
461
  __metadata("design:type", HTMLFormElement)
488
462
  ], ApplicationEditor.prototype, "form", void 0);
489
- __decorate([
490
- state(),
491
- __metadata("design:type", Object)
492
- ], ApplicationEditor.prototype, "application", void 0);
493
463
  __decorate([
494
464
  state(),
495
465
  __metadata("design:type", Array)
496
466
  ], ApplicationEditor.prototype, "missing", void 0);
497
467
  __decorate([
498
468
  state(),
499
- __metadata("design:type", Boolean)
500
- ], ApplicationEditor.prototype, "failed", void 0);
501
- __decorate([
502
- state(),
503
- __metadata("design:type", Boolean)
504
- ], ApplicationEditor.prototype, "loadFailed", void 0);
469
+ __metadata("design:type", Object)
470
+ ], ApplicationEditor.prototype, "regenerated", void 0);
505
471
  ApplicationEditor = __decorate([
506
472
  customElement('application-editor')
507
473
  ], ApplicationEditor);
@@ -1 +1 @@
1
- {"version":3,"file":"application-editor.js","sourceRoot":"","sources":["../../client/components/application-editor.ts"],"names":[],"mappings":"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","sourcesContent":["import '@operato/popup/ox-popup-actions.js'\nimport '@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, ScrollbarStyles } 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 /*\n * ── Where the scrolling happens ─────────────────────────────────────────\n * The button row stays put and only the middle scrolls. That is the house shape for a\n * popup, and this had it wrong: the host itself scrolled, so the row you press to finish\n * walked off the bottom of the card along with everything else.\n *\n * So the host clips, and the content area between the title and the buttons is the one\n * thing with overflow. Two details carry it:\n *\n * min-height: 0 A flex item defaults to min-height auto, which refuses to shrink below\n * its own content. Without this the overflow rule sits there doing\n * nothing and the card grows instead — it reads as a working rule.\n * no padding CommonPopupStyles already pads the host. Padding here again doubles the\n * inset and pushes the scrollbar in off the card edge.\n *\n * ScrollbarStyles gives the scrollbar the product's own shape rather than the browser's.\n *\n * ── Why the in-content buttons need a rule of their own ─────────────────\n * CommonPopupStyles makes controls pill-shaped because a popup is a rounded card and the\n * things inside it follow the card. But it says that through `.buttons`, so only the commit\n * row got it: the actions sitting inside a fieldset — generate a new token, generate a new\n * secret — kept the page radius and read as a different kind of button on the same card.\n * Measured on a live popup: 5px inside the content, 13986px in the row below it.\n */\n static styles = [\n CommonHeaderStyles,\n CommonPopupStyles,\n ScrollbarStyles,\n css`\n /* in-content actions follow the card too, not just the commit row. see the note on styles. */\n fieldset md-text-button,\n fieldset md-filled-button {\n --md-text-button-container-shape: var(--popup-control-radius);\n --md-filled-button-container-shape: var(--popup-control-radius);\n }\n\n /* the host does not scroll; the content area below does. see the note on styles. */\n :host {\n overflow: hidden;\n }\n\n /* min-height:0 is load-bearing — see the note on styles. */\n form {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n\n display: flex;\n flex-direction: column;\n gap: var(--popup-gap);\n }\n\n fieldset {\n border-radius: var(--border-radius);\n border: var(--border-dim-color);\n margin: 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 /* one labelled box. .row puts two of them side by side; alone it spans the card. */\n [field] {\n display: flex;\n flex-direction: column;\n flex: 1;\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 @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>\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>\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>\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>\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>\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>\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>\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>\n <label for=\"webhook\">${i18next.t('field.webhook')}</label>\n <input id=\"webhook\" type=\"url\" name=\"webhook\" .value=${app.webhook || ''} />\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 <ox-popup-actions>\n ${\n this.editing\n ? html`\n <md-text-button slot=\"start\" destructive @click=${this.deleteApplication.bind(this)}>\n ${i18next.t('button.delete')}\n </md-text-button>\n `\n : ''\n }\n <md-text-button slot=\"cancel\" @click=${this.cancel.bind(this)}>${i18next.t('button.cancel')}</md-text-button>\n <md-filled-button slot=\"confirm\" @click=${this.save.bind(this)}>\n ${this.editing ? i18next.t('button.update') : i18next.t('button.register')}\n </md-filled-button>\n </ox-popup-actions>\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>\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>\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>\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 </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>\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>\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 </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"]}
1
+ {"version":3,"file":"application-editor.js","sourceRoot":"","sources":["../../client/components/application-editor.ts"],"names":[],"mappings":"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","sourcesContent":["import '@operato/popup/ox-popup-actions.js'\nimport '@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 { MutationController, QueryController } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { OxPrompt } from '@operato/popup/ox-prompt.js'\nimport { CommonHeaderStyles, CommonPopupStyles, ScrollbarStyles } 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 /*\n * ── Where the scrolling happens ─────────────────────────────────────────\n * The button row stays put and only the middle scrolls. That is the house shape for a\n * popup, and this had it wrong: the host itself scrolled, so the row you press to finish\n * walked off the bottom of the card along with everything else.\n *\n * So the host clips, and the content area between the title and the buttons is the one\n * thing with overflow. Two details carry it:\n *\n * min-height: 0 A flex item defaults to min-height auto, which refuses to shrink below\n * its own content. Without this the overflow rule sits there doing\n * nothing and the card grows instead — it reads as a working rule.\n * no padding CommonPopupStyles already pads the host. Padding here again doubles the\n * inset and pushes the scrollbar in off the card edge.\n *\n * ScrollbarStyles gives the scrollbar the product's own shape rather than the browser's.\n *\n * ── Why the in-content buttons need a rule of their own ─────────────────\n * CommonPopupStyles makes controls pill-shaped because a popup is a rounded card and the\n * things inside it follow the card. But it says that through `.buttons`, so only the commit\n * row got it: the actions sitting inside a fieldset — generate a new token, generate a new\n * secret — kept the page radius and read as a different kind of button on the same card.\n * Measured on a live popup: 5px inside the content, 13986px in the row below it.\n */\n static styles = [\n CommonHeaderStyles,\n CommonPopupStyles,\n ScrollbarStyles,\n css`\n /* in-content actions follow the card too, not just the commit row. see the note on styles. */\n fieldset md-text-button,\n fieldset md-filled-button {\n --md-text-button-container-shape: var(--popup-control-radius);\n --md-filled-button-container-shape: var(--popup-control-radius);\n }\n\n /* the host does not scroll; the content area below does. see the note on styles. */\n :host {\n overflow: hidden;\n }\n\n /* min-height:0 is load-bearing — see the note on styles. */\n form {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n\n display: flex;\n flex-direction: column;\n gap: var(--popup-gap);\n }\n\n fieldset {\n border-radius: var(--border-radius);\n border: var(--border-dim-color);\n margin: 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 /* one labelled box. .row puts two of them side by side; alone it spans the card. */\n [field] {\n display: flex;\n flex-direction: column;\n flex: 1;\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 @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 missing: string[] = []\n\n /** The application as a new secret left it - shown once, so it has to be copied before the popup closes. */\n @state() private regenerated: any = null\n\n private read = new QueryController(\n this,\n gql`\n query ($id: String!) {\n application(id: $id) { ${APPLICATION_FIELDS} }\n }\n `,\n { variables: () => (this.applicationId ? { id: this.applicationId } : undefined) }\n )\n\n /* A failed save: the GraphQL link says why, the form says it was not saved. */\n private saves = {\n create: new MutationController(\n this,\n gql`\n mutation ($application: NewApplication!) {\n createApplication(application: $application) {\n id\n }\n }\n `\n ),\n update: new MutationController(\n this,\n gql`\n mutation ($id: String!, $patch: ApplicationPatch!) {\n updateApplication(id: $id, patch: $patch) { ${APPLICATION_FIELDS} }\n }\n `\n ),\n secret: new MutationController(\n this,\n gql`\n mutation ($id: String!) {\n generateApplicationSecret(id: $id) { ${APPLICATION_FIELDS} }\n }\n `\n ),\n delete: new MutationController(\n this,\n gql`\n mutation ($id: String!) {\n deleteApplication(id: $id)\n }\n `\n )\n }\n\n private get application(): any {\n return this.regenerated ?? this.read.data?.application ?? null\n }\n\n private get failed(): boolean {\n return Object.values(this.saves).some(save => save.error)\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 private get loadFailed(): boolean {\n return !!this.read.error || (!!this.read.data && !this.read.data.application)\n }\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>\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>\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>\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>\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>\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>\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>\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>\n <label for=\"webhook\">${i18next.t('field.webhook')}</label>\n <input id=\"webhook\" type=\"url\" name=\"webhook\" .value=${app.webhook || ''} />\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 <ox-popup-actions>\n ${\n this.editing\n ? html`\n <md-text-button slot=\"start\" destructive @click=${this.deleteApplication.bind(this)}>\n ${i18next.t('button.delete')}\n </md-text-button>\n `\n : ''\n }\n <md-text-button slot=\"cancel\" @click=${this.cancel.bind(this)}>${i18next.t('button.cancel')}</md-text-button>\n <md-filled-button slot=\"confirm\" @click=${this.save.bind(this)}>\n ${this.editing ? i18next.t('button.update') : i18next.t('button.register')}\n </md-filled-button>\n </ox-popup-actions>\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>\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>\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>\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 </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>\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>\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 </fieldset>\n `\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 if (await this.saves.create.run({ application: filledValues(formData) })) {\n this.saved()\n }\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 if (await this.saves.update.run({ id: this.applicationId, patch })) {\n this.saved()\n }\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 generated = await this.saves.secret.run({ id: this.applicationId })\n\n if (!generated) {\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.regenerated = generated.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 if (await this.saves.delete.run({ id: this.applicationId })) {\n this.saved()\n }\n }\n\n private saved() {\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"]}
@@ -3,14 +3,27 @@ import '@operato/data-grist';
3
3
  import gql from 'graphql-tag';
4
4
  import { css, html, LitElement } from 'lit';
5
5
  import { customElement, property } from 'lit/decorators.js';
6
- import { client } from '@operato/graphql';
6
+ import { QueryController } from '@operato/graphql';
7
7
  import { i18next } from '@operato/i18n';
8
8
  import { isMobileDevice } from '@operato/utils';
9
9
  import { ScrollbarStyles } from '@operato/styles';
10
10
  let MyLoginHistory = class MyLoginHistory extends LitElement {
11
11
  constructor() {
12
12
  super(...arguments);
13
- this.histories = [];
13
+ /* One's own check-ins, from the audit (ADR-0141 §8) - the address is the request's. */
14
+ this.checkins = new QueryController(this, gql `
15
+ query ($limit: Int) {
16
+ myAuditActions(kind: "auth.checkin", limit: $limit) {
17
+ domain {
18
+ name
19
+ }
20
+ createdAt
21
+ request {
22
+ ip
23
+ }
24
+ }
25
+ }
26
+ `, { variables: () => ({ limit: this.limit || 10 }) });
14
27
  }
15
28
  static { this.styles = [
16
29
  ScrollbarStyles,
@@ -27,6 +40,9 @@ let MyLoginHistory = class MyLoginHistory extends LitElement {
27
40
  }
28
41
  `
29
42
  ]; }
43
+ get histories() {
44
+ return (this.checkins.data?.myAuditActions ?? []).map((one) => ({ ...one, ip: one.request?.ip }));
45
+ }
30
46
  render() {
31
47
  if (!this.histories?.length)
32
48
  return html ``;
@@ -66,45 +82,7 @@ let MyLoginHistory = class MyLoginHistory extends LitElement {
66
82
  ></ox-grist>
67
83
  `;
68
84
  }
69
- firstUpdated() {
70
- this.fetchLoginHistories();
71
- }
72
- async fetchLoginHistories() {
73
- try {
74
- /* One's own check-ins, from the audit (ADR-0141 §8) - the address is the request's. */
75
- const response = await client.query({
76
- query: gql `
77
- query ($limit: Int) {
78
- myAuditActions(kind: "auth.checkin", limit: $limit) {
79
- domain {
80
- name
81
- }
82
- createdAt
83
- request {
84
- ip
85
- }
86
- }
87
- }
88
- `,
89
- variables: { limit: this.limit || 10 },
90
- fetchPolicy: 'no-cache'
91
- });
92
- if (response.errors?.length)
93
- return;
94
- this.histories = response.data.myAuditActions.map((one) => ({ ...one, ip: one.request?.ip }));
95
- }
96
- catch (e) {
97
- this.showToast('message' in e ? e.message : e);
98
- }
99
- }
100
- showToast(message) {
101
- document.dispatchEvent(new CustomEvent('notify', { detail: { message } }));
102
- }
103
85
  };
104
- __decorate([
105
- property({ type: Array }),
106
- __metadata("design:type", Array)
107
- ], MyLoginHistory.prototype, "histories", void 0);
108
86
  __decorate([
109
87
  property({ type: Number }),
110
88
  __metadata("design:type", Number)
@@ -1 +1 @@
1
- {"version":3,"file":"my-login-history.js","sourceRoot":"","sources":["../../client/components/my-login-history.ts"],"names":[],"mappings":"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","sourcesContent":["import '@operato/data-grist'\n\nimport gql from 'graphql-tag'\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { client } from '@operato/graphql'\nimport { i18next } from '@operato/i18n'\nimport { isMobileDevice } from '@operato/utils'\nimport { ScrollbarStyles } from '@operato/styles'\n\n@customElement('my-login-history')\nclass MyLoginHistory extends LitElement {\n static styles = [\n ScrollbarStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n background-color: var(--md-sys-color-background);\n padding: var(--spacing-large);\n overflow: auto;\n }\n ox-grist {\n flex: 1;\n }\n `\n ]\n\n @property({ type: Array }) histories: any[] = []\n @property({ type: Number }) limit?: number\n\n render() {\n if (!this.histories?.length) return html``\n\n const config = {\n rows: { appendable: false },\n pagination: { infinite: true },\n columns: [\n { type: 'gutter', gutterName: 'sequence' },\n {\n type: 'object',\n name: 'domain',\n header: i18next.t('field.domain'),\n record: { editable: false },\n width: 200\n },\n {\n type: 'datetime',\n name: 'createdAt',\n header: i18next.t('field.accessed-at'),\n record: { editable: false },\n width: 200\n },\n {\n type: 'string',\n name: 'ip',\n header: i18next.t('field.ip_address'),\n record: { editable: false },\n width: 200\n }\n ]\n }\n\n return html`\n <ox-grist\n .mode=${isMobileDevice() ? 'LIST' : 'GRID'}\n .config=${config}\n .data=\"${{ records: this.histories }}\"\n ></ox-grist>\n `\n }\n\n firstUpdated() {\n this.fetchLoginHistories()\n }\n\n async fetchLoginHistories() {\n try {\n /* One's own check-ins, from the audit (ADR-0141 §8) - the address is the request's. */\n const response = await client.query({\n query: gql`\n query ($limit: Int) {\n myAuditActions(kind: \"auth.checkin\", limit: $limit) {\n domain {\n name\n }\n createdAt\n request {\n ip\n }\n }\n }\n `,\n variables: { limit: this.limit || 10 },\n fetchPolicy: 'no-cache'\n })\n\n if (response.errors?.length) return\n this.histories = response.data.myAuditActions.map((one: any) => ({ ...one, ip: one.request?.ip }))\n } catch (e: any) {\n this.showToast('message' in e ? e.message : e)\n }\n }\n\n showToast(message) {\n document.dispatchEvent(new CustomEvent('notify', { detail: { message } }))\n }\n}\n"]}
1
+ {"version":3,"file":"my-login-history.js","sourceRoot":"","sources":["../../client/components/my-login-history.ts"],"names":[],"mappings":";AAAA,OAAO,qBAAqB,CAAA;AAE5B,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAE3D,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAGjD,IAAM,cAAc,GAApB,MAAM,cAAe,SAAQ,UAAU;IAAvC;;QAmBE,uFAAuF;QAC/E,aAAQ,GAAG,IAAI,eAAe,CACpC,IAAI,EACJ,GAAG,CAAA;;;;;;;;;;;;KAYF,EACD,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC,EAAE,CACnD,CAAA;IA8CH,CAAC;aAjFQ,WAAM,GAAG;QACd,eAAe;QACf,GAAG,CAAA;;;;;;;;;;;KAWF;KACF,AAdY,CAcZ;IAuBD,IAAY,SAAS;QACnB,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,cAAc,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAQ,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,GAAG,EAAE,EAAE,EAAE,GAAG,CAAC,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,CAAA;IACxG,CAAC;IAED,MAAM;QACJ,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM;YAAE,OAAO,IAAI,CAAA,EAAE,CAAA;QAE1C,MAAM,MAAM,GAAG;YACb,IAAI,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE;YAC3B,UAAU,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;YAC9B,OAAO,EAAE;gBACP,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE;gBAC1C;oBACE,IAAI,EAAE,QAAQ;oBACd,IAAI,EAAE,QAAQ;oBACd,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,cAAc,CAAC;oBACjC,MAAM,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE;oBAC3B,KAAK,EAAE,GAAG;iBACX;gBACD;oBACE,IAAI,EAAE,UAAU;oBAChB,IAAI,EAAE,WAAW;oBACjB,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,mBAAmB,CAAC;oBACtC,MAAM,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE;oBAC3B,KAAK,EAAE,GAAG;iBACX;gBACD;oBACE,IAAI,EAAE,QAAQ;oBACd,IAAI,EAAE,IAAI;oBACV,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,kBAAkB,CAAC;oBACrC,MAAM,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE;oBAC3B,KAAK,EAAE,GAAG;iBACX;aACF;SACF,CAAA;QAED,OAAO,IAAI,CAAA;;gBAEC,cAAc,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM;kBAChC,MAAM;iBACP,EAAE,OAAO,EAAE,IAAI,CAAC,SAAS,EAAE;;KAEvC,CAAA;IACH,CAAC;;AAhE2B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;6CAAe;AAjBtC,cAAc;IADnB,aAAa,CAAC,kBAAkB,CAAC;GAC5B,cAAc,CAkFnB","sourcesContent":["import '@operato/data-grist'\n\nimport gql from 'graphql-tag'\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { QueryController } from '@operato/graphql'\nimport { i18next } from '@operato/i18n'\nimport { isMobileDevice } from '@operato/utils'\nimport { ScrollbarStyles } from '@operato/styles'\n\n@customElement('my-login-history')\nclass MyLoginHistory extends LitElement {\n static styles = [\n ScrollbarStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n background-color: var(--md-sys-color-background);\n padding: var(--spacing-large);\n overflow: auto;\n }\n ox-grist {\n flex: 1;\n }\n `\n ]\n\n @property({ type: Number }) limit?: number\n\n /* One's own check-ins, from the audit (ADR-0141 §8) - the address is the request's. */\n private checkins = new QueryController(\n this,\n gql`\n query ($limit: Int) {\n myAuditActions(kind: \"auth.checkin\", limit: $limit) {\n domain {\n name\n }\n createdAt\n request {\n ip\n }\n }\n }\n `,\n { variables: () => ({ limit: this.limit || 10 }) }\n )\n\n private get histories(): any[] {\n return (this.checkins.data?.myAuditActions ?? []).map((one: any) => ({ ...one, ip: one.request?.ip }))\n }\n\n render() {\n if (!this.histories?.length) return html``\n\n const config = {\n rows: { appendable: false },\n pagination: { infinite: true },\n columns: [\n { type: 'gutter', gutterName: 'sequence' },\n {\n type: 'object',\n name: 'domain',\n header: i18next.t('field.domain'),\n record: { editable: false },\n width: 200\n },\n {\n type: 'datetime',\n name: 'createdAt',\n header: i18next.t('field.accessed-at'),\n record: { editable: false },\n width: 200\n },\n {\n type: 'string',\n name: 'ip',\n header: i18next.t('field.ip_address'),\n record: { editable: false },\n width: 200\n }\n ]\n }\n\n return html`\n <ox-grist\n .mode=${isMobileDevice() ? 'LIST' : 'GRID'}\n .config=${config}\n .data=\"${{ records: this.histories }}\"\n ></ox-grist>\n `\n }\n}\n"]}
@@ -15,13 +15,14 @@ declare const MyPasskeys_base: (new (...args: any[]) => LitElement) & typeof Lit
15
15
  */
16
16
  export declare class MyPasskeys extends MyPasskeys_base {
17
17
  static styles: import("lit").CSSResult;
18
- passkeys: Passkey[];
19
18
  renaming?: string;
20
19
  private supported;
20
+ private credentials;
21
+ private renameCredential;
22
+ private deleteCredential;
23
+ get passkeys(): Passkey[];
21
24
  render(): import("lit-html").TemplateResult<1> | typeof nothing;
22
- firstUpdated(): void;
23
25
  private when;
24
- private fetch;
25
26
  register(): Promise<void>;
26
27
  private startRenaming;
27
28
  /** Enter or leaving the field keeps the name; an empty or unchanged name keeps the old one. */
@@ -6,7 +6,7 @@ import { gql } from 'graphql-tag';
6
6
  import { css, html, LitElement, nothing } from 'lit';
7
7
  import { customElement, state } from 'lit/decorators.js';
8
8
  import { startRegistration } from '@simplewebauthn/browser';
9
- import { client } from '@operato/graphql';
9
+ import { MutationController, QueryController } from '@operato/graphql';
10
10
  import { i18next, localize } from '@operato/i18n';
11
11
  import { notify } from '@operato/layout';
12
12
  import { OxPrompt } from '@operato/popup/ox-prompt.js';
@@ -19,8 +19,29 @@ import { readVerification } from '../utils/webauthn-verification.js';
19
19
  let MyPasskeys = class MyPasskeys extends localize(i18next)(LitElement) {
20
20
  constructor() {
21
21
  super(...arguments);
22
- this.passkeys = [];
23
22
  this.supported = 'PublicKeyCredential' in window;
23
+ this.credentials = new QueryController(this, gql `
24
+ query {
25
+ myWebAuthCredentials {
26
+ id
27
+ name
28
+ createdAt
29
+ lastUsedAt
30
+ }
31
+ }
32
+ `, { variables: () => (this.supported ? {} : undefined) });
33
+ this.renameCredential = new MutationController(this, gql `
34
+ mutation ($id: String!, $name: String!) {
35
+ updateMyWebAuthCredential(id: $id, name: $name) {
36
+ id
37
+ }
38
+ }
39
+ `, { refetch: () => [this.credentials] });
40
+ this.deleteCredential = new MutationController(this, gql `
41
+ mutation ($id: String!) {
42
+ deleteMyWebAuthCredential(id: $id)
43
+ }
44
+ `, { refetch: () => [this.credentials] });
24
45
  }
25
46
  static { this.styles = css `
26
47
  :host {
@@ -80,6 +101,9 @@ let MyPasskeys = class MyPasskeys extends localize(i18next)(LitElement) {
80
101
  color: var(--md-sys-color-on-surface-variant);
81
102
  }
82
103
  `; }
104
+ get passkeys() {
105
+ return this.credentials.data?.myWebAuthCredentials ?? [];
106
+ }
83
107
  render() {
84
108
  if (!this.supported)
85
109
  return nothing;
@@ -123,33 +147,11 @@ let MyPasskeys = class MyPasskeys extends localize(i18next)(LitElement) {
123
147
  </md-text-button>
124
148
  `;
125
149
  }
126
- firstUpdated() {
127
- if (this.supported)
128
- this.fetch();
129
- }
130
150
  when(passkey) {
131
151
  const date = (value) => (value ? new Date(value).toLocaleDateString(i18next.language) : '');
132
152
  const made = i18next.t('text.passkey added on', { date: date(passkey.createdAt) });
133
153
  return passkey.lastUsedAt ? `${made} · ${i18next.t('text.passkey last used on', { date: date(passkey.lastUsedAt) })}` : made;
134
154
  }
135
- async fetch() {
136
- const response = await client.query({
137
- query: gql `
138
- query {
139
- myWebAuthCredentials {
140
- id
141
- name
142
- createdAt
143
- lastUsedAt
144
- }
145
- }
146
- `,
147
- fetchPolicy: 'no-cache'
148
- });
149
- if (!response.errors?.length) {
150
- this.passkeys = response.data.myWebAuthCredentials;
151
- }
152
- }
153
155
  /*
154
156
  * The three endings stay apart (see `utils/webauthn-verification.ts`): registered, refused by the
155
157
  * server, or never got there - and a prompt the person closed is not reported at all.
@@ -165,7 +167,7 @@ let MyPasskeys = class MyPasskeys extends localize(i18next)(LitElement) {
165
167
  }));
166
168
  if (verification.outcome === 'registered') {
167
169
  notify({ level: 'info', message: i18next.t('text.user credential registered successfully') });
168
- await this.fetch();
170
+ await this.credentials.refetch();
169
171
  return;
170
172
  }
171
173
  /* The server's own words, in English, for whoever reads the console - not for the screen. */
@@ -197,18 +199,7 @@ let MyPasskeys = class MyPasskeys extends localize(i18next)(LitElement) {
197
199
  const name = value.trim();
198
200
  if (!name || name === passkey.name)
199
201
  return;
200
- const response = await client.mutate({
201
- mutation: gql `
202
- mutation ($id: String!, $name: String!) {
203
- updateMyWebAuthCredential(id: $id, name: $name) {
204
- id
205
- }
206
- }
207
- `,
208
- variables: { id: passkey.id, name }
209
- });
210
- if (!response.errors?.length)
211
- await this.fetch();
202
+ await this.renameCredential.run({ id: passkey.id, name });
212
203
  }
213
204
  async removePasskey(passkey) {
214
205
  const confirmed = await OxPrompt.open({
@@ -220,22 +211,9 @@ let MyPasskeys = class MyPasskeys extends localize(i18next)(LitElement) {
220
211
  });
221
212
  if (!confirmed)
222
213
  return;
223
- const response = await client.mutate({
224
- mutation: gql `
225
- mutation ($id: String!) {
226
- deleteMyWebAuthCredential(id: $id)
227
- }
228
- `,
229
- variables: { id: passkey.id }
230
- });
231
- if (!response.errors?.length)
232
- await this.fetch();
214
+ await this.deleteCredential.run({ id: passkey.id });
233
215
  }
234
216
  };
235
- __decorate([
236
- state(),
237
- __metadata("design:type", Array)
238
- ], MyPasskeys.prototype, "passkeys", void 0);
239
217
  __decorate([
240
218
  state(),
241
219
  __metadata("design:type", String)