@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.
Files changed (59) hide show
  1. package/dist-client/bootstrap.js +18 -14
  2. package/dist-client/bootstrap.js.map +1 -1
  3. package/dist-client/components/abstract-password-reset.d.ts +5 -0
  4. package/dist-client/components/abstract-password-reset.js +20 -0
  5. package/dist-client/components/abstract-password-reset.js.map +1 -1
  6. package/dist-client/components/ownership-transfer-popup.d.ts +3 -1
  7. package/dist-client/components/ownership-transfer-popup.js +38 -41
  8. package/dist-client/components/ownership-transfer-popup.js.map +1 -1
  9. package/dist-client/components/password-setup-link-popup.d.ts +46 -0
  10. package/dist-client/components/password-setup-link-popup.js +254 -0
  11. package/dist-client/components/password-setup-link-popup.js.map +1 -0
  12. package/dist-client/components/user-detail.d.ts +20 -30
  13. package/dist-client/components/user-detail.js +114 -186
  14. package/dist-client/components/user-detail.js.map +1 -1
  15. package/dist-client/components/user-invite.d.ts +18 -25
  16. package/dist-client/components/user-invite.js +157 -181
  17. package/dist-client/components/user-invite.js.map +1 -1
  18. package/dist-client/components/user-popups-standard.test.js +69 -0
  19. package/dist-client/components/user-popups-standard.test.js.map +1 -0
  20. package/dist-client/components/user-role-editor.js +5 -26
  21. package/dist-client/components/user-role-editor.js.map +1 -1
  22. package/dist-client/components/user-status-renderer.js +4 -0
  23. package/dist-client/components/user-status-renderer.js.map +1 -1
  24. package/dist-client/entries/auth/reset-password.d.ts +1 -0
  25. package/dist-client/entries/auth/reset-password.js +5 -1
  26. package/dist-client/entries/auth/reset-password.js.map +1 -1
  27. package/dist-client/index.js +10 -28
  28. package/dist-client/index.js.map +1 -1
  29. package/dist-client/pages/user/user-list-page.d.ts +5 -1
  30. package/dist-client/pages/user/user-list-page.js +14 -5
  31. package/dist-client/pages/user/user-list-page.js.map +1 -1
  32. package/dist-client/route.js +0 -3
  33. package/dist-client/route.js.map +1 -1
  34. package/dist-client/tsconfig.tsbuildinfo +1 -1
  35. package/dist-client/utils/link-token.d.ts +3 -3
  36. package/dist-client/utils/link-token.js +4 -4
  37. package/dist-client/utils/link-token.js.map +1 -1
  38. package/dist-client/utils/link-token.test.js +2 -2
  39. package/dist-client/utils/link-token.test.js.map +1 -1
  40. package/package.json +6 -5
  41. package/things-factory.config.js +0 -1
  42. package/translations/en.json +30 -20
  43. package/translations/es.json +30 -20
  44. package/translations/ja.json +30 -20
  45. package/translations/ko.json +30 -20
  46. package/translations/ms.json +30 -20
  47. package/translations/zh.json +30 -20
  48. package/dist-client/components/accept-invitation.d.ts +0 -47
  49. package/dist-client/components/accept-invitation.js +0 -258
  50. package/dist-client/components/accept-invitation.js.map +0 -1
  51. package/dist-client/entries/auth/accept-invitation.d.ts +0 -3
  52. package/dist-client/entries/auth/accept-invitation.js +0 -10
  53. package/dist-client/entries/auth/accept-invitation.js.map +0 -1
  54. package/dist-client/pages/invitation/invitation-list-query.test.js +0 -69
  55. package/dist-client/pages/invitation/invitation-list-query.test.js.map +0 -1
  56. package/dist-client/pages/invitation/invitations.d.ts +0 -63
  57. package/dist-client/pages/invitation/invitations.js +0 -284
  58. package/dist-client/pages/invitation/invitations.js.map +0 -1
  59. /package/dist-client/{pages/invitation/invitation-list-query.test.d.ts → components/user-popups-standard.test.d.ts} +0 -0
@@ -0,0 +1,254 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import '@material/web/button/text-button.js';
3
+ import '@material/web/button/filled-button.js';
4
+ import '@material/web/button/outlined-button.js';
5
+ import '@operato/popup/ox-popup-actions.js';
6
+ import gql from 'graphql-tag';
7
+ import { css, html, LitElement, nothing } from 'lit';
8
+ import { customElement, property, state } from 'lit/decorators.js';
9
+ import { client, gqlContext } from '@operato/graphql';
10
+ import { i18next, localize } from '@operato/i18n';
11
+ import { openPopup } from '@operato/layout';
12
+ import { CommonPopupStyles, FormFieldStyles, PopupBodyStyles, PopupFieldStyles } from '@operato/styles';
13
+ /**
14
+ * Issuing a password setup link - the one way a person gets a password here that an administrator did not set
15
+ * (ADR-0161).
16
+ *
17
+ * ── Before ───────────────────────────────────────────────────────────────────
18
+ * What happens is said before it happens: the password the account has stops working and its open sessions end, and
19
+ * the link works once, for 48 hours, and asks for the address. Then the administrator picks how it travels - by mail,
20
+ * or copied and handed over by them.
21
+ *
22
+ * ── After ────────────────────────────────────────────────────────────────────
23
+ * A copied link is on screen **only now**: the server keeps its hash and nothing else, and there is no way to read it
24
+ * again. Losing it means issuing another, which retires this one.
25
+ *
26
+ * The user detail, the add-user flow and the old role editor all open this one popup (`openPasswordSetupLink`).
27
+ */
28
+ let PasswordSetupLinkPopup = class PasswordSetupLinkPopup extends localize(i18next)(LitElement) {
29
+ constructor() {
30
+ super(...arguments);
31
+ this.mail = true;
32
+ this.issuing = false;
33
+ this.message = '';
34
+ this.copied = false;
35
+ }
36
+ static { this.styles = [
37
+ CommonPopupStyles,
38
+ PopupBodyStyles,
39
+ FormFieldStyles,
40
+ PopupFieldStyles,
41
+ css `
42
+ [ways] {
43
+ display: flex;
44
+ flex-direction: column;
45
+ gap: var(--spacing-small, 4px);
46
+ margin: 0;
47
+ padding: 0;
48
+ border: 0;
49
+ }
50
+
51
+ [ways] legend {
52
+ margin-bottom: var(--spacing-small, 4px);
53
+ padding: 0;
54
+ color: var(--ui-muted);
55
+ font-size: var(--ui-t-small, 11.5px);
56
+ }
57
+
58
+ [ways] label {
59
+ display: flex;
60
+ align-items: baseline;
61
+ gap: var(--spacing-medium, 8px);
62
+ }
63
+
64
+ [ways] [desc] {
65
+ color: var(--ui-muted);
66
+ font-size: var(--ui-t-small, 11.5px);
67
+ }
68
+
69
+ [copy-row] {
70
+ display: flex;
71
+ gap: var(--spacing-medium, 8px);
72
+ align-items: center;
73
+ }
74
+
75
+ [copy-row] input {
76
+ flex: 1;
77
+ min-width: 0;
78
+ }
79
+
80
+ /* Controls in a popup follow the card - pills (CommonPopupStyles holds that only inside the action row). */
81
+ [copy-row] md-outlined-button {
82
+ --md-outlined-button-container-height: var(--popup-action-height, 32px);
83
+ --md-outlined-button-container-shape: var(--popup-control-radius);
84
+ }
85
+ `
86
+ ]; }
87
+ render() {
88
+ const user = this.user || {};
89
+ return html `
90
+ <div class="body">
91
+ <div class="what">${user.name || user.username || user.email} · ${user.email}</div>
92
+ ${this.issued ? this.renderIssued() : this.renderChoice(user)}
93
+ </div>
94
+
95
+ <ox-popup-actions>
96
+ ${this.issued
97
+ ? nothing
98
+ : html `<md-text-button slot="cancel" @click=${() => this.close()}>${i18next.t('button.cancel')}</md-text-button>`}
99
+ <!-- Issued, the one thing left is to close: the link cannot be read again, so nothing here takes it back. -->
100
+ <md-filled-button
101
+ slot="confirm"
102
+ ?disabled=${this.issuing}
103
+ @click=${() => (this.issued ? this.close() : this.issue())}
104
+ >
105
+ ${this.issued ? i18next.t('button.close') : i18next.t('button.issue link')}
106
+ </md-filled-button>
107
+ </ox-popup-actions>
108
+ `;
109
+ }
110
+ renderChoice(user) {
111
+ const waiting = user.status === 'password_reset_required';
112
+ return html `
113
+ <div class="note">
114
+ ${waiting
115
+ ? i18next.t('text.a new link retires the last one')
116
+ : i18next.t('text.the current password stops and sessions end')}
117
+ ${i18next.t('text.the link works once for 48 hours and asks for the address')}
118
+ </div>
119
+
120
+ <fieldset ways>
121
+ <legend>${i18next.t('label.how the link travels')}</legend>
122
+ <label>
123
+ <input type="radio" name="way" .checked=${this.mail} @change=${() => (this.mail = true)} />
124
+ <span>${i18next.t('label.send by mail')}</span>
125
+ <span desc>${user.email}</span>
126
+ </label>
127
+ <label>
128
+ <input type="radio" name="way" .checked=${!this.mail} @change=${() => (this.mail = false)} />
129
+ <span>${i18next.t('label.copy the link')}</span>
130
+ <span desc>${i18next.t('text.hand it over yourself')}</span>
131
+ </label>
132
+ </fieldset>
133
+
134
+ ${this.message ? html `<div class="failure">${this.message}</div>` : nothing}
135
+ `;
136
+ }
137
+ renderIssued() {
138
+ const { link, mailedTo, expiresAt } = this.issued;
139
+ const until = new Date(expiresAt).toLocaleString(i18next.language);
140
+ if (mailedTo) {
141
+ return html `<div class="note">${i18next.t('text.the link was mailed to x until y', { x: mailedTo, y: until })}</div>`;
142
+ }
143
+ return html `
144
+ <label class="field">
145
+ <span>${i18next.t('title.password setup link')}</span>
146
+ <div copy-row>
147
+ <input name="link" readonly .value=${link || ''} @focus=${(e) => e.target.select()} />
148
+ <md-outlined-button @click=${() => this.copy()}>
149
+ ${this.copied ? i18next.t('button.copied') : i18next.t('button.copy')}
150
+ </md-outlined-button>
151
+ </div>
152
+ </label>
153
+ <div class="note">${i18next.t('text.this link is shown only now until y', { y: until })}</div>
154
+ `;
155
+ }
156
+ async issue() {
157
+ const username = this.user?.username || this.user?.email;
158
+ if (!username || this.issuing) {
159
+ return;
160
+ }
161
+ this.issuing = true;
162
+ this.message = '';
163
+ try {
164
+ const response = await client.mutate({
165
+ mutation: gql `
166
+ mutation ($username: String!, $mail: Boolean!) {
167
+ issuePasswordSetupLink(username: $username, mail: $mail) {
168
+ link
169
+ mailedTo
170
+ expiresAt
171
+ }
172
+ }
173
+ `,
174
+ variables: { username, mail: this.mail },
175
+ context: gqlContext()
176
+ });
177
+ if (response.errors?.length) {
178
+ this.message = response.errors[0]?.message;
179
+ return;
180
+ }
181
+ this.issued = response.data?.issuePasswordSetupLink;
182
+ this.dispatchEvent(new CustomEvent('password-setup-issued', { bubbles: true, composed: true }));
183
+ }
184
+ catch (e) {
185
+ this.message = e?.message || String(e);
186
+ }
187
+ finally {
188
+ this.issuing = false;
189
+ }
190
+ }
191
+ async copy() {
192
+ const link = this.issued?.link;
193
+ if (!link) {
194
+ return;
195
+ }
196
+ try {
197
+ await navigator.clipboard.writeText(link);
198
+ this.copied = true;
199
+ }
200
+ catch (e) {
201
+ /* No clipboard (an insecure origin, a refused permission): leave the link selected for the person to copy. */
202
+ const input = this.renderRoot.querySelector('input[name=link]');
203
+ input?.select();
204
+ }
205
+ }
206
+ close() {
207
+ this.dispatchEvent(new CustomEvent('close'));
208
+ }
209
+ };
210
+ __decorate([
211
+ property({ type: Object }),
212
+ __metadata("design:type", Object)
213
+ ], PasswordSetupLinkPopup.prototype, "user", void 0);
214
+ __decorate([
215
+ state(),
216
+ __metadata("design:type", Boolean)
217
+ ], PasswordSetupLinkPopup.prototype, "mail", void 0);
218
+ __decorate([
219
+ state(),
220
+ __metadata("design:type", Boolean)
221
+ ], PasswordSetupLinkPopup.prototype, "issuing", void 0);
222
+ __decorate([
223
+ state(),
224
+ __metadata("design:type", Object)
225
+ ], PasswordSetupLinkPopup.prototype, "issued", void 0);
226
+ __decorate([
227
+ state(),
228
+ __metadata("design:type", String)
229
+ ], PasswordSetupLinkPopup.prototype, "message", void 0);
230
+ __decorate([
231
+ state(),
232
+ __metadata("design:type", Boolean)
233
+ ], PasswordSetupLinkPopup.prototype, "copied", void 0);
234
+ PasswordSetupLinkPopup = __decorate([
235
+ customElement('password-setup-link-popup')
236
+ ], PasswordSetupLinkPopup);
237
+ export { PasswordSetupLinkPopup };
238
+ /**
239
+ * Open the popup for `user`. `onIssued` runs once a link was issued - the account is now waiting, so a list showing
240
+ * its status reads it again.
241
+ */
242
+ export function openPasswordSetupLink(user, { onIssued } = {}) {
243
+ const popup = openPopup(html `<password-setup-link-popup
244
+ .user=${user}
245
+ @close=${() => popup.close()}
246
+ @password-setup-issued=${() => onIssued?.()}
247
+ ></password-setup-link-popup>`, {
248
+ backdrop: true,
249
+ size: 'medium',
250
+ title: i18next.t('title.password setup link')
251
+ });
252
+ return popup;
253
+ }
254
+ //# sourceMappingURL=password-setup-link-popup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"password-setup-link-popup.js","sourceRoot":"","sources":["../../client/components/password-setup-link-popup.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/button/text-button.js'\nimport '@material/web/button/filled-button.js'\nimport '@material/web/button/outlined-button.js'\nimport '@operato/popup/ox-popup-actions.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport type { CSSResultGroup } from 'lit'\n\nimport { client, gqlContext } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { openPopup } from '@operato/layout'\nimport { CommonPopupStyles, FormFieldStyles, PopupBodyStyles, PopupFieldStyles } from '@operato/styles'\n\n/**\n * Issuing a password setup link - the one way a person gets a password here that an administrator did not set\n * (ADR-0161).\n *\n * ── Before ───────────────────────────────────────────────────────────────────\n * What happens is said before it happens: the password the account has stops working and its open sessions end, and\n * the link works once, for 48 hours, and asks for the address. Then the administrator picks how it travels - by mail,\n * or copied and handed over by them.\n *\n * ── After ────────────────────────────────────────────────────────────────────\n * A copied link is on screen **only now**: the server keeps its hash and nothing else, and there is no way to read it\n * again. Losing it means issuing another, which retires this one.\n *\n * The user detail, the add-user flow and the old role editor all open this one popup (`openPasswordSetupLink`).\n */\n@customElement('password-setup-link-popup')\nexport class PasswordSetupLinkPopup extends localize(i18next)(LitElement) {\n static styles: CSSResultGroup = [\n CommonPopupStyles,\n PopupBodyStyles,\n FormFieldStyles,\n PopupFieldStyles,\n css`\n [ways] {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-small, 4px);\n margin: 0;\n padding: 0;\n border: 0;\n }\n\n [ways] legend {\n margin-bottom: var(--spacing-small, 4px);\n padding: 0;\n color: var(--ui-muted);\n font-size: var(--ui-t-small, 11.5px);\n }\n\n [ways] label {\n display: flex;\n align-items: baseline;\n gap: var(--spacing-medium, 8px);\n }\n\n [ways] [desc] {\n color: var(--ui-muted);\n font-size: var(--ui-t-small, 11.5px);\n }\n\n [copy-row] {\n display: flex;\n gap: var(--spacing-medium, 8px);\n align-items: center;\n }\n\n [copy-row] input {\n flex: 1;\n min-width: 0;\n }\n\n /* Controls in a popup follow the card - pills (CommonPopupStyles holds that only inside the action row). */\n [copy-row] md-outlined-button {\n --md-outlined-button-container-height: var(--popup-action-height, 32px);\n --md-outlined-button-container-shape: var(--popup-control-radius);\n }\n `\n ]\n\n /** The person: `username` (or `email`), `email`, `name`, `status`. */\n @property({ type: Object }) user: any\n\n @state() private mail: boolean = true\n @state() private issuing: boolean = false\n @state() private issued?: { link?: string; mailedTo?: string; expiresAt: string }\n @state() private message: string = ''\n @state() private copied: boolean = false\n\n render() {\n const user = this.user || {}\n\n return html`\n <div class=\"body\">\n <div class=\"what\">${user.name || user.username || user.email} · ${user.email}</div>\n ${this.issued ? this.renderIssued() : this.renderChoice(user)}\n </div>\n\n <ox-popup-actions>\n ${this.issued\n ? nothing\n : html`<md-text-button slot=\"cancel\" @click=${() => this.close()}>${i18next.t('button.cancel')}</md-text-button>`}\n <!-- Issued, the one thing left is to close: the link cannot be read again, so nothing here takes it back. -->\n <md-filled-button\n slot=\"confirm\"\n ?disabled=${this.issuing}\n @click=${() => (this.issued ? this.close() : this.issue())}\n >\n ${this.issued ? i18next.t('button.close') : i18next.t('button.issue link')}\n </md-filled-button>\n </ox-popup-actions>\n `\n }\n\n private renderChoice(user: any) {\n const waiting = user.status === 'password_reset_required'\n\n return html`\n <div class=\"note\">\n ${waiting\n ? i18next.t('text.a new link retires the last one')\n : i18next.t('text.the current password stops and sessions end')}\n ${i18next.t('text.the link works once for 48 hours and asks for the address')}\n </div>\n\n <fieldset ways>\n <legend>${i18next.t('label.how the link travels')}</legend>\n <label>\n <input type=\"radio\" name=\"way\" .checked=${this.mail} @change=${() => (this.mail = true)} />\n <span>${i18next.t('label.send by mail')}</span>\n <span desc>${user.email}</span>\n </label>\n <label>\n <input type=\"radio\" name=\"way\" .checked=${!this.mail} @change=${() => (this.mail = false)} />\n <span>${i18next.t('label.copy the link')}</span>\n <span desc>${i18next.t('text.hand it over yourself')}</span>\n </label>\n </fieldset>\n\n ${this.message ? html`<div class=\"failure\">${this.message}</div>` : nothing}\n `\n }\n\n private renderIssued() {\n const { link, mailedTo, expiresAt } = this.issued!\n const until = new Date(expiresAt).toLocaleString(i18next.language)\n\n if (mailedTo) {\n return html`<div class=\"note\">${i18next.t('text.the link was mailed to x until y', { x: mailedTo, y: until })}</div>`\n }\n\n return html`\n <label class=\"field\">\n <span>${i18next.t('title.password setup link')}</span>\n <div copy-row>\n <input name=\"link\" readonly .value=${link || ''} @focus=${(e: Event) => (e.target as HTMLInputElement).select()} />\n <md-outlined-button @click=${() => this.copy()}>\n ${this.copied ? i18next.t('button.copied') : i18next.t('button.copy')}\n </md-outlined-button>\n </div>\n </label>\n <div class=\"note\">${i18next.t('text.this link is shown only now until y', { y: until })}</div>\n `\n }\n\n private async issue() {\n const username = this.user?.username || this.user?.email\n if (!username || this.issuing) {\n return\n }\n\n this.issuing = true\n this.message = ''\n\n try {\n const response = await client.mutate({\n mutation: gql`\n mutation ($username: String!, $mail: Boolean!) {\n issuePasswordSetupLink(username: $username, mail: $mail) {\n link\n mailedTo\n expiresAt\n }\n }\n `,\n variables: { username, mail: this.mail },\n context: gqlContext()\n })\n\n if (response.errors?.length) {\n this.message = response.errors[0]?.message\n return\n }\n\n this.issued = response.data?.issuePasswordSetupLink\n this.dispatchEvent(new CustomEvent('password-setup-issued', { bubbles: true, composed: true }))\n } catch (e: any) {\n this.message = e?.message || String(e)\n } finally {\n this.issuing = false\n }\n }\n\n private async copy() {\n const link = this.issued?.link\n if (!link) {\n return\n }\n\n try {\n await navigator.clipboard.writeText(link)\n this.copied = true\n } catch (e) {\n /* No clipboard (an insecure origin, a refused permission): leave the link selected for the person to copy. */\n const input = this.renderRoot.querySelector('input[name=link]') as HTMLInputElement | null\n input?.select()\n }\n }\n\n private close() {\n this.dispatchEvent(new CustomEvent('close'))\n }\n}\n\n/**\n * Open the popup for `user`. `onIssued` runs once a link was issued - the account is now waiting, so a list showing\n * its status reads it again.\n */\nexport function openPasswordSetupLink(user: any, { onIssued }: { onIssued?: () => void } = {}) {\n const popup = openPopup(\n html`<password-setup-link-popup\n .user=${user}\n @close=${() => popup.close()}\n @password-setup-issued=${() => onIssued?.()}\n ></password-setup-link-popup>`,\n {\n backdrop: true,\n size: 'medium',\n title: i18next.t('title.password setup link')\n }\n )\n\n return popup\n}\n"]}
@@ -1,7 +1,8 @@
1
1
  import '@material/web/icon/icon.js';
2
- import '@material/web/button/elevated-button.js';
2
+ import '@material/web/button/text-button.js';
3
+ import '@material/web/button/filled-button.js';
3
4
  import '@material/web/button/outlined-button.js';
4
- import '@material/web/checkbox/checkbox.js';
5
+ import '@operato/popup/ox-popup-actions.js';
5
6
  import './ownership-transfer-popup.js';
6
7
  import { LitElement } from 'lit';
7
8
  import type { CSSResultGroup } from 'lit';
@@ -40,15 +41,13 @@ export declare class UserDetail extends UserDetail_base {
40
41
  private canManage;
41
42
  private canOwn;
42
43
  private myEmail;
43
- /** 기본 비밀번호가 설정돼 있지 않으면 초기화는 서버에서 던진다 — 단추를 내주지 않는다. */
44
- private defaultPasswordConfigured;
45
44
  /**
46
- * 이 사람의 비밀번호를 이 도메인이 초기화해도 되는가 — 서버 규칙(`password-reset-rule.ts`)의 답.
45
+ * 이 사람에게 이 도메인이 비밀번호 설정 링크를 발급해도 되는가 — 서버 규칙(`password-reset-rule.ts`)의 답.
47
46
  *
48
47
  * 비밀번호는 계정 하나에 하나다. 다른 도메인에서도 일하는 사람, 어느 도메인이든 오너인 사람
49
- * (수퍼유저 포함)은 이 도메인이 초기화하지 않는다. 기본 비밀번호를 아는 누구나 그 계정을 가져갈 수 있다.
48
+ * (수퍼유저 포함)의 비밀번호는 이 도메인이 거두지 않는다(ADR-0161 결정 4).
50
49
  */
51
- private passwordResettable;
50
+ private setupLinkIssuable;
52
51
  /**
53
52
  * 이 도메인(과 부모)의 역할 id.
54
53
  *
@@ -63,20 +62,17 @@ export declare class UserDetail extends UserDetail_base {
63
62
  private get savable();
64
63
  render(): import("lit-html").TemplateResult<1>;
65
64
  /**
66
- * 되돌릴 수 없는 조치.
65
+ * 되돌릴 수 없는 조치 — 단추 줄의 왼쪽 끝, 빨간 글자(page-standard §8, `ox-popup-actions` 의 `start`).
67
66
  *
68
- * ── 왜 아래에 따로 두나 ─────────────────────────────────────────────────────
69
67
  * 옛 화면은 저장·소유권 이전·초기화·제외·비활성화를 **한 줄에 나란히** 뒀다. 매일 누르는
70
- * 「저장」 옆에 한 번 누르면 끝인 「제외」가 같은 크기로 있었다.
71
- *
72
- * 그래서 선을 하나 긋고, 위는 채운 단추(주 동작), 아래는 테두리 단추로 둔다. 색은
73
- * error 짝으로 글자에만 쓴다 — 면을 다 칠하면 「지금 문제가 있다」로 읽힌다.
68
+ * 「저장」 옆에 한 번 누르면 끝인 「제외」가 같은 크기로 있었다. 단추 줄이 그 거리를 맡는다 —
69
+ * 저장은 오른쪽 끝, 이것들은 왼쪽 끝이고, 색은 글자에만 쓴다.
74
70
  *
75
71
  * ── 무엇을 언제 내주나 ──────────────────────────────────────────────────────
76
72
  * 조건은 전부 **서버가 실제로 던지는 것**을 그대로 옮긴 것이다. 눌리는 단추를 보여 주고
77
73
  * 서버가 거절하면 그것은 안내가 아니라 오류다.
78
74
  *
79
- * 비밀번호 초기화 기본 비밀번호가 설정돼 있고, 내가 아니다
75
+ * 비밀번호 설정 링크 서버가 된다고 하고(`mayIssuePasswordSetupLink`), 내가 아니다
80
76
  * 소유권 이전 활성 상태고, 소유자가 아니고, 이 도메인에 역할이 하나라도 있다
81
77
  * 도메인에서 제외 소유자가 아니고, 내가 아니다
82
78
  *
@@ -84,9 +80,9 @@ export declare class UserDetail extends UserDetail_base {
84
80
  * `deleteDomainUser` 는 계정을 지우지 않는다 — **이 도메인에서만 뗀다**(서버 설명도 그렇게
85
81
  * 적혀 있다). 옛 화면의 「사용자 삭제」는 하지 않는 일을 말하고 있었다.
86
82
  */
87
- private renderDangerZone;
83
+ private renderIrreversible;
88
84
  /**
89
- * 계정 상태와, 그것을 되돌리는 단추.
85
+ * 계정 상태 한 줄과, 그것을 바꾸는 단추를 그 줄 끝에(page-standard §8 「줄 하나」).
90
86
  *
91
87
  * **잠금 해제와 활성화는 서버에서 같은 것이다** — `activateUser` 가 `failCount` 를 0 으로
92
88
  * 두고 `ACTIVATED` 로 되돌린다. 그래서 부르는 것은 하나이고, **글자만 상황에 맞춰 바꾼다.**
@@ -94,24 +90,19 @@ export declare class UserDetail extends UserDetail_base {
94
90
  *
95
91
  * 비활성화는 소유자와 나 자신에게는 내주지 않는다. 서버도 같은 것을 막지만
96
92
  * (`inactivateUser` 가 소유자·admin 을 거절한다), **화면이 눌리는 단추를 보여 주고 서버가
97
- * 거절하면 그것은 안내가 아니라 오류다.**
93
+ * 거절하면 그것은 안내가 아니라 오류다.** 되돌릴 수 있으므로 빨간 글자가 아니다.
98
94
  */
99
95
  private renderAccountState;
96
+ private renderStateAction;
100
97
  private renderRoles;
101
98
  connectedCallback(): void;
102
99
  disconnectedCallback(): void;
103
100
  private applyMe;
104
- /**
105
- * 서버 지시자에 적힌 인자를 그대로 되묻는다.
106
- *
107
- * 기본 비밀번호 여부도 여기서 함께 읽는다. 서버에 같은 답을 주는 질의가 둘 있는데
108
- * (`checkDefaultPassword` 와 `checkResettablePasswordToDefault` — 둘 다
109
- * `Boolean(defaultPassword)`), 하나만 쓴다.
110
- */
101
+ /** 서버 지시자에 적힌 인자를 그대로 되묻는다. */
111
102
  private checkGates;
112
103
  updated(changes: Map<string, unknown>): void;
113
104
  /** 관리 권한이 없으면 서버가 이 질의를 거절한다 — 그때도 단추는 없다. */
114
- private fetchPasswordResettable;
105
+ private fetchSetupLinkIssuable;
115
106
  /**
116
107
  * 부여할 수 있는 역할을 읽는다.
117
108
  *
@@ -128,13 +119,12 @@ export declare class UserDetail extends UserDetail_base {
128
119
  private onActivate;
129
120
  private onInactivate;
130
121
  /**
131
- * 비밀번호를 기본값으로 되돌린다.
122
+ * 비밀번호 설정 링크를 발급한다 — 관리자는 비밀번호를 정하지 않는다(ADR-0161).
132
123
  *
133
- * **무엇이 되는지 사람이 알 수 있게 말한다.** 「초기화하시겠습니까」는 그 뒤에 무엇을 알려
134
- * 줘야 하는지를 안 말한다 — 초기화된 비밀번호는 관리자가 상대에게 따로 전해야 한다(서버가
135
- * 메일을 보내지 않는다).
124
+ * 무엇이 일어나는지 · 어떻게 전할지는 그 팝업이 말하고 묻는다. 발급되면 계정이 「설정 대기」가 되므로
125
+ * 목록을 다시 읽게 알린다(`user-updated`).
136
126
  */
137
- private onResetPassword;
127
+ private onPasswordSetup;
138
128
  /**
139
129
  * 이 도메인에서만 뗀다 — 계정은 남는다.
140
130
  *