@things-factory/auth-ui 10.1.215 → 10.1.219
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 +18 -14
- package/dist-client/bootstrap.js.map +1 -1
- package/dist-client/components/abstract-password-reset.d.ts +5 -0
- package/dist-client/components/abstract-password-reset.js +20 -0
- package/dist-client/components/abstract-password-reset.js.map +1 -1
- package/dist-client/components/ownership-transfer-popup.d.ts +3 -1
- package/dist-client/components/ownership-transfer-popup.js +38 -41
- package/dist-client/components/ownership-transfer-popup.js.map +1 -1
- package/dist-client/components/password-setup-link-popup.d.ts +46 -0
- package/dist-client/components/password-setup-link-popup.js +254 -0
- package/dist-client/components/password-setup-link-popup.js.map +1 -0
- package/dist-client/components/user-detail.d.ts +20 -30
- package/dist-client/components/user-detail.js +114 -186
- package/dist-client/components/user-detail.js.map +1 -1
- package/dist-client/components/user-invite.d.ts +18 -25
- package/dist-client/components/user-invite.js +157 -181
- package/dist-client/components/user-invite.js.map +1 -1
- package/dist-client/components/user-popups-standard.test.js +69 -0
- package/dist-client/components/user-popups-standard.test.js.map +1 -0
- package/dist-client/components/user-role-editor.js +5 -26
- package/dist-client/components/user-role-editor.js.map +1 -1
- package/dist-client/components/user-status-renderer.js +4 -0
- package/dist-client/components/user-status-renderer.js.map +1 -1
- package/dist-client/entries/auth/reset-password.d.ts +1 -0
- package/dist-client/entries/auth/reset-password.js +5 -1
- package/dist-client/entries/auth/reset-password.js.map +1 -1
- package/dist-client/index.js +10 -28
- package/dist-client/index.js.map +1 -1
- package/dist-client/pages/user/user-list-page.d.ts +5 -1
- package/dist-client/pages/user/user-list-page.js +14 -5
- package/dist-client/pages/user/user-list-page.js.map +1 -1
- package/dist-client/route.js +0 -3
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/link-token.d.ts +3 -3
- package/dist-client/utils/link-token.js +4 -4
- package/dist-client/utils/link-token.js.map +1 -1
- package/dist-client/utils/link-token.test.js +2 -2
- package/dist-client/utils/link-token.test.js.map +1 -1
- package/package.json +6 -5
- package/things-factory.config.js +0 -1
- package/translations/en.json +30 -20
- package/translations/es.json +30 -20
- package/translations/ja.json +30 -20
- package/translations/ko.json +30 -20
- package/translations/ms.json +30 -20
- package/translations/zh.json +30 -20
- package/dist-client/components/accept-invitation.d.ts +0 -47
- package/dist-client/components/accept-invitation.js +0 -258
- package/dist-client/components/accept-invitation.js.map +0 -1
- package/dist-client/entries/auth/accept-invitation.d.ts +0 -3
- package/dist-client/entries/auth/accept-invitation.js +0 -10
- package/dist-client/entries/auth/accept-invitation.js.map +0 -1
- package/dist-client/pages/invitation/invitation-list-query.test.js +0 -69
- package/dist-client/pages/invitation/invitation-list-query.test.js.map +0 -1
- package/dist-client/pages/invitation/invitations.d.ts +0 -63
- package/dist-client/pages/invitation/invitations.js +0 -284
- package/dist-client/pages/invitation/invitations.js.map +0 -1
- /package/dist-client/{pages/invitation/invitation-list-query.test.d.ts → components/user-popups-standard.test.d.ts} +0 -0
package/translations/zh.json
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
{
|
|
2
|
-
"button.accept invitation": "接受邀请",
|
|
3
2
|
"button.activate": "激活",
|
|
4
3
|
"button.add passkey": "添加通行密钥",
|
|
5
4
|
"button.close": "关闭",
|
|
@@ -36,7 +35,7 @@
|
|
|
36
35
|
"button.stand it here": "在此建立",
|
|
37
36
|
"button.submit": "提交",
|
|
38
37
|
"button.terminate contract": "终止合同",
|
|
39
|
-
"button.transfer owner": "
|
|
38
|
+
"button.transfer owner": "指定为$t(term.domain)所有者",
|
|
40
39
|
"button.withdraw": "撤回邀请",
|
|
41
40
|
"error.cannot_modify_system_subdomain": "无法修改'system'子域。",
|
|
42
41
|
"error.domain not allowed": "此域名对此用户不可用",
|
|
@@ -119,8 +118,6 @@
|
|
|
119
118
|
"label.draft": "草稿",
|
|
120
119
|
"label.email": "电子邮件",
|
|
121
120
|
"label.inactive-user": "非活动用户",
|
|
122
|
-
"label.invitation code": "邀请码",
|
|
123
|
-
"label.invitations": "邀请",
|
|
124
121
|
"label.invite customer": "邀请客户或合作伙伴",
|
|
125
122
|
"label.invite user": "邀请用户",
|
|
126
123
|
"label.language": "语言",
|
|
@@ -137,7 +134,6 @@
|
|
|
137
134
|
"label.search_domain": "搜索$t(term.domain)",
|
|
138
135
|
"label.select_domain": "选择$t(term.domain)",
|
|
139
136
|
"label.send activation email": "发送激活电子邮件",
|
|
140
|
-
"label.send invitation": "发送邀请邮件",
|
|
141
137
|
"label.signin with": "通过 {title} 登录",
|
|
142
138
|
"label.state": "状态",
|
|
143
139
|
"label.subject": "主题",
|
|
@@ -163,7 +159,7 @@
|
|
|
163
159
|
"text.are you sure you want to delete x": "删除 {x}?此操作无法撤销。",
|
|
164
160
|
"text.are_you_sure_to_delete_x": "您确定是否删除{x}?",
|
|
165
161
|
"text.are_you_sure_to_terminate_contract": "您确定要终止合同吗?",
|
|
166
|
-
"text.are_you_sure_to_transfer_owner": "
|
|
162
|
+
"text.are_you_sure_to_transfer_owner": "确定将此用户指定为$t(term.domain)所有者吗?",
|
|
167
163
|
"text.are_you_sure_to_x": "您确定是否{x}?",
|
|
168
164
|
"text.are_you_sure_to_x_user": "您确定要删除用户{x}?",
|
|
169
165
|
"text.attribute management": "属性管理",
|
|
@@ -175,14 +171,31 @@
|
|
|
175
171
|
"text.bound applications": "已绑定应用程序",
|
|
176
172
|
"text.cannot_delete": "无法删除",
|
|
177
173
|
"text.change password": "修改密码",
|
|
178
|
-
"text.choose a password to finish joining": "设置密码后即可加入。",
|
|
179
174
|
"text.click login history": "如果您想查看最近的登录记录,请点击这里。",
|
|
180
175
|
"text.completed": "已完成",
|
|
181
176
|
"text.confirm password": "认证密码",
|
|
182
|
-
"text.confirm the address this invitation was sent to": "请输入收到邀请的邮箱地址。",
|
|
183
177
|
"text.copied": "已复制",
|
|
184
178
|
"text.copying needs a secure connection": "复制需要安全连接 — 请手动选择并复制该值",
|
|
185
179
|
"text.could not be saved": "保存失败 — 原因请看上方提示",
|
|
180
|
+
"text.could not look up the account": "无法确认账户。",
|
|
181
|
+
"button.issue link": "发放链接",
|
|
182
|
+
"button.copied": "已复制",
|
|
183
|
+
"button.reissue setup link": "重新发放设置链接",
|
|
184
|
+
"button.password reset link": "密码重置链接",
|
|
185
|
+
"label.how the link travels": "传递方式",
|
|
186
|
+
"label.send by mail": "通过邮件发送",
|
|
187
|
+
"label.copy the link": "复制链接",
|
|
188
|
+
"label.password-setup-pending-user": "待设置",
|
|
189
|
+
"title.password setup link": "密码设置链接",
|
|
190
|
+
"title.set a new password": "设置新密码",
|
|
191
|
+
"text.a new link retires the last one": "该账户正在等待设置密码。发放新链接后,旧链接将无法使用。",
|
|
192
|
+
"text.the current password stops and sessions end": "发放后当前密码立即失效,已登录的会话也会断开。由本人通过链接设置新密码。",
|
|
193
|
+
"text.the link works once for 48 hours and asks for the address": "链接在48小时内可使用一次,打开时会要求输入此邮箱地址。",
|
|
194
|
+
"text.hand it over yourself": "通过即时通讯或纸面亲自转交",
|
|
195
|
+
"text.the link was mailed to x until y": "已将链接发送至 {x}。可使用至 {y}。",
|
|
196
|
+
"text.this link is shown only now until y": "此链接仅此时显示一次。可使用至 {y},丢失请重新发放。",
|
|
197
|
+
"text.an account is made and a setup link handed over": "创建账户并转交密码设置链接。本人在48小时内通过该链接设置密码。",
|
|
198
|
+
"text.the address this link was sent for": "收到此链接的邮箱地址",
|
|
186
199
|
"text.could not register": "注册失败 — 原因见上方提示",
|
|
187
200
|
"text.create role": "创建角色",
|
|
188
201
|
"text.credentials": "凭据",
|
|
@@ -202,19 +215,17 @@
|
|
|
202
215
|
"text.inherited from x": "继承自 {x}",
|
|
203
216
|
"text.invalid-email": "电子邮件地址格式不正确",
|
|
204
217
|
"text.invalid-username": "请输入 ID 或电子邮件地址",
|
|
205
|
-
"text.invitation resent": "已重新发送邀请邮件。",
|
|
206
218
|
"text.missing against template": "缺少 {count} 项",
|
|
207
219
|
"text.new password": "新密码",
|
|
208
|
-
"text.no account so an invitation will be mailed": "尚无账号,将发送邀请邮件,由本人设置密码。",
|
|
209
220
|
"text.no domain available": "抱歉,没有$t(term.domain)名",
|
|
210
221
|
"text.no role to show": "打开此页面时未指定角色",
|
|
211
222
|
"text.not issued": "未签发",
|
|
212
223
|
"text.opens nothing, receives approvals": "opens nothing — this seat receives approvals",
|
|
213
|
-
"text.owner_transfer_completed": "
|
|
224
|
+
"text.owner_transfer_completed": "已指定为$t(term.domain)所有者。",
|
|
214
225
|
"text.password rule": "输入的密码必须15个字符或8字符包括号码和小写字符",
|
|
215
226
|
"text.passwords do not match": "密码不匹配",
|
|
216
227
|
"text.pick a seat to see what it opens": "Pick a seat to see what it opens.",
|
|
217
|
-
"text.please enter the email of the user you want to transfer owner": "
|
|
228
|
+
"text.please enter the email of the user you want to transfer owner": "请输入要指定为$t(term.domain)所有者的用户邮箱。",
|
|
218
229
|
"text.please_fill_all_required_fields": "请填写所有必填字段",
|
|
219
230
|
"text.privilege no longer declared": "已没有代码需要此权限",
|
|
220
231
|
"text.register new appliance": "注册终端设备",
|
|
@@ -236,14 +247,12 @@
|
|
|
236
247
|
"text.the appliance could not be loaded": "无法读取终端设备",
|
|
237
248
|
"text.the application could not be loaded": "无法读取应用程序",
|
|
238
249
|
"text.the binding could not be loaded": "无法读取绑定",
|
|
239
|
-
"text.the code is printed in the invitation email": "邀请邮件中以文字形式提供了该邀请码。",
|
|
240
250
|
"text.the current access token stops working immediately": "当前访问令牌将立即失效,使用它的地方需要换成新的令牌。",
|
|
241
251
|
"text.the current secret stops working immediately": "当前密钥将立即失效,使用它的地方需要换成新的密钥。",
|
|
242
252
|
"text.the list could not be loaded": "无法加载列表",
|
|
243
253
|
"text.this credential has been revoked": "该凭据已被撤销。在生成新的访问令牌之前,此设备无法访问系统。",
|
|
244
254
|
"text.this domain is top level and inherits from nowhere": "顶级$t(term.domain) — 没有可继承的来源",
|
|
245
255
|
"text.this field is required": "此项为必填",
|
|
246
|
-
"text.this invitation cannot be used": "该邀请无法使用。",
|
|
247
256
|
"text.this plant may call the seat what it likes": "Call the seat whatever this plant calls it.",
|
|
248
257
|
"text.this role no longer exists": "此角色已不存在",
|
|
249
258
|
"text.token expiry time": "令牌过期时间",
|
|
@@ -259,11 +268,9 @@
|
|
|
259
268
|
"text.x_cannot_delete": "{x}无法删除",
|
|
260
269
|
"text.x_created_successfully": "{x}创建成功",
|
|
261
270
|
"text.x_deleted_successfully": "{x}删除成功",
|
|
262
|
-
"text.you have joined. sign in to continue": "已加入。请登录。",
|
|
263
271
|
"text.your account is locked": "您的账号已被锁定",
|
|
264
272
|
"text.your account is not activated": "您的账号还没验证",
|
|
265
273
|
"text.your email address": "您的邮件地址",
|
|
266
|
-
"title.accept invitation": "接受邀请",
|
|
267
274
|
"title.activate account": "激活帐户",
|
|
268
275
|
"title.add template privileges": "添加模板权限",
|
|
269
276
|
"title.attribute model": "属性模型",
|
|
@@ -280,7 +287,7 @@
|
|
|
280
287
|
"title.move to this domain": "移动到此$t(term.domain)名",
|
|
281
288
|
"title.nothing selected": "未选择",
|
|
282
289
|
"title.open attributes view": "打开属性视图",
|
|
283
|
-
"title.owner_transfer": "
|
|
290
|
+
"title.owner_transfer": "指定$t(term.domain)所有者",
|
|
284
291
|
"title.partners": "合作伙伴",
|
|
285
292
|
"title.passkey offer": "添加通行密钥",
|
|
286
293
|
"title.protected-privileges": "受保护的权限",
|
|
@@ -290,6 +297,7 @@
|
|
|
290
297
|
"title.sign in": "登录",
|
|
291
298
|
"title.sign up": "注册",
|
|
292
299
|
"title.unlock account": "解锁帐户",
|
|
300
|
+
"title.user details": "用户详情",
|
|
293
301
|
"button.add what it can do": "添加可执行的操作",
|
|
294
302
|
"button.drop": "移除",
|
|
295
303
|
"button.edit privileges": "编辑权限",
|
|
@@ -314,8 +322,11 @@
|
|
|
314
322
|
"label.passkey": "通行密钥",
|
|
315
323
|
"label.read": "读取",
|
|
316
324
|
"label.write": "写入",
|
|
317
|
-
"text.account
|
|
318
|
-
"
|
|
325
|
+
"text.add user puts an account into this domain": "一个账户可在多个$t(term.domain)中使用。通过邮箱找到账户,加入此$t(term.domain)并分配角色。",
|
|
326
|
+
"label.account exists": "已有账户时",
|
|
327
|
+
"text.they join this domain at once": "立即加入此$t(term.domain)。",
|
|
328
|
+
"label.no account": "没有账户时",
|
|
329
|
+
"text.roles decide the screens they open": "决定加入者可以打开的画面。",
|
|
319
330
|
"text.add_domain_owner_description": "将所有者权限授予$t(term.domain)的一名成员。",
|
|
320
331
|
"text.and x more": "等另外 {x} 个",
|
|
321
332
|
"text.are_you_sure_to_remove_domain_owner": "确定要解除 {x} 的$t(term.domain)所有者身份吗?",
|
|
@@ -343,7 +354,6 @@
|
|
|
343
354
|
"text.nothing is on for this role yet": "此角色尚未开放任何内容。请在下方添加可执行的操作。",
|
|
344
355
|
"text.nothing matches x": "没有模块名称包含“{x}”。",
|
|
345
356
|
"text.owner opens everything without a role": "所有者无需角色即可打开所有页面。其他用户需要角色。",
|
|
346
|
-
"text.password becomes the default and must be told to them": "密码将恢复为默认值。不会发送邮件,请亲自告知对方新密码。",
|
|
347
357
|
"text.pick a module on the left": "在左侧选择模块,即可看到此角色在该模块中能做什么。",
|
|
348
358
|
"text.please enter username or email": "请输入用户名或邮箱",
|
|
349
359
|
"text.please try again": "请稍后再试。",
|
|
@@ -1,47 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,258 +0,0 @@
|
|
|
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 { readLinkToken } from '../utils/link-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
|
-
/*
|
|
63
|
-
* ⚠ `/auth/…` — the server router carries that prefix. This read `/accept-invitation` until
|
|
64
|
-
* 2026-09-24, which is no route; see the note beside `ACCEPT_INVITATION_PATH` in auth-base.
|
|
65
|
-
*/
|
|
66
|
-
get actionUrl() {
|
|
67
|
-
return '/auth/accept-invitation';
|
|
68
|
-
}
|
|
69
|
-
get title() {
|
|
70
|
-
return 'accept invitation';
|
|
71
|
-
}
|
|
72
|
-
connectedCallback() {
|
|
73
|
-
super.connectedCallback();
|
|
74
|
-
const { token, clean } = readLinkToken(window.location.href);
|
|
75
|
-
this.token = token;
|
|
76
|
-
/*
|
|
77
|
-
* Taken out of the address bar and out of this history entry. `replaceState` rather than
|
|
78
|
-
* `pushState`: going back should not put the token back on screen.
|
|
79
|
-
*/
|
|
80
|
-
if (clean && clean !== window.location.href) {
|
|
81
|
-
window.history.replaceState(null, '', clean);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
render() {
|
|
85
|
-
const { icon, title, description } = this.applicationMeta;
|
|
86
|
-
const { disableUserFavoredLanguage, languages } = this.data || {};
|
|
87
|
-
return html `
|
|
88
|
-
<div class="wrap">
|
|
89
|
-
<div class="auth-brand">
|
|
90
|
-
<img src=${icon} />
|
|
91
|
-
<strong class="name">${title}</strong>
|
|
92
|
-
<span class="welcome-msg">${description}</span>
|
|
93
|
-
</div>
|
|
94
|
-
|
|
95
|
-
<div class="auth-form">
|
|
96
|
-
<h3><ox-i18n msgid="title.accept invitation"></ox-i18n></h3>
|
|
97
|
-
|
|
98
|
-
${this.joined ? this.renderDone() : this.renderForm(disableUserFavoredLanguage, languages)}
|
|
99
|
-
</div>
|
|
100
|
-
</div>
|
|
101
|
-
|
|
102
|
-
<ox-snack-bar id="snackbar"></ox-snack-bar>
|
|
103
|
-
`;
|
|
104
|
-
}
|
|
105
|
-
renderDone() {
|
|
106
|
-
return html `
|
|
107
|
-
<p class="note">${i18next.t('text.you have joined. sign in to continue')}</p>
|
|
108
|
-
<md-elevated-button @click=${() => (window.location.href = '/auth/signin')}>
|
|
109
|
-
<ox-i18n msgid="button.sign in"></ox-i18n>
|
|
110
|
-
</md-elevated-button>
|
|
111
|
-
`;
|
|
112
|
-
}
|
|
113
|
-
renderForm(disableUserFavoredLanguage, languages) {
|
|
114
|
-
return html `
|
|
115
|
-
<form id="form" @keypress=${(e) => e.key === 'Enter' && this.accept()}>
|
|
116
|
-
<p class="note">${i18next.t('text.confirm the address this invitation was sent to')}</p>
|
|
117
|
-
|
|
118
|
-
<div class="field">
|
|
119
|
-
<md-filled-text-field
|
|
120
|
-
name="email"
|
|
121
|
-
type="email"
|
|
122
|
-
label=${String(i18next.t('field.email'))}
|
|
123
|
-
autocomplete="email"
|
|
124
|
-
required
|
|
125
|
-
><md-icon slot="leading-icon">mail</md-icon></md-filled-text-field
|
|
126
|
-
>
|
|
127
|
-
</div>
|
|
128
|
-
|
|
129
|
-
${this.token
|
|
130
|
-
? nothing
|
|
131
|
-
: html `
|
|
132
|
-
<div class="field">
|
|
133
|
-
<md-filled-text-field
|
|
134
|
-
name="token"
|
|
135
|
-
label=${String(i18next.t('label.invitation code'))}
|
|
136
|
-
supporting-text=${String(i18next.t('text.the code is printed in the invitation email'))}
|
|
137
|
-
autocomplete="off"
|
|
138
|
-
required
|
|
139
|
-
><md-icon slot="leading-icon">key</md-icon></md-filled-text-field
|
|
140
|
-
>
|
|
141
|
-
</div>
|
|
142
|
-
`}
|
|
143
|
-
${this.needsPassword ? this.renderNewAccount() : nothing}
|
|
144
|
-
|
|
145
|
-
<md-elevated-button id="submit-button" type="button" ?disabled=${this.busy} @click=${() => this.accept()}>
|
|
146
|
-
<ox-i18n msgid="button.accept invitation"></ox-i18n>
|
|
147
|
-
</md-elevated-button>
|
|
148
|
-
|
|
149
|
-
${!disableUserFavoredLanguage
|
|
150
|
-
? html `<div id="locale-area">
|
|
151
|
-
<label for="locale-selector"><md-icon>language</md-icon></label>
|
|
152
|
-
<ox-i18n-selector
|
|
153
|
-
id="locale-selector"
|
|
154
|
-
value=${i18next.language || 'en-US'}
|
|
155
|
-
.languages=${languages}
|
|
156
|
-
@change=${(e) => e.detail && i18next.changeLanguage(e.detail)}
|
|
157
|
-
></ox-i18n-selector>
|
|
158
|
-
</div>`
|
|
159
|
-
: nothing}
|
|
160
|
-
</form>
|
|
161
|
-
`;
|
|
162
|
-
}
|
|
163
|
-
renderNewAccount() {
|
|
164
|
-
return html `
|
|
165
|
-
<p class="note">${i18next.t('text.choose a password to finish joining')}</p>
|
|
166
|
-
|
|
167
|
-
<div class="field">
|
|
168
|
-
<md-filled-text-field name="name" label=${String(i18next.t('field.name'))} autocomplete="name"
|
|
169
|
-
><md-icon slot="leading-icon">person</md-icon></md-filled-text-field
|
|
170
|
-
>
|
|
171
|
-
</div>
|
|
172
|
-
|
|
173
|
-
<div class="field">
|
|
174
|
-
<md-filled-text-field
|
|
175
|
-
name="password"
|
|
176
|
-
type="password"
|
|
177
|
-
label=${String(i18next.t('label.password'))}
|
|
178
|
-
autocomplete="new-password"
|
|
179
|
-
required
|
|
180
|
-
><md-icon slot="leading-icon">password</md-icon></md-filled-text-field
|
|
181
|
-
>
|
|
182
|
-
</div>
|
|
183
|
-
`;
|
|
184
|
-
}
|
|
185
|
-
/*
|
|
186
|
-
* ⚠ Not called `valueOf`. Overriding `Object.prototype.valueOf` with a different signature
|
|
187
|
-
* makes the class type unassignable to `Object`, and every `@property` decorator on it then
|
|
188
|
-
* fails to resolve — with an error that talks about decorators and never mentions the method.
|
|
189
|
-
*/
|
|
190
|
-
fieldValue(name) {
|
|
191
|
-
return this.renderRoot.querySelector(`[name="${name}"]`)?.value?.trim() || '';
|
|
192
|
-
}
|
|
193
|
-
/*
|
|
194
|
-
* The base class calls this to commit the page. It does not submit the form element: the
|
|
195
|
-
* answer here is read rather than navigated to, because a refusal can mean "now ask for a
|
|
196
|
-
* password" instead of "stop".
|
|
197
|
-
*/
|
|
198
|
-
async submit() {
|
|
199
|
-
await this.accept();
|
|
200
|
-
}
|
|
201
|
-
async accept() {
|
|
202
|
-
if (this.busy) {
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
this.busy = true;
|
|
206
|
-
this.message = '';
|
|
207
|
-
try {
|
|
208
|
-
const response = await fetch(this.actionUrl, {
|
|
209
|
-
method: 'POST',
|
|
210
|
-
headers: { 'Content-Type': 'application/json' },
|
|
211
|
-
body: JSON.stringify({
|
|
212
|
-
token: this.token || this.fieldValue('token'),
|
|
213
|
-
email: this.fieldValue('email'),
|
|
214
|
-
name: this.fieldValue('name'),
|
|
215
|
-
password: this.fieldValue('password')
|
|
216
|
-
})
|
|
217
|
-
});
|
|
218
|
-
if (response.ok) {
|
|
219
|
-
this.joined = true;
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
const refusal = await response.json().catch(() => ({}));
|
|
223
|
-
/*
|
|
224
|
-
* The one refusal that is not the end of the attempt — it means this person has no account
|
|
225
|
-
* yet, which is exactly who this page is for.
|
|
226
|
-
*/
|
|
227
|
-
if (refusal?.refusal?.code === 'invitation-needs-a-password' || refusal?.code === 'invitation-needs-a-password') {
|
|
228
|
-
this.needsPassword = true;
|
|
229
|
-
this.message = String(i18next.t('text.choose a password to finish joining'));
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
this.message = refusal?.message || String(i18next.t('text.this invitation cannot be used'));
|
|
233
|
-
}
|
|
234
|
-
catch (e) {
|
|
235
|
-
this.message = String(i18next.t('text.this invitation cannot be used'));
|
|
236
|
-
}
|
|
237
|
-
finally {
|
|
238
|
-
this.busy = false;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
__decorate([
|
|
243
|
-
property({ type: String }),
|
|
244
|
-
__metadata("design:type", String)
|
|
245
|
-
], AcceptInvitation.prototype, "token", void 0);
|
|
246
|
-
__decorate([
|
|
247
|
-
property({ type: Boolean }),
|
|
248
|
-
__metadata("design:type", Boolean)
|
|
249
|
-
], AcceptInvitation.prototype, "needsPassword", void 0);
|
|
250
|
-
__decorate([
|
|
251
|
-
property({ type: Boolean }),
|
|
252
|
-
__metadata("design:type", Boolean)
|
|
253
|
-
], AcceptInvitation.prototype, "joined", void 0);
|
|
254
|
-
__decorate([
|
|
255
|
-
property({ type: Boolean }),
|
|
256
|
-
__metadata("design:type", Boolean)
|
|
257
|
-
], AcceptInvitation.prototype, "busy", void 0);
|
|
258
|
-
//# sourceMappingURL=accept-invitation.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"accept-invitation.js","sourceRoot":"","sources":["../../client/components/accept-invitation.ts"],"names":[],"mappings":";AAAA,OAAO,4BAA4B,CAAA;AACnC,OAAO,yCAAyC,CAAA;AAChD,OAAO,8CAA8C,CAAA;AAErD,OAAO,0BAA0B,CAAA;AACjC,OAAO,mCAAmC,CAAA;AAC1C,OAAO,iCAAiC,CAAA;AAExC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,KAAK,CAAA;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAE5C,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAEvC,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AACvD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAA;AACxE,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAEtD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,OAAO,gBAAiB,SAAQ,gBAAgB;aAC7C,WAAM,GAAG;QACd,sBAAsB;QACtB,GAAG,CAAA;;;;;;;;;;;;;;;;;;;KAmBF;QACD,eAAe;KAChB,CAAA;IAED,IAAI,QAAQ;QACV,OAAO,mBAAmB,CAAA;IAC5B,CAAC;IAED;;;OAGG;IACH,IAAI,SAAS;QACX,OAAO,yBAAyB,CAAA;IAClC,CAAC;IAED,IAAI,KAAK;QACP,OAAO,mBAAmB,CAAA;IAC5B,CAAC;IAaD,iBAAiB;QACf,KAAK,CAAC,iBAAiB,EAAE,CAAA;QAEzB,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;QAE5D,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;QAElB;;;WAGG;QACH,IAAI,KAAK,IAAI,KAAK,KAAK,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;YAC5C,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,EAAE,EAAE,EAAE,KAAK,CAAC,CAAA;QAC9C,CAAC;IACH,CAAC;IAED,MAAM;QACJ,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC,eAAe,CAAA;QACzD,MAAM,EAAE,0BAA0B,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC,IAAI,IAAI,EAAE,CAAA;QAEjE,OAAO,IAAI,CAAA;;;qBAGM,IAAI;iCACQ,KAAK;sCACA,WAAW;;;;;;YAMrC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,0BAA0B,EAAE,SAAS,CAAC;;;;;KAK/F,CAAA;IACH,CAAC;IAEO,UAAU;QAChB,OAAO,IAAI,CAAA;wBACS,OAAO,CAAC,CAAC,CAAC,2CAA2C,CAAC;mCAC3C,GAAG,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,GAAG,cAAc,CAAC;;;KAG3E,CAAA;IACH,CAAC;IAEO,UAAU,CAAC,0BAAmC,EAAE,SAAc;QACpE,OAAO,IAAI,CAAA;kCACmB,CAAC,CAAgB,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,OAAO,IAAI,IAAI,CAAC,MAAM,EAAE;0BAChE,OAAO,CAAC,CAAC,CAAC,sDAAsD,CAAC;;;;;;oBAMvE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC;;;;;;;UAO1C,IAAI,CAAC,KAAK;YACV,CAAC,CAAC,OAAO;YACT,CAAC,CAAC,IAAI,CAAA;;;;0BAIU,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC;oCAChC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,kDAAkD,CAAC,CAAC;;;;;;aAM5F;UACH,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC,OAAO;;yEAES,IAAI,CAAC,IAAI,WAAW,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE;;;;UAItG,CAAC,0BAA0B;YAC3B,CAAC,CAAC,IAAI,CAAA;;;;wBAIQ,OAAO,CAAC,QAAQ,IAAI,OAAO;6BACtB,SAAS;0BACZ,CAAC,CAAc,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,IAAI,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,MAAa,CAAC;;mBAE9E;YACT,CAAC,CAAC,OAAO;;KAEd,CAAA;IACH,CAAC;IAEO,gBAAgB;QACtB,OAAO,IAAI,CAAA;wBACS,OAAO,CAAC,CAAC,CAAC,0CAA0C,CAAC;;;kDAG3B,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;;;;;;;;;kBAS/D,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC;;;;;;KAMhD,CAAA;IACH,CAAC;IAED;;;;OAIG;IACK,UAAU,CAAC,IAAY;QAC7B,OAAQ,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,UAAU,IAAI,IAAI,CAAS,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA;IACxF,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,MAAM;QACV,MAAM,IAAI,CAAC,MAAM,EAAE,CAAA;IACrB,CAAC;IAEO,KAAK,CAAC,MAAM;QAClB,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,OAAM;QACR,CAAC;QAED,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,OAAO,GAAG,EAAE,CAAA;QAEjB,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE;gBAC3C,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBACnB,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;oBAC7C,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;oBAC/B,IAAI,EAAE,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;oBAC7B,QAAQ,EAAE,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC;iBACtC,CAAC;aACH,CAAC,CAAA;YAEF,IAAI,QAAQ,CAAC,EAAE,EAAE,CAAC;gBAChB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;gBAClB,OAAM;YACR,CAAC;YAED,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;YAEvD;;;eAGG;YACH,IAAI,OAAO,EAAE,OAAO,EAAE,IAAI,KAAK,6BAA6B,IAAI,OAAO,EAAE,IAAI,KAAK,6BAA6B,EAAE,CAAC;gBAChH,IAAI,CAAC,aAAa,GAAG,IAAI,CAAA;gBACzB,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,0CAA0C,CAAC,CAAC,CAAA;gBAC5E,OAAM;YACR,CAAC;YAED,IAAI,CAAC,OAAO,GAAG,OAAO,EAAE,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,qCAAqC,CAAC,CAAC,CAAA;QAC7F,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,qCAAqC,CAAC,CAAC,CAAA;QACzE,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,IAAI,GAAG,KAAK,CAAA;QACnB,CAAC;IACH,CAAC;;AAjM2B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;+CAAe;AAGb;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;;uDAAwB;AAGvB;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;;gDAAiB;AAEhB;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;;8CAAe","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 { readLinkToken } from '../utils/link-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 /*\n * ⚠ `/auth/…` — the server router carries that prefix. This read `/accept-invitation` until\n * 2026-09-24, which is no route; see the note beside `ACCEPT_INVITATION_PATH` in auth-base.\n */\n get actionUrl() {\n return '/auth/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 } = readLinkToken(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\"></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(this.actionUrl, {\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"]}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { __decorate } from "tslib";
|
|
2
|
-
import { customElement } from 'lit/decorators.js';
|
|
3
|
-
import { AcceptInvitation } from '../../components/accept-invitation.js';
|
|
4
|
-
let AuthAcceptInvitation = class AuthAcceptInvitation extends AcceptInvitation {
|
|
5
|
-
};
|
|
6
|
-
AuthAcceptInvitation = __decorate([
|
|
7
|
-
customElement('auth-accept-invitation')
|
|
8
|
-
], AuthAcceptInvitation);
|
|
9
|
-
export { AuthAcceptInvitation };
|
|
10
|
-
//# sourceMappingURL=accept-invitation.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"accept-invitation.js","sourceRoot":"","sources":["../../../client/entries/auth/accept-invitation.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAA;AAGjE,IAAM,oBAAoB,GAA1B,MAAM,oBAAqB,SAAQ,gBAAgB;CAAG,CAAA;AAAhD,oBAAoB;IADhC,aAAa,CAAC,wBAAwB,CAAC;GAC3B,oBAAoB,CAA4B","sourcesContent":["import { customElement } from 'lit/decorators.js'\n\nimport { AcceptInvitation } from '../../components/accept-invitation.js'\n\n@customElement('auth-accept-invitation')\nexport class AuthAcceptInvitation extends AcceptInvitation {}\n"]}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
/**
|
|
4
|
-
* The invitation list must not ask for the token.
|
|
5
|
-
*
|
|
6
|
-
* The token in an invitation mail is not a reference to a credential — it **is** the credential.
|
|
7
|
-
* Whoever holds it joins the domain, and for somebody with no account yet it creates one. It is
|
|
8
|
-
* not a GraphQL field on `Invitation` at all, for that reason; this holds the screen to the same
|
|
9
|
-
* line so that restoring the field later does not quietly put it in a grid cell.
|
|
10
|
-
*
|
|
11
|
-
* A credential in a grid cell is a credential in plain text beside a name — in every screenshot,
|
|
12
|
-
* on every shared display, for everyone who can open the screen. The people who *can* open this
|
|
13
|
-
* one are administrators, which is exactly who has it open on a projector.
|
|
14
|
-
*
|
|
15
|
-
* ⚠ Comments are stripped before searching. The prose above says `token` several times
|
|
16
|
-
* explaining why it is absent; searching the raw file would find the explanation and pass over a
|
|
17
|
-
* query that really asks for it.
|
|
18
|
-
*/
|
|
19
|
-
function source(name) {
|
|
20
|
-
const text = readFileSync(join(__dirname, name), 'utf-8');
|
|
21
|
-
return text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^[ \t]*\/\/.*$/gm, '');
|
|
22
|
-
}
|
|
23
|
-
describe('the invitation list query', () => {
|
|
24
|
-
it('does not ask for the token', () => {
|
|
25
|
-
expect(source('./invitations.ts')).not.toContain('token');
|
|
26
|
-
});
|
|
27
|
-
it('asks for the fields the columns render', () => {
|
|
28
|
-
const text = source('./invitations.ts');
|
|
29
|
-
for (const field of ['email', 'state', 'createdAt', 'expiresAt', 'acceptedAt', 'roles', 'creator']) {
|
|
30
|
-
expect(text).toContain(field);
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
it('reads the state the server resolves, not the stored column', () => {
|
|
34
|
-
/*
|
|
35
|
-
* ⚠ `status` is what is written down; `state` is what the server answers after measuring a
|
|
36
|
-
* pending row against the clock. Showing `status` would call an invitation live for as long
|
|
37
|
-
* as nothing had been round to mark it expired.
|
|
38
|
-
*/
|
|
39
|
-
const text = source('./invitations.ts');
|
|
40
|
-
expect(text).toContain('state');
|
|
41
|
-
expect(text).not.toMatch(/name:\s*'status'/);
|
|
42
|
-
});
|
|
43
|
-
it('passes the grist options through, so paging and sorting reach the server', () => {
|
|
44
|
-
const text = source('./invitations.ts');
|
|
45
|
-
expect(text).toContain('$pagination: Pagination');
|
|
46
|
-
expect(text).toContain('$sortings: [Sorting!]');
|
|
47
|
-
expect(text).toContain('$filters: [Filter!]');
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
describe('the accept page', () => {
|
|
51
|
-
function componentSource() {
|
|
52
|
-
return readFileSync(join(__dirname, '..', '..', 'components', 'accept-invitation.ts'), 'utf-8')
|
|
53
|
-
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
54
|
-
.replace(/^[ \t]*\/\/.*$/gm, '');
|
|
55
|
-
}
|
|
56
|
-
it('takes the token out of the address as well as reading it', () => {
|
|
57
|
-
/*
|
|
58
|
-
* ⚠ Reading the fragment is half of it. The fragment keeps the token out of server and proxy
|
|
59
|
-
* logs; replacing the address is what keeps it out of the browser history and off the screen.
|
|
60
|
-
* A page that read it and left it in the bar would undo the reason it is in the fragment.
|
|
61
|
-
*/
|
|
62
|
-
expect(componentSource()).toContain('replaceState');
|
|
63
|
-
});
|
|
64
|
-
it('never puts the token in a field the reader can see', () => {
|
|
65
|
-
const text = componentSource();
|
|
66
|
-
expect(text).not.toMatch(/\.value=\$\{[^}]*\btoken\b[^}]*\}/);
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
//# sourceMappingURL=invitation-list-query.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"invitation-list-query.test.js","sourceRoot":"","sources":["../../../client/pages/invitation/invitation-list-query.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAA;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAE3B;;;;;;;;;;;;;;;GAeG;AACH,SAAS,MAAM,CAAC,IAAY;IAC1B,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,CAAA;IAEzD,OAAO,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAA;AAC9E,CAAC;AAED,QAAQ,CAAC,2BAA2B,EAAE,GAAG,EAAE;IACzC,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;IAC3D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,IAAI,GAAG,MAAM,CAAC,kBAAkB,CAAC,CAAA;QAEvC,KAAK,MAAM,KAAK,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC;YACnG,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;QAC/B,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;QACpE;;;;WAIG;QACH,MAAM,IAAI,GAAG,MAAM,CAAC,kBAAkB,CAAC,CAAA;QAEvC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;QAC/B,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAA;IAC9C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,0EAA0E,EAAE,GAAG,EAAE;QAClF,MAAM,IAAI,GAAG,MAAM,CAAC,kBAAkB,CAAC,CAAA;QAEvC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,yBAAyB,CAAC,CAAA;QACjD,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,uBAAuB,CAAC,CAAA;QAC/C,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,qBAAqB,CAAC,CAAA;IAC/C,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,SAAS,eAAe;QACtB,OAAO,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,sBAAsB,CAAC,EAAE,OAAO,CAAC;aAC5F,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC;aAChC,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAA;IACpC,CAAC;IAED,EAAE,CAAC,0DAA0D,EAAE,GAAG,EAAE;QAClE;;;;WAIG;QACH,MAAM,CAAC,eAAe,EAAE,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAA;IACrD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,MAAM,IAAI,GAAG,eAAe,EAAE,CAAA;QAE9B,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,mCAAmC,CAAC,CAAA;IAC/D,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import { readFileSync } from 'fs'\nimport { join } from 'path'\n\n/**\n * The invitation list must not ask for the token.\n *\n * The token in an invitation mail is not a reference to a credential — it **is** the credential.\n * Whoever holds it joins the domain, and for somebody with no account yet it creates one. It is\n * not a GraphQL field on `Invitation` at all, for that reason; this holds the screen to the same\n * line so that restoring the field later does not quietly put it in a grid cell.\n *\n * A credential in a grid cell is a credential in plain text beside a name — in every screenshot,\n * on every shared display, for everyone who can open the screen. The people who *can* open this\n * one are administrators, which is exactly who has it open on a projector.\n *\n * ⚠ Comments are stripped before searching. The prose above says `token` several times\n * explaining why it is absent; searching the raw file would find the explanation and pass over a\n * query that really asks for it.\n */\nfunction source(name: string): string {\n const text = readFileSync(join(__dirname, name), 'utf-8')\n\n return text.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '').replace(/^[ \\t]*\\/\\/.*$/gm, '')\n}\n\ndescribe('the invitation list query', () => {\n it('does not ask for the token', () => {\n expect(source('./invitations.ts')).not.toContain('token')\n })\n\n it('asks for the fields the columns render', () => {\n const text = source('./invitations.ts')\n\n for (const field of ['email', 'state', 'createdAt', 'expiresAt', 'acceptedAt', 'roles', 'creator']) {\n expect(text).toContain(field)\n }\n })\n\n it('reads the state the server resolves, not the stored column', () => {\n /*\n * ⚠ `status` is what is written down; `state` is what the server answers after measuring a\n * pending row against the clock. Showing `status` would call an invitation live for as long\n * as nothing had been round to mark it expired.\n */\n const text = source('./invitations.ts')\n\n expect(text).toContain('state')\n expect(text).not.toMatch(/name:\\s*'status'/)\n })\n\n it('passes the grist options through, so paging and sorting reach the server', () => {\n const text = source('./invitations.ts')\n\n expect(text).toContain('$pagination: Pagination')\n expect(text).toContain('$sortings: [Sorting!]')\n expect(text).toContain('$filters: [Filter!]')\n })\n})\n\ndescribe('the accept page', () => {\n function componentSource(): string {\n return readFileSync(join(__dirname, '..', '..', 'components', 'accept-invitation.ts'), 'utf-8')\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')\n .replace(/^[ \\t]*\\/\\/.*$/gm, '')\n }\n\n it('takes the token out of the address as well as reading it', () => {\n /*\n * ⚠ Reading the fragment is half of it. The fragment keeps the token out of server and proxy\n * logs; replacing the address is what keeps it out of the browser history and off the screen.\n * A page that read it and left it in the bar would undo the reason it is in the fragment.\n */\n expect(componentSource()).toContain('replaceState')\n })\n\n it('never puts the token in a field the reader can see', () => {\n const text = componentSource()\n\n expect(text).not.toMatch(/\\.value=\\$\\{[^}]*\\btoken\\b[^}]*\\}/)\n })\n})\n"]}
|