@things-factory/auth-ui 10.1.213 → 10.1.215
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/auth-style-sign.js +2 -1
- package/dist-client/auth-style-sign.js.map +1 -1
- package/dist-client/components/user-detail.d.ts +9 -0
- package/dist-client/components/user-detail.js +36 -1
- package/dist-client/components/user-detail.js.map +1 -1
- package/dist-client/entries/auth/passkey-offer.d.ts +4 -3
- package/dist-client/entries/auth/passkey-offer.js +26 -23
- package/dist-client/entries/auth/passkey-offer.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/passkey.d.ts +20 -6
- package/dist-client/utils/passkey.js +25 -7
- package/dist-client/utils/passkey.js.map +1 -1
- package/dist-client/utils/passkey.test.js +18 -7
- package/dist-client/utils/passkey.test.js.map +1 -1
- package/package.json +4 -4
- package/tests/auth-text-button-colour.test.ts +27 -0
|
@@ -112,7 +112,8 @@ export const AUTH_STYLE_SIGN = css `
|
|
|
112
112
|
justify-self: flex-start;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
/* Every text button here sits on the primary surface - a link below the form, or a choice in it. */
|
|
116
|
+
.wrap md-text-button {
|
|
116
117
|
--md-text-button-label-text-color: var(--md-sys-color-on-primary);
|
|
117
118
|
--md-text-button-focus-label-text-color: var(--md-sys-color-on-primary);
|
|
118
119
|
--md-text-button-hover-label-text-color: var(--md-sys-color-on-primary);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-style-sign.js","sourceRoot":"","sources":["../client/auth-style-sign.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAEzB,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,CAAA
|
|
1
|
+
{"version":3,"file":"auth-style-sign.js","sourceRoot":"","sources":["../client/auth-style-sign.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAEzB,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwMjC,CAAA","sourcesContent":["import { css } from 'lit'\n\nexport const AUTH_STYLE_SIGN = css`\n :host {\n display: flex;\n background-color: var(--md-sys-color-primary);\n color: var(--md-sys-color-on-primary);\n\n --md-filled-text-field-supporting-text-color: var(--md-sys-color-on-primary);\n }\n\n :host *:focus {\n outline: none;\n }\n\n :host * {\n box-sizing: border-box;\n }\n\n .wrap {\n display: block;\n width: 450px;\n min-width: 350px;\n margin: 0 auto;\n padding-bottom: 100px;\n text-align: center;\n }\n\n .auth-brand {\n color: var(--md-sys-color-on-primary);\n }\n\n .auth-brand img {\n margin: 15% auto 5px auto;\n width: 100px;\n border: 3px solid var(--md-sys-color-on-primary);\n border-radius: 25px;\n box-shadow: var(--box-shadow);\n }\n .name {\n display: block;\n font: var(--auth-brand-name);\n text-shadow: var(--auth-brand-name-shadow);\n }\n .auth-brand .welcome-msg {\n font: var(--auth-brand-welcome-msg);\n }\n .auth-form {\n display: grid;\n grid-gap: var(--spacing-medium);\n grid-template-columns: 1fr 1fr;\n }\n\n form {\n grid-column: 1 / -1;\n display: grid;\n grid-template-columns: 1fr 1fr;\n grid-gap: var(--spacing-medium);\n align-items: center;\n }\n\n h3 {\n grid-column: 1 / -1;\n margin: 50px 0 0 0;\n font: var(--auth-title-font);\n color: var(--auth-title-color, var(--md-sys-color-on-primary));\n text-transform: uppercase;\n }\n\n .field {\n grid-column: 1 / -1;\n text-align: left;\n }\n\n /* Equal columns, one per button - the buttons' own paddings differ, so flex would not share evenly. */\n .submit-buttons-container {\n grid-column: 1 / -1;\n text-align: center;\n\n display: grid;\n grid-auto-flow: column;\n grid-auto-columns: 1fr;\n align-items: center;\n gap: 10px;\n }\n\n /* The passkey button stands beside the submit button, drawn in the page's own on-primary colour. */\n md-outlined-button.passkey {\n --md-outlined-button-outline-color: var(--md-sys-color-on-primary);\n --md-outlined-button-label-text-color: var(--md-sys-color-on-primary);\n --md-outlined-button-icon-color: var(--md-sys-color-on-primary);\n --md-outlined-button-hover-label-text-color: var(--md-sys-color-on-primary);\n --md-outlined-button-hover-icon-color: var(--md-sys-color-on-primary);\n --md-outlined-button-focus-label-text-color: var(--md-sys-color-on-primary);\n --md-outlined-button-focus-icon-color: var(--md-sys-color-on-primary);\n --md-outlined-button-pressed-label-text-color: var(--md-sys-color-on-primary);\n --md-outlined-button-pressed-icon-color: var(--md-sys-color-on-primary);\n }\n\n .field md-filled-text-field {\n grid-column: 1 / -1;\n width: 100%;\n }\n\n md-text-button,\n md-elevated-button {\n grid-column: 1 / -1;\n flex: 1;\n }\n\n .wrap .link {\n text-decoration: none;\n justify-self: flex-start;\n }\n\n /* Every text button here sits on the primary surface - a link below the form, or a choice in it. */\n .wrap md-text-button {\n --md-text-button-label-text-color: var(--md-sys-color-on-primary);\n --md-text-button-focus-label-text-color: var(--md-sys-color-on-primary);\n --md-text-button-hover-label-text-color: var(--md-sys-color-on-primary);\n }\n\n .wrap .link md-icon {\n color: var(--md-sys-color-on-primary);\n }\n\n #locale-area {\n display: flex;\n grid-column: 1 / -1;\n padding: 0 var(--spacing-medium);\n }\n\n #locale-area > label {\n display: flex;\n align-items: center;\n color: var(--md-sys-color-on-primary);\n --md-icon-size: 16px;\n }\n\n #locale-selector {\n font-size: 16px;\n width: 100%;\n }\n\n #locale-selector {\n --i18n-selector-field-border: none;\n --i18n-selector-field-background-color: none;\n --i18n-selector-field-font-size: 14px;\n --i18n-selector-field-color: var(--md-sys-color-on-primary);\n }\n\n .lottie-container {\n width: 100%;\n height: 300px;\n position: absolute;\n left: 0;\n bottom: 0;\n pointer-events: none;\n }\n .lottie-container lottie-player {\n position: absolute;\n bottom: -6%;\n width: 100%;\n height: auto;\n }\n\n @media (max-width: 450px) {\n .wrap {\n width: 85%;\n min-width: 320px;\n }\n .auth-form {\n grid-template-columns: 1fr;\n }\n .auth-brand img {\n margin: 12% auto 5px auto;\n width: 75px;\n }\n h3 {\n margin: 15px 0 0 0;\n }\n .lottie-container {\n overflow: hidden;\n height: 200px;\n pointer-events: none;\n }\n .lottie-container lottie-player {\n width: 1200px;\n left: -30%;\n }\n }\n\n @media screen and (min-width: 1400px) {\n .wrap {\n padding-bottom: 150px;\n }\n }\n @media screen and (min-width: 2500px) {\n .wrap {\n padding-bottom: 280px;\n }\n }\n`\n"]}
|
|
@@ -42,6 +42,13 @@ export declare class UserDetail extends UserDetail_base {
|
|
|
42
42
|
private myEmail;
|
|
43
43
|
/** 기본 비밀번호가 설정돼 있지 않으면 초기화는 서버에서 던진다 — 단추를 내주지 않는다. */
|
|
44
44
|
private defaultPasswordConfigured;
|
|
45
|
+
/**
|
|
46
|
+
* 이 사람의 비밀번호를 이 도메인이 초기화해도 되는가 — 서버 규칙(`password-reset-rule.ts`)의 답.
|
|
47
|
+
*
|
|
48
|
+
* 비밀번호는 계정 하나에 하나다. 다른 도메인에서도 일하는 사람, 어느 도메인이든 오너인 사람
|
|
49
|
+
* (수퍼유저 포함)은 이 도메인이 초기화하지 않는다. 기본 비밀번호를 아는 누구나 그 계정을 가져갈 수 있다.
|
|
50
|
+
*/
|
|
51
|
+
private passwordResettable;
|
|
45
52
|
/**
|
|
46
53
|
* 이 도메인(과 부모)의 역할 id.
|
|
47
54
|
*
|
|
@@ -103,6 +110,8 @@ export declare class UserDetail extends UserDetail_base {
|
|
|
103
110
|
*/
|
|
104
111
|
private checkGates;
|
|
105
112
|
updated(changes: Map<string, unknown>): void;
|
|
113
|
+
/** 관리 권한이 없으면 서버가 이 질의를 거절한다 — 그때도 단추는 없다. */
|
|
114
|
+
private fetchPasswordResettable;
|
|
106
115
|
/**
|
|
107
116
|
* 부여할 수 있는 역할을 읽는다.
|
|
108
117
|
*
|
|
@@ -70,6 +70,13 @@ let UserDetail = class UserDetail extends localize(i18next)(LitElement) {
|
|
|
70
70
|
this.myEmail = '';
|
|
71
71
|
/** 기본 비밀번호가 설정돼 있지 않으면 초기화는 서버에서 던진다 — 단추를 내주지 않는다. */
|
|
72
72
|
this.defaultPasswordConfigured = false;
|
|
73
|
+
/**
|
|
74
|
+
* 이 사람의 비밀번호를 이 도메인이 초기화해도 되는가 — 서버 규칙(`password-reset-rule.ts`)의 답.
|
|
75
|
+
*
|
|
76
|
+
* 비밀번호는 계정 하나에 하나다. 다른 도메인에서도 일하는 사람, 어느 도메인이든 오너인 사람
|
|
77
|
+
* (수퍼유저 포함)은 이 도메인이 초기화하지 않는다. 기본 비밀번호를 아는 누구나 그 계정을 가져갈 수 있다.
|
|
78
|
+
*/
|
|
79
|
+
this.passwordResettable = false;
|
|
73
80
|
/**
|
|
74
81
|
* 이 도메인(과 부모)의 역할 id.
|
|
75
82
|
*
|
|
@@ -237,7 +244,7 @@ let UserDetail = class UserDetail extends localize(i18next)(LitElement) {
|
|
|
237
244
|
*/
|
|
238
245
|
renderDangerZone(user) {
|
|
239
246
|
const me = this.myEmail && user.email === this.myEmail;
|
|
240
|
-
const resettable = this.canManage && this.defaultPasswordConfigured && !me;
|
|
247
|
+
const resettable = this.canManage && this.defaultPasswordConfigured && this.passwordResettable && !me;
|
|
241
248
|
const transferable = this.canOwn &&
|
|
242
249
|
!user.owner &&
|
|
243
250
|
user.status === 'activated' &&
|
|
@@ -373,6 +380,30 @@ let UserDetail = class UserDetail extends localize(i18next)(LitElement) {
|
|
|
373
380
|
updated(changes) {
|
|
374
381
|
if (changes.has('user')) {
|
|
375
382
|
this.fetch();
|
|
383
|
+
this.fetchPasswordResettable();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/** 관리 권한이 없으면 서버가 이 질의를 거절한다 — 그때도 단추는 없다. */
|
|
387
|
+
async fetchPasswordResettable() {
|
|
388
|
+
const id = this.user?.id;
|
|
389
|
+
this.passwordResettable = false;
|
|
390
|
+
if (!id) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
try {
|
|
394
|
+
const response = await client.query({
|
|
395
|
+
query: gql `
|
|
396
|
+
query ($id: String!) {
|
|
397
|
+
mayResetPasswordToDefault(id: $id)
|
|
398
|
+
}
|
|
399
|
+
`,
|
|
400
|
+
variables: { id },
|
|
401
|
+
context: gqlContext()
|
|
402
|
+
});
|
|
403
|
+
this.passwordResettable = this.user?.id === id && response.data?.mayResetPasswordToDefault === true;
|
|
404
|
+
}
|
|
405
|
+
catch (e) {
|
|
406
|
+
this.passwordResettable = false;
|
|
376
407
|
}
|
|
377
408
|
}
|
|
378
409
|
/**
|
|
@@ -635,6 +666,10 @@ __decorate([
|
|
|
635
666
|
state(),
|
|
636
667
|
__metadata("design:type", Boolean)
|
|
637
668
|
], UserDetail.prototype, "defaultPasswordConfigured", void 0);
|
|
669
|
+
__decorate([
|
|
670
|
+
state(),
|
|
671
|
+
__metadata("design:type", Boolean)
|
|
672
|
+
], UserDetail.prototype, "passwordResettable", void 0);
|
|
638
673
|
__decorate([
|
|
639
674
|
state(),
|
|
640
675
|
__metadata("design:type", Set)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user-detail.js","sourceRoot":"","sources":["../../client/components/user-detail.ts"],"names":[],"mappings":";AAAA,OAAO,4BAA4B,CAAA;AACnC,OAAO,yCAAyC,CAAA;AAChD,OAAO,yCAAyC,CAAA;AAChD,OAAO,oCAAoC,CAAA;AAE3C,OAAO,+BAA+B,CAAA;AAEtC,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAC3D,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,KAAK,CAAA;AACpD,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAIlE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAC3C,OAAO,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAA;AACtD,OAAO,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAC1D,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACxE,OAAO,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAA;AAEpE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEI,IAAM,UAAU,GAAhB,MAAM,UAAW,SAAQ,QAAQ,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC;IAAtD;;QA8FY,mBAAc,GAAU,EAAE,CAAA;QAC1B,aAAQ,GAAgB,IAAI,GAAG,EAAE,CAAA;QACjC,YAAO,GAAY,IAAI,CAAA;QACvB,WAAM,GAAY,KAAK,CAAA;QAExC;;;;;;;;;;;;;;;;;;;WAmBG;QACc,cAAS,GAAY,KAAK,CAAA;QAC1B,WAAM,GAAY,KAAK,CAAA;QACvB,YAAO,GAAW,EAAE,CAAA;QAErC,uDAAuD;QACtC,8BAAyB,GAAY,KAAK,CAAA;QAE3D;;;;;;WAMG;QACc,kBAAa,GAAgB,IAAI,GAAG,EAAE,CAAA;QACvD,0GAA0G;QACzF,WAAM,GAAgB,IAAI,GAAG,EAAE,CAAA;IAqflD,CAAC;aA3nBQ,WAAM,GAAmB;QAC9B,eAAe;QACf,qBAAqB;QACrB,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAoFF;KACF,AAxFY,CAwFZ;IAiDD;;;;;;;;;;OAUG;IACH,IAAY,OAAO;QACjB,OAAO,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,CAAA;IACxE,CAAC;IAED,MAAM;QACJ,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,EAAE,CAAA;QAE5B,OAAO,IAAI,CAAA;sBACO,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,KAAK;iBAC7C,IAAI,CAAC,KAAK;;QAGnB,IAAI,CAAC,KAAK;YACR,CAAC,CAAC,IAAI,CAAA;;gBAEA,OAAO,CAAC,CAAC,CAAC,4CAA4C,CAAC;mBACpD;YACT,CAAC,CAAC,OACN;QACE,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC;;qBAEhB,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC;QACrC,IAAI,CAAC,WAAW,EAAE;;;wCAGc,CAAC,IAAI,CAAC,OAAO,WAAW,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE;YACvE,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC;;qCAEC,GAAG,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,OAAO,CAAC,CAAC;YAC3E,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC;;;;QAI9B,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC;KAC9B,CAAA;IACH,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;OAqBG;IACK,gBAAgB,CAAC,IAAS;QAChC,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,OAAO,CAAA;QAEtD,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,yBAAyB,IAAI,CAAC,EAAE,CAAA;QAC1E,MAAM,YAAY,GAChB,IAAI,CAAC,MAAM;YACX,CAAC,IAAI,CAAC,KAAK;YACX,IAAI,CAAC,MAAM,KAAK,WAAW;YAC3B,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,IAAS,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAA;QACzE,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,EAAE,CAAA;QAEtD,IAAI,CAAC,UAAU,IAAI,CAAC,YAAY,IAAI,CAAC,SAAS,EAAE,CAAC;YAC/C,OAAO,OAAO,CAAA;QAChB,CAAC;QAED,OAAO,IAAI,CAAA;;uBAEQ,OAAO,CAAC,CAAC,CAAC,uBAAuB,CAAC;;YAG7C,UAAU;YACR,CAAC,CAAC,IAAI,CAAA,8BAA8B,GAAG,EAAE,CAAC,IAAI,CAAC,eAAe,EAAE;oBAC1D,OAAO,CAAC,CAAC,CAAC,sBAAsB,CAAC;sCACf;YACxB,CAAC,CAAC,OACN;YAEE,YAAY;YACV,CAAC,CAAC,IAAI,CAAA,8BAA8B,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE;oBACrD,OAAO,CAAC,CAAC,CAAC,uBAAuB,CAAC;sCAChB;YACxB,CAAC,CAAC,OACN;YAEE,SAAS;YACP,CAAC,CAAC,IAAI,CAAA,8BAA8B,GAAG,EAAE,CAAC,IAAI,CAAC,kBAAkB,EAAE;oBAC7D,OAAO,CAAC,CAAC,CAAC,2BAA2B,CAAC;sCACpB;YACxB,CAAC,CAAC,OACN;;;KAGL,CAAA;IACH,CAAC;IAED;;;;;;;;;;OAUG;IACK,kBAAkB,CAAC,IAAS;QAClC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAA;QAE1B,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACpB,OAAO,OAAO,CAAA;QAChB,CAAC;QAED,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;YACxB,OAAO,IAAI,CAAA;qCACoB,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC;aAC9E,CAAA;QACT,CAAC;QAED,IAAI,MAAM,KAAK,WAAW,EAAE,CAAC;YAC3B,OAAO,IAAI,CAAA;qCACoB,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,OAAO,CAAC,CAAC,CAAC,iBAAiB,CAAC;aAChF,CAAA;QACT,CAAC;QAED,qDAAqD;QACrD,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YAChE,OAAO,OAAO,CAAA;QAChB,CAAC;QAED,OAAO,IAAI,CAAA;0CAC2B,GAAG,EAAE,CAAC,IAAI,CAAC,YAAY,EAAE;UACzD,OAAO,CAAC,CAAC,CAAC,mBAAmB,CAAC;;WAE7B,CAAA;IACT,CAAC;IAEO,WAAW;QACjB,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,OAAO,IAAI,CAAA,YAAY,OAAO,CAAC,CAAC,CAAC,cAAc,CAAC,QAAQ,CAAA;QAC1D,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,CAAC;YAChC;;;eAGG;YACH,OAAO,IAAI,CAAA,uBAAuB,OAAO,CAAC,CAAC,CAAC,qCAAqC,CAAC,QAAQ,CAAA;QAC5F,CAAC;QAED,OAAO,IAAI,CAAA;;UAEL,IAAI,CAAC,cAAc,CAAC,GAAG,CACvB,IAAI,CAAC,EAAE,CAAC,IAAI,CAAA;;;2BAGK,IAAI,CAAC,EAAE;2BACP,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;4BACzB,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;0BAC1B,CAAC,CAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAG,CAAC,CAAC,MAA2B,CAAC,OAAO,CAAC;;iCAErE,IAAI,CAAC,EAAE,KAAK,IAAI,CAAC,IAAI;gBACtC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAA,cAAc,IAAI,CAAC,WAAW,SAAS,CAAC,CAAC,CAAC,OAAO;;WAE7E,CACF;;QAED,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAC1D,CAAC,CAAC,IAAI,CAAA,aAAa,OAAO,CAAC,CAAC,CAAC,oDAAoD,CAAC,QAAQ;YAC1F,CAAC,CAAC,OAAO;KACZ,CAAA;IACH,CAAC;IAED,iBAAiB;QACf,KAAK,CAAC,iBAAiB,EAAE,CAAA;QACzB,IAAI,CAAC,OAAO,EAAE,CAAA;QACd,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAA;QACxD,IAAI,CAAC,UAAU,EAAE,CAAA;IACnB,CAAC;IAED,oBAAoB;QAClB,KAAK,CAAC,oBAAoB,EAAE,CAAA;QAC5B,IAAI,CAAC,WAAW,EAAE,EAAE,CAAA;IACtB,CAAC;IAEO,OAAO;QACb,MAAM,EAAE,GAAI,KAAK,CAAC,QAAQ,EAAU,CAAC,IAAI,EAAE,IAAI,IAAI,EAAE,CAAA;QACrD,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC,KAAK,IAAI,EAAE,CAAA;IAC/B,CAAC;IAED;;;;;;OAMG;IACK,KAAK,CAAC,UAAU;QACtB,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,MAAM,YAAY,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,kBAAkB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;QACnH,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,YAAY,CAAC,EAAE,kBAAkB,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;QAE/F,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;YAClC,KAAK,EAAE,GAAG,CAAA;;;;OAIT;YACD,OAAO,EAAE,UAAU,EAAE;SACtB,CAAC,CAAA;QAEF,IAAI,CAAC,yBAAyB,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC,CAAA;IAC/E,CAAC;IAED,OAAO,CAAC,OAA6B;QACnC,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YACxB,IAAI,CAAC,KAAK,EAAE,CAAA;QACd,CAAC;IACH,CAAC;IAED;;;;;;;;;;OAUG;IACK,KAAK,CAAC,KAAK;QACjB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAA;QACtB,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC;YACd,OAAM;QACR,CAAC;QAED,IAAI,CAAC,OAAO,GAAG,IAAI,CAAA;QAEnB,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;YAClC,KAAK,EAAE,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;OAuBT;YACD,SAAS,EAAE,EAAE,SAAS,EAAE,kBAAkB,CAAC,OAAO,EAAE;YACpD,OAAO,EAAE,UAAU,EAAE;SACtB,CAAC,CAAA;QAEF,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,IAAI,EAAE,CAAA;QAC/C,MAAM,OAAO,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,YAAY,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QAE3F,0CAA0C;QAC1C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAe,CAAA;QACnC,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,KAAK,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC;YAC1C,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAA;QACzB,CAAC;QAED,IAAI,CAAC,cAAc,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAA;QACxC,2DAA2D;QAC3D,IAAI,CAAC,aAAa,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAS,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAA;QAC/D,IAAI,CAAC,QAAQ,GAAG,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;QACjE,IAAI,CAAC,MAAM,GAAG,iBAAiB,CAC7B,CAAC,QAAQ,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAS,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EACrE,QAAQ,CAAC,IAAI,EAAE,oBAAoB,KAAK,IAAI,CAC7C,CAAA;QACD,IAAI,CAAC,OAAO,GAAG,KAAK,CAAA;IACtB,CAAC;IAED,yDAAyD;IACjD,KAAK,CAAC,UAAU;QACtB,MAAM,IAAI,CAAC,eAAe,CAAC,GAAG,CAAA;;;;KAI7B,CAAC,CAAA;IACJ,CAAC;IAEO,KAAK,CAAC,YAAY;QACxB,MAAM,IAAI,CAAC,eAAe,CAAC,GAAG,CAAA;;;;KAI7B,CAAC,CAAA;IACJ,CAAC;IAED;;;;;;OAMG;IACK,KAAK,CAAC,eAAe;QAC3B,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC;YACpC,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,sBAAsB,CAAC;YACxC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,4DAA4D,CAAC;YAC7E,aAAa,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC,EAAE;YACpD,YAAY,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,EAAE;SACnD,CAAC,CAAA;QAEF,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAM;QACR,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,eAAe,CACrC,GAAG,CAAA;;;;OAIF,EACD,EAAE,MAAM,EAAE,KAAK,EAAE,CAClB,CAAA;QAED,IAAI,IAAI,EAAE,CAAC;YACT,MAAM,QAAQ,CAAC,IAAI,CAAC;gBAClB,IAAI,EAAE,SAAS;gBACf,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC;gBAClC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,6BAA6B,CAAC;gBAC9C,aAAa,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC,EAAE;aACrD,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACK,KAAK,CAAC,kBAAkB;QAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,EAAE,CAAA;QAE5B,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC;YACpC,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,2BAA2B,CAAC;YAC7C,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,2DAA2D,CAAC;YAC5E,aAAa,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC,EAAE;YACpD,YAAY,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,EAAE;SACnD,CAAC,CAAA;QAEF,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAM;QACR,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,GAAG,CAAA;;;;KAI1C,CAAC,CAAA;QAEF,IAAI,IAAI,EAAE,CAAC;YACT,wCAAwC;YACxC,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,OAAO,CAAC,CAAC,CAAA;QAC9C,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACK,UAAU;QAChB,MAAM,KAAK,GAAG,SAAS,CACrB,IAAI,CAAA;gBACM,IAAI,CAAC,IAAI;gCACO,GAAG,EAAE;YAC3B,KAAK,CAAC,KAAK,EAAE,CAAA;YACb,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;QAC3G,CAAC;mCAC0B,EAC7B;YACE,QAAQ,EAAE,IAAI;YACd,IAAI,EAAE,OAAO;YACb,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,sBAAsB,CAAC;SACzC,CACF,CAAA;IACH,CAAC;IAEO,KAAK,CAAC,eAAe,CAAC,QAAa,EAAE,EAAE,MAAM,GAAG,IAAI,KAA2B,EAAE;QACvF,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,EAAE,QAAQ,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,CAAA;QACxD,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAC7B,OAAO,KAAK,CAAA;QACd,CAAC;QAED,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;QAElB,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,CAAC,CAAA;YAElG,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;gBACpB,OAAO,KAAK,CAAA;YACd,CAAC;YAED;;;eAGG;YACH,IAAI,MAAM,EAAE,CAAC;gBACX,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;YAC3G,CAAC;YAED,OAAO,IAAI,CAAA;QACb,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,MAAM,GAAG,KAAK,CAAA;QACrB,CAAC;IACH,CAAC;IAEO,QAAQ,CAAC,EAAU,EAAE,OAAgB;QAC3C,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QACnC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;QACxC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAA;IACtB,CAAC;IAED;;;;;OAKG;IACK,KAAK,CAAC,MAAM;QAClB,8CAA8C;QAC9C,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YAClB,OAAM;QACR,CAAC;QAED,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAA;QACtB,MAAM,QAAQ,GAAG,IAAI,EAAE,QAAQ,IAAI,IAAI,EAAE,KAAK,CAAA;QAC9C,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,OAAM;QACR,CAAC;QAED,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;QAElB,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;gBACnC,QAAQ,EAAE,GAAG,CAAA;;;;;;SAMZ;gBACD,SAAS,EAAE;oBACT,QAAQ;oBACR,cAAc,EAAE,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;oBAClE,aAAa,EAAE,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;iBAC7G;gBACD,OAAO,EAAE,UAAU,EAAE;aACtB,CAAC,CAAA;YAEF,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;gBACrB,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;YACtG,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,MAAM,GAAG,KAAK,CAAA;QACrB,CAAC;IACH,CAAC;;AA/hB2B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;wCAAU;AAEpB;IAAhB,KAAK,EAAE;;kDAAmC;AAC1B;IAAhB,KAAK,EAAE;8BAAmB,GAAG;4CAAoB;AACjC;IAAhB,KAAK,EAAE;;2CAAgC;AACvB;IAAhB,KAAK,EAAE;;0CAAgC;AAsBvB;IAAhB,KAAK,EAAE;;6CAAmC;AAC1B;IAAhB,KAAK,EAAE;;0CAAgC;AACvB;IAAhB,KAAK,EAAE;;2CAA6B;AAGpB;IAAhB,KAAK,EAAE;;6DAAmD;AAS1C;IAAhB,KAAK,EAAE;8BAAwB,GAAG;iDAAoB;AAEtC;IAAhB,KAAK,EAAE;8BAAiB,GAAG;0CAAoB;AAvIrC,UAAU;IADtB,aAAa,CAAC,aAAa,CAAC;GAChB,UAAU,CA4nBtB","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@material/web/button/elevated-button.js'\nimport '@material/web/button/outlined-button.js'\nimport '@material/web/checkbox/checkbox.js'\n\nimport './ownership-transfer-popup.js'\n\nimport gql from 'graphql-tag'\nimport { lockedParentRoles } from '../utils/role-origin.js'\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 { OxPrompt } from '@operato/popup/ox-prompt.js'\nimport { InheritedValueType, store } from '@operato/shell'\nimport { ButtonContainerStyles, ScrollbarStyles } from '@operato/styles'\nimport { hasPrivilege } from '@things-factory/auth-base/dist-client'\n\n/**\n * 한 사람의 역할을 고치는 자리.\n *\n * ── 담는 그릇을 모른다 ──────────────────────────────────────────────────────\n * 팝업이든 서랍이든 그 안에 들어간다. 그래서 여기서는 **아무것도 열거나 닫지 않고** 두 가지만\n * 알린다 — 저장이 끝났다(`user-updated`), 닫아 달라(`close`). 그릇이 어느 것이 될지는 아직\n * 정해지지 않았고, 그 결정이 이 파일을 바꾸지 않게 한다.\n *\n * ── 옛 화면이 하던 것 (`components/user-role-editor.ts`) ────────────────────\n * 저장할 때 `querySelectorAll('role-selector')` 로 **화면을 훑어** 「가능한 역할 전부」와\n * 「고른 역할」을 만들어 보냈다.\n *\n * `updateUserRoles` 의 계약은 이렇다 — `availableRoles` 는 **부르는 쪽이 책임지는 범위**이고,\n * 그 범위 안의 역할을 일단 다 뗀 다음 `selectedRoles` 를 다시 붙인다. 범위 밖의 역할은\n * 건드리지 않는다(`user-mutation.ts` 의 `updateUserRoles`).\n *\n * 계약 자체는 옳다 — 화면이 일부만 편집할 수 있게 해 준다. 잘못된 것은 **그 범위를 DOM 에서\n * 만든 것**이다. 목록이 늦게 오거나 잘려 있으면 화면은 그것을 모른 채 좁은 범위를 보내고,\n * 안 보낸 역할이 어떻게 되는지가 그날의 렌더링에 달린다.\n *\n * 여기서는 범위를 **상태에서** 만든다. 읽어 온 목록이 곧 범위이고, 아직 못 읽었으면 저장하지\n * 않는다.\n */\n@customElement('user-detail')\nexport class UserDetail extends localize(i18next)(LitElement) {\n static styles: CSSResultGroup = [\n ScrollbarStyles,\n ButtonContainerStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-medium, 8px);\n padding: var(--spacing-large, 12px);\n background-color: var(--md-sys-color-surface);\n color: var(--md-sys-color-on-surface);\n overflow: auto;\n }\n\n [headline] {\n font: var(--headline-font, inherit);\n font-size: 1.05rem;\n font-weight: 600;\n }\n\n [sub] {\n color: var(--md-sys-color-on-surface-variant);\n font-size: 0.85rem;\n }\n\n [section] {\n margin-top: var(--spacing-medium, 8px);\n color: var(--md-sys-color-on-surface-variant);\n font-size: 0.8rem;\n text-transform: capitalize;\n }\n\n ul[roles] {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n }\n\n ul[roles] li {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 5px 2px;\n }\n\n ul[roles] li [desc] {\n color: var(--md-sys-color-on-surface-variant);\n font-size: 0.8rem;\n }\n\n /*\n 색은 짝으로 쓰고, 「무엇이 나를 기다리나」에만 쓴다.\n 소유자 안내는 조치가 필요한 것이 아니라 사실이므로 조용한 면을 쓴다.\n */\n [note] {\n padding: 8px 10px;\n border-radius: var(--md-sys-shape-corner-small, 6px);\n font-size: 0.82rem;\n background-color: var(--md-sys-color-surface-variant);\n color: var(--md-sys-color-on-surface-variant);\n }\n\n [note][attention] {\n background-color: var(--md-sys-color-error-container);\n color: var(--md-sys-color-on-error-container);\n }\n\n /*\n 되돌릴 수 없는 조치 — 선으로 갈라 두고, 색은 글자에만 쓴다.\n 면을 error 로 칠하면 「지금 문제가 있다」로 읽힌다.\n */\n [danger-zone] {\n margin-top: var(--spacing-large, 12px);\n border-top: 1px solid var(--md-sys-color-outline-variant);\n padding-top: var(--spacing-small, 4px);\n }\n\n [danger-zone] md-outlined-button {\n --md-outlined-button-label-text-color: var(--md-sys-color-error);\n --md-outlined-button-outline-color: var(--md-sys-color-outline-variant);\n --md-outlined-button-hover-label-text-color: var(--md-sys-color-error);\n --md-outlined-button-hover-state-layer-color: var(--md-sys-color-error);\n --md-outlined-button-pressed-label-text-color: var(--md-sys-color-error);\n }\n `\n ]\n\n /** 목록이 골라 준 사람. `id` 와 `username`(없으면 `email`)이 있어야 한다. */\n @property({ type: Object }) user: any\n\n @state() private availableRoles: any[] = []\n @state() private selected: Set<string> = new Set()\n @state() private loading: boolean = true\n @state() private saving: boolean = false\n\n /*\n * **내가 이 도메인에서 무엇을 할 수 있나 — 서버 선언을 그대로 되묻는다.**\n *\n * `/auth/profile` 이 `privileges`·`owner`·`super` 를 전부 담아 주고, `hasPrivilege`\n * (`auth-base/client/profiled.ts`)가 서버의 `checkPermission` 과 같은 규칙으로 그것을 판정한다\n * — 안전하지 않은 IP 에서 소유권 우회를 빼는 것까지 같다. 그래서 **서버 지시자에 적힌 인자를\n * 그대로 넘기는 것**이 화면과 문을 어긋나지 않게 하는 방법이다.\n *\n * 옛 화면은 이것을 두고 `me.email === domainOwner.email` 로 판정했다 — 목록에서 뽑은 대표\n * 소유자 한 명과 견주는 것이라 **공동 소유자가 자기 권한을 잃었고**, `user:mutation` 을 역할로\n * 받은 사람은 아예 단추를 못 봤다.\n *\n * 둘이 다른 선언을 쓴다.\n *\n * 활성/비활성·초기화·제외 user:mutation + domainOwnerGranted\n * 소유권 이전 domainOwnerGranted + superUserGranted (권한 쌍이 없다)\n *\n * ⚠ 수퍼유저가 자동으로 다 통과하지 않는다 — `checkPermission` 은 지시자가\n * `superUserGranted` 를 선언한 곳에서만 그것을 본다. 그래서 위 둘을 뭉치면 안 된다.\n */\n @state() private canManage: boolean = false\n @state() private canOwn: boolean = false\n @state() private myEmail: string = ''\n\n /** 기본 비밀번호가 설정돼 있지 않으면 초기화는 서버에서 던진다 — 단추를 내주지 않는다. */\n @state() private defaultPasswordConfigured: boolean = false\n\n /**\n * 이 도메인(과 부모)의 역할 id.\n *\n * 소유권 이전은 **받는 사람이 이 도메인에 역할을 하나라도 가질 때만** 된다\n * (`transferOwner` 가 상속 역할까지 자격으로 인정하고, 그 밖은 던진다). 파트너십으로 넘어온\n * 역할은 자격이 아니므로 `availableRoles`(합친 것)로는 판정할 수 없다.\n */\n @state() private domainRoleIds: Set<string> = new Set()\n /** The parent domain's roles this person may not change here - locked, with the reason under the list. */\n @state() private locked: Set<string> = new Set()\n private unsubscribe?: () => void\n\n /*\n * **저장할 수 있나 — 단추와 처리기가 같은 답을 본다.**\n *\n * 셋을 갈라야 한다. 화면에서 갈라 놓고 저장에서 뭉치면, 「아직 못 읽었다」와 「읽었는데\n * 역할이 없다」가 똑같이 **눌러도 아무 일이 없는 단추**가 된다. 그것이 조용히 되돌아가는\n * 자리이고, 사람은 저장이 됐는지 안 됐는지 알 수 없다.\n *\n * 못 읽었다 아직 범위를 모른다 — 눌리면 안 된다\n * 역할이 0개 보낼 것이 없다 — 눌리면 안 되고, 무엇을 해야 하는지는 목록 자리가 말한다\n * 읽었고 있다 저장한다\n */\n private get savable(): boolean {\n return !this.loading && !this.saving && this.availableRoles.length > 0\n }\n\n render() {\n const user = this.user || {}\n\n return html`\n <div headline>${user.name || user.username || user.email}</div>\n <div sub>${user.email}</div>\n\n ${\n user.owner\n ? html`<div note>\n <md-icon style=\"--md-icon-size: 16px; vertical-align: -3px;\">supervisor_account</md-icon>\n ${i18next.t('text.owner opens everything without a role')}\n </div>`\n : nothing\n }\n ${this.renderAccountState(user)}\n\n <div section>${i18next.t('field.roles')}</div>\n ${this.renderRoles()}\n\n <div class=\"button-container\">\n <md-elevated-button ?disabled=${!this.savable} @click=${() => this.onSave()}>\n ${i18next.t('button.save')}\n </md-elevated-button>\n <md-elevated-button @click=${() => this.dispatchEvent(new CustomEvent('close'))}>\n ${i18next.t('button.cancel')}\n </md-elevated-button>\n </div>\n\n ${this.renderDangerZone(user)}\n `\n }\n\n /**\n * 되돌릴 수 없는 조치.\n *\n * ── 왜 아래에 따로 두나 ─────────────────────────────────────────────────────\n * 옛 화면은 저장·소유권 이전·초기화·제외·비활성화를 **한 줄에 나란히** 뒀다. 매일 누르는\n * 「저장」 옆에 한 번 누르면 끝인 「제외」가 같은 크기로 있었다.\n *\n * 그래서 선을 하나 긋고, 위는 채운 단추(주 동작), 아래는 테두리 단추로 둔다. 색은\n * error 짝으로 글자에만 쓴다 — 면을 다 칠하면 「지금 문제가 있다」로 읽힌다.\n *\n * ── 무엇을 언제 내주나 ──────────────────────────────────────────────────────\n * 조건은 전부 **서버가 실제로 던지는 것**을 그대로 옮긴 것이다. 눌리는 단추를 보여 주고\n * 서버가 거절하면 그것은 안내가 아니라 오류다.\n *\n * 비밀번호 초기화 기본 비밀번호가 설정돼 있고, 내가 아니다\n * 소유권 이전 활성 상태고, 소유자가 아니고, 이 도메인에 역할이 하나라도 있다\n * 도메인에서 제외 소유자가 아니고, 내가 아니다\n *\n * ── 이름 ────────────────────────────────────────────────────────────────────\n * `deleteDomainUser` 는 계정을 지우지 않는다 — **이 도메인에서만 뗀다**(서버 설명도 그렇게\n * 적혀 있다). 옛 화면의 「사용자 삭제」는 하지 않는 일을 말하고 있었다.\n */\n private renderDangerZone(user: any) {\n const me = this.myEmail && user.email === this.myEmail\n\n const resettable = this.canManage && this.defaultPasswordConfigured && !me\n const transferable =\n this.canOwn &&\n !user.owner &&\n user.status === 'activated' &&\n (user.roles || []).some((role: any) => this.domainRoleIds.has(role.id))\n const removable = this.canManage && !user.owner && !me\n\n if (!resettable && !transferable && !removable) {\n return nothing\n }\n\n return html`\n <div danger-zone>\n <div section>${i18next.t('text.cannot be undone')}</div>\n <div class=\"button-container\">\n ${\n resettable\n ? html`<md-outlined-button @click=${() => this.onResetPassword()}>\n ${i18next.t('title.reset password')}\n </md-outlined-button>`\n : nothing\n }\n ${\n transferable\n ? html`<md-outlined-button @click=${() => this.onTransfer()}>\n ${i18next.t('button.transfer owner')}\n </md-outlined-button>`\n : nothing\n }\n ${\n removable\n ? html`<md-outlined-button @click=${() => this.onRemoveFromDomain()}>\n ${i18next.t('button.remove from domain')}\n </md-outlined-button>`\n : nothing\n }\n </div>\n </div>\n `\n }\n\n /**\n * 계정 상태와, 그것을 되돌리는 단추.\n *\n * **잠금 해제와 활성화는 서버에서 같은 것이다** — `activateUser` 가 `failCount` 를 0 으로\n * 두고 `ACTIVATED` 로 되돌린다. 그래서 부르는 것은 하나이고, **글자만 상황에 맞춰 바꾼다.**\n * 잠긴 사람에게 「활성화」라고 하면 무슨 일이 일어나는지 알 수 없다.\n *\n * 비활성화는 소유자와 나 자신에게는 내주지 않는다. 서버도 같은 것을 막지만\n * (`inactivateUser` 가 소유자·admin 을 거절한다), **화면이 눌리는 단추를 보여 주고 서버가\n * 거절하면 그것은 안내가 아니라 오류다.**\n */\n private renderAccountState(user: any) {\n const status = user.status\n\n if (!this.canManage) {\n return nothing\n }\n\n if (status === 'locked') {\n return html`<div class=\"button-container\">\n <md-elevated-button @click=${() => this.onActivate()}> ${i18next.t('button.unlock')} </md-elevated-button>\n </div>`\n }\n\n if (status !== 'activated') {\n return html`<div class=\"button-container\">\n <md-elevated-button @click=${() => this.onActivate()}> ${i18next.t('button.activate')} </md-elevated-button>\n </div>`\n }\n\n /* 활성 상태 — 되돌릴 수 있는 사람에게만, 그리고 자기 자신과 소유자에게는 안 준다. */\n if (user.owner || (this.myEmail && user.email === this.myEmail)) {\n return nothing\n }\n\n return html`<div class=\"button-container\">\n <md-elevated-button danger @click=${() => this.onInactivate()}>\n ${i18next.t('button.deactivate')}\n </md-elevated-button>\n </div>`\n }\n\n private renderRoles() {\n if (this.loading) {\n return html`<div sub>${i18next.t('text.loading')}</div>`\n }\n\n if (!this.availableRoles.length) {\n /*\n * 「역할이 없다」와 「못 읽었다」는 다른 말이다. 여기까지 왔다는 것은 읽었다는 뜻이므로\n * 없는 것이 맞고, 그때는 무엇을 해야 하는지를 말한다.\n */\n return html`<div note attention>${i18next.t('text.no role defined in this domain')}</div>`\n }\n\n return html`\n <ul roles>\n ${this.availableRoles.map(\n role => html`\n <li>\n <md-checkbox\n id=\"role-${role.id}\"\n .checked=${this.selected.has(role.id)}\n ?disabled=${this.locked.has(role.id)}\n @change=${(e: Event) => this.onToggle(role.id, (e.target as HTMLInputElement).checked)}\n ></md-checkbox>\n <label for=\"role-${role.id}\">${role.name}</label>\n ${role.description ? html`<span desc>${role.description}</span>` : nothing}\n </li>\n `\n )}\n </ul>\n ${this.availableRoles.some(role => this.locked.has(role.id))\n ? html`<div note>${i18next.t('text.parent roles are changed by the parent domain')}</div>`\n : nothing}\n `\n }\n\n connectedCallback() {\n super.connectedCallback()\n this.applyMe()\n this.unsubscribe = store.subscribe(() => this.applyMe())\n this.checkGates()\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n this.unsubscribe?.()\n }\n\n private applyMe() {\n const me = (store.getState() as any).auth?.user || {}\n this.myEmail = me.email || ''\n }\n\n /**\n * 서버 지시자에 적힌 인자를 그대로 되묻는다.\n *\n * 기본 비밀번호 여부도 여기서 함께 읽는다. 서버에 같은 답을 주는 질의가 둘 있는데\n * (`checkDefaultPassword` 와 `checkResettablePasswordToDefault` — 둘 다\n * `Boolean(defaultPassword)`), 하나만 쓴다.\n */\n private async checkGates() {\n this.canManage = Boolean(await hasPrivilege({ category: 'user', privilege: 'mutation', domainOwnerGranted: true }))\n this.canOwn = Boolean(await hasPrivilege({ domainOwnerGranted: true, superUserGranted: true }))\n\n const response = await client.query({\n query: gql`\n query {\n checkDefaultPassword\n }\n `,\n context: gqlContext()\n })\n\n this.defaultPasswordConfigured = Boolean(response.data?.checkDefaultPassword)\n }\n\n updated(changes: Map<string, unknown>) {\n if (changes.has('user')) {\n this.fetch()\n }\n }\n\n /**\n * 부여할 수 있는 역할을 읽는다.\n *\n * **이 사람이 지금 가진 역할은 다시 묻지 않는다** — 목록이 이미 실어 왔다(`user.roles`).\n * 옛 화면은 email 로 사용자를 다시 찾았는데, 역할 이름과 달리 email 은 유일하긴 해도\n * 손에 든 `id` 를 두고 다른 열쇠로 같은 사람을 찾는 것이었다. 안 물으면 그 문제가 없다.\n *\n * **이 도메인의 역할과 파트너십으로 넘어온 역할을 한 목록으로 합친다.** 옛 화면은 그 둘을\n * 따로 그리고 저장할 때 화면에서 다시 모았는데, 합치는 자리가 화면이면 화면이 못 그린 것은\n * 저장에서도 빠진다.\n */\n private async fetch() {\n const user = this.user\n if (!user?.id) {\n return\n }\n\n this.loading = true\n\n const response = await client.query({\n query: gql`\n query ($inherited: InheritedValueType) {\n roles(inherited: $inherited) {\n items {\n id\n name\n description\n }\n }\n grantedRoles {\n role {\n id\n name\n description\n }\n }\n parentRoles: roles(inherited: Only) {\n items {\n id\n }\n }\n mayChangeParentRoles\n }\n `,\n variables: { inherited: InheritedValueType.Include },\n context: gqlContext()\n })\n\n const roles = response.data?.roles?.items || []\n const granted = (response.data?.grantedRoles || []).map((g: any) => g.role).filter(Boolean)\n\n /* 같은 역할이 두 경로로 올 수 있다 — id 로 한 번만 담는다. */\n const byId = new Map<string, any>()\n for (const role of [...roles, ...granted]) {\n byId.set(role.id, role)\n }\n\n this.availableRoles = [...byId.values()]\n /* 소유권 이전 자격은 **이 도메인(과 부모)의 역할**만 인정한다 — 합친 목록으로는 못 본다. */\n this.domainRoleIds = new Set(roles.map((role: any) => role.id))\n this.selected = new Set((user.roles || []).map((r: any) => r.id))\n this.locked = lockedParentRoles(\n (response.data?.parentRoles?.items || []).map((role: any) => role.id),\n response.data?.mayChangeParentRoles === true\n )\n this.loading = false\n }\n\n /** 잠금 해제와 활성화가 같은 부름이다 — 서버가 `failCount` 도 0 으로 되돌린다. */\n private async onActivate() {\n await this.runUserMutation(gql`\n mutation ($username: String!) {\n activateUser(username: $username)\n }\n `)\n }\n\n private async onInactivate() {\n await this.runUserMutation(gql`\n mutation ($username: String!) {\n inactivateUser(username: $username)\n }\n `)\n }\n\n /**\n * 비밀번호를 기본값으로 되돌린다.\n *\n * **무엇이 되는지 사람이 알 수 있게 말한다.** 「초기화하시겠습니까」는 그 뒤에 무엇을 알려\n * 줘야 하는지를 안 말한다 — 초기화된 비밀번호는 관리자가 상대에게 따로 전해야 한다(서버가\n * 메일을 보내지 않는다).\n */\n private async onResetPassword() {\n const confirmed = await OxPrompt.open({\n type: 'warning',\n title: i18next.t('title.reset password'),\n text: i18next.t('text.password becomes the default and must be told to them'),\n confirmButton: { text: i18next.t('button.confirm') },\n cancelButton: { text: i18next.t('button.cancel') }\n })\n\n if (!confirmed) {\n return\n }\n\n const done = await this.runUserMutation(\n gql`\n mutation ($username: String!) {\n resetPasswordToDefault(username: $username)\n }\n `,\n { notify: false }\n )\n\n if (done) {\n await OxPrompt.open({\n type: 'success',\n title: i18next.t('text.completed'),\n text: i18next.t('text.password reset succeed'),\n confirmButton: { text: i18next.t('button.confirm') }\n })\n }\n }\n\n /**\n * 이 도메인에서만 뗀다 — 계정은 남는다.\n *\n * 그래서 확인 문구도 「지웁니다」가 아니다. 이 사람은 다른 도메인에서 계속 일할 수 있고,\n * 다시 부르면 된다.\n */\n private async onRemoveFromDomain() {\n const user = this.user || {}\n\n const confirmed = await OxPrompt.open({\n type: 'warning',\n title: i18next.t('button.remove from domain'),\n text: i18next.t('text.they lose access to this domain but keep the account'),\n confirmButton: { text: i18next.t('button.confirm') },\n cancelButton: { text: i18next.t('button.cancel') }\n })\n\n if (!confirmed) {\n return\n }\n\n const done = await this.runUserMutation(gql`\n mutation ($username: String!) {\n deleteDomainUser(username: $username)\n }\n `)\n\n if (done) {\n /* 이 사람은 이제 목록에 없다 — 상세를 열어 둘 이유가 없다. */\n this.dispatchEvent(new CustomEvent('close'))\n }\n }\n\n /**\n * 소유권을 넘긴다.\n *\n * 이미 있는 `ownership-transfer-popup` 을 그대로 쓴다 — 그 안에 **이메일을 다시 적게 하는**\n * 확인이 들어 있다. 되돌릴 수 없는 것 중에서 유일하게 두 단계인 것이고, 그 판단이 맞다.\n */\n private onTransfer() {\n const popup = openPopup(\n html`<ownership-transfer-popup\n .user=${this.user}\n @ownershipTransferred=${() => {\n popup.close()\n this.dispatchEvent(new CustomEvent('user-updated', { detail: this.user, bubbles: true, composed: true }))\n }}\n ></ownership-transfer-popup>`,\n {\n backdrop: true,\n size: 'large',\n title: i18next.t('title.owner_transfer')\n }\n )\n }\n\n private async runUserMutation(mutation: any, { notify = true }: { notify?: boolean } = {}) {\n const username = this.user?.username || this.user?.email\n if (!username || this.saving) {\n return false\n }\n\n this.saving = true\n\n try {\n const response = await client.mutate({ mutation, variables: { username }, context: gqlContext() })\n\n if (response.errors) {\n return false\n }\n\n /*\n * 비밀번호 초기화만 목록을 바꾸지 않는다 — 상태도 역할도 그대로다. 그때 목록을 다시\n * 읽으면 아무것도 안 달라진 화면이 깜빡이고, 사람은 뭐가 됐는지 더 모르게 된다.\n */\n if (notify) {\n this.dispatchEvent(new CustomEvent('user-updated', { detail: this.user, bubbles: true, composed: true }))\n }\n\n return true\n } finally {\n this.saving = false\n }\n }\n\n private onToggle(id: string, checked: boolean) {\n const next = new Set(this.selected)\n checked ? next.add(id) : next.delete(id)\n this.selected = next\n }\n\n /**\n * **범위를 상태에서 만든다.** 읽어 온 목록이 곧 `availableRoles` 다.\n *\n * 빈 범위를 보내면 서버는 「뗄 것이 없다」로 읽어 아무것도 안 한다 — 우연히 무해한 것이지\n * 뜻한 바가 아니므로, 그 경우는 여기까지 오지 않는다(`savable`).\n */\n private async onSave() {\n /* 단추가 이미 막고 있다 — 여기 걸리면 단추와 이 자리가 어긋난 것이다. */\n if (!this.savable) {\n return\n }\n\n const user = this.user\n const username = user?.username || user?.email\n if (!username) {\n return\n }\n\n this.saving = true\n\n try {\n const response = await client.mutate({\n mutation: gql`\n mutation ($username: String!, $availableRoles: [ObjectRef!]!, $selectedRoles: [ObjectRef!]!) {\n updateUserRoles(username: $username, availableRoles: $availableRoles, selectedRoles: $selectedRoles) {\n id\n }\n }\n `,\n variables: {\n username,\n availableRoles: this.availableRoles.map(role => ({ id: role.id })),\n selectedRoles: this.availableRoles.filter(role => this.selected.has(role.id)).map(role => ({ id: role.id }))\n },\n context: gqlContext()\n })\n\n if (!response.errors) {\n this.dispatchEvent(new CustomEvent('user-updated', { detail: user, bubbles: true, composed: true }))\n }\n } finally {\n this.saving = false\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"user-detail.js","sourceRoot":"","sources":["../../client/components/user-detail.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@material/web/button/elevated-button.js'\nimport '@material/web/button/outlined-button.js'\nimport '@material/web/checkbox/checkbox.js'\n\nimport './ownership-transfer-popup.js'\n\nimport gql from 'graphql-tag'\nimport { lockedParentRoles } from '../utils/role-origin.js'\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 { OxPrompt } from '@operato/popup/ox-prompt.js'\nimport { InheritedValueType, store } from '@operato/shell'\nimport { ButtonContainerStyles, ScrollbarStyles } from '@operato/styles'\nimport { hasPrivilege } from '@things-factory/auth-base/dist-client'\n\n/**\n * 한 사람의 역할을 고치는 자리.\n *\n * ── 담는 그릇을 모른다 ──────────────────────────────────────────────────────\n * 팝업이든 서랍이든 그 안에 들어간다. 그래서 여기서는 **아무것도 열거나 닫지 않고** 두 가지만\n * 알린다 — 저장이 끝났다(`user-updated`), 닫아 달라(`close`). 그릇이 어느 것이 될지는 아직\n * 정해지지 않았고, 그 결정이 이 파일을 바꾸지 않게 한다.\n *\n * ── 옛 화면이 하던 것 (`components/user-role-editor.ts`) ────────────────────\n * 저장할 때 `querySelectorAll('role-selector')` 로 **화면을 훑어** 「가능한 역할 전부」와\n * 「고른 역할」을 만들어 보냈다.\n *\n * `updateUserRoles` 의 계약은 이렇다 — `availableRoles` 는 **부르는 쪽이 책임지는 범위**이고,\n * 그 범위 안의 역할을 일단 다 뗀 다음 `selectedRoles` 를 다시 붙인다. 범위 밖의 역할은\n * 건드리지 않는다(`user-mutation.ts` 의 `updateUserRoles`).\n *\n * 계약 자체는 옳다 — 화면이 일부만 편집할 수 있게 해 준다. 잘못된 것은 **그 범위를 DOM 에서\n * 만든 것**이다. 목록이 늦게 오거나 잘려 있으면 화면은 그것을 모른 채 좁은 범위를 보내고,\n * 안 보낸 역할이 어떻게 되는지가 그날의 렌더링에 달린다.\n *\n * 여기서는 범위를 **상태에서** 만든다. 읽어 온 목록이 곧 범위이고, 아직 못 읽었으면 저장하지\n * 않는다.\n */\n@customElement('user-detail')\nexport class UserDetail extends localize(i18next)(LitElement) {\n static styles: CSSResultGroup = [\n ScrollbarStyles,\n ButtonContainerStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-medium, 8px);\n padding: var(--spacing-large, 12px);\n background-color: var(--md-sys-color-surface);\n color: var(--md-sys-color-on-surface);\n overflow: auto;\n }\n\n [headline] {\n font: var(--headline-font, inherit);\n font-size: 1.05rem;\n font-weight: 600;\n }\n\n [sub] {\n color: var(--md-sys-color-on-surface-variant);\n font-size: 0.85rem;\n }\n\n [section] {\n margin-top: var(--spacing-medium, 8px);\n color: var(--md-sys-color-on-surface-variant);\n font-size: 0.8rem;\n text-transform: capitalize;\n }\n\n ul[roles] {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n }\n\n ul[roles] li {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 5px 2px;\n }\n\n ul[roles] li [desc] {\n color: var(--md-sys-color-on-surface-variant);\n font-size: 0.8rem;\n }\n\n /*\n 색은 짝으로 쓰고, 「무엇이 나를 기다리나」에만 쓴다.\n 소유자 안내는 조치가 필요한 것이 아니라 사실이므로 조용한 면을 쓴다.\n */\n [note] {\n padding: 8px 10px;\n border-radius: var(--md-sys-shape-corner-small, 6px);\n font-size: 0.82rem;\n background-color: var(--md-sys-color-surface-variant);\n color: var(--md-sys-color-on-surface-variant);\n }\n\n [note][attention] {\n background-color: var(--md-sys-color-error-container);\n color: var(--md-sys-color-on-error-container);\n }\n\n /*\n 되돌릴 수 없는 조치 — 선으로 갈라 두고, 색은 글자에만 쓴다.\n 면을 error 로 칠하면 「지금 문제가 있다」로 읽힌다.\n */\n [danger-zone] {\n margin-top: var(--spacing-large, 12px);\n border-top: 1px solid var(--md-sys-color-outline-variant);\n padding-top: var(--spacing-small, 4px);\n }\n\n [danger-zone] md-outlined-button {\n --md-outlined-button-label-text-color: var(--md-sys-color-error);\n --md-outlined-button-outline-color: var(--md-sys-color-outline-variant);\n --md-outlined-button-hover-label-text-color: var(--md-sys-color-error);\n --md-outlined-button-hover-state-layer-color: var(--md-sys-color-error);\n --md-outlined-button-pressed-label-text-color: var(--md-sys-color-error);\n }\n `\n ]\n\n /** 목록이 골라 준 사람. `id` 와 `username`(없으면 `email`)이 있어야 한다. */\n @property({ type: Object }) user: any\n\n @state() private availableRoles: any[] = []\n @state() private selected: Set<string> = new Set()\n @state() private loading: boolean = true\n @state() private saving: boolean = false\n\n /*\n * **내가 이 도메인에서 무엇을 할 수 있나 — 서버 선언을 그대로 되묻는다.**\n *\n * `/auth/profile` 이 `privileges`·`owner`·`super` 를 전부 담아 주고, `hasPrivilege`\n * (`auth-base/client/profiled.ts`)가 서버의 `checkPermission` 과 같은 규칙으로 그것을 판정한다\n * — 안전하지 않은 IP 에서 소유권 우회를 빼는 것까지 같다. 그래서 **서버 지시자에 적힌 인자를\n * 그대로 넘기는 것**이 화면과 문을 어긋나지 않게 하는 방법이다.\n *\n * 옛 화면은 이것을 두고 `me.email === domainOwner.email` 로 판정했다 — 목록에서 뽑은 대표\n * 소유자 한 명과 견주는 것이라 **공동 소유자가 자기 권한을 잃었고**, `user:mutation` 을 역할로\n * 받은 사람은 아예 단추를 못 봤다.\n *\n * 둘이 다른 선언을 쓴다.\n *\n * 활성/비활성·초기화·제외 user:mutation + domainOwnerGranted\n * 소유권 이전 domainOwnerGranted + superUserGranted (권한 쌍이 없다)\n *\n * ⚠ 수퍼유저가 자동으로 다 통과하지 않는다 — `checkPermission` 은 지시자가\n * `superUserGranted` 를 선언한 곳에서만 그것을 본다. 그래서 위 둘을 뭉치면 안 된다.\n */\n @state() private canManage: boolean = false\n @state() private canOwn: boolean = false\n @state() private myEmail: string = ''\n\n /** 기본 비밀번호가 설정돼 있지 않으면 초기화는 서버에서 던진다 — 단추를 내주지 않는다. */\n @state() private defaultPasswordConfigured: boolean = false\n\n /**\n * 이 사람의 비밀번호를 이 도메인이 초기화해도 되는가 — 서버 규칙(`password-reset-rule.ts`)의 답.\n *\n * 비밀번호는 계정 하나에 하나다. 다른 도메인에서도 일하는 사람, 어느 도메인이든 오너인 사람\n * (수퍼유저 포함)은 이 도메인이 초기화하지 않는다. 기본 비밀번호를 아는 누구나 그 계정을 가져갈 수 있다.\n */\n @state() private passwordResettable: boolean = false\n\n /**\n * 이 도메인(과 부모)의 역할 id.\n *\n * 소유권 이전은 **받는 사람이 이 도메인에 역할을 하나라도 가질 때만** 된다\n * (`transferOwner` 가 상속 역할까지 자격으로 인정하고, 그 밖은 던진다). 파트너십으로 넘어온\n * 역할은 자격이 아니므로 `availableRoles`(합친 것)로는 판정할 수 없다.\n */\n @state() private domainRoleIds: Set<string> = new Set()\n /** The parent domain's roles this person may not change here - locked, with the reason under the list. */\n @state() private locked: Set<string> = new Set()\n private unsubscribe?: () => void\n\n /*\n * **저장할 수 있나 — 단추와 처리기가 같은 답을 본다.**\n *\n * 셋을 갈라야 한다. 화면에서 갈라 놓고 저장에서 뭉치면, 「아직 못 읽었다」와 「읽었는데\n * 역할이 없다」가 똑같이 **눌러도 아무 일이 없는 단추**가 된다. 그것이 조용히 되돌아가는\n * 자리이고, 사람은 저장이 됐는지 안 됐는지 알 수 없다.\n *\n * 못 읽었다 아직 범위를 모른다 — 눌리면 안 된다\n * 역할이 0개 보낼 것이 없다 — 눌리면 안 되고, 무엇을 해야 하는지는 목록 자리가 말한다\n * 읽었고 있다 저장한다\n */\n private get savable(): boolean {\n return !this.loading && !this.saving && this.availableRoles.length > 0\n }\n\n render() {\n const user = this.user || {}\n\n return html`\n <div headline>${user.name || user.username || user.email}</div>\n <div sub>${user.email}</div>\n\n ${\n user.owner\n ? html`<div note>\n <md-icon style=\"--md-icon-size: 16px; vertical-align: -3px;\">supervisor_account</md-icon>\n ${i18next.t('text.owner opens everything without a role')}\n </div>`\n : nothing\n }\n ${this.renderAccountState(user)}\n\n <div section>${i18next.t('field.roles')}</div>\n ${this.renderRoles()}\n\n <div class=\"button-container\">\n <md-elevated-button ?disabled=${!this.savable} @click=${() => this.onSave()}>\n ${i18next.t('button.save')}\n </md-elevated-button>\n <md-elevated-button @click=${() => this.dispatchEvent(new CustomEvent('close'))}>\n ${i18next.t('button.cancel')}\n </md-elevated-button>\n </div>\n\n ${this.renderDangerZone(user)}\n `\n }\n\n /**\n * 되돌릴 수 없는 조치.\n *\n * ── 왜 아래에 따로 두나 ─────────────────────────────────────────────────────\n * 옛 화면은 저장·소유권 이전·초기화·제외·비활성화를 **한 줄에 나란히** 뒀다. 매일 누르는\n * 「저장」 옆에 한 번 누르면 끝인 「제외」가 같은 크기로 있었다.\n *\n * 그래서 선을 하나 긋고, 위는 채운 단추(주 동작), 아래는 테두리 단추로 둔다. 색은\n * error 짝으로 글자에만 쓴다 — 면을 다 칠하면 「지금 문제가 있다」로 읽힌다.\n *\n * ── 무엇을 언제 내주나 ──────────────────────────────────────────────────────\n * 조건은 전부 **서버가 실제로 던지는 것**을 그대로 옮긴 것이다. 눌리는 단추를 보여 주고\n * 서버가 거절하면 그것은 안내가 아니라 오류다.\n *\n * 비밀번호 초기화 기본 비밀번호가 설정돼 있고, 내가 아니다\n * 소유권 이전 활성 상태고, 소유자가 아니고, 이 도메인에 역할이 하나라도 있다\n * 도메인에서 제외 소유자가 아니고, 내가 아니다\n *\n * ── 이름 ────────────────────────────────────────────────────────────────────\n * `deleteDomainUser` 는 계정을 지우지 않는다 — **이 도메인에서만 뗀다**(서버 설명도 그렇게\n * 적혀 있다). 옛 화면의 「사용자 삭제」는 하지 않는 일을 말하고 있었다.\n */\n private renderDangerZone(user: any) {\n const me = this.myEmail && user.email === this.myEmail\n\n const resettable = this.canManage && this.defaultPasswordConfigured && this.passwordResettable && !me\n const transferable =\n this.canOwn &&\n !user.owner &&\n user.status === 'activated' &&\n (user.roles || []).some((role: any) => this.domainRoleIds.has(role.id))\n const removable = this.canManage && !user.owner && !me\n\n if (!resettable && !transferable && !removable) {\n return nothing\n }\n\n return html`\n <div danger-zone>\n <div section>${i18next.t('text.cannot be undone')}</div>\n <div class=\"button-container\">\n ${\n resettable\n ? html`<md-outlined-button @click=${() => this.onResetPassword()}>\n ${i18next.t('title.reset password')}\n </md-outlined-button>`\n : nothing\n }\n ${\n transferable\n ? html`<md-outlined-button @click=${() => this.onTransfer()}>\n ${i18next.t('button.transfer owner')}\n </md-outlined-button>`\n : nothing\n }\n ${\n removable\n ? html`<md-outlined-button @click=${() => this.onRemoveFromDomain()}>\n ${i18next.t('button.remove from domain')}\n </md-outlined-button>`\n : nothing\n }\n </div>\n </div>\n `\n }\n\n /**\n * 계정 상태와, 그것을 되돌리는 단추.\n *\n * **잠금 해제와 활성화는 서버에서 같은 것이다** — `activateUser` 가 `failCount` 를 0 으로\n * 두고 `ACTIVATED` 로 되돌린다. 그래서 부르는 것은 하나이고, **글자만 상황에 맞춰 바꾼다.**\n * 잠긴 사람에게 「활성화」라고 하면 무슨 일이 일어나는지 알 수 없다.\n *\n * 비활성화는 소유자와 나 자신에게는 내주지 않는다. 서버도 같은 것을 막지만\n * (`inactivateUser` 가 소유자·admin 을 거절한다), **화면이 눌리는 단추를 보여 주고 서버가\n * 거절하면 그것은 안내가 아니라 오류다.**\n */\n private renderAccountState(user: any) {\n const status = user.status\n\n if (!this.canManage) {\n return nothing\n }\n\n if (status === 'locked') {\n return html`<div class=\"button-container\">\n <md-elevated-button @click=${() => this.onActivate()}> ${i18next.t('button.unlock')} </md-elevated-button>\n </div>`\n }\n\n if (status !== 'activated') {\n return html`<div class=\"button-container\">\n <md-elevated-button @click=${() => this.onActivate()}> ${i18next.t('button.activate')} </md-elevated-button>\n </div>`\n }\n\n /* 활성 상태 — 되돌릴 수 있는 사람에게만, 그리고 자기 자신과 소유자에게는 안 준다. */\n if (user.owner || (this.myEmail && user.email === this.myEmail)) {\n return nothing\n }\n\n return html`<div class=\"button-container\">\n <md-elevated-button danger @click=${() => this.onInactivate()}>\n ${i18next.t('button.deactivate')}\n </md-elevated-button>\n </div>`\n }\n\n private renderRoles() {\n if (this.loading) {\n return html`<div sub>${i18next.t('text.loading')}</div>`\n }\n\n if (!this.availableRoles.length) {\n /*\n * 「역할이 없다」와 「못 읽었다」는 다른 말이다. 여기까지 왔다는 것은 읽었다는 뜻이므로\n * 없는 것이 맞고, 그때는 무엇을 해야 하는지를 말한다.\n */\n return html`<div note attention>${i18next.t('text.no role defined in this domain')}</div>`\n }\n\n return html`\n <ul roles>\n ${this.availableRoles.map(\n role => html`\n <li>\n <md-checkbox\n id=\"role-${role.id}\"\n .checked=${this.selected.has(role.id)}\n ?disabled=${this.locked.has(role.id)}\n @change=${(e: Event) => this.onToggle(role.id, (e.target as HTMLInputElement).checked)}\n ></md-checkbox>\n <label for=\"role-${role.id}\">${role.name}</label>\n ${role.description ? html`<span desc>${role.description}</span>` : nothing}\n </li>\n `\n )}\n </ul>\n ${this.availableRoles.some(role => this.locked.has(role.id))\n ? html`<div note>${i18next.t('text.parent roles are changed by the parent domain')}</div>`\n : nothing}\n `\n }\n\n connectedCallback() {\n super.connectedCallback()\n this.applyMe()\n this.unsubscribe = store.subscribe(() => this.applyMe())\n this.checkGates()\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n this.unsubscribe?.()\n }\n\n private applyMe() {\n const me = (store.getState() as any).auth?.user || {}\n this.myEmail = me.email || ''\n }\n\n /**\n * 서버 지시자에 적힌 인자를 그대로 되묻는다.\n *\n * 기본 비밀번호 여부도 여기서 함께 읽는다. 서버에 같은 답을 주는 질의가 둘 있는데\n * (`checkDefaultPassword` 와 `checkResettablePasswordToDefault` — 둘 다\n * `Boolean(defaultPassword)`), 하나만 쓴다.\n */\n private async checkGates() {\n this.canManage = Boolean(await hasPrivilege({ category: 'user', privilege: 'mutation', domainOwnerGranted: true }))\n this.canOwn = Boolean(await hasPrivilege({ domainOwnerGranted: true, superUserGranted: true }))\n\n const response = await client.query({\n query: gql`\n query {\n checkDefaultPassword\n }\n `,\n context: gqlContext()\n })\n\n this.defaultPasswordConfigured = Boolean(response.data?.checkDefaultPassword)\n }\n\n updated(changes: Map<string, unknown>) {\n if (changes.has('user')) {\n this.fetch()\n this.fetchPasswordResettable()\n }\n }\n\n /** 관리 권한이 없으면 서버가 이 질의를 거절한다 — 그때도 단추는 없다. */\n private async fetchPasswordResettable() {\n const id = this.user?.id\n this.passwordResettable = false\n if (!id) {\n return\n }\n\n try {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n mayResetPasswordToDefault(id: $id)\n }\n `,\n variables: { id },\n context: gqlContext()\n })\n\n this.passwordResettable = this.user?.id === id && response.data?.mayResetPasswordToDefault === true\n } catch (e) {\n this.passwordResettable = false\n }\n }\n\n /**\n * 부여할 수 있는 역할을 읽는다.\n *\n * **이 사람이 지금 가진 역할은 다시 묻지 않는다** — 목록이 이미 실어 왔다(`user.roles`).\n * 옛 화면은 email 로 사용자를 다시 찾았는데, 역할 이름과 달리 email 은 유일하긴 해도\n * 손에 든 `id` 를 두고 다른 열쇠로 같은 사람을 찾는 것이었다. 안 물으면 그 문제가 없다.\n *\n * **이 도메인의 역할과 파트너십으로 넘어온 역할을 한 목록으로 합친다.** 옛 화면은 그 둘을\n * 따로 그리고 저장할 때 화면에서 다시 모았는데, 합치는 자리가 화면이면 화면이 못 그린 것은\n * 저장에서도 빠진다.\n */\n private async fetch() {\n const user = this.user\n if (!user?.id) {\n return\n }\n\n this.loading = true\n\n const response = await client.query({\n query: gql`\n query ($inherited: InheritedValueType) {\n roles(inherited: $inherited) {\n items {\n id\n name\n description\n }\n }\n grantedRoles {\n role {\n id\n name\n description\n }\n }\n parentRoles: roles(inherited: Only) {\n items {\n id\n }\n }\n mayChangeParentRoles\n }\n `,\n variables: { inherited: InheritedValueType.Include },\n context: gqlContext()\n })\n\n const roles = response.data?.roles?.items || []\n const granted = (response.data?.grantedRoles || []).map((g: any) => g.role).filter(Boolean)\n\n /* 같은 역할이 두 경로로 올 수 있다 — id 로 한 번만 담는다. */\n const byId = new Map<string, any>()\n for (const role of [...roles, ...granted]) {\n byId.set(role.id, role)\n }\n\n this.availableRoles = [...byId.values()]\n /* 소유권 이전 자격은 **이 도메인(과 부모)의 역할**만 인정한다 — 합친 목록으로는 못 본다. */\n this.domainRoleIds = new Set(roles.map((role: any) => role.id))\n this.selected = new Set((user.roles || []).map((r: any) => r.id))\n this.locked = lockedParentRoles(\n (response.data?.parentRoles?.items || []).map((role: any) => role.id),\n response.data?.mayChangeParentRoles === true\n )\n this.loading = false\n }\n\n /** 잠금 해제와 활성화가 같은 부름이다 — 서버가 `failCount` 도 0 으로 되돌린다. */\n private async onActivate() {\n await this.runUserMutation(gql`\n mutation ($username: String!) {\n activateUser(username: $username)\n }\n `)\n }\n\n private async onInactivate() {\n await this.runUserMutation(gql`\n mutation ($username: String!) {\n inactivateUser(username: $username)\n }\n `)\n }\n\n /**\n * 비밀번호를 기본값으로 되돌린다.\n *\n * **무엇이 되는지 사람이 알 수 있게 말한다.** 「초기화하시겠습니까」는 그 뒤에 무엇을 알려\n * 줘야 하는지를 안 말한다 — 초기화된 비밀번호는 관리자가 상대에게 따로 전해야 한다(서버가\n * 메일을 보내지 않는다).\n */\n private async onResetPassword() {\n const confirmed = await OxPrompt.open({\n type: 'warning',\n title: i18next.t('title.reset password'),\n text: i18next.t('text.password becomes the default and must be told to them'),\n confirmButton: { text: i18next.t('button.confirm') },\n cancelButton: { text: i18next.t('button.cancel') }\n })\n\n if (!confirmed) {\n return\n }\n\n const done = await this.runUserMutation(\n gql`\n mutation ($username: String!) {\n resetPasswordToDefault(username: $username)\n }\n `,\n { notify: false }\n )\n\n if (done) {\n await OxPrompt.open({\n type: 'success',\n title: i18next.t('text.completed'),\n text: i18next.t('text.password reset succeed'),\n confirmButton: { text: i18next.t('button.confirm') }\n })\n }\n }\n\n /**\n * 이 도메인에서만 뗀다 — 계정은 남는다.\n *\n * 그래서 확인 문구도 「지웁니다」가 아니다. 이 사람은 다른 도메인에서 계속 일할 수 있고,\n * 다시 부르면 된다.\n */\n private async onRemoveFromDomain() {\n const user = this.user || {}\n\n const confirmed = await OxPrompt.open({\n type: 'warning',\n title: i18next.t('button.remove from domain'),\n text: i18next.t('text.they lose access to this domain but keep the account'),\n confirmButton: { text: i18next.t('button.confirm') },\n cancelButton: { text: i18next.t('button.cancel') }\n })\n\n if (!confirmed) {\n return\n }\n\n const done = await this.runUserMutation(gql`\n mutation ($username: String!) {\n deleteDomainUser(username: $username)\n }\n `)\n\n if (done) {\n /* 이 사람은 이제 목록에 없다 — 상세를 열어 둘 이유가 없다. */\n this.dispatchEvent(new CustomEvent('close'))\n }\n }\n\n /**\n * 소유권을 넘긴다.\n *\n * 이미 있는 `ownership-transfer-popup` 을 그대로 쓴다 — 그 안에 **이메일을 다시 적게 하는**\n * 확인이 들어 있다. 되돌릴 수 없는 것 중에서 유일하게 두 단계인 것이고, 그 판단이 맞다.\n */\n private onTransfer() {\n const popup = openPopup(\n html`<ownership-transfer-popup\n .user=${this.user}\n @ownershipTransferred=${() => {\n popup.close()\n this.dispatchEvent(new CustomEvent('user-updated', { detail: this.user, bubbles: true, composed: true }))\n }}\n ></ownership-transfer-popup>`,\n {\n backdrop: true,\n size: 'large',\n title: i18next.t('title.owner_transfer')\n }\n )\n }\n\n private async runUserMutation(mutation: any, { notify = true }: { notify?: boolean } = {}) {\n const username = this.user?.username || this.user?.email\n if (!username || this.saving) {\n return false\n }\n\n this.saving = true\n\n try {\n const response = await client.mutate({ mutation, variables: { username }, context: gqlContext() })\n\n if (response.errors) {\n return false\n }\n\n /*\n * 비밀번호 초기화만 목록을 바꾸지 않는다 — 상태도 역할도 그대로다. 그때 목록을 다시\n * 읽으면 아무것도 안 달라진 화면이 깜빡이고, 사람은 뭐가 됐는지 더 모르게 된다.\n */\n if (notify) {\n this.dispatchEvent(new CustomEvent('user-updated', { detail: this.user, bubbles: true, composed: true }))\n }\n\n return true\n } finally {\n this.saving = false\n }\n }\n\n private onToggle(id: string, checked: boolean) {\n const next = new Set(this.selected)\n checked ? next.add(id) : next.delete(id)\n this.selected = next\n }\n\n /**\n * **범위를 상태에서 만든다.** 읽어 온 목록이 곧 `availableRoles` 다.\n *\n * 빈 범위를 보내면 서버는 「뗄 것이 없다」로 읽어 아무것도 안 한다 — 우연히 무해한 것이지\n * 뜻한 바가 아니므로, 그 경우는 여기까지 오지 않는다(`savable`).\n */\n private async onSave() {\n /* 단추가 이미 막고 있다 — 여기 걸리면 단추와 이 자리가 어긋난 것이다. */\n if (!this.savable) {\n return\n }\n\n const user = this.user\n const username = user?.username || user?.email\n if (!username) {\n return\n }\n\n this.saving = true\n\n try {\n const response = await client.mutate({\n mutation: gql`\n mutation ($username: String!, $availableRoles: [ObjectRef!]!, $selectedRoles: [ObjectRef!]!) {\n updateUserRoles(username: $username, availableRoles: $availableRoles, selectedRoles: $selectedRoles) {\n id\n }\n }\n `,\n variables: {\n username,\n availableRoles: this.availableRoles.map(role => ({ id: role.id })),\n selectedRoles: this.availableRoles.filter(role => this.selected.has(role.id)).map(role => ({ id: role.id }))\n },\n context: gqlContext()\n })\n\n if (!response.errors) {\n this.dispatchEvent(new CustomEvent('user-updated', { detail: user, bubbles: true, composed: true }))\n }\n } finally {\n this.saving = false\n }\n }\n}\n"]}
|
|
@@ -5,7 +5,8 @@ import { AbstractAuthPage } from '../../components/abstract-auth-page.js';
|
|
|
5
5
|
/**
|
|
6
6
|
* After a password sign-in, before check-in: an offer to put a passkey on this device (ADR-0156
|
|
7
7
|
* decision 5). Where there is nothing to offer - no authenticator built into this device, or the
|
|
8
|
-
* person said not to ask
|
|
8
|
+
* offer is settled on this browser: the person said not to ask, or put a passkey here - the page
|
|
9
|
+
* moves on before it shows anything.
|
|
9
10
|
*/
|
|
10
11
|
export declare class AuthPasskeyOffer extends AbstractAuthPage {
|
|
11
12
|
static styles: import("lit").CSSResult[];
|
|
@@ -20,8 +21,8 @@ export declare class AuthPasskeyOffer extends AbstractAuthPage {
|
|
|
20
21
|
get formfields(): import("lit-html").TemplateResult<1>;
|
|
21
22
|
get links(): import("lit-html").TemplateResult<1>;
|
|
22
23
|
submit(): void;
|
|
23
|
-
private
|
|
24
|
-
private
|
|
24
|
+
private settled;
|
|
25
|
+
private settle;
|
|
25
26
|
private moveOn;
|
|
26
27
|
private register;
|
|
27
28
|
}
|
|
@@ -8,12 +8,13 @@ import { platformAuthenticatorIsAvailable, startRegistration } from '@simpleweba
|
|
|
8
8
|
import { i18next } from '@operato/i18n';
|
|
9
9
|
import { notify } from '@operato/layout';
|
|
10
10
|
import { AbstractAuthPage } from '../../components/abstract-auth-page.js';
|
|
11
|
-
import {
|
|
11
|
+
import { deviceLabel, offerSettledKey, registrationEnd, shouldOffer } from '../../utils/passkey.js';
|
|
12
12
|
import { readVerification } from '../../utils/webauthn-verification.js';
|
|
13
13
|
/**
|
|
14
14
|
* After a password sign-in, before check-in: an offer to put a passkey on this device (ADR-0156
|
|
15
15
|
* decision 5). Where there is nothing to offer - no authenticator built into this device, or the
|
|
16
|
-
* person said not to ask
|
|
16
|
+
* offer is settled on this browser: the person said not to ask, or put a passkey here - the page
|
|
17
|
+
* moves on before it shows anything.
|
|
17
18
|
*/
|
|
18
19
|
let AuthPasskeyOffer = class AuthPasskeyOffer extends AbstractAuthPage {
|
|
19
20
|
constructor() {
|
|
@@ -62,7 +63,7 @@ let AuthPasskeyOffer = class AuthPasskeyOffer extends AbstractAuthPage {
|
|
|
62
63
|
super.updated(changed);
|
|
63
64
|
if (changed.has('data') && this.data) {
|
|
64
65
|
const platformAuthenticator = await platformAuthenticatorIsAvailable().catch(() => false);
|
|
65
|
-
if (shouldOffer({ platformAuthenticator,
|
|
66
|
+
if (shouldOffer({ platformAuthenticator, settled: this.settled() })) {
|
|
66
67
|
this.offering = true;
|
|
67
68
|
}
|
|
68
69
|
else {
|
|
@@ -83,7 +84,7 @@ let AuthPasskeyOffer = class AuthPasskeyOffer extends AbstractAuthPage {
|
|
|
83
84
|
<md-text-button type="button" ?disabled=${this.busy} @click=${() => this.moveOn()}>
|
|
84
85
|
${i18next.t('button.later')}
|
|
85
86
|
</md-text-button>
|
|
86
|
-
<md-text-button type="button" ?disabled=${this.busy} @click=${() => this.
|
|
87
|
+
<md-text-button type="button" ?disabled=${this.busy} @click=${() => this.settle()}>
|
|
87
88
|
${i18next.t('button.do not ask on this device')}
|
|
88
89
|
</md-text-button>
|
|
89
90
|
</div>
|
|
@@ -95,17 +96,18 @@ let AuthPasskeyOffer = class AuthPasskeyOffer extends AbstractAuthPage {
|
|
|
95
96
|
}
|
|
96
97
|
/* Enter in the form does nothing - each choice is its own button. */
|
|
97
98
|
submit() { }
|
|
98
|
-
|
|
99
|
+
settled() {
|
|
99
100
|
try {
|
|
100
|
-
return localStorage.getItem(
|
|
101
|
+
return localStorage.getItem(offerSettledKey(this.username)) === 'true';
|
|
101
102
|
}
|
|
102
103
|
catch {
|
|
103
104
|
return false;
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
|
-
|
|
107
|
+
/* 「Do not ask again」, or a passkey of this person's is on this device now. */
|
|
108
|
+
settle() {
|
|
107
109
|
try {
|
|
108
|
-
localStorage.setItem(
|
|
110
|
+
localStorage.setItem(offerSettledKey(this.username), 'true');
|
|
109
111
|
}
|
|
110
112
|
catch { }
|
|
111
113
|
this.moveOn();
|
|
@@ -115,6 +117,7 @@ let AuthPasskeyOffer = class AuthPasskeyOffer extends AbstractAuthPage {
|
|
|
115
117
|
}
|
|
116
118
|
async register() {
|
|
117
119
|
this.busy = true;
|
|
120
|
+
let end;
|
|
118
121
|
try {
|
|
119
122
|
const optionsJSON = await fetch('/auth/register-webauthn/challenge').then(res => res.json());
|
|
120
123
|
const response = await startRegistration({ optionsJSON });
|
|
@@ -123,27 +126,27 @@ let AuthPasskeyOffer = class AuthPasskeyOffer extends AbstractAuthPage {
|
|
|
123
126
|
headers: { 'Content-Type': 'application/json' },
|
|
124
127
|
body: JSON.stringify({ response, name: deviceLabel(navigator.userAgent) })
|
|
125
128
|
}));
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
console.error('passkey registration refused:', verification.reason);
|
|
131
|
-
notify({ level: 'error', message: i18next.t('error.user credential registration not allowed') });
|
|
129
|
+
end = registrationEnd({ verification });
|
|
130
|
+
if (verification.outcome === 'refused')
|
|
131
|
+
console.error('passkey registration refused:', verification.reason);
|
|
132
132
|
}
|
|
133
133
|
catch (error) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (alreadyRegistered(error)) {
|
|
138
|
-
this.decline();
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
console.error('passkey registration failed:', error);
|
|
142
|
-
notify({ level: 'error', message: i18next.t('error.user credential registration failed') });
|
|
134
|
+
end = registrationEnd({ error });
|
|
135
|
+
if (end === 'failed')
|
|
136
|
+
console.error('passkey registration failed:', error);
|
|
143
137
|
}
|
|
144
138
|
finally {
|
|
145
139
|
this.busy = false;
|
|
146
140
|
}
|
|
141
|
+
if (end === 'settled') {
|
|
142
|
+
this.settle();
|
|
143
|
+
}
|
|
144
|
+
else if (end === 'refused') {
|
|
145
|
+
notify({ level: 'error', message: i18next.t('error.user credential registration not allowed') });
|
|
146
|
+
}
|
|
147
|
+
else if (end === 'failed') {
|
|
148
|
+
notify({ level: 'error', message: i18next.t('error.user credential registration failed') });
|
|
149
|
+
}
|
|
147
150
|
}
|
|
148
151
|
};
|
|
149
152
|
__decorate([
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"passkey-offer.js","sourceRoot":"","sources":["../../../client/entries/auth/passkey-offer.ts"],"names":[],"mappings":";AAAA,OAAO,yCAAyC,CAAA;AAChD,OAAO,qCAAqC,CAAA;AAC5C,OAAO,4BAA4B,CAAA;AAEnC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAA;AAC/B,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AACxD,OAAO,EAAE,gCAAgC,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAE7F,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAExC,OAAO,EAAE,gBAAgB,EAAE,MAAM,wCAAwC,CAAA;AACzE,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"passkey-offer.js","sourceRoot":"","sources":["../../../client/entries/auth/passkey-offer.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/button/elevated-button.js'\nimport '@material/web/button/text-button.js'\nimport '@material/web/icon/icon.js'\n\nimport { css, html } from 'lit'\nimport { customElement, state } from 'lit/decorators.js'\nimport { platformAuthenticatorIsAvailable, startRegistration } from '@simplewebauthn/browser'\n\nimport { i18next } from '@operato/i18n'\nimport { notify } from '@operato/layout'\n\nimport { AbstractAuthPage } from '../../components/abstract-auth-page.js'\nimport { deviceLabel, offerSettledKey, registrationEnd, shouldOffer } from '../../utils/passkey.js'\nimport { readVerification } from '../../utils/webauthn-verification.js'\n\n/**\n * After a password sign-in, before check-in: an offer to put a passkey on this device (ADR-0156\n * decision 5). Where there is nothing to offer - no authenticator built into this device, or the\n * offer is settled on this browser: the person said not to ask, or put a passkey here - the page\n * moves on before it shows anything.\n */\n@customElement('auth-passkey-offer')\nexport class AuthPasskeyOffer extends AbstractAuthPage {\n static styles = [\n ...AbstractAuthPage.styles,\n css`\n .offer {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-medium);\n }\n\n .offer p {\n margin: 0;\n font: var(--auth-description-font);\n color: var(--md-sys-color-on-primary);\n }\n\n .later {\n display: flex;\n gap: var(--spacing-small);\n }\n `\n ]\n\n @state() offering = false\n @state() busy = false\n\n get pageName() {\n return 'passkey offer'\n }\n\n get actionUrl() {\n return ''\n }\n\n get username(): string {\n return this.data?.username || ''\n }\n\n render() {\n /* Nothing shows while it decides - most visits that have nothing to offer leave at once. */\n return this.offering ? super.render() : html``\n }\n\n firstUpdated() {\n /* No text field here for the base to focus. */\n }\n\n async updated(changed) {\n super.updated(changed)\n\n if (changed.has('data') && this.data) {\n const platformAuthenticator = await platformAuthenticatorIsAvailable().catch(() => false)\n if (shouldOffer({ platformAuthenticator, settled: this.settled() })) {\n this.offering = true\n } else {\n this.moveOn()\n }\n }\n }\n\n get formfields() {\n return html`\n <div class=\"offer\">\n <p>${i18next.t('text.passkey offer')}</p>\n\n <md-elevated-button type=\"button\" ?disabled=${this.busy} @click=${() => this.register()}>\n <md-icon slot=\"icon\">passkey</md-icon>${i18next.t('button.register on this device')}\n </md-elevated-button>\n\n <div class=\"later\">\n <md-text-button type=\"button\" ?disabled=${this.busy} @click=${() => this.moveOn()}>\n ${i18next.t('button.later')}\n </md-text-button>\n <md-text-button type=\"button\" ?disabled=${this.busy} @click=${() => this.settle()}>\n ${i18next.t('button.do not ask on this device')}\n </md-text-button>\n </div>\n </div>\n `\n }\n\n get links() {\n return html``\n }\n\n /* Enter in the form does nothing - each choice is its own button. */\n submit() {}\n\n private settled(): boolean {\n try {\n return localStorage.getItem(offerSettledKey(this.username)) === 'true'\n } catch {\n return false\n }\n }\n\n /* 「Do not ask again」, or a passkey of this person's is on this device now. */\n private settle() {\n try {\n localStorage.setItem(offerSettledKey(this.username), 'true')\n } catch {}\n this.moveOn()\n }\n\n private moveOn() {\n window.location.replace(this.redirectTo || '/')\n }\n\n private async register() {\n this.busy = true\n\n let end: ReturnType<typeof registrationEnd>\n try {\n const optionsJSON = await fetch('/auth/register-webauthn/challenge').then(res => res.json())\n const response = await startRegistration({ optionsJSON })\n\n const verification = await readVerification(\n await fetch('/auth/verify-registration', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ response, name: deviceLabel(navigator.userAgent) })\n })\n )\n\n end = registrationEnd({ verification })\n if (verification.outcome === 'refused') console.error('passkey registration refused:', verification.reason)\n } catch (error) {\n end = registrationEnd({ error })\n if (end === 'failed') console.error('passkey registration failed:', error)\n } finally {\n this.busy = false\n }\n\n if (end === 'settled') {\n this.settle()\n } else if (end === 'refused') {\n notify({ level: 'error', message: i18next.t('error.user credential registration not allowed') })\n } else if (end === 'failed') {\n notify({ level: 'error', message: i18next.t('error.user credential registration failed') })\n }\n }\n}\n"]}
|