@things-factory/auth-ui 10.1.29 → 10.1.33
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/accept-invitation.d.ts +48 -0
- package/dist-client/components/accept-invitation.js +265 -0
- package/dist-client/components/accept-invitation.js.map +1 -0
- package/dist-client/components/appliance-editor.d.ts +3 -0
- package/dist-client/components/appliance-editor.js +102 -1
- package/dist-client/components/appliance-editor.js.map +1 -1
- package/dist-client/components/role-selector.js +23 -1
- package/dist-client/components/role-selector.js.map +1 -1
- package/dist-client/components/user-invite.d.ts +8 -0
- package/dist-client/components/user-invite.js +53 -6
- package/dist-client/components/user-invite.js.map +1 -1
- package/dist-client/entries/auth/accept-invitation.d.ts +3 -0
- package/dist-client/entries/auth/accept-invitation.js +10 -0
- package/dist-client/entries/auth/accept-invitation.js.map +1 -0
- package/dist-client/pages/invitation/invitation-list-query.test.d.ts +1 -0
- package/dist-client/pages/invitation/invitation-list-query.test.js +69 -0
- package/dist-client/pages/invitation/invitation-list-query.test.js.map +1 -0
- package/dist-client/pages/invitation/invitations.d.ts +63 -0
- package/dist-client/pages/invitation/invitations.js +282 -0
- package/dist-client/pages/invitation/invitations.js.map +1 -0
- package/dist-client/pages/role/role-list-page.d.ts +3 -2
- package/dist-client/pages/role/role-list-page.js +39 -6
- package/dist-client/pages/role/role-list-page.js.map +1 -1
- package/dist-client/route.js +3 -0
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/credential-panel-state.d.ts +51 -0
- package/dist-client/utils/credential-panel-state.js +49 -0
- package/dist-client/utils/credential-panel-state.js.map +1 -0
- package/dist-client/utils/credential-panel-state.test.d.ts +1 -0
- package/dist-client/utils/credential-panel-state.test.js +88 -0
- package/dist-client/utils/credential-panel-state.test.js.map +1 -0
- package/dist-client/utils/inherited-name-refusal.d.ts +44 -0
- package/dist-client/utils/inherited-name-refusal.js +46 -0
- package/dist-client/utils/inherited-name-refusal.js.map +1 -0
- package/dist-client/utils/inherited-name-refusal.test.d.ts +1 -0
- package/dist-client/utils/inherited-name-refusal.test.js +60 -0
- package/dist-client/utils/inherited-name-refusal.test.js.map +1 -0
- package/dist-client/utils/invitation-token.d.ts +28 -0
- package/dist-client/utils/invitation-token.js +40 -0
- package/dist-client/utils/invitation-token.js.map +1 -0
- package/dist-client/utils/invitation-token.test.d.ts +1 -0
- package/dist-client/utils/invitation-token.test.js +60 -0
- package/dist-client/utils/invitation-token.test.js.map +1 -0
- package/dist-client/utils/role-origin.d.ts +44 -0
- package/dist-client/utils/role-origin.js +47 -0
- package/dist-client/utils/role-origin.js.map +1 -0
- package/dist-client/utils/role-origin.test.d.ts +1 -0
- package/dist-client/utils/role-origin.test.js +60 -0
- package/dist-client/utils/role-origin.test.js.map +1 -0
- package/package.json +5 -5
- package/things-factory.config.js +1 -0
- package/translations/en.json +28 -0
- package/translations/ja.json +28 -0
- package/translations/ko.json +28 -0
- package/translations/ms.json +28 -0
- package/translations/zh.json +28 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import '@material/web/icon/icon.js';
|
|
2
|
+
import '@material/web/button/elevated-button.js';
|
|
3
|
+
import '@material/web/textfield/filled-text-field.js';
|
|
4
|
+
import '@operato/i18n/ox-i18n.js';
|
|
5
|
+
import '@operato/i18n/ox-i18n-selector.js';
|
|
6
|
+
import '@operato/layout/ox-snack-bar.js';
|
|
7
|
+
import { AbstractAuthPage } from './abstract-auth-page.js';
|
|
8
|
+
/**
|
|
9
|
+
* Accepting an invitation.
|
|
10
|
+
*
|
|
11
|
+
* ── Why the token is not in the address ─────────────────────────────────────
|
|
12
|
+
* The link is `…/accept-invitation#<token>`, and a browser does not send what follows the `#`.
|
|
13
|
+
* So the request that loads this page carries nothing, and the token stays out of server access
|
|
14
|
+
* logs, proxy logs and `Referer`. This page reads it from the fragment and then clears it from
|
|
15
|
+
* the address bar, so it does not sit in the browser's history either.
|
|
16
|
+
*
|
|
17
|
+
* The one place it can still travel is the mail being forwarded, and nothing about where the
|
|
18
|
+
* token sits can prevent that. So the address it was sent to is asked for as well: holding the
|
|
19
|
+
* link is not enough on its own.
|
|
20
|
+
*
|
|
21
|
+
* ── Why the password fields only appear after a try ─────────────────────────
|
|
22
|
+
* Whether an address already has an account is not this page's to announce. It sends what it
|
|
23
|
+
* has, and the server answers `invitation-needs-a-password` when it needs one — so the extra
|
|
24
|
+
* fields appear for the person who is actually creating an account, and nobody learns anything
|
|
25
|
+
* by loading the page.
|
|
26
|
+
*/
|
|
27
|
+
export declare class AcceptInvitation extends AbstractAuthPage {
|
|
28
|
+
static styles: import("lit").CSSResult[];
|
|
29
|
+
get pageName(): string;
|
|
30
|
+
get actionUrl(): string;
|
|
31
|
+
get title(): string;
|
|
32
|
+
/** Read from the fragment on connect. Empty when the link was opened without one. */
|
|
33
|
+
token?: string;
|
|
34
|
+
/** The server asked for a password, so this person is creating an account. */
|
|
35
|
+
needsPassword?: boolean;
|
|
36
|
+
/** Joined. The page stops being a form and says where to go. */
|
|
37
|
+
joined?: boolean;
|
|
38
|
+
busy?: boolean;
|
|
39
|
+
connectedCallback(): void;
|
|
40
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
41
|
+
private renderDone;
|
|
42
|
+
private renderForm;
|
|
43
|
+
private renderNewAccount;
|
|
44
|
+
private fieldValue;
|
|
45
|
+
submit(): Promise<void>;
|
|
46
|
+
private accept;
|
|
47
|
+
updated(changed: any): void;
|
|
48
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { __decorate, __metadata } from "tslib";
|
|
2
|
+
import '@material/web/icon/icon.js';
|
|
3
|
+
import '@material/web/button/elevated-button.js';
|
|
4
|
+
import '@material/web/textfield/filled-text-field.js';
|
|
5
|
+
import '@operato/i18n/ox-i18n.js';
|
|
6
|
+
import '@operato/i18n/ox-i18n-selector.js';
|
|
7
|
+
import '@operato/layout/ox-snack-bar.js';
|
|
8
|
+
import { css, html, nothing } from 'lit';
|
|
9
|
+
import { property } from 'lit/decorators.js';
|
|
10
|
+
import { i18next } from '@operato/i18n';
|
|
11
|
+
import { AUTH_STYLE_SIGN } from '../auth-style-sign.js';
|
|
12
|
+
import { EntryControlSizeStyles } from '../entry-control-size-styles.js';
|
|
13
|
+
import { AbstractAuthPage } from './abstract-auth-page.js';
|
|
14
|
+
import { readInvitationToken } from '../utils/invitation-token.js';
|
|
15
|
+
/**
|
|
16
|
+
* Accepting an invitation.
|
|
17
|
+
*
|
|
18
|
+
* ── Why the token is not in the address ─────────────────────────────────────
|
|
19
|
+
* The link is `…/accept-invitation#<token>`, and a browser does not send what follows the `#`.
|
|
20
|
+
* So the request that loads this page carries nothing, and the token stays out of server access
|
|
21
|
+
* logs, proxy logs and `Referer`. This page reads it from the fragment and then clears it from
|
|
22
|
+
* the address bar, so it does not sit in the browser's history either.
|
|
23
|
+
*
|
|
24
|
+
* The one place it can still travel is the mail being forwarded, and nothing about where the
|
|
25
|
+
* token sits can prevent that. So the address it was sent to is asked for as well: holding the
|
|
26
|
+
* link is not enough on its own.
|
|
27
|
+
*
|
|
28
|
+
* ── Why the password fields only appear after a try ─────────────────────────
|
|
29
|
+
* Whether an address already has an account is not this page's to announce. It sends what it
|
|
30
|
+
* has, and the server answers `invitation-needs-a-password` when it needs one — so the extra
|
|
31
|
+
* fields appear for the person who is actually creating an account, and nobody learns anything
|
|
32
|
+
* by loading the page.
|
|
33
|
+
*/
|
|
34
|
+
export class AcceptInvitation extends AbstractAuthPage {
|
|
35
|
+
static { this.styles = [
|
|
36
|
+
EntryControlSizeStyles,
|
|
37
|
+
css `
|
|
38
|
+
:host {
|
|
39
|
+
position: relative;
|
|
40
|
+
overflow: hidden;
|
|
41
|
+
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-direction: row;
|
|
44
|
+
|
|
45
|
+
width: 100vw;
|
|
46
|
+
height: 100vh;
|
|
47
|
+
height: 100dvh;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.note {
|
|
51
|
+
margin: 0 0 var(--spacing-medium) 0;
|
|
52
|
+
font-size: 0.85rem;
|
|
53
|
+
line-height: 1.5;
|
|
54
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
55
|
+
}
|
|
56
|
+
`,
|
|
57
|
+
AUTH_STYLE_SIGN
|
|
58
|
+
]; }
|
|
59
|
+
get pageName() {
|
|
60
|
+
return 'accept invitation';
|
|
61
|
+
}
|
|
62
|
+
get actionUrl() {
|
|
63
|
+
return '/accept-invitation';
|
|
64
|
+
}
|
|
65
|
+
get title() {
|
|
66
|
+
return 'accept invitation';
|
|
67
|
+
}
|
|
68
|
+
connectedCallback() {
|
|
69
|
+
super.connectedCallback();
|
|
70
|
+
const { token, clean } = readInvitationToken(window.location.href);
|
|
71
|
+
this.token = token;
|
|
72
|
+
/*
|
|
73
|
+
* Taken out of the address bar and out of this history entry. `replaceState` rather than
|
|
74
|
+
* `pushState`: going back should not put the token back on screen.
|
|
75
|
+
*/
|
|
76
|
+
if (clean && clean !== window.location.href) {
|
|
77
|
+
window.history.replaceState(null, '', clean);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
render() {
|
|
81
|
+
const { icon, title, description } = this.applicationMeta;
|
|
82
|
+
const { disableUserFavoredLanguage, languages } = this.data || {};
|
|
83
|
+
return html `
|
|
84
|
+
<div class="wrap">
|
|
85
|
+
<div class="auth-brand">
|
|
86
|
+
<img src=${icon} />
|
|
87
|
+
<strong class="name">${title}</strong>
|
|
88
|
+
<span class="welcome-msg">${description}</span>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div class="auth-form">
|
|
92
|
+
<h3><ox-i18n msgid="title.accept invitation"></ox-i18n></h3>
|
|
93
|
+
|
|
94
|
+
${this.joined ? this.renderDone() : this.renderForm(disableUserFavoredLanguage, languages)}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<ox-snack-bar id="snackbar" level="error" .message=${this.message}></ox-snack-bar>
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
101
|
+
renderDone() {
|
|
102
|
+
return html `
|
|
103
|
+
<p class="note">${i18next.t('text.you have joined. sign in to continue')}</p>
|
|
104
|
+
<md-elevated-button @click=${() => (window.location.href = '/auth/signin')}>
|
|
105
|
+
<ox-i18n msgid="button.sign in"></ox-i18n>
|
|
106
|
+
</md-elevated-button>
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
renderForm(disableUserFavoredLanguage, languages) {
|
|
110
|
+
return html `
|
|
111
|
+
<form id="form" @keypress=${(e) => e.key === 'Enter' && this.accept()}>
|
|
112
|
+
<p class="note">${i18next.t('text.confirm the address this invitation was sent to')}</p>
|
|
113
|
+
|
|
114
|
+
<div class="field">
|
|
115
|
+
<md-filled-text-field
|
|
116
|
+
name="email"
|
|
117
|
+
type="email"
|
|
118
|
+
label=${String(i18next.t('field.email'))}
|
|
119
|
+
autocomplete="email"
|
|
120
|
+
required
|
|
121
|
+
><md-icon slot="leading-icon">mail</md-icon></md-filled-text-field
|
|
122
|
+
>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
${this.token
|
|
126
|
+
? nothing
|
|
127
|
+
: html `
|
|
128
|
+
<div class="field">
|
|
129
|
+
<md-filled-text-field
|
|
130
|
+
name="token"
|
|
131
|
+
label=${String(i18next.t('label.invitation code'))}
|
|
132
|
+
supporting-text=${String(i18next.t('text.the code is printed in the invitation email'))}
|
|
133
|
+
autocomplete="off"
|
|
134
|
+
required
|
|
135
|
+
><md-icon slot="leading-icon">key</md-icon></md-filled-text-field
|
|
136
|
+
>
|
|
137
|
+
</div>
|
|
138
|
+
`}
|
|
139
|
+
${this.needsPassword ? this.renderNewAccount() : nothing}
|
|
140
|
+
|
|
141
|
+
<md-elevated-button id="submit-button" type="button" ?disabled=${this.busy} @click=${() => this.accept()}>
|
|
142
|
+
<ox-i18n msgid="button.accept invitation"></ox-i18n>
|
|
143
|
+
</md-elevated-button>
|
|
144
|
+
|
|
145
|
+
${!disableUserFavoredLanguage
|
|
146
|
+
? html `<div id="locale-area">
|
|
147
|
+
<label for="locale-selector"><md-icon>language</md-icon></label>
|
|
148
|
+
<ox-i18n-selector
|
|
149
|
+
id="locale-selector"
|
|
150
|
+
value=${i18next.language || 'en-US'}
|
|
151
|
+
.languages=${languages}
|
|
152
|
+
@change=${(e) => e.detail && i18next.changeLanguage(e.detail)}
|
|
153
|
+
></ox-i18n-selector>
|
|
154
|
+
</div>`
|
|
155
|
+
: nothing}
|
|
156
|
+
</form>
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
159
|
+
renderNewAccount() {
|
|
160
|
+
return html `
|
|
161
|
+
<p class="note">${i18next.t('text.choose a password to finish joining')}</p>
|
|
162
|
+
|
|
163
|
+
<div class="field">
|
|
164
|
+
<md-filled-text-field name="name" label=${String(i18next.t('field.name'))} autocomplete="name"
|
|
165
|
+
><md-icon slot="leading-icon">person</md-icon></md-filled-text-field
|
|
166
|
+
>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<div class="field">
|
|
170
|
+
<md-filled-text-field
|
|
171
|
+
name="password"
|
|
172
|
+
type="password"
|
|
173
|
+
label=${String(i18next.t('label.password'))}
|
|
174
|
+
autocomplete="new-password"
|
|
175
|
+
required
|
|
176
|
+
><md-icon slot="leading-icon">password</md-icon></md-filled-text-field
|
|
177
|
+
>
|
|
178
|
+
</div>
|
|
179
|
+
`;
|
|
180
|
+
}
|
|
181
|
+
/*
|
|
182
|
+
* ⚠ Not called `valueOf`. Overriding `Object.prototype.valueOf` with a different signature
|
|
183
|
+
* makes the class type unassignable to `Object`, and every `@property` decorator on it then
|
|
184
|
+
* fails to resolve — with an error that talks about decorators and never mentions the method.
|
|
185
|
+
*/
|
|
186
|
+
fieldValue(name) {
|
|
187
|
+
return this.renderRoot.querySelector(`[name="${name}"]`)?.value?.trim() || '';
|
|
188
|
+
}
|
|
189
|
+
/*
|
|
190
|
+
* The base class calls this to commit the page. It does not submit the form element: the
|
|
191
|
+
* answer here is read rather than navigated to, because a refusal can mean "now ask for a
|
|
192
|
+
* password" instead of "stop".
|
|
193
|
+
*/
|
|
194
|
+
async submit() {
|
|
195
|
+
await this.accept();
|
|
196
|
+
}
|
|
197
|
+
async accept() {
|
|
198
|
+
if (this.busy) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
this.busy = true;
|
|
202
|
+
this.message = '';
|
|
203
|
+
try {
|
|
204
|
+
const response = await fetch('/accept-invitation', {
|
|
205
|
+
method: 'POST',
|
|
206
|
+
headers: { 'Content-Type': 'application/json' },
|
|
207
|
+
body: JSON.stringify({
|
|
208
|
+
token: this.token || this.fieldValue('token'),
|
|
209
|
+
email: this.fieldValue('email'),
|
|
210
|
+
name: this.fieldValue('name'),
|
|
211
|
+
password: this.fieldValue('password')
|
|
212
|
+
})
|
|
213
|
+
});
|
|
214
|
+
if (response.ok) {
|
|
215
|
+
this.joined = true;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const refusal = await response.json().catch(() => ({}));
|
|
219
|
+
/*
|
|
220
|
+
* The one refusal that is not the end of the attempt — it means this person has no account
|
|
221
|
+
* yet, which is exactly who this page is for.
|
|
222
|
+
*/
|
|
223
|
+
if (refusal?.refusal?.code === 'invitation-needs-a-password' || refusal?.code === 'invitation-needs-a-password') {
|
|
224
|
+
this.needsPassword = true;
|
|
225
|
+
this.message = String(i18next.t('text.choose a password to finish joining'));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
this.message = refusal?.message || String(i18next.t('text.this invitation cannot be used'));
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
this.message = String(i18next.t('text.this invitation cannot be used'));
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
this.busy = false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
updated(changed) {
|
|
238
|
+
super.updated(changed);
|
|
239
|
+
if (changed.has('message')) {
|
|
240
|
+
if (!this.message) {
|
|
241
|
+
this.hideSnackbar();
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
this.showSnackbar({ timer: -1 });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
__decorate([
|
|
250
|
+
property({ type: String }),
|
|
251
|
+
__metadata("design:type", String)
|
|
252
|
+
], AcceptInvitation.prototype, "token", void 0);
|
|
253
|
+
__decorate([
|
|
254
|
+
property({ type: Boolean }),
|
|
255
|
+
__metadata("design:type", Boolean)
|
|
256
|
+
], AcceptInvitation.prototype, "needsPassword", void 0);
|
|
257
|
+
__decorate([
|
|
258
|
+
property({ type: Boolean }),
|
|
259
|
+
__metadata("design:type", Boolean)
|
|
260
|
+
], AcceptInvitation.prototype, "joined", void 0);
|
|
261
|
+
__decorate([
|
|
262
|
+
property({ type: Boolean }),
|
|
263
|
+
__metadata("design:type", Boolean)
|
|
264
|
+
], AcceptInvitation.prototype, "busy", void 0);
|
|
265
|
+
//# sourceMappingURL=accept-invitation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accept-invitation.js","sourceRoot":"","sources":["../../client/components/accept-invitation.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@material/web/button/elevated-button.js'\nimport '@material/web/textfield/filled-text-field.js'\n\nimport '@operato/i18n/ox-i18n.js'\nimport '@operato/i18n/ox-i18n-selector.js'\nimport '@operato/layout/ox-snack-bar.js'\n\nimport { css, html, nothing } from 'lit'\nimport { property } from 'lit/decorators.js'\n\nimport { i18next } from '@operato/i18n'\n\nimport { AUTH_STYLE_SIGN } from '../auth-style-sign.js'\nimport { EntryControlSizeStyles } from '../entry-control-size-styles.js'\nimport { AbstractAuthPage } from './abstract-auth-page.js'\nimport { readInvitationToken } from '../utils/invitation-token.js'\n\n/**\n * Accepting an invitation.\n *\n * ── Why the token is not in the address ─────────────────────────────────────\n * The link is `…/accept-invitation#<token>`, and a browser does not send what follows the `#`.\n * So the request that loads this page carries nothing, and the token stays out of server access\n * logs, proxy logs and `Referer`. This page reads it from the fragment and then clears it from\n * the address bar, so it does not sit in the browser's history either.\n *\n * The one place it can still travel is the mail being forwarded, and nothing about where the\n * token sits can prevent that. So the address it was sent to is asked for as well: holding the\n * link is not enough on its own.\n *\n * ── Why the password fields only appear after a try ─────────────────────────\n * Whether an address already has an account is not this page's to announce. It sends what it\n * has, and the server answers `invitation-needs-a-password` when it needs one — so the extra\n * fields appear for the person who is actually creating an account, and nobody learns anything\n * by loading the page.\n */\nexport class AcceptInvitation extends AbstractAuthPage {\n static styles = [\n EntryControlSizeStyles,\n css`\n :host {\n position: relative;\n overflow: hidden;\n\n display: flex;\n flex-direction: row;\n\n width: 100vw;\n height: 100vh;\n height: 100dvh;\n }\n\n .note {\n margin: 0 0 var(--spacing-medium) 0;\n font-size: 0.85rem;\n line-height: 1.5;\n color: var(--md-sys-color-on-surface-variant);\n }\n `,\n AUTH_STYLE_SIGN\n ]\n\n get pageName() {\n return 'accept invitation'\n }\n\n get actionUrl() {\n return '/accept-invitation'\n }\n\n get title() {\n return 'accept invitation'\n }\n\n /** Read from the fragment on connect. Empty when the link was opened without one. */\n @property({ type: String }) token?: string\n\n /** The server asked for a password, so this person is creating an account. */\n @property({ type: Boolean }) needsPassword?: boolean\n\n /** Joined. The page stops being a form and says where to go. */\n @property({ type: Boolean }) joined?: boolean\n\n @property({ type: Boolean }) busy?: boolean\n\n connectedCallback() {\n super.connectedCallback()\n\n const { token, clean } = readInvitationToken(window.location.href)\n\n this.token = token\n\n /*\n * Taken out of the address bar and out of this history entry. `replaceState` rather than\n * `pushState`: going back should not put the token back on screen.\n */\n if (clean && clean !== window.location.href) {\n window.history.replaceState(null, '', clean)\n }\n }\n\n render() {\n const { icon, title, description } = this.applicationMeta\n const { disableUserFavoredLanguage, languages } = this.data || {}\n\n return html`\n <div class=\"wrap\">\n <div class=\"auth-brand\">\n <img src=${icon} />\n <strong class=\"name\">${title}</strong>\n <span class=\"welcome-msg\">${description}</span>\n </div>\n\n <div class=\"auth-form\">\n <h3><ox-i18n msgid=\"title.accept invitation\"></ox-i18n></h3>\n\n ${this.joined ? this.renderDone() : this.renderForm(disableUserFavoredLanguage, languages)}\n </div>\n </div>\n\n <ox-snack-bar id=\"snackbar\" level=\"error\" .message=${this.message}></ox-snack-bar>\n `\n }\n\n private renderDone() {\n return html`\n <p class=\"note\">${i18next.t('text.you have joined. sign in to continue')}</p>\n <md-elevated-button @click=${() => (window.location.href = '/auth/signin')}>\n <ox-i18n msgid=\"button.sign in\"></ox-i18n>\n </md-elevated-button>\n `\n }\n\n private renderForm(disableUserFavoredLanguage: boolean, languages: any) {\n return html`\n <form id=\"form\" @keypress=${(e: KeyboardEvent) => e.key === 'Enter' && this.accept()}>\n <p class=\"note\">${i18next.t('text.confirm the address this invitation was sent to')}</p>\n\n <div class=\"field\">\n <md-filled-text-field\n name=\"email\"\n type=\"email\"\n label=${String(i18next.t('field.email'))}\n autocomplete=\"email\"\n required\n ><md-icon slot=\"leading-icon\">mail</md-icon></md-filled-text-field\n >\n </div>\n\n ${this.token\n ? nothing\n : html`\n <div class=\"field\">\n <md-filled-text-field\n name=\"token\"\n label=${String(i18next.t('label.invitation code'))}\n supporting-text=${String(i18next.t('text.the code is printed in the invitation email'))}\n autocomplete=\"off\"\n required\n ><md-icon slot=\"leading-icon\">key</md-icon></md-filled-text-field\n >\n </div>\n `}\n ${this.needsPassword ? this.renderNewAccount() : nothing}\n\n <md-elevated-button id=\"submit-button\" type=\"button\" ?disabled=${this.busy} @click=${() => this.accept()}>\n <ox-i18n msgid=\"button.accept invitation\"></ox-i18n>\n </md-elevated-button>\n\n ${!disableUserFavoredLanguage\n ? html`<div id=\"locale-area\">\n <label for=\"locale-selector\"><md-icon>language</md-icon></label>\n <ox-i18n-selector\n id=\"locale-selector\"\n value=${i18next.language || 'en-US'}\n .languages=${languages}\n @change=${(e: CustomEvent) => e.detail && i18next.changeLanguage(e.detail as any)}\n ></ox-i18n-selector>\n </div>`\n : nothing}\n </form>\n `\n }\n\n private renderNewAccount() {\n return html`\n <p class=\"note\">${i18next.t('text.choose a password to finish joining')}</p>\n\n <div class=\"field\">\n <md-filled-text-field name=\"name\" label=${String(i18next.t('field.name'))} autocomplete=\"name\"\n ><md-icon slot=\"leading-icon\">person</md-icon></md-filled-text-field\n >\n </div>\n\n <div class=\"field\">\n <md-filled-text-field\n name=\"password\"\n type=\"password\"\n label=${String(i18next.t('label.password'))}\n autocomplete=\"new-password\"\n required\n ><md-icon slot=\"leading-icon\">password</md-icon></md-filled-text-field\n >\n </div>\n `\n }\n\n /*\n * ⚠ Not called `valueOf`. Overriding `Object.prototype.valueOf` with a different signature\n * makes the class type unassignable to `Object`, and every `@property` decorator on it then\n * fails to resolve — with an error that talks about decorators and never mentions the method.\n */\n private fieldValue(name: string): string {\n return (this.renderRoot.querySelector(`[name=\"${name}\"]`) as any)?.value?.trim() || ''\n }\n\n /*\n * The base class calls this to commit the page. It does not submit the form element: the\n * answer here is read rather than navigated to, because a refusal can mean \"now ask for a\n * password\" instead of \"stop\".\n */\n async submit() {\n await this.accept()\n }\n\n private async accept() {\n if (this.busy) {\n return\n }\n\n this.busy = true\n this.message = ''\n\n try {\n const response = await fetch('/accept-invitation', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n token: this.token || this.fieldValue('token'),\n email: this.fieldValue('email'),\n name: this.fieldValue('name'),\n password: this.fieldValue('password')\n })\n })\n\n if (response.ok) {\n this.joined = true\n return\n }\n\n const refusal = await response.json().catch(() => ({}))\n\n /*\n * The one refusal that is not the end of the attempt — it means this person has no account\n * yet, which is exactly who this page is for.\n */\n if (refusal?.refusal?.code === 'invitation-needs-a-password' || refusal?.code === 'invitation-needs-a-password') {\n this.needsPassword = true\n this.message = String(i18next.t('text.choose a password to finish joining'))\n return\n }\n\n this.message = refusal?.message || String(i18next.t('text.this invitation cannot be used'))\n } catch (e) {\n this.message = String(i18next.t('text.this invitation cannot be used'))\n } finally {\n this.busy = false\n }\n }\n\n updated(changed: any) {\n super.updated(changed)\n\n if (changed.has('message')) {\n if (!this.message) {\n this.hideSnackbar()\n } else {\n this.showSnackbar({ timer: -1 })\n }\n }\n }\n}\n"]}
|
|
@@ -17,17 +17,20 @@ export declare class ApplianceEditor extends ApplianceEditor_base {
|
|
|
17
17
|
private failed;
|
|
18
18
|
/** The single-record read did not answer. Distinct from answering with nothing. */
|
|
19
19
|
private loadFailed;
|
|
20
|
+
private revocable;
|
|
20
21
|
private get editing();
|
|
21
22
|
render(): import("lit-html").TemplateResult<1>;
|
|
22
23
|
private renderCredential;
|
|
23
24
|
private fieldError;
|
|
24
25
|
connectedCallback(): Promise<void>;
|
|
26
|
+
private fetchRevocable;
|
|
25
27
|
fetchAppliance(): Promise<void>;
|
|
26
28
|
private cancel;
|
|
27
29
|
save(e: Event): Promise<void>;
|
|
28
30
|
private createAppliance;
|
|
29
31
|
private updateAppliance;
|
|
30
32
|
private generateSecret;
|
|
33
|
+
private revokeCredential;
|
|
31
34
|
private deleteAppliance;
|
|
32
35
|
/** The list has to refetch, and the popup is done. */
|
|
33
36
|
private saved;
|
|
@@ -12,6 +12,7 @@ import { OxPrompt } from '@operato/popup/ox-prompt.js';
|
|
|
12
12
|
import { CommonHeaderStyles, CommonPopupStyles, ScrollbarStyles } from '@operato/styles';
|
|
13
13
|
import { parseJwt, sleep } from '@operato/utils';
|
|
14
14
|
import './secret-field.js';
|
|
15
|
+
import { readRevocable, showsRevokeButton, showsRevokedNote } from '../utils/credential-panel-state.js';
|
|
15
16
|
import { changedValues, filledValues, missingRequired } from '../utils/form-values.js';
|
|
16
17
|
/**
|
|
17
18
|
* Registering and editing an appliance, in a popup.
|
|
@@ -44,6 +45,7 @@ const APPLIANCE_FIELDS = `
|
|
|
44
45
|
model
|
|
45
46
|
netmask
|
|
46
47
|
accessToken
|
|
48
|
+
credentialRevoked
|
|
47
49
|
`;
|
|
48
50
|
let ApplianceEditor = class ApplianceEditor extends localize(i18next)(LitElement) {
|
|
49
51
|
constructor() {
|
|
@@ -56,6 +58,15 @@ let ApplianceEditor = class ApplianceEditor extends localize(i18next)(LitElement
|
|
|
56
58
|
this.failed = false;
|
|
57
59
|
/** The single-record read did not answer. Distinct from answering with nothing. */
|
|
58
60
|
this.loadFailed = false;
|
|
61
|
+
/*
|
|
62
|
+
* Does revoking work on this installation, for this reader?
|
|
63
|
+
*
|
|
64
|
+
* Revocation is behind a configuration switch, and the server answers one question that folds
|
|
65
|
+
* the switch and the reader's privilege together — see `applianceCredentialRevocable`. False
|
|
66
|
+
* until that answer comes back, so a slow or refused read shows no button rather than one that
|
|
67
|
+
* refuses when pressed.
|
|
68
|
+
*/
|
|
69
|
+
this.revocable = false;
|
|
59
70
|
}
|
|
60
71
|
/*
|
|
61
72
|
* ── Where the scrolling happens ─────────────────────────────────────────
|
|
@@ -164,6 +175,15 @@ let ApplianceEditor = class ApplianceEditor extends localize(i18next)(LitElement
|
|
|
164
175
|
margin-right: auto;
|
|
165
176
|
}
|
|
166
177
|
|
|
178
|
+
/* taking a credential away is not the same kind of act as issuing one. */
|
|
179
|
+
md-text-button.danger {
|
|
180
|
+
--md-text-button-label-text-color: var(--md-sys-color-error);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.note.revoked {
|
|
184
|
+
color: var(--md-sys-color-error);
|
|
185
|
+
}
|
|
186
|
+
|
|
167
187
|
@media screen and (max-width: 480px) {
|
|
168
188
|
[field] {
|
|
169
189
|
grid-column: span 2;
|
|
@@ -274,6 +294,15 @@ let ApplianceEditor = class ApplianceEditor extends localize(i18next)(LitElement
|
|
|
274
294
|
/*
|
|
275
295
|
* The credential panel only exists once the appliance does — there is no token to show
|
|
276
296
|
* before it is created, and the list query deliberately never asks for one.
|
|
297
|
+
*
|
|
298
|
+
* ── Why a revoked appliance says so ─────────────────────────────────────
|
|
299
|
+
* Revoking clears the token, so without a word for it the panel looks exactly like one whose
|
|
300
|
+
* credential was never issued. Those are opposite situations: one is waiting to be set up,
|
|
301
|
+
* the other has been deliberately shut off, and only one of them wants somebody to act.
|
|
302
|
+
*
|
|
303
|
+
* ── Why the revoke button is not always here ────────────────────────────
|
|
304
|
+
* It appears only where the installation turned revocation on and the reader may use it. See
|
|
305
|
+
* the note on `revocable`.
|
|
277
306
|
*/
|
|
278
307
|
renderCredential(appliance, exp) {
|
|
279
308
|
return html `
|
|
@@ -282,6 +311,9 @@ let ApplianceEditor = class ApplianceEditor extends localize(i18next)(LitElement
|
|
|
282
311
|
<div field>
|
|
283
312
|
<label for="access-token">${i18next.t('label.access token')}</label>
|
|
284
313
|
<secret-field id="access-token" .value=${appliance.accessToken || ''}></secret-field>
|
|
314
|
+
${showsRevokedNote(appliance)
|
|
315
|
+
? html `<div class="note revoked">${i18next.t('text.this credential has been revoked')}</div>`
|
|
316
|
+
: ''}
|
|
285
317
|
${exp
|
|
286
318
|
? html `<div class="note">
|
|
287
319
|
${i18next.t('text.token expiry time')} ${new Date(exp * 1000).toLocaleString()} :
|
|
@@ -292,6 +324,13 @@ let ApplianceEditor = class ApplianceEditor extends localize(i18next)(LitElement
|
|
|
292
324
|
<md-text-button @click=${this.generateSecret.bind(this)}>
|
|
293
325
|
${i18next.t('button.generate new access token')}
|
|
294
326
|
</md-text-button>
|
|
327
|
+
${showsRevokeButton(this.revocable, appliance)
|
|
328
|
+
? html `
|
|
329
|
+
<md-text-button class="danger" @click=${this.revokeCredential.bind(this)}>
|
|
330
|
+
${i18next.t('button.revoke credential')}
|
|
331
|
+
</md-text-button>
|
|
332
|
+
`
|
|
333
|
+
: ''}
|
|
295
334
|
</div>
|
|
296
335
|
</div>
|
|
297
336
|
</fieldset>
|
|
@@ -303,9 +342,26 @@ let ApplianceEditor = class ApplianceEditor extends localize(i18next)(LitElement
|
|
|
303
342
|
async connectedCallback() {
|
|
304
343
|
super.connectedCallback();
|
|
305
344
|
if (this.editing) {
|
|
306
|
-
await this.fetchAppliance();
|
|
345
|
+
await Promise.all([this.fetchAppliance(), this.fetchRevocable()]);
|
|
307
346
|
}
|
|
308
347
|
}
|
|
348
|
+
/*
|
|
349
|
+
* Whether the revoke button exists at all.
|
|
350
|
+
*
|
|
351
|
+
* A refusal arrives as `response.errors` rather than a throw, and both a refusal and a `false`
|
|
352
|
+
* mean the same thing to this screen: no button. So there is nothing to report here — the
|
|
353
|
+
* absence of the button is the whole answer.
|
|
354
|
+
*/
|
|
355
|
+
async fetchRevocable() {
|
|
356
|
+
const response = await client.query({
|
|
357
|
+
query: gql `
|
|
358
|
+
query {
|
|
359
|
+
applianceCredentialRevocable
|
|
360
|
+
}
|
|
361
|
+
`
|
|
362
|
+
});
|
|
363
|
+
this.revocable = readRevocable(response);
|
|
364
|
+
}
|
|
309
365
|
async fetchAppliance() {
|
|
310
366
|
const response = await client.query({
|
|
311
367
|
query: gql `
|
|
@@ -420,6 +476,47 @@ let ApplianceEditor = class ApplianceEditor extends localize(i18next)(LitElement
|
|
|
420
476
|
this.appliance = response.data.generateApplianceSecret;
|
|
421
477
|
this.dispatchEvent(new CustomEvent('appliance-changed', { bubbles: true, composed: true }));
|
|
422
478
|
}
|
|
479
|
+
/*
|
|
480
|
+
* Take the credential away and leave the appliance where it is.
|
|
481
|
+
*
|
|
482
|
+
* ── Why this is not the delete button ───────────────────────────────────
|
|
483
|
+
* Deleting takes the appliance and its history with it. This is for a device that is lost,
|
|
484
|
+
* decommissioned or suspect and whose record still has to stand — the row keeps pointing at
|
|
485
|
+
* the same identity, and issuing a new secret later picks that identity back up.
|
|
486
|
+
*
|
|
487
|
+
* ── Why the popup stays open ────────────────────────────────────────────
|
|
488
|
+
* The panel has to show the result: the token is gone and the appliance now says it was
|
|
489
|
+
* revoked. Closing would leave the reader guessing whether the press landed, and the list
|
|
490
|
+
* behind it is refetched either way by the event below.
|
|
491
|
+
*/
|
|
492
|
+
async revokeCredential(e) {
|
|
493
|
+
e.preventDefault();
|
|
494
|
+
const confirmed = await OxPrompt.open({
|
|
495
|
+
type: 'warning',
|
|
496
|
+
title: i18next.t('button.revoke credential'),
|
|
497
|
+
text: i18next.t('text.revoking stops every token this appliance holds'),
|
|
498
|
+
confirmButton: { text: i18next.t('button.revoke credential') },
|
|
499
|
+
cancelButton: { text: i18next.t('button.cancel') }
|
|
500
|
+
});
|
|
501
|
+
if (!confirmed) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const response = await client.mutate({
|
|
505
|
+
mutation: gql `
|
|
506
|
+
mutation ($id: String!) {
|
|
507
|
+
revokeApplianceCredential(id: $id) { ${APPLIANCE_FIELDS} }
|
|
508
|
+
}
|
|
509
|
+
`,
|
|
510
|
+
variables: { id: this.applianceId }
|
|
511
|
+
});
|
|
512
|
+
if (response.errors) {
|
|
513
|
+
this.failed = true;
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
this.failed = false;
|
|
517
|
+
this.appliance = response.data.revokeApplianceCredential;
|
|
518
|
+
this.dispatchEvent(new CustomEvent('appliance-changed', { bubbles: true, composed: true }));
|
|
519
|
+
}
|
|
423
520
|
async deleteAppliance(e) {
|
|
424
521
|
e.preventDefault();
|
|
425
522
|
const confirmed = await OxPrompt.open({
|
|
@@ -506,6 +603,10 @@ __decorate([
|
|
|
506
603
|
state(),
|
|
507
604
|
__metadata("design:type", Boolean)
|
|
508
605
|
], ApplianceEditor.prototype, "loadFailed", void 0);
|
|
606
|
+
__decorate([
|
|
607
|
+
state(),
|
|
608
|
+
__metadata("design:type", Boolean)
|
|
609
|
+
], ApplianceEditor.prototype, "revocable", void 0);
|
|
509
610
|
ApplianceEditor = __decorate([
|
|
510
611
|
customElement('appliance-editor')
|
|
511
612
|
], ApplianceEditor);
|