@things-factory/auth-ui 10.1.256 → 10.1.257
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-client/bootstrap.js +2 -2
- package/dist-client/bootstrap.js.map +1 -1
- package/dist-client/components/access-decision-popup.d.ts +9 -8
- package/dist-client/components/access-decision-popup.js +46 -58
- package/dist-client/components/access-decision-popup.js.map +1 -1
- package/dist-client/components/access-request-popup.d.ts +13 -8
- package/dist-client/components/access-request-popup.js +78 -102
- package/dist-client/components/access-request-popup.js.map +1 -1
- package/dist-client/components/app-binding-detail.d.ts +7 -5
- package/dist-client/components/app-binding-detail.js +62 -82
- package/dist-client/components/app-binding-detail.js.map +1 -1
- package/dist-client/components/appliance-editor.d.ts +10 -9
- package/dist-client/components/appliance-editor.js +87 -138
- package/dist-client/components/appliance-editor.js.map +1 -1
- package/dist-client/components/application-editor.d.ts +7 -5
- package/dist-client/components/application-editor.js +58 -92
- package/dist-client/components/application-editor.js.map +1 -1
- package/dist-client/components/my-login-history.js +18 -40
- package/dist-client/components/my-login-history.js.map +1 -1
- package/dist-client/components/my-passkeys.d.ts +4 -3
- package/dist-client/components/my-passkeys.js +29 -51
- package/dist-client/components/my-passkeys.js.map +1 -1
- package/dist-client/components/my-roles.d.ts +3 -4
- package/dist-client/components/my-roles.js +36 -55
- package/dist-client/components/my-roles.js.map +1 -1
- package/dist-client/components/partner-role-editor.js +27 -50
- package/dist-client/components/partner-role-editor.js.map +1 -1
- package/dist-client/components/profile-component.d.ts +2 -2
- package/dist-client/components/profile-component.js +18 -26
- package/dist-client/components/profile-component.js.map +1 -1
- package/dist-client/components/role-template-picker.d.ts +3 -3
- package/dist-client/components/role-template-picker.js +28 -42
- package/dist-client/components/role-template-picker.js.map +1 -1
- package/dist-client/components/user-detail.d.ts +23 -22
- package/dist-client/components/user-detail.js +125 -179
- package/dist-client/components/user-detail.js.map +1 -1
- package/dist-client/components/user-invite.d.ts +8 -6
- package/dist-client/components/user-invite.js +106 -141
- package/dist-client/components/user-invite.js.map +1 -1
- package/dist-client/components/user-popups-standard.test.js +1 -1
- package/dist-client/components/user-popups-standard.test.js.map +1 -1
- package/dist-client/components/user-role-editor.js +72 -122
- package/dist-client/components/user-role-editor.js.map +1 -1
- package/dist-client/entity360/person-360.d.ts +8 -11
- package/dist-client/entity360/person-360.js +49 -68
- package/dist-client/entity360/person-360.js.map +1 -1
- package/dist-client/entries/oauth2/oauth2-decision-page.js +13 -25
- package/dist-client/entries/oauth2/oauth2-decision-page.js.map +1 -1
- package/dist-client/identity-confirmation/identity-confirmation.d.ts +13 -14
- package/dist-client/identity-confirmation/identity-confirmation.js +39 -45
- package/dist-client/identity-confirmation/identity-confirmation.js.map +1 -1
- package/dist-client/pages/audit/audit-page.d.ts +4 -4
- package/dist-client/pages/audit/audit-page.js +28 -39
- package/dist-client/pages/audit/audit-page.js.map +1 -1
- package/dist-client/pages/domain-link/domain-link-management.js +1 -1
- package/dist-client/pages/domain-link/domain-link-management.js.map +1 -1
- package/dist-client/pages/partner/partner-management.js +30 -68
- package/dist-client/pages/partner/partner-management.js.map +1 -1
- package/dist-client/pages/role/role-extra-removal.test.d.mts +1 -0
- package/dist-client/pages/role/role-extra-removal.test.mjs +73 -0
- package/dist-client/pages/role/role-extra-removal.test.mjs.map +1 -0
- package/dist-client/pages/role/role-list-page.js +20 -22
- package/dist-client/pages/role/role-list-page.js.map +1 -1
- package/dist-client/pages/role/role-privilege-page.d.ts +36 -8
- package/dist-client/pages/role/role-privilege-page.js +216 -158
- package/dist-client/pages/role/role-privilege-page.js.map +1 -1
- package/dist-client/pages/role/role-task-review.test.js +20 -16
- package/dist-client/pages/role/role-task-review.test.js.map +1 -1
- package/dist-client/pages/role/role-template-gap.test.js +8 -3
- package/dist-client/pages/role/role-template-gap.test.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/helps/auth/roles.en.md +13 -49
- package/helps/auth/roles.ko.md +13 -32
- package/package.json +9 -8
- package/tests/page-state.test.ts +2 -1
- package/tests/password-rule-shown.test.ts +2 -2
- package/tests/person-360-sections.test.ts +9 -5
- package/translations/en.json +5 -1
- package/translations/es.json +5 -1
- package/translations/ja.json +5 -1
- package/translations/ko.json +5 -1
- package/translations/ms.json +5 -1
- 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 {
|
|
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
|
-
|
|
69
|
-
this.
|
|
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
|
-
|
|
379
|
-
|
|
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
|
-
|
|
401
|
-
|
|
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
|
|
427
|
-
|
|
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.
|
|
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
|
-
|
|
459
|
-
|
|
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",
|
|
500
|
-
], ApplicationEditor.prototype, "
|
|
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 {
|
|
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
|
-
|
|
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":";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,
|
|
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 {
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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)
|