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