@things-factory/auth-ui 10.1.31 → 10.1.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-client/components/accept-invitation.d.ts +48 -0
- package/dist-client/components/accept-invitation.js +265 -0
- package/dist-client/components/accept-invitation.js.map +1 -0
- package/dist-client/components/appliance-editor.d.ts +3 -0
- package/dist-client/components/appliance-editor.js +102 -1
- package/dist-client/components/appliance-editor.js.map +1 -1
- package/dist-client/components/role-selector.js +23 -1
- package/dist-client/components/role-selector.js.map +1 -1
- package/dist-client/components/user-invite.d.ts +8 -0
- package/dist-client/components/user-invite.js +53 -6
- package/dist-client/components/user-invite.js.map +1 -1
- package/dist-client/entries/auth/accept-invitation.d.ts +3 -0
- package/dist-client/entries/auth/accept-invitation.js +10 -0
- package/dist-client/entries/auth/accept-invitation.js.map +1 -0
- package/dist-client/pages/invitation/invitation-list-query.test.d.ts +1 -0
- package/dist-client/pages/invitation/invitation-list-query.test.js +69 -0
- package/dist-client/pages/invitation/invitation-list-query.test.js.map +1 -0
- package/dist-client/pages/invitation/invitations.d.ts +63 -0
- package/dist-client/pages/invitation/invitations.js +282 -0
- package/dist-client/pages/invitation/invitations.js.map +1 -0
- package/dist-client/pages/role/role-list-page.d.ts +3 -2
- package/dist-client/pages/role/role-list-page.js +39 -6
- package/dist-client/pages/role/role-list-page.js.map +1 -1
- package/dist-client/route.js +3 -0
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/credential-panel-state.d.ts +51 -0
- package/dist-client/utils/credential-panel-state.js +49 -0
- package/dist-client/utils/credential-panel-state.js.map +1 -0
- package/dist-client/utils/credential-panel-state.test.d.ts +1 -0
- package/dist-client/utils/credential-panel-state.test.js +88 -0
- package/dist-client/utils/credential-panel-state.test.js.map +1 -0
- package/dist-client/utils/inherited-name-refusal.d.ts +44 -0
- package/dist-client/utils/inherited-name-refusal.js +46 -0
- package/dist-client/utils/inherited-name-refusal.js.map +1 -0
- package/dist-client/utils/inherited-name-refusal.test.d.ts +1 -0
- package/dist-client/utils/inherited-name-refusal.test.js +60 -0
- package/dist-client/utils/inherited-name-refusal.test.js.map +1 -0
- package/dist-client/utils/invitation-token.d.ts +28 -0
- package/dist-client/utils/invitation-token.js +40 -0
- package/dist-client/utils/invitation-token.js.map +1 -0
- package/dist-client/utils/invitation-token.test.d.ts +1 -0
- package/dist-client/utils/invitation-token.test.js +60 -0
- package/dist-client/utils/invitation-token.test.js.map +1 -0
- package/dist-client/utils/role-origin.d.ts +44 -0
- package/dist-client/utils/role-origin.js +47 -0
- package/dist-client/utils/role-origin.js.map +1 -0
- package/dist-client/utils/role-origin.test.d.ts +1 -0
- package/dist-client/utils/role-origin.test.js +60 -0
- package/dist-client/utils/role-origin.test.js.map +1 -0
- package/package.json +5 -5
- package/things-factory.config.js +1 -0
- package/translations/en.json +28 -0
- package/translations/ja.json +28 -0
- package/translations/ko.json +28 -0
- package/translations/ms.json +28 -0
- package/translations/zh.json +28 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"appliance-editor.js","sourceRoot":"","sources":["../../client/components/appliance-editor.ts"],"names":[],"mappings":";AAAA,OAAO,4BAA4B,CAAA;AACnC,OAAO,qCAAqC,CAAA;AAC5C,OAAO,uCAAuC,CAAA;AAE9C,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AACzE,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAA;AAE9D,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAA;AACtD,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACxF,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAEhD,OAAO,mBAAmB,CAAA;AAC1B,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAEtF;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,yFAAyF;AACzF,MAAM,QAAQ,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;AAEvD,MAAM,gBAAgB,GAAG;;;;;;;;;CASxB,CAAA;AAGM,IAAM,eAAe,GAArB,MAAM,eAAgB,SAAQ,QAAQ,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC;IAA3D;;QAyHL,iGAAiG;QAChF,cAAS,GAAQ,IAAI,CAAA;QAEtC,6FAA6F;QAC5E,YAAO,GAAa,EAAE,CAAA;QAEvC,8FAA8F;QAC7E,WAAM,GAAY,KAAK,CAAA;QAExC,mFAAmF;QAClE,eAAU,GAAY,KAAK,CAAA;IAuW9C,CAAC;IAzeC;;;;;;;;;;;;;;;;;;;;;;;OAuBG;aACI,WAAM,GAAG;QACd,kBAAkB;QAClB,iBAAiB;QACjB,eAAe;QACf,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAoFF;KACF,AAzFY,CAyFZ;IAmBD,IAAY,OAAO;QACjB,OAAO,CAAC,CAAC,IAAI,CAAC,WAAW,CAAA;IAC3B,CAAC;IAED,MAAM;QACJ,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,EAAE,CAAA;QACtC,MAAM,GAAG,GAAG,SAAS,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,WAAW,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,SAAS,CAAA;QAEpF,IAAI,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpC,OAAO,IAAI,CAAA,wBAAwB,OAAO,CAAC,CAAC,CAAC,wCAAwC,CAAC,QAAQ,CAAA;QAChG,CAAC;QAED,OAAO,IAAI,CAAA;sBACO,CAAC,CAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,cAAc,EAAE;;oBAElC,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC;;gCAEf,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC;;;;;uBAKhC,SAAS,CAAC,IAAI,IAAI,EAAE;;;yBAGlB,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;;cAExC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;;;;uCAIE,OAAO,CAAC,CAAC,CAAC,mBAAmB,CAAC;4EACO,SAAS,CAAC,WAAW,IAAI,EAAE;;;;;uCAKhE,OAAO,CAAC,CAAC,CAAC,iBAAiB,CAAC;;;;;yBAK1C,SAAS,CAAC,QAAQ,IAAI,EAAE;;2BAEtB,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC;;gBAE5C,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC;;;;mCAIR,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC;;;;;yBAKlC,SAAS,CAAC,KAAK,IAAI,EAAE;;2BAEnB,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;;gBAEzC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;;;;;;mCAML,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC;;;;;yBAKlC,SAAS,CAAC,KAAK,IAAI,EAAE;;2BAEnB,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;;gBAEzC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;;;;qCAIH,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC;sEACO,SAAS,CAAC,OAAO,IAAI,EAAE;;;;;UAKnF,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE;UACzD,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAA,wBAAwB,OAAO,CAAC,CAAC,CAAC,yBAAyB,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE;;;;UAK3F,IAAI,CAAC,OAAO;YACV,CAAC,CAAC,IAAI,CAAA;sDACoC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC;oBACjE,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC;;eAE/B;YACH,CAAC,CAAC,EACN;iCACyB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC;mCAClD,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;YAC3C,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,iBAAiB,CAAC;;;KAG/E,CAAA;IACH,CAAC;IAED;;;OAGG;IACK,gBAAgB,CAAC,SAAc,EAAE,GAAY;QACnD,OAAO,IAAI,CAAA;;kBAEG,OAAO,CAAC,CAAC,CAAC,2BAA2B,CAAC;;sCAElB,OAAO,CAAC,CAAC,CAAC,oBAAoB,CAAC;mDAClB,SAAS,CAAC,WAAW,IAAI,EAAE;YAElE,GAAG;YACD,CAAC,CAAC,IAAI,CAAA;oBACA,OAAO,CAAC,CAAC,CAAC,wBAAwB,CAAC,IAAI,IAAI,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC,cAAc,EAAE;oBAC5E,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;uBAC7B;YACT,CAAC,CAAC,EACN;;qCAE2B,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC;gBACnD,OAAO,CAAC,CAAC,CAAC,kCAAkC,CAAC;;;;;KAKxD,CAAA;IACH,CAAC;IAEO,UAAU,CAAC,IAAY;QAC7B,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA,oBAAoB,OAAO,CAAC,CAAC,CAAC,6BAA6B,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAA;IACpH,CAAC;IAED,KAAK,CAAC,iBAAiB;QACrB,KAAK,CAAC,iBAAiB,EAAE,CAAA;QAEzB,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,MAAM,IAAI,CAAC,cAAc,EAAE,CAAA;QAC7B,CAAC;IACH,CAAC;IAED,KAAK,CAAC,cAAc;QAClB,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;YAClC,KAAK,EAAE,GAAG,CAAA;;iCAEiB,gBAAgB;;OAE1C;YACD,SAAS,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,WAAW,EAAE;SACpC,CAAC,CAAA;QAEF;;;;WAIG;QACH,IAAI,QAAQ,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,EAAE,CAAC;YACjD,IAAI,CAAC,UAAU,GAAG,IAAI,CAAA;YACtB,OAAM;QACR,CAAC;QAED,IAAI,CAAC,UAAU,GAAG,KAAK,CAAA;QACvB,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAA;IAC1C,CAAC;IAEO,MAAM,CAAC,CAAQ;QACrB,CAAC,CAAC,cAAc,EAAE,CAAA;QAClB,IAAI,CAAC,KAAK,EAAE,CAAA;IACd,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,CAAQ;QACjB,CAAC,CAAC,cAAc,EAAE,CAAA;QAElB,MAAM,QAAQ,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAExC;;;;WAIG;QACH,MAAM,OAAO,GAAG,eAAe,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;QAEnD,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YACnB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;YACtB,OAAM;QACR,CAAC;QAED,IAAI,CAAC,OAAO,GAAG,EAAE,CAAA;QAEjB,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAA;IACvF,CAAC;IAEO,KAAK,CAAC,eAAe,CAAC,QAAkB;QAC9C,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;YACnC,QAAQ,EAAE,GAAG,CAAA;;;;;;OAMZ;YACD,SAAS,EAAE,EAAE,SAAS,EAAE,YAAY,CAAC,QAAQ,CAAC,EAAE;SACjD,CAAC,CAAA;QAEF,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;YACpB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;YAClB,OAAM;QACR,CAAC;QAED,IAAI,CAAC,KAAK,EAAE,CAAA;IACd,CAAC;IAEO,KAAK,CAAC,eAAe,CAAC,QAAkB;QAC9C,MAAM,KAAK,GAAG,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;QAErD;;;WAGG;QACH,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC;YAC/B,IAAI,CAAC,KAAK,EAAE,CAAA;YACZ,OAAM;QACR,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;YACnC,QAAQ,EAAE,GAAG,CAAA;;sDAEmC,gBAAgB;;OAE/D;YACD,SAAS,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,WAAW,EAAE,KAAK,EAAE;SAC3C,CAAC,CAAA;QAEF,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;YACpB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;YAClB,OAAM;QACR,CAAC;QAED,IAAI,CAAC,KAAK,EAAE,CAAA;IACd,CAAC;IAEO,KAAK,CAAC,cAAc,CAAC,CAAQ;QACnC,CAAC,CAAC,cAAc,EAAE,CAAA;QAElB,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC;YACpC,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,kCAAkC,CAAC;YACpD,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,yDAAyD,CAAC;YAC1E,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,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;YACnC,QAAQ,EAAE,GAAG,CAAA;;+CAE4B,gBAAgB;;OAExD;YACD,SAAS,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,WAAW,EAAE;SACpC,CAAC,CAAA;QAEF,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;YACpB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;YAClB,OAAM;QACR,CAAC;QAED;;;WAGG;QACH,IAAI,CAAC,MAAM,GAAG,KAAK,CAAA;QACnB,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC,IAAI,CAAC,uBAAuB,CAAA;QACtD,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,mBAAmB,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;IAC7F,CAAC;IAEO,KAAK,CAAC,eAAe,CAAC,CAAQ;QACpC,CAAC,CAAC,cAAc,EAAE,CAAA;QAElB,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC;YACpC,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC;YACjC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,wCAAwC,EAAE,EAAE,CAAC,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC;YAC5F,aAAa,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,EAAE;YACnD,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,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;YACnC,QAAQ,EAAE,GAAG,CAAA;;;;OAIZ;YACD,SAAS,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,WAAW,EAAE;SACpC,CAAC,CAAA;QAEF,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;YACpB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;YAClB,OAAM;QACR,CAAC;QAED,IAAI,CAAC,KAAK,EAAE,CAAA;IACd,CAAC;IAED,sDAAsD;IAC9C,KAAK;QACX,IAAI,CAAC,MAAM,GAAG,KAAK,CAAA;QACnB,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,mBAAmB,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;QAC3F,IAAI,CAAC,KAAK,EAAE,CAAA;IACd,CAAC;IAEO,KAAK;QACX,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;IACjF,CAAC;IAEO,KAAK,CAAC,CAAC,QAAQ,CAAC,GAAW;QACjC,MAAM,GAAG,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAA;QACxB,MAAM,IAAI,GAAG,EAAE,GAAG,EAAE,CAAA;QACpB,MAAM,GAAG,GAAG,EAAE,CAAA;QAEd,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC;YACxB,IAAI,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA;YAExD;;;eAGG;YACH,IAAI,MAAM,IAAI,CAAC,EAAE,CAAC;gBAChB,MAAM,OAAO,CAAC,CAAC,CAAC,cAAc,CAAC,CAAA;gBAC/B,OAAM;YACR,CAAC;YAED,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,GAAG,CAAC,CAAA;YACrC,MAAM,IAAI,IAAI,GAAG,GAAG,CAAA;YACpB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;YACvC,MAAM,IAAI,KAAK,GAAG,IAAI,CAAA;YACtB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,GAAG,CAAC,CAAA;YACrC,MAAM,IAAI,GAAG,MAAM,GAAG,IAAI,GAAG,GAAG,CAAA;YAEhC,MAAM,OAAO,CAAC,CAAC,CAAC,qBAAqB,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;YAEnE,MAAM,KAAK,CAAC,IAAI,CAAC,CAAA;QACnB,CAAC;IACH,CAAC;;AApX2B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;oDAAqB;AAEjC;IAAd,KAAK,CAAC,MAAM,CAAC;8BAAQ,eAAe;IAErC,iGAAiG;;6CAF5D;AAGpB;IAAhB,KAAK,EAAE;;kDAA8B;AAGrB;IAAhB,KAAK,EAAE;;gDAA+B;AAGtB;IAAhB,KAAK,EAAE;;+CAAgC;AAGvB;IAAhB,KAAK,EAAE;;mDAAoC;AAnIjC,eAAe;IAD3B,aAAa,CAAC,kBAAkB,CAAC;GACrB,eAAe,CA0e3B","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@material/web/button/text-button.js'\nimport '@material/web/button/filled-button.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\nimport { asyncReplace } from 'lit/directives/async-replace.js'\n\nimport { client } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { OxPrompt } from '@operato/popup/ox-prompt.js'\nimport { CommonHeaderStyles, CommonPopupStyles, ScrollbarStyles } from '@operato/styles'\nimport { parseJwt, sleep } from '@operato/utils'\n\nimport './secret-field.js'\nimport { changedValues, filledValues, missingRequired } from '../utils/form-values.js'\n\n/**\n * Registering and editing an appliance, in a popup.\n *\n * ── Why this replaced two pages ─────────────────────────────────────────────\n * `appliance-register` and `appliance/{id}` were full routes. Nothing outside auth-ui linked\n * to either, and every other resource in this package — role, user, domain, domain owner,\n * auth provider, attribute set — is a grist list with a popup over it. The two pages were not\n * a different kind of resource, only older code.\n *\n * ── The defect that came with them ──────────────────────────────────────────\n * Both pages sent the whole form. On the register page that meant `serialNo: ''`, and\n * `createAppliance` has no validation at all — it saves `{ domain, creator, updater,\n * ...appliance }` — so `''` passed GraphQL's non-null check (an empty string is a String) and\n * an appliance with no serial number, brand or model was created **and the screen navigated\n * away as if it had worked.** On the edit page the same habit cleared fields nobody touched.\n *\n * `NewAppliance` is the source of the required list below. `AppliancePatch` is spread over the\n * loaded row, so the update sends a diff — see `utils/form-values.ts` for why those are two\n * different rules rather than one.\n */\n\n/** Fields `NewAppliance` declares non-null. Not a house rule — the schema's own list. */\nconst REQUIRED = ['serialNo', 'name', 'brand', 'model']\n\nconst APPLIANCE_FIELDS = `\n id\n name\n description\n serialNo\n brand\n model\n netmask\n accessToken\n`\n\n@customElement('appliance-editor')\nexport class ApplianceEditor extends localize(i18next)(LitElement) {\n /*\n * ── Where the scrolling happens ─────────────────────────────────────────\n * The button row stays put and only the middle scrolls. That is the house shape for a\n * popup, and this had it wrong: the host itself scrolled, so the row you press to finish\n * walked off the bottom of the card along with everything else.\n *\n * So the host clips, and the content area between the title and the buttons is the one\n * thing with overflow. Two details carry it:\n *\n * min-height: 0 A flex item defaults to min-height auto, which refuses to shrink below\n * its own content. Without this the overflow rule sits there doing\n * nothing and the card grows instead — it reads as a working rule.\n * no padding CommonPopupStyles already pads the host. Padding here again doubles the\n * inset and pushes the scrollbar in off the card edge.\n *\n * ScrollbarStyles gives the scrollbar the product's own shape rather than the browser's.\n *\n * ── Why the in-content buttons need a rule of their own ─────────────────\n * CommonPopupStyles makes controls pill-shaped because a popup is a rounded card and the\n * things inside it follow the card. But it says that through `.buttons`, so only the commit\n * row got it: the actions sitting inside a fieldset — generate a new token, generate a new\n * secret — kept the page radius and read as a different kind of button on the same card.\n * Measured on a live popup: 5px inside the content, 13986px in the row below it.\n */\n static styles = [\n CommonHeaderStyles,\n CommonPopupStyles,\n ScrollbarStyles,\n css`\n /* in-content actions follow the card too, not just the commit row. see the note on styles. */\n fieldset md-text-button,\n fieldset md-filled-button {\n --md-text-button-container-shape: var(--popup-control-radius);\n --md-filled-button-container-shape: var(--popup-control-radius);\n }\n\n /* the host does not scroll; the content area below does. see the note on styles. */\n :host {\n overflow: hidden;\n }\n\n /* min-height:0 is load-bearing — see the note on styles. */\n form {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n\n display: flex;\n flex-direction: column;\n gap: var(--popup-gap);\n }\n\n fieldset {\n border-radius: var(--border-radius);\n border: var(--border-dim-color);\n margin: 0;\n padding: var(--popup-padding);\n }\n\n legend {\n padding: var(--legend-padding);\n font-weight: bold;\n color: var(--legend-color);\n }\n\n /* one labelled box. .row puts two of them side by side; alone it spans the card. */\n [field] {\n display: flex;\n flex-direction: column;\n flex: 1;\n }\n\n label {\n font: var(--label-font);\n color: var(--label-color, var(--md-sys-color-on-surface));\n text-transform: var(--label-text-transform);\n }\n\n input {\n background-color: var(--input-field-background, var(--md-sys-color-surface-container-highest));\n color: var(--input-field-color, var(--md-sys-color-on-surface));\n border: var(--border-dim-color);\n border-radius: var(--border-radius);\n margin: var(--input-margin);\n padding: var(--input-padding);\n font: var(--input-font);\n }\n\n input:focus {\n outline: none;\n }\n\n input[invalid] {\n border-color: var(--md-sys-color-error);\n }\n\n [field-error] {\n color: var(--md-sys-color-error);\n font-size: var(--popup-font-size-small);\n margin-top: var(--spacing-tiny, 2px);\n }\n\n /* The one reverting action goes left, away from the ones that commit. */\n .buttons .left {\n margin-right: auto;\n }\n\n @media screen and (max-width: 480px) {\n [field] {\n grid-column: span 2;\n }\n }\n `\n ]\n\n /** The appliance to edit. Absent means this popup is registering a new one. */\n @property({ type: String }) applianceId?: string\n\n @query('form') form!: HTMLFormElement\n\n /** The loaded record. The patch is a diff against this, so it is never rebuilt from the form. */\n @state() private appliance: any = null\n\n /** Required fields still empty. Filled in when the person presses save, not while typing. */\n @state() private missing: string[] = []\n\n /** The mutation was refused. The shared link already said why; this says the press landed. */\n @state() private failed: boolean = false\n\n /** The single-record read did not answer. Distinct from answering with nothing. */\n @state() private loadFailed: boolean = false\n\n private get editing(): boolean {\n return !!this.applianceId\n }\n\n render() {\n const appliance = this.appliance || {}\n const exp = appliance.accessToken ? parseJwt(appliance.accessToken)?.exp : undefined\n\n if (this.editing && this.loadFailed) {\n return html`<div class=\"failure\">${i18next.t('text.the appliance could not be loaded')}</div>`\n }\n\n return html`\n <form @submit=${(e: Event) => e.preventDefault()}>\n <fieldset>\n <legend>${i18next.t('text.appliance')}</legend>\n <div field>\n <label for=\"name\">${i18next.t('field.name')}</label>\n <input\n id=\"name\"\n type=\"text\"\n name=\"name\"\n .value=${appliance.name || ''}\n required\n autofocus\n ?invalid=${this.missing.includes('name')}\n />\n ${this.fieldError('name')}\n </div>\n\n <div field>\n <label for=\"description\">${i18next.t('field.description')}</label>\n <input id=\"description\" type=\"text\" name=\"description\" .value=${appliance.description || ''} />\n </div>\n\n <div class=\"row\">\n <div field>\n <label for=\"serial-no\">${i18next.t('field.serial-no')}</label>\n <input\n id=\"serial-no\"\n type=\"text\"\n name=\"serialNo\"\n .value=${appliance.serialNo || ''}\n required\n ?invalid=${this.missing.includes('serialNo')}\n />\n ${this.fieldError('serialNo')}\n </div>\n\n <div field>\n <label for=\"brand\">${i18next.t('field.brand')}</label>\n <input\n id=\"brand\"\n type=\"text\"\n name=\"brand\"\n .value=${appliance.brand || ''}\n required\n ?invalid=${this.missing.includes('brand')}\n />\n ${this.fieldError('brand')}\n </div>\n </div>\n\n <div class=\"row\">\n <div field>\n <label for=\"model\">${i18next.t('field.model')}</label>\n <input\n id=\"model\"\n type=\"text\"\n name=\"model\"\n .value=${appliance.model || ''}\n required\n ?invalid=${this.missing.includes('model')}\n />\n ${this.fieldError('model')}\n </div>\n\n <div field>\n <label for=\"netmask\">${i18next.t('field.netmask')}</label>\n <input id=\"netmask\" type=\"text\" name=\"netmask\" .value=${appliance.netmask || ''} />\n </div>\n </div>\n </fieldset>\n\n ${this.editing ? this.renderCredential(appliance, exp) : ''}\n ${this.failed ? html`<div class=\"failure\">${i18next.t('text.could not be saved')}</div>` : ''}\n </form>\n\n <div class=\"buttons\">\n ${\n this.editing\n ? html`\n <md-text-button class=\"left\" @click=${this.deleteAppliance.bind(this)}>\n ${i18next.t('button.delete')}\n </md-text-button>\n `\n : ''\n }\n <md-text-button @click=${this.cancel.bind(this)}>${i18next.t('button.cancel')}</md-text-button>\n <md-filled-button @click=${this.save.bind(this)}>\n ${this.editing ? i18next.t('button.update') : i18next.t('button.register')}\n </md-filled-button>\n </div>\n `\n }\n\n /*\n * The credential panel only exists once the appliance does — there is no token to show\n * before it is created, and the list query deliberately never asks for one.\n */\n private renderCredential(appliance: any, exp?: number) {\n return html`\n <fieldset>\n <legend>${i18next.t('text.appliance credential')}</legend>\n <div field>\n <label for=\"access-token\">${i18next.t('label.access token')}</label>\n <secret-field id=\"access-token\" .value=${appliance.accessToken || ''}></secret-field>\n ${\n exp\n ? html`<div class=\"note\">\n ${i18next.t('text.token expiry time')} ${new Date(exp * 1000).toLocaleString()} :\n ${asyncReplace(this.expTimer(exp))}\n </div>`\n : ''\n }\n <div class=\"row\">\n <md-text-button @click=${this.generateSecret.bind(this)}>\n ${i18next.t('button.generate new access token')}\n </md-text-button>\n </div>\n </div>\n </fieldset>\n `\n }\n\n private fieldError(name: string) {\n return this.missing.includes(name) ? html`<div field-error>${i18next.t('text.this field is required')}</div>` : ''\n }\n\n async connectedCallback() {\n super.connectedCallback()\n\n if (this.editing) {\n await this.fetchAppliance()\n }\n }\n\n async fetchAppliance() {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n appliance(id: $id) { ${APPLIANCE_FIELDS} }\n }\n `,\n variables: { id: this.applianceId }\n })\n\n /*\n * `errorPolicy: 'all'` means a refusal arrives here rather than throwing. Reading\n * `data.appliance` without checking would show an empty form that looks like a record\n * with every field blank — and saving it would then write those blanks.\n */\n if (response.errors || !response.data?.appliance) {\n this.loadFailed = true\n return\n }\n\n this.loadFailed = false\n this.appliance = response.data.appliance\n }\n\n private cancel(e: Event) {\n e.preventDefault()\n this.close()\n }\n\n async save(e: Event) {\n e.preventDefault()\n\n const formData = new FormData(this.form)\n\n /*\n * The same four fields are required on both paths. They come from `NewAppliance`, and an\n * update is held to them too: the patch is spread over the row, so `name: ''` would clear\n * a field the product will not let you create without.\n */\n const missing = missingRequired(formData, REQUIRED)\n\n if (missing.length) {\n this.missing = missing\n return\n }\n\n this.missing = []\n\n return this.editing ? this.updateAppliance(formData) : this.createAppliance(formData)\n }\n\n private async createAppliance(formData: FormData) {\n const response = await client.mutate({\n mutation: gql`\n mutation ($appliance: NewAppliance!) {\n createAppliance(appliance: $appliance) {\n id\n }\n }\n `,\n variables: { appliance: filledValues(formData) }\n })\n\n if (response.errors) {\n this.failed = true\n return\n }\n\n this.saved()\n }\n\n private async updateAppliance(formData: FormData) {\n const patch = changedValues(formData, this.appliance)\n\n /*\n * Nothing moved. Sending an empty patch would still stamp `updater` and `updatedAt`,\n * which makes an untouched record look edited to whoever reads the list next.\n */\n if (!Object.keys(patch).length) {\n this.close()\n return\n }\n\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $patch: AppliancePatch!) {\n updateAppliance(id: $id, patch: $patch) { ${APPLIANCE_FIELDS} }\n }\n `,\n variables: { id: this.applianceId, patch }\n })\n\n if (response.errors) {\n this.failed = true\n return\n }\n\n this.saved()\n }\n\n private async generateSecret(e: Event) {\n e.preventDefault()\n\n const confirmed = await OxPrompt.open({\n type: 'warning',\n title: i18next.t('button.generate new access token'),\n text: i18next.t('text.the current access token stops working immediately'),\n confirmButton: { text: i18next.t('button.confirm') },\n cancelButton: { text: i18next.t('button.cancel') }\n })\n\n if (!confirmed) {\n return\n }\n\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n generateApplianceSecret(id: $id) { ${APPLIANCE_FIELDS} }\n }\n `,\n variables: { id: this.applianceId }\n })\n\n if (response.errors) {\n this.failed = true\n return\n }\n\n /*\n * Stays open on purpose. A new token is the one value on this screen the reader has to\n * copy before it is out of reach — closing here would hand it to them and take it away.\n */\n this.failed = false\n this.appliance = response.data.generateApplianceSecret\n this.dispatchEvent(new CustomEvent('appliance-changed', { bubbles: true, composed: true }))\n }\n\n private async deleteAppliance(e: Event) {\n e.preventDefault()\n\n const confirmed = await OxPrompt.open({\n type: 'warning',\n title: i18next.t('button.delete'),\n text: i18next.t('text.are you sure you want to delete x', { x: this.appliance?.name || '' }),\n confirmButton: { text: i18next.t('button.delete') },\n cancelButton: { text: i18next.t('button.cancel') }\n })\n\n if (!confirmed) {\n return\n }\n\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n deleteAppliance(id: $id)\n }\n `,\n variables: { id: this.applianceId }\n })\n\n if (response.errors) {\n this.failed = true\n return\n }\n\n this.saved()\n }\n\n /** The list has to refetch, and the popup is done. */\n private saved() {\n this.failed = false\n this.dispatchEvent(new CustomEvent('appliance-changed', { bubbles: true, composed: true }))\n this.close()\n }\n\n private close() {\n this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }))\n }\n\n private async *expTimer(exp: number) {\n const DAY = 24 * 60 * 60\n const HOUR = 60 * 60\n const MIN = 60\n\n while (this.isConnected) {\n let remain = Math.floor(Number(exp) - Date.now() / 1000)\n\n /*\n * Past the expiry the arithmetic below produces \"-5 days -3 hours\", which reads as a\n * duration rather than as the token being dead. Say it plainly and stop counting.\n */\n if (remain <= 0) {\n yield i18next.t('text.expired')\n return\n }\n\n const days = Math.floor(remain / DAY)\n remain -= days * DAY\n const hours = Math.floor(remain / HOUR)\n remain -= hours * HOUR\n const mins = Math.floor(remain / MIN)\n const secs = remain - mins * MIN\n\n yield i18next.t('text.remaining time', { days, hours, mins, secs })\n\n await sleep(1000)\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"appliance-editor.js","sourceRoot":"","sources":["../../client/components/appliance-editor.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@material/web/button/text-button.js'\nimport '@material/web/button/filled-button.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\nimport { asyncReplace } from 'lit/directives/async-replace.js'\n\nimport { client } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { OxPrompt } from '@operato/popup/ox-prompt.js'\nimport { CommonHeaderStyles, CommonPopupStyles, ScrollbarStyles } from '@operato/styles'\nimport { parseJwt, sleep } from '@operato/utils'\n\nimport './secret-field.js'\nimport { readRevocable, showsRevokeButton, showsRevokedNote } from '../utils/credential-panel-state.js'\nimport { changedValues, filledValues, missingRequired } from '../utils/form-values.js'\n\n/**\n * Registering and editing an appliance, in a popup.\n *\n * ── Why this replaced two pages ─────────────────────────────────────────────\n * `appliance-register` and `appliance/{id}` were full routes. Nothing outside auth-ui linked\n * to either, and every other resource in this package — role, user, domain, domain owner,\n * auth provider, attribute set — is a grist list with a popup over it. The two pages were not\n * a different kind of resource, only older code.\n *\n * ── The defect that came with them ──────────────────────────────────────────\n * Both pages sent the whole form. On the register page that meant `serialNo: ''`, and\n * `createAppliance` has no validation at all — it saves `{ domain, creator, updater,\n * ...appliance }` — so `''` passed GraphQL's non-null check (an empty string is a String) and\n * an appliance with no serial number, brand or model was created **and the screen navigated\n * away as if it had worked.** On the edit page the same habit cleared fields nobody touched.\n *\n * `NewAppliance` is the source of the required list below. `AppliancePatch` is spread over the\n * loaded row, so the update sends a diff — see `utils/form-values.ts` for why those are two\n * different rules rather than one.\n */\n\n/** Fields `NewAppliance` declares non-null. Not a house rule — the schema's own list. */\nconst REQUIRED = ['serialNo', 'name', 'brand', 'model']\n\nconst APPLIANCE_FIELDS = `\n id\n name\n description\n serialNo\n brand\n model\n netmask\n accessToken\n credentialRevoked\n`\n\n@customElement('appliance-editor')\nexport class ApplianceEditor extends localize(i18next)(LitElement) {\n /*\n * ── Where the scrolling happens ─────────────────────────────────────────\n * The button row stays put and only the middle scrolls. That is the house shape for a\n * popup, and this had it wrong: the host itself scrolled, so the row you press to finish\n * walked off the bottom of the card along with everything else.\n *\n * So the host clips, and the content area between the title and the buttons is the one\n * thing with overflow. Two details carry it:\n *\n * min-height: 0 A flex item defaults to min-height auto, which refuses to shrink below\n * its own content. Without this the overflow rule sits there doing\n * nothing and the card grows instead — it reads as a working rule.\n * no padding CommonPopupStyles already pads the host. Padding here again doubles the\n * inset and pushes the scrollbar in off the card edge.\n *\n * ScrollbarStyles gives the scrollbar the product's own shape rather than the browser's.\n *\n * ── Why the in-content buttons need a rule of their own ─────────────────\n * CommonPopupStyles makes controls pill-shaped because a popup is a rounded card and the\n * things inside it follow the card. But it says that through `.buttons`, so only the commit\n * row got it: the actions sitting inside a fieldset — generate a new token, generate a new\n * secret — kept the page radius and read as a different kind of button on the same card.\n * Measured on a live popup: 5px inside the content, 13986px in the row below it.\n */\n static styles = [\n CommonHeaderStyles,\n CommonPopupStyles,\n ScrollbarStyles,\n css`\n /* in-content actions follow the card too, not just the commit row. see the note on styles. */\n fieldset md-text-button,\n fieldset md-filled-button {\n --md-text-button-container-shape: var(--popup-control-radius);\n --md-filled-button-container-shape: var(--popup-control-radius);\n }\n\n /* the host does not scroll; the content area below does. see the note on styles. */\n :host {\n overflow: hidden;\n }\n\n /* min-height:0 is load-bearing — see the note on styles. */\n form {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n\n display: flex;\n flex-direction: column;\n gap: var(--popup-gap);\n }\n\n fieldset {\n border-radius: var(--border-radius);\n border: var(--border-dim-color);\n margin: 0;\n padding: var(--popup-padding);\n }\n\n legend {\n padding: var(--legend-padding);\n font-weight: bold;\n color: var(--legend-color);\n }\n\n /* one labelled box. .row puts two of them side by side; alone it spans the card. */\n [field] {\n display: flex;\n flex-direction: column;\n flex: 1;\n }\n\n label {\n font: var(--label-font);\n color: var(--label-color, var(--md-sys-color-on-surface));\n text-transform: var(--label-text-transform);\n }\n\n input {\n background-color: var(--input-field-background, var(--md-sys-color-surface-container-highest));\n color: var(--input-field-color, var(--md-sys-color-on-surface));\n border: var(--border-dim-color);\n border-radius: var(--border-radius);\n margin: var(--input-margin);\n padding: var(--input-padding);\n font: var(--input-font);\n }\n\n input:focus {\n outline: none;\n }\n\n input[invalid] {\n border-color: var(--md-sys-color-error);\n }\n\n [field-error] {\n color: var(--md-sys-color-error);\n font-size: var(--popup-font-size-small);\n margin-top: var(--spacing-tiny, 2px);\n }\n\n /* The one reverting action goes left, away from the ones that commit. */\n .buttons .left {\n margin-right: auto;\n }\n\n /* taking a credential away is not the same kind of act as issuing one. */\n md-text-button.danger {\n --md-text-button-label-text-color: var(--md-sys-color-error);\n }\n\n .note.revoked {\n color: var(--md-sys-color-error);\n }\n\n @media screen and (max-width: 480px) {\n [field] {\n grid-column: span 2;\n }\n }\n `\n ]\n\n /** The appliance to edit. Absent means this popup is registering a new one. */\n @property({ type: String }) applianceId?: string\n\n @query('form') form!: HTMLFormElement\n\n /** The loaded record. The patch is a diff against this, so it is never rebuilt from the form. */\n @state() private appliance: any = null\n\n /** Required fields still empty. Filled in when the person presses save, not while typing. */\n @state() private missing: string[] = []\n\n /** The mutation was refused. The shared link already said why; this says the press landed. */\n @state() private failed: boolean = false\n\n /** The single-record read did not answer. Distinct from answering with nothing. */\n @state() private loadFailed: boolean = false\n\n /*\n * Does revoking work on this installation, for this reader?\n *\n * Revocation is behind a configuration switch, and the server answers one question that folds\n * the switch and the reader's privilege together — see `applianceCredentialRevocable`. False\n * until that answer comes back, so a slow or refused read shows no button rather than one that\n * refuses when pressed.\n */\n @state() private revocable: boolean = false\n\n private get editing(): boolean {\n return !!this.applianceId\n }\n\n render() {\n const appliance = this.appliance || {}\n const exp = appliance.accessToken ? parseJwt(appliance.accessToken)?.exp : undefined\n\n if (this.editing && this.loadFailed) {\n return html`<div class=\"failure\">${i18next.t('text.the appliance could not be loaded')}</div>`\n }\n\n return html`\n <form @submit=${(e: Event) => e.preventDefault()}>\n <fieldset>\n <legend>${i18next.t('text.appliance')}</legend>\n <div field>\n <label for=\"name\">${i18next.t('field.name')}</label>\n <input\n id=\"name\"\n type=\"text\"\n name=\"name\"\n .value=${appliance.name || ''}\n required\n autofocus\n ?invalid=${this.missing.includes('name')}\n />\n ${this.fieldError('name')}\n </div>\n\n <div field>\n <label for=\"description\">${i18next.t('field.description')}</label>\n <input id=\"description\" type=\"text\" name=\"description\" .value=${appliance.description || ''} />\n </div>\n\n <div class=\"row\">\n <div field>\n <label for=\"serial-no\">${i18next.t('field.serial-no')}</label>\n <input\n id=\"serial-no\"\n type=\"text\"\n name=\"serialNo\"\n .value=${appliance.serialNo || ''}\n required\n ?invalid=${this.missing.includes('serialNo')}\n />\n ${this.fieldError('serialNo')}\n </div>\n\n <div field>\n <label for=\"brand\">${i18next.t('field.brand')}</label>\n <input\n id=\"brand\"\n type=\"text\"\n name=\"brand\"\n .value=${appliance.brand || ''}\n required\n ?invalid=${this.missing.includes('brand')}\n />\n ${this.fieldError('brand')}\n </div>\n </div>\n\n <div class=\"row\">\n <div field>\n <label for=\"model\">${i18next.t('field.model')}</label>\n <input\n id=\"model\"\n type=\"text\"\n name=\"model\"\n .value=${appliance.model || ''}\n required\n ?invalid=${this.missing.includes('model')}\n />\n ${this.fieldError('model')}\n </div>\n\n <div field>\n <label for=\"netmask\">${i18next.t('field.netmask')}</label>\n <input id=\"netmask\" type=\"text\" name=\"netmask\" .value=${appliance.netmask || ''} />\n </div>\n </div>\n </fieldset>\n\n ${this.editing ? this.renderCredential(appliance, exp) : ''}\n ${this.failed ? html`<div class=\"failure\">${i18next.t('text.could not be saved')}</div>` : ''}\n </form>\n\n <div class=\"buttons\">\n ${\n this.editing\n ? html`\n <md-text-button class=\"left\" @click=${this.deleteAppliance.bind(this)}>\n ${i18next.t('button.delete')}\n </md-text-button>\n `\n : ''\n }\n <md-text-button @click=${this.cancel.bind(this)}>${i18next.t('button.cancel')}</md-text-button>\n <md-filled-button @click=${this.save.bind(this)}>\n ${this.editing ? i18next.t('button.update') : i18next.t('button.register')}\n </md-filled-button>\n </div>\n `\n }\n\n /*\n * The credential panel only exists once the appliance does — there is no token to show\n * before it is created, and the list query deliberately never asks for one.\n *\n * ── Why a revoked appliance says so ─────────────────────────────────────\n * Revoking clears the token, so without a word for it the panel looks exactly like one whose\n * credential was never issued. Those are opposite situations: one is waiting to be set up,\n * the other has been deliberately shut off, and only one of them wants somebody to act.\n *\n * ── Why the revoke button is not always here ────────────────────────────\n * It appears only where the installation turned revocation on and the reader may use it. See\n * the note on `revocable`.\n */\n private renderCredential(appliance: any, exp?: number) {\n return html`\n <fieldset>\n <legend>${i18next.t('text.appliance credential')}</legend>\n <div field>\n <label for=\"access-token\">${i18next.t('label.access token')}</label>\n <secret-field id=\"access-token\" .value=${appliance.accessToken || ''}></secret-field>\n ${\n showsRevokedNote(appliance)\n ? html`<div class=\"note revoked\">${i18next.t('text.this credential has been revoked')}</div>`\n : ''\n }\n ${\n exp\n ? html`<div class=\"note\">\n ${i18next.t('text.token expiry time')} ${new Date(exp * 1000).toLocaleString()} :\n ${asyncReplace(this.expTimer(exp))}\n </div>`\n : ''\n }\n <div class=\"row\">\n <md-text-button @click=${this.generateSecret.bind(this)}>\n ${i18next.t('button.generate new access token')}\n </md-text-button>\n ${\n showsRevokeButton(this.revocable, appliance)\n ? html`\n <md-text-button class=\"danger\" @click=${this.revokeCredential.bind(this)}>\n ${i18next.t('button.revoke credential')}\n </md-text-button>\n `\n : ''\n }\n </div>\n </div>\n </fieldset>\n `\n }\n\n private fieldError(name: string) {\n return this.missing.includes(name) ? html`<div field-error>${i18next.t('text.this field is required')}</div>` : ''\n }\n\n async connectedCallback() {\n super.connectedCallback()\n\n if (this.editing) {\n await Promise.all([this.fetchAppliance(), this.fetchRevocable()])\n }\n }\n\n /*\n * Whether the revoke button exists at all.\n *\n * A refusal arrives as `response.errors` rather than a throw, and both a refusal and a `false`\n * mean the same thing to this screen: no button. So there is nothing to report here — the\n * absence of the button is the whole answer.\n */\n private async fetchRevocable() {\n const response = await client.query({\n query: gql`\n query {\n applianceCredentialRevocable\n }\n `\n })\n\n this.revocable = readRevocable(response as any)\n }\n\n async fetchAppliance() {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n appliance(id: $id) { ${APPLIANCE_FIELDS} }\n }\n `,\n variables: { id: this.applianceId }\n })\n\n /*\n * `errorPolicy: 'all'` means a refusal arrives here rather than throwing. Reading\n * `data.appliance` without checking would show an empty form that looks like a record\n * with every field blank — and saving it would then write those blanks.\n */\n if (response.errors || !response.data?.appliance) {\n this.loadFailed = true\n return\n }\n\n this.loadFailed = false\n this.appliance = response.data.appliance\n }\n\n private cancel(e: Event) {\n e.preventDefault()\n this.close()\n }\n\n async save(e: Event) {\n e.preventDefault()\n\n const formData = new FormData(this.form)\n\n /*\n * The same four fields are required on both paths. They come from `NewAppliance`, and an\n * update is held to them too: the patch is spread over the row, so `name: ''` would clear\n * a field the product will not let you create without.\n */\n const missing = missingRequired(formData, REQUIRED)\n\n if (missing.length) {\n this.missing = missing\n return\n }\n\n this.missing = []\n\n return this.editing ? this.updateAppliance(formData) : this.createAppliance(formData)\n }\n\n private async createAppliance(formData: FormData) {\n const response = await client.mutate({\n mutation: gql`\n mutation ($appliance: NewAppliance!) {\n createAppliance(appliance: $appliance) {\n id\n }\n }\n `,\n variables: { appliance: filledValues(formData) }\n })\n\n if (response.errors) {\n this.failed = true\n return\n }\n\n this.saved()\n }\n\n private async updateAppliance(formData: FormData) {\n const patch = changedValues(formData, this.appliance)\n\n /*\n * Nothing moved. Sending an empty patch would still stamp `updater` and `updatedAt`,\n * which makes an untouched record look edited to whoever reads the list next.\n */\n if (!Object.keys(patch).length) {\n this.close()\n return\n }\n\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $patch: AppliancePatch!) {\n updateAppliance(id: $id, patch: $patch) { ${APPLIANCE_FIELDS} }\n }\n `,\n variables: { id: this.applianceId, patch }\n })\n\n if (response.errors) {\n this.failed = true\n return\n }\n\n this.saved()\n }\n\n private async generateSecret(e: Event) {\n e.preventDefault()\n\n const confirmed = await OxPrompt.open({\n type: 'warning',\n title: i18next.t('button.generate new access token'),\n text: i18next.t('text.the current access token stops working immediately'),\n confirmButton: { text: i18next.t('button.confirm') },\n cancelButton: { text: i18next.t('button.cancel') }\n })\n\n if (!confirmed) {\n return\n }\n\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n generateApplianceSecret(id: $id) { ${APPLIANCE_FIELDS} }\n }\n `,\n variables: { id: this.applianceId }\n })\n\n if (response.errors) {\n this.failed = true\n return\n }\n\n /*\n * Stays open on purpose. A new token is the one value on this screen the reader has to\n * copy before it is out of reach — closing here would hand it to them and take it away.\n */\n this.failed = false\n this.appliance = response.data.generateApplianceSecret\n this.dispatchEvent(new CustomEvent('appliance-changed', { bubbles: true, composed: true }))\n }\n\n /*\n * Take the credential away and leave the appliance where it is.\n *\n * ── Why this is not the delete button ───────────────────────────────────\n * Deleting takes the appliance and its history with it. This is for a device that is lost,\n * decommissioned or suspect and whose record still has to stand — the row keeps pointing at\n * the same identity, and issuing a new secret later picks that identity back up.\n *\n * ── Why the popup stays open ────────────────────────────────────────────\n * The panel has to show the result: the token is gone and the appliance now says it was\n * revoked. Closing would leave the reader guessing whether the press landed, and the list\n * behind it is refetched either way by the event below.\n */\n private async revokeCredential(e: Event) {\n e.preventDefault()\n\n const confirmed = await OxPrompt.open({\n type: 'warning',\n title: i18next.t('button.revoke credential'),\n text: i18next.t('text.revoking stops every token this appliance holds'),\n confirmButton: { text: i18next.t('button.revoke credential') },\n cancelButton: { text: i18next.t('button.cancel') }\n })\n\n if (!confirmed) {\n return\n }\n\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n revokeApplianceCredential(id: $id) { ${APPLIANCE_FIELDS} }\n }\n `,\n variables: { id: this.applianceId }\n })\n\n if (response.errors) {\n this.failed = true\n return\n }\n\n this.failed = false\n this.appliance = response.data.revokeApplianceCredential\n this.dispatchEvent(new CustomEvent('appliance-changed', { bubbles: true, composed: true }))\n }\n\n private async deleteAppliance(e: Event) {\n e.preventDefault()\n\n const confirmed = await OxPrompt.open({\n type: 'warning',\n title: i18next.t('button.delete'),\n text: i18next.t('text.are you sure you want to delete x', { x: this.appliance?.name || '' }),\n confirmButton: { text: i18next.t('button.delete') },\n cancelButton: { text: i18next.t('button.cancel') }\n })\n\n if (!confirmed) {\n return\n }\n\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n deleteAppliance(id: $id)\n }\n `,\n variables: { id: this.applianceId }\n })\n\n if (response.errors) {\n this.failed = true\n return\n }\n\n this.saved()\n }\n\n /** The list has to refetch, and the popup is done. */\n private saved() {\n this.failed = false\n this.dispatchEvent(new CustomEvent('appliance-changed', { bubbles: true, composed: true }))\n this.close()\n }\n\n private close() {\n this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }))\n }\n\n private async *expTimer(exp: number) {\n const DAY = 24 * 60 * 60\n const HOUR = 60 * 60\n const MIN = 60\n\n while (this.isConnected) {\n let remain = Math.floor(Number(exp) - Date.now() / 1000)\n\n /*\n * Past the expiry the arithmetic below produces \"-5 days -3 hours\", which reads as a\n * duration rather than as the token being dead. Say it plainly and stop counting.\n */\n if (remain <= 0) {\n yield i18next.t('text.expired')\n return\n }\n\n const days = Math.floor(remain / DAY)\n remain -= days * DAY\n const hours = Math.floor(remain / HOUR)\n remain -= hours * HOUR\n const mins = Math.floor(remain / MIN)\n const secs = remain - mins * MIN\n\n yield i18next.t('text.remaining time', { days, hours, mins, secs })\n\n await sleep(1000)\n }\n }\n}\n"]}
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { __decorate, __metadata } from "tslib";
|
|
2
2
|
import { css, html, LitElement, nothing } from 'lit';
|
|
3
3
|
import { customElement, property } from 'lit/decorators.js';
|
|
4
|
+
import { i18next } from '@operato/i18n';
|
|
5
|
+
import { roleOrigin, showsRoleOrigin } from '../utils/role-origin.js';
|
|
6
|
+
/*
|
|
7
|
+
* ── Why a role can need to say where it came from ───────────────────────────
|
|
8
|
+
* A domain sees its own roles and its parent's, and nothing stops it having a role with the
|
|
9
|
+
* same name as one it inherits — the duplicate checks on the server ask only about the domain
|
|
10
|
+
* itself. This offered two checkboxes both reading `approver`, and ticking one of them decides
|
|
11
|
+
* who a gate lets through. See `utils/role-origin.ts` for when the label appears.
|
|
12
|
+
*/
|
|
4
13
|
let RoleSelector = class RoleSelector extends LitElement {
|
|
5
14
|
constructor() {
|
|
6
15
|
super(...arguments);
|
|
@@ -41,6 +50,13 @@ let RoleSelector = class RoleSelector extends LitElement {
|
|
|
41
50
|
font: var(--label-font);
|
|
42
51
|
color: var(--label-color, var(--md-sys-color-on-surface));
|
|
43
52
|
}
|
|
53
|
+
|
|
54
|
+
/* quieter than the name — it separates two rows, it is not what they are called. */
|
|
55
|
+
label [origin] {
|
|
56
|
+
text-transform: none;
|
|
57
|
+
font-size: var(--font-size-small, 0.8em);
|
|
58
|
+
color: var(--md-sys-color-on-surface-variant, rgba(0, 0, 0, 0.6));
|
|
59
|
+
}
|
|
44
60
|
li {
|
|
45
61
|
padding: var(--spacing-small);
|
|
46
62
|
}
|
|
@@ -54,6 +70,7 @@ let RoleSelector = class RoleSelector extends LitElement {
|
|
|
54
70
|
]; }
|
|
55
71
|
render() {
|
|
56
72
|
const userRoleIds = (this.userRoles || []).map(userRole => userRole.id);
|
|
73
|
+
const showsOrigin = showsRoleOrigin(this.roles);
|
|
57
74
|
return html `
|
|
58
75
|
${this.roles.length
|
|
59
76
|
? html `
|
|
@@ -72,7 +89,12 @@ let RoleSelector = class RoleSelector extends LitElement {
|
|
|
72
89
|
.checked=${userRoleIds.indexOf(role.id) >= 0}
|
|
73
90
|
.data-role=${role}
|
|
74
91
|
/>
|
|
75
|
-
<label for="${role.id}"
|
|
92
|
+
<label for="${role.id}">
|
|
93
|
+
${role.name}
|
|
94
|
+
${showsOrigin && roleOrigin(role)
|
|
95
|
+
? html `<span origin>${i18next.t('text.from x', { x: roleOrigin(role) })}</span>`
|
|
96
|
+
: nothing}
|
|
97
|
+
</label>
|
|
76
98
|
</li>
|
|
77
99
|
`)}
|
|
78
100
|
</ul>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role-selector.js","sourceRoot":"","sources":["../../client/components/role-selector.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,KAAK,CAAA;AACpD,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"role-selector.js","sourceRoot":"","sources":["../../client/components/role-selector.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { i18next } from '@operato/i18n'\n\nimport { roleOrigin, showsRoleOrigin } from '../utils/role-origin.js'\n\n/*\n * ── Why a role can need to say where it came from ───────────────────────────\n * A domain sees its own roles and its parent's, and nothing stops it having a role with the\n * same name as one it inherits — the duplicate checks on the server ask only about the domain\n * itself. This offered two checkboxes both reading `approver`, and ticking one of them decides\n * who a gate lets through. See `utils/role-origin.ts` for when the label appears.\n */\n\n@customElement('role-selector')\nexport class RoleSelector extends LitElement {\n static styles = [\n css`\n [subtitle] {\n margin: 0;\n padding: var(--subtitle-padding);\n font: var(--subtitle-font);\n color: var(--subtitle-text-color);\n }\n div {\n margin: 0 var(--spacing-medium) var(--spacing-medium) var(--spacing-medium);\n }\n ul {\n flex: 1;\n display: grid;\n grid-template-columns: 1fr 1fr;\n column-gap: 20px;\n overflow: auto;\n margin: 0;\n padding: var(--spacing-medium);\n list-style: none;\n border: 1px dashed rgba(0, 0, 0, 0.1);\n border-width: 1px 0;\n }\n input[type='checkbox'] {\n display: inline;\n }\n label {\n padding-left: var(--spacing-small);\n text-transform: capitalize;\n font: var(--label-font);\n color: var(--label-color, var(--md-sys-color-on-surface));\n }\n\n /* quieter than the name — it separates two rows, it is not what they are called. */\n label [origin] {\n text-transform: none;\n font-size: var(--font-size-small, 0.8em);\n color: var(--md-sys-color-on-surface-variant, rgba(0, 0, 0, 0.6));\n }\n li {\n padding: var(--spacing-small);\n }\n #checkAll,\n [for='checkAll'] {\n margin-bottom: var(--spacing-medium);\n padding-bottom: var(--spacing-small);\n font-weight: bold;\n }\n `\n ]\n\n @property({ type: String }) roleCategory: string = 'Roles'\n @property({ type: Object }) user: any = {}\n @property({ type: Array }) roles: any[] = []\n @property({ type: Array }) userRoles: any[] = []\n\n render() {\n const userRoleIds = (this.userRoles || []).map(userRole => userRole.id)\n const showsOrigin = showsRoleOrigin(this.roles)\n\n return html`\n ${this.roles.length\n ? html`\n <div>\n ${this.roleCategory ? html`<h3 subtitle>${this.roleCategory}</h3>` : nothing}\n <input id=\"checkAll\" type=\"checkbox\" @change=${this.checkAll.bind(this)} />\n <label for=\"checkAll\">Check all</label>\n </div>\n\n <ul roles>\n ${this.roles.map(\n role => html`\n <li>\n <input\n id=\"${role.id}\"\n type=\"checkbox\"\n .checked=${userRoleIds.indexOf(role.id) >= 0}\n .data-role=${role}\n />\n <label for=\"${role.id}\">\n ${role.name}\n ${showsOrigin && roleOrigin(role)\n ? html`<span origin>${i18next.t('text.from x', { x: roleOrigin(role) })}</span>`\n : nothing}\n </label>\n </li>\n `\n )}\n </ul>\n `\n : ''}\n `\n }\n\n checkAll(e) {\n const checked = e.currentTarget.checked\n Array.from(this.renderRoot.querySelectorAll('ul[roles] input[type=checkbox]')).forEach(\n checkbox => ((checkbox as HTMLInputElement).checked = checked)\n )\n }\n\n selectedRoles() {\n return Array.from(this.renderRoot.querySelectorAll('ul[roles] input[type=checkbox]'))\n .filter(input => (input as HTMLInputElement).checked)\n .map(input => {\n const { id, name } = input['data-role']\n return { id, name }\n })\n }\n}\n"]}
|
|
@@ -53,6 +53,13 @@ export declare class UserInvite extends UserInvite_base {
|
|
|
53
53
|
private timer?;
|
|
54
54
|
/** 계정을 새로 만드는 경로인가. */
|
|
55
55
|
private get creating();
|
|
56
|
+
/**
|
|
57
|
+
* 메일로 초대하는 경로인가.
|
|
58
|
+
*
|
|
59
|
+
* 계정이 없고 기본 비밀번호도 없을 때다. 관리자가 비밀번호를 대신 정해 줄 수 없으므로
|
|
60
|
+
* **본인이 메일을 받아 스스로 정하는 것이 유일한 길**이다.
|
|
61
|
+
*/
|
|
62
|
+
private get inviting();
|
|
56
63
|
/** 부를 수 있나 — 단추와 처리기가 같은 답을 본다. */
|
|
57
64
|
private get callable();
|
|
58
65
|
render(): import("lit-html").TemplateResult<1>;
|
|
@@ -63,6 +70,7 @@ export declare class UserInvite extends UserInvite_base {
|
|
|
63
70
|
disconnectedCallback(): void;
|
|
64
71
|
private onEmail;
|
|
65
72
|
private check;
|
|
73
|
+
private sendInvitation;
|
|
66
74
|
private checkDefaultPassword;
|
|
67
75
|
private fetchRoles;
|
|
68
76
|
private onToggle;
|
|
@@ -123,9 +123,18 @@ let UserInvite = class UserInvite extends localize(i18next)(LitElement) {
|
|
|
123
123
|
get creating() {
|
|
124
124
|
return this.lookup === 'absent' && this.defaultPasswordConfigured;
|
|
125
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* 메일로 초대하는 경로인가.
|
|
128
|
+
*
|
|
129
|
+
* 계정이 없고 기본 비밀번호도 없을 때다. 관리자가 비밀번호를 대신 정해 줄 수 없으므로
|
|
130
|
+
* **본인이 메일을 받아 스스로 정하는 것이 유일한 길**이다.
|
|
131
|
+
*/
|
|
132
|
+
get inviting() {
|
|
133
|
+
return this.lookup === 'absent' && !this.defaultPasswordConfigured;
|
|
134
|
+
}
|
|
126
135
|
/** 부를 수 있나 — 단추와 처리기가 같은 답을 본다. */
|
|
127
136
|
get callable() {
|
|
128
|
-
return !this.working && (this.lookup === 'found' || this.creating);
|
|
137
|
+
return !this.working && (this.lookup === 'found' || this.creating || this.inviting);
|
|
129
138
|
}
|
|
130
139
|
render() {
|
|
131
140
|
return html `
|
|
@@ -141,11 +150,15 @@ let UserInvite = class UserInvite extends localize(i18next)(LitElement) {
|
|
|
141
150
|
<div invalid>${this.message}</div>
|
|
142
151
|
|
|
143
152
|
${this.renderLookup()} ${this.creating ? this.renderNewAccount() : nothing}
|
|
144
|
-
${this.lookup === 'found' || this.creating ? this.renderRoles() : nothing}
|
|
153
|
+
${this.lookup === 'found' || this.creating || this.inviting ? this.renderRoles() : nothing}
|
|
145
154
|
|
|
146
155
|
<div class="button-container">
|
|
147
156
|
<md-elevated-button ?disabled=${!this.callable} @click=${() => this.onCall()}>
|
|
148
|
-
${this.
|
|
157
|
+
${this.inviting
|
|
158
|
+
? i18next.t('label.send invitation')
|
|
159
|
+
: this.creating
|
|
160
|
+
? i18next.t('label.create account and add')
|
|
161
|
+
: i18next.t('label.add to domain')}
|
|
149
162
|
</md-elevated-button>
|
|
150
163
|
<md-elevated-button @click=${() => this.dispatchEvent(new CustomEvent('close'))}>
|
|
151
164
|
${i18next.t('button.cancel')}
|
|
@@ -163,10 +176,11 @@ let UserInvite = class UserInvite extends localize(i18next)(LitElement) {
|
|
|
163
176
|
return this.defaultPasswordConfigured
|
|
164
177
|
? html `<div note>${i18next.t('text.account will be created with the default password')}</div>`
|
|
165
178
|
: /*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
179
|
+
* 전에는 여기가 막다른 자리였다 — 관리자가 비밀번호를 대신 정해 줄 수 없으니 계정을
|
|
180
|
+
* 만들 길이 없었다. 이제 초대장이 그 길이다. 본인이 메일을 받아 스스로 비밀번호를
|
|
181
|
+
* 정하고 들어온다.
|
|
168
182
|
*/
|
|
169
|
-
html `<div note
|
|
183
|
+
html `<div note>${i18next.t('text.no account so an invitation will be mailed')}</div>`;
|
|
170
184
|
default:
|
|
171
185
|
return nothing;
|
|
172
186
|
}
|
|
@@ -251,6 +265,31 @@ let UserInvite = class UserInvite extends localize(i18next)(LitElement) {
|
|
|
251
265
|
}
|
|
252
266
|
this.lookup = response.data?.checkUserExistence ? 'found' : 'absent';
|
|
253
267
|
}
|
|
268
|
+
async sendInvitation(email) {
|
|
269
|
+
this.working = true;
|
|
270
|
+
try {
|
|
271
|
+
const response = await client.mutate({
|
|
272
|
+
mutation: gql `
|
|
273
|
+
mutation ($email: String!, $roleIds: [String!]) {
|
|
274
|
+
sendInvitation(email: $email, roleIds: $roleIds) {
|
|
275
|
+
id
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
`,
|
|
279
|
+
variables: { email, roleIds: [...this.selected] },
|
|
280
|
+
context: gqlContext()
|
|
281
|
+
});
|
|
282
|
+
if (response.errors?.length) {
|
|
283
|
+
this.message = response.errors[0]?.message;
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
this.dispatchEvent(new CustomEvent('invited', { detail: { email }, bubbles: true, composed: true }));
|
|
287
|
+
this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }));
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
this.working = false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
254
293
|
async checkDefaultPassword() {
|
|
255
294
|
const response = await client.query({
|
|
256
295
|
query: gql `
|
|
@@ -289,6 +328,14 @@ let UserInvite = class UserInvite extends localize(i18next)(LitElement) {
|
|
|
289
328
|
return;
|
|
290
329
|
}
|
|
291
330
|
const email = this.email.trim();
|
|
331
|
+
if (this.inviting) {
|
|
332
|
+
/*
|
|
333
|
+
* 초대는 여기서 끝난다 — 역할은 초대장이 싣고 가고, 수락할 때 함께 붙는다. 아래의
|
|
334
|
+
* 「넣고 나서 역할」 두 호출과 달리 한 번이다.
|
|
335
|
+
*/
|
|
336
|
+
await this.sendInvitation(email);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
292
339
|
const username = this.creating ? this.usernameInput?.value.trim() || email : email;
|
|
293
340
|
if (this.creating && !this.nameInput?.value.trim()) {
|
|
294
341
|
this.message = i18next.t('text.x is required', { x: i18next.t('field.name') });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user-invite.js","sourceRoot":"","sources":["../../client/components/user-invite.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@material/web/button/elevated-button.js'\nimport '@material/web/textfield/filled-text-field.js'\nimport '@material/web/checkbox/checkbox.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, query, 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 { InheritedValueType } from '@operato/shell'\nimport { ButtonContainerStyles, FormFieldStyles } from '@operato/styles'\n\n/**\n * 사람을 이 도메인으로 부르는 자리.\n *\n * ── 하나의 단추가 두 일을 덮고 있었다 ───────────────────────────────────────\n * 옛 화면의 「초대」는 `inviteUser` 를 부르는데, 그것이 하는 일은 **이미 계정이 있는 사람을 이\n * 도메인에 즉시 넣는 것**이다(`user-mutation.ts` 의 `inviteUser` — `invitee.domains` 에 도메인을\n * 더해 저장하고 끝난다). 메일도 가지 않고 상대의 동의도 없다.\n *\n * 그리고 계정이 없는 사람은 **거절했다** — 「사용자를 찾을 수 없습니다」. 관리자는 초대장을\n * 보냈다고 믿고 기다린다. 계정을 만드는 길은 화면의 **다른 자리**(`create-user`)에 있었고, 그\n * 자리는 기본 비밀번호가 설정된 도메인에서만 나타났다.\n *\n * ── 그래서 이메일 하나로 시작해 갈라 보여 준다 ──────────────────────────────\n * `checkUserExistence` 가 시스템 전체에서 그 이메일을 찾는다. 관리자는 「초대인가 생성인가」를\n * 먼저 고르지 않는다 — 이메일을 넣으면 화면이 무엇이 일어날지 말하고, 단추 글자도 그에 맞춘다.\n *\n * 계정이 있다 이 도메인에 바로 들어온다 (`inviteUser`)\n * 없고 기본 비밀번호 있다 계정을 만들어 넣는다 (`createUser`) — 비밀번호를 관리자가 전한다\n * 없고 기본 비밀번호 없다 **길이 없다.** 단추를 내주지 않고 무엇이 막고 있는지 말한다\n *\n * ⚠ **셋 중 어느 것도 「초대장」이 아니다.** 상대가 수락하는 흐름은 서버에 아직 없다\n * (`sendInvitation` 도 가입한 사용자만 받고, `Invitation` 에 상태도 만료도 없다). 그래서 이\n * 화면은 초대라는 말을 쓰지 않고 **실제로 일어나는 일**을 쓴다.\n *\n * ── 역할을 함께 고른다 ──────────────────────────────────────────────────────\n * 역할 없이 들어온 사람은 로그인해도 화면이 하나도 없다. 부르는 자리에서 함께 정하면 그 사람이\n * 처음 들어왔을 때 빈 화면을 보지 않는다.\n *\n * 넣기와 역할 부여는 **호출이 둘**이다(`inviteUser` 는 역할을 받지 않고, `createUser` 는 받지만\n * **이 도메인 소유의 역할만** 붙인다 — 상속 역할과 파트너 역할을 조용히 버린다). 그래서 두\n * 경로 모두 넣은 뒤 `updateUserRoles` 로 한 번에 준다. 앞이 되고 뒤가 실패하면 역할 없이 들어온\n * 상태가 되는데, 목록이 그것을 빨갛게 말하므로 관리자가 이어서 고칠 수 있다.\n */\n@customElement('user-invite')\nexport class UserInvite extends localize(i18next)(LitElement) {\n static styles: CSSResultGroup = [\n FormFieldStyles,\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 min-width: min(420px, 86vw);\n background-color: var(--md-sys-color-surface);\n color: var(--md-sys-color-on-surface);\n }\n\n label {\n color: var(--md-sys-color-on-surface-variant);\n font-size: 0.8rem;\n }\n\n input {\n width: 100%;\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 }\n\n /* 색은 짝으로. 막고 있는 것만 눈에 걸리게 한다. */\n [note] {\n padding: 8px 10px;\n border-radius: var(--md-sys-shape-corner-small, 6px);\n font-size: 0.82rem;\n line-height: 1.5;\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 [invalid] {\n color: var(--md-sys-color-error);\n font-size: 0.8rem;\n min-height: 1.1rem;\n }\n\n ul[roles] {\n list-style: none;\n margin: 0;\n padding: 0;\n }\n\n ul[roles] li {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 4px 2px;\n }\n `\n ]\n\n @state() private email: string = ''\n @state() private lookup: 'idle' | 'checking' | 'found' | 'absent' = 'idle'\n @state() private roles: any[] = []\n @state() private selected: Set<string> = new Set()\n @state() private working: boolean = false\n @state() private message: string = ''\n\n /** 기본 비밀번호가 설정돼 있지 않으면 계정을 만드는 길이 없다 — 서버가 던진다. */\n @state() private defaultPasswordConfigured: boolean = false\n\n @query('[name=name]') private nameInput!: HTMLInputElement\n @query('[name=username]') private usernameInput!: HTMLInputElement\n\n private timer?: ReturnType<typeof setTimeout>\n\n /** 계정을 새로 만드는 경로인가. */\n private get creating(): boolean {\n return this.lookup === 'absent' && this.defaultPasswordConfigured\n }\n\n /** 부를 수 있나 — 단추와 처리기가 같은 답을 본다. */\n private get callable(): boolean {\n return !this.working && (this.lookup === 'found' || this.creating)\n }\n\n render() {\n return html`\n <md-filled-text-field\n id=\"email\"\n name=\"email\"\n type=\"email\"\n label=${i18next.t('field.email')}\n .value=${this.email}\n placeholder=\"name@company.com\"\n @input=${(e: Event) => this.onEmail((e.target as HTMLInputElement).value)}\n ></md-filled-text-field>\n <div invalid>${this.message}</div>\n\n ${this.renderLookup()} ${this.creating ? this.renderNewAccount() : nothing}\n ${this.lookup === 'found' || this.creating ? this.renderRoles() : nothing}\n\n <div class=\"button-container\">\n <md-elevated-button ?disabled=${!this.callable} @click=${() => this.onCall()}>\n ${this.creating ? i18next.t('label.create account and add') : i18next.t('label.add to domain')}\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 }\n\n private renderLookup() {\n switch (this.lookup) {\n case 'checking':\n return html`<div note>${i18next.t('text.loading')}</div>`\n case 'found':\n return html`<div note>${i18next.t('text.account exists so joins immediately')}</div>`\n case 'absent':\n return this.defaultPasswordConfigured\n ? html`<div note>${i18next.t('text.account will be created with the default password')}</div>`\n : /*\n * 무엇이 막고 있는지 말한다. 여기에 단추를 두면 눌러도 아무 일이 없다 — 서버에\n * 그 길이 없기 때문이다.\n */\n html`<div note attention>${i18next.t('text.no account and no default password')}</div>`\n default:\n return nothing\n }\n }\n\n private renderNewAccount() {\n return html`\n <md-filled-text-field\n id=\"new-name\"\n name=\"name\"\n type=\"text\"\n label=${i18next.t('field.name')}\n @input=${() => this.requestUpdate()}\n ></md-filled-text-field>\n\n <!-- 서버가 받는 글자는 A-Z a-z 0-9 . _ @ - 다. 비우면 이메일을 그대로 쓴다. -->\n <md-filled-text-field\n id=\"new-username\"\n name=\"username\"\n type=\"text\"\n label=${i18next.t('field.username')}\n placeholder=${this.email}\n ></md-filled-text-field>\n `\n }\n\n private renderRoles() {\n return html`\n <div section>${i18next.t('field.roles')}</div>\n ${\n this.roles.length\n ? html`<ul roles>\n ${this.roles.map(\n role => html`\n <li>\n <md-checkbox\n id=\"invite-role-${role.id}\"\n .checked=${this.selected.has(role.id)}\n @change=${(e: Event) => this.onToggle(role.id, (e.target as HTMLInputElement).checked)}\n ></md-checkbox>\n <label for=\"invite-role-${role.id}\">${role.name}</label>\n </li>\n `\n )}\n </ul>`\n : html`<div note attention>${i18next.t('text.no role defined in this domain')}</div>`\n }\n ${\n this.roles.length && this.selected.size === 0\n ? html`<div note attention>${i18next.t('text.without a role they see nothing')}</div>`\n : nothing\n }\n `\n }\n\n connectedCallback() {\n super.connectedCallback()\n this.fetchRoles()\n this.checkDefaultPassword()\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n clearTimeout(this.timer)\n }\n\n private onEmail(value: string) {\n this.email = value\n this.message = ''\n this.lookup = 'idle'\n\n clearTimeout(this.timer)\n\n if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value.trim())) {\n return\n }\n\n /* 한 글자마다 묻지 않는다. 사람이 멈추면 묻는다. */\n this.timer = setTimeout(() => this.check(), 400)\n }\n\n private async check() {\n const email = this.email.trim()\n this.lookup = 'checking'\n\n const response = await client.query({\n query: gql`\n query ($email: EmailAddress!) {\n checkUserExistence(email: $email)\n }\n `,\n variables: { email },\n context: gqlContext()\n })\n\n /* 묻는 사이에 사람이 더 쳤으면 그 답은 이미 옛것이다. */\n if (email !== this.email.trim()) {\n return\n }\n\n this.lookup = response.data?.checkUserExistence ? 'found' : 'absent'\n }\n\n private async checkDefaultPassword() {\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 private async fetchRoles() {\n const response = await client.query({\n query: gql`\n query ($inherited: InheritedValueType) {\n roles(inherited: $inherited) {\n items {\n id\n name\n }\n }\n }\n `,\n variables: { inherited: InheritedValueType.Include },\n context: gqlContext()\n })\n\n this.roles = response.data?.roles?.items || []\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 private async onCall() {\n if (!this.callable) {\n return\n }\n\n const email = this.email.trim()\n const username = this.creating ? this.usernameInput?.value.trim() || email : email\n\n if (this.creating && !this.nameInput?.value.trim()) {\n this.message = i18next.t('text.x is required', { x: i18next.t('field.name') })\n return\n }\n\n this.working = true\n\n try {\n const entered = this.creating ? await this.createAccount(email, username) : await this.addToDomain(username)\n\n if (!entered) {\n return\n }\n\n if (this.selected.size) {\n /*\n * 넣기와 역할 부여가 두 호출이다. 여기가 실패하면 그 사람은 역할 없이 들어와 있고,\n * 목록이 그것을 빨갛게 말한다 — 사라지지 않는다.\n *\n * `createUser` 도 역할을 받지만 **이 도메인 소유의 역할만** 붙인다. 상속 역할과 파트너\n * 역할을 조용히 버리므로, 고른 것을 그대로 주려면 이 호출을 쓴다.\n */\n 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.roles.map(role => ({ id: role.id })),\n selectedRoles: this.roles.filter(role => this.selected.has(role.id)).map(role => ({ id: role.id }))\n },\n context: gqlContext()\n })\n }\n\n this.dispatchEvent(new CustomEvent('user-invited', { detail: { username }, bubbles: true, composed: true }))\n } finally {\n this.working = false\n }\n }\n\n /** 이미 계정이 있는 사람 — 이 도메인에 바로 들어온다. */\n private async addToDomain(username: string): Promise<boolean> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($username: String!) {\n inviteUser(username: $username)\n }\n `,\n variables: { username },\n context: gqlContext()\n })\n\n if (response.errors) {\n this.message = response.errors[0]?.message || ''\n return false\n }\n\n return true\n }\n\n /**\n * 계정이 없는 사람 — 기본 비밀번호로 만든다.\n *\n * 역할은 여기서 안 넘긴다. `createUser` 는 **이 도메인 소유의 역할만** 붙이므로 상속·파트너\n * 역할이 조용히 빠진다. 부르는 쪽에서 `updateUserRoles` 로 한 번에 준다.\n */\n private async createAccount(email: string, username: string): Promise<boolean> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($user: NewUser!) {\n createUser(user: $user) {\n id\n }\n }\n `,\n variables: {\n user: {\n username,\n name: this.nameInput.value.trim(),\n email,\n userType: 'user'\n }\n },\n context: gqlContext()\n })\n\n if (response.errors) {\n this.message = response.errors[0]?.message || ''\n return false\n }\n\n return true\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"user-invite.js","sourceRoot":"","sources":["../../client/components/user-invite.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@material/web/button/elevated-button.js'\nimport '@material/web/textfield/filled-text-field.js'\nimport '@material/web/checkbox/checkbox.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, query, 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 { InheritedValueType } from '@operato/shell'\nimport { ButtonContainerStyles, FormFieldStyles } from '@operato/styles'\n\n/**\n * 사람을 이 도메인으로 부르는 자리.\n *\n * ── 하나의 단추가 두 일을 덮고 있었다 ───────────────────────────────────────\n * 옛 화면의 「초대」는 `inviteUser` 를 부르는데, 그것이 하는 일은 **이미 계정이 있는 사람을 이\n * 도메인에 즉시 넣는 것**이다(`user-mutation.ts` 의 `inviteUser` — `invitee.domains` 에 도메인을\n * 더해 저장하고 끝난다). 메일도 가지 않고 상대의 동의도 없다.\n *\n * 그리고 계정이 없는 사람은 **거절했다** — 「사용자를 찾을 수 없습니다」. 관리자는 초대장을\n * 보냈다고 믿고 기다린다. 계정을 만드는 길은 화면의 **다른 자리**(`create-user`)에 있었고, 그\n * 자리는 기본 비밀번호가 설정된 도메인에서만 나타났다.\n *\n * ── 그래서 이메일 하나로 시작해 갈라 보여 준다 ──────────────────────────────\n * `checkUserExistence` 가 시스템 전체에서 그 이메일을 찾는다. 관리자는 「초대인가 생성인가」를\n * 먼저 고르지 않는다 — 이메일을 넣으면 화면이 무엇이 일어날지 말하고, 단추 글자도 그에 맞춘다.\n *\n * 계정이 있다 이 도메인에 바로 들어온다 (`inviteUser`)\n * 없고 기본 비밀번호 있다 계정을 만들어 넣는다 (`createUser`) — 비밀번호를 관리자가 전한다\n * 없고 기본 비밀번호 없다 **길이 없다.** 단추를 내주지 않고 무엇이 막고 있는지 말한다\n *\n * ⚠ **셋 중 어느 것도 「초대장」이 아니다.** 상대가 수락하는 흐름은 서버에 아직 없다\n * (`sendInvitation` 도 가입한 사용자만 받고, `Invitation` 에 상태도 만료도 없다). 그래서 이\n * 화면은 초대라는 말을 쓰지 않고 **실제로 일어나는 일**을 쓴다.\n *\n * ── 역할을 함께 고른다 ──────────────────────────────────────────────────────\n * 역할 없이 들어온 사람은 로그인해도 화면이 하나도 없다. 부르는 자리에서 함께 정하면 그 사람이\n * 처음 들어왔을 때 빈 화면을 보지 않는다.\n *\n * 넣기와 역할 부여는 **호출이 둘**이다(`inviteUser` 는 역할을 받지 않고, `createUser` 는 받지만\n * **이 도메인 소유의 역할만** 붙인다 — 상속 역할과 파트너 역할을 조용히 버린다). 그래서 두\n * 경로 모두 넣은 뒤 `updateUserRoles` 로 한 번에 준다. 앞이 되고 뒤가 실패하면 역할 없이 들어온\n * 상태가 되는데, 목록이 그것을 빨갛게 말하므로 관리자가 이어서 고칠 수 있다.\n */\n@customElement('user-invite')\nexport class UserInvite extends localize(i18next)(LitElement) {\n static styles: CSSResultGroup = [\n FormFieldStyles,\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 min-width: min(420px, 86vw);\n background-color: var(--md-sys-color-surface);\n color: var(--md-sys-color-on-surface);\n }\n\n label {\n color: var(--md-sys-color-on-surface-variant);\n font-size: 0.8rem;\n }\n\n input {\n width: 100%;\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 }\n\n /* 색은 짝으로. 막고 있는 것만 눈에 걸리게 한다. */\n [note] {\n padding: 8px 10px;\n border-radius: var(--md-sys-shape-corner-small, 6px);\n font-size: 0.82rem;\n line-height: 1.5;\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 [invalid] {\n color: var(--md-sys-color-error);\n font-size: 0.8rem;\n min-height: 1.1rem;\n }\n\n ul[roles] {\n list-style: none;\n margin: 0;\n padding: 0;\n }\n\n ul[roles] li {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 4px 2px;\n }\n `\n ]\n\n @state() private email: string = ''\n @state() private lookup: 'idle' | 'checking' | 'found' | 'absent' = 'idle'\n @state() private roles: any[] = []\n @state() private selected: Set<string> = new Set()\n @state() private working: boolean = false\n @state() private message: string = ''\n\n /** 기본 비밀번호가 설정돼 있지 않으면 계정을 만드는 길이 없다 — 서버가 던진다. */\n @state() private defaultPasswordConfigured: boolean = false\n\n @query('[name=name]') private nameInput!: HTMLInputElement\n @query('[name=username]') private usernameInput!: HTMLInputElement\n\n private timer?: ReturnType<typeof setTimeout>\n\n /** 계정을 새로 만드는 경로인가. */\n private get creating(): boolean {\n return this.lookup === 'absent' && this.defaultPasswordConfigured\n }\n\n /**\n * 메일로 초대하는 경로인가.\n *\n * 계정이 없고 기본 비밀번호도 없을 때다. 관리자가 비밀번호를 대신 정해 줄 수 없으므로\n * **본인이 메일을 받아 스스로 정하는 것이 유일한 길**이다.\n */\n private get inviting(): boolean {\n return this.lookup === 'absent' && !this.defaultPasswordConfigured\n }\n\n /** 부를 수 있나 — 단추와 처리기가 같은 답을 본다. */\n private get callable(): boolean {\n return !this.working && (this.lookup === 'found' || this.creating || this.inviting)\n }\n\n render() {\n return html`\n <md-filled-text-field\n id=\"email\"\n name=\"email\"\n type=\"email\"\n label=${i18next.t('field.email')}\n .value=${this.email}\n placeholder=\"name@company.com\"\n @input=${(e: Event) => this.onEmail((e.target as HTMLInputElement).value)}\n ></md-filled-text-field>\n <div invalid>${this.message}</div>\n\n ${this.renderLookup()} ${this.creating ? this.renderNewAccount() : nothing}\n ${this.lookup === 'found' || this.creating || this.inviting ? this.renderRoles() : nothing}\n\n <div class=\"button-container\">\n <md-elevated-button ?disabled=${!this.callable} @click=${() => this.onCall()}>\n ${this.inviting\n ? i18next.t('label.send invitation')\n : this.creating\n ? i18next.t('label.create account and add')\n : i18next.t('label.add to domain')}\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 }\n\n private renderLookup() {\n switch (this.lookup) {\n case 'checking':\n return html`<div note>${i18next.t('text.loading')}</div>`\n case 'found':\n return html`<div note>${i18next.t('text.account exists so joins immediately')}</div>`\n case 'absent':\n return this.defaultPasswordConfigured\n ? html`<div note>${i18next.t('text.account will be created with the default password')}</div>`\n : /*\n * 전에는 여기가 막다른 자리였다 — 관리자가 비밀번호를 대신 정해 줄 수 없으니 계정을\n * 만들 길이 없었다. 이제 초대장이 그 길이다. 본인이 메일을 받아 스스로 비밀번호를\n * 정하고 들어온다.\n */\n html`<div note>${i18next.t('text.no account so an invitation will be mailed')}</div>`\n default:\n return nothing\n }\n }\n\n private renderNewAccount() {\n return html`\n <md-filled-text-field\n id=\"new-name\"\n name=\"name\"\n type=\"text\"\n label=${i18next.t('field.name')}\n @input=${() => this.requestUpdate()}\n ></md-filled-text-field>\n\n <!-- 서버가 받는 글자는 A-Z a-z 0-9 . _ @ - 다. 비우면 이메일을 그대로 쓴다. -->\n <md-filled-text-field\n id=\"new-username\"\n name=\"username\"\n type=\"text\"\n label=${i18next.t('field.username')}\n placeholder=${this.email}\n ></md-filled-text-field>\n `\n }\n\n private renderRoles() {\n return html`\n <div section>${i18next.t('field.roles')}</div>\n ${\n this.roles.length\n ? html`<ul roles>\n ${this.roles.map(\n role => html`\n <li>\n <md-checkbox\n id=\"invite-role-${role.id}\"\n .checked=${this.selected.has(role.id)}\n @change=${(e: Event) => this.onToggle(role.id, (e.target as HTMLInputElement).checked)}\n ></md-checkbox>\n <label for=\"invite-role-${role.id}\">${role.name}</label>\n </li>\n `\n )}\n </ul>`\n : html`<div note attention>${i18next.t('text.no role defined in this domain')}</div>`\n }\n ${\n this.roles.length && this.selected.size === 0\n ? html`<div note attention>${i18next.t('text.without a role they see nothing')}</div>`\n : nothing\n }\n `\n }\n\n connectedCallback() {\n super.connectedCallback()\n this.fetchRoles()\n this.checkDefaultPassword()\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n clearTimeout(this.timer)\n }\n\n private onEmail(value: string) {\n this.email = value\n this.message = ''\n this.lookup = 'idle'\n\n clearTimeout(this.timer)\n\n if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value.trim())) {\n return\n }\n\n /* 한 글자마다 묻지 않는다. 사람이 멈추면 묻는다. */\n this.timer = setTimeout(() => this.check(), 400)\n }\n\n private async check() {\n const email = this.email.trim()\n this.lookup = 'checking'\n\n const response = await client.query({\n query: gql`\n query ($email: EmailAddress!) {\n checkUserExistence(email: $email)\n }\n `,\n variables: { email },\n context: gqlContext()\n })\n\n /* 묻는 사이에 사람이 더 쳤으면 그 답은 이미 옛것이다. */\n if (email !== this.email.trim()) {\n return\n }\n\n this.lookup = response.data?.checkUserExistence ? 'found' : 'absent'\n }\n\n private async sendInvitation(email: string) {\n this.working = true\n\n try {\n const response = await client.mutate({\n mutation: gql`\n mutation ($email: String!, $roleIds: [String!]) {\n sendInvitation(email: $email, roleIds: $roleIds) {\n id\n }\n }\n `,\n variables: { email, roleIds: [...this.selected] },\n context: gqlContext()\n })\n\n if (response.errors?.length) {\n this.message = response.errors[0]?.message\n return\n }\n\n this.dispatchEvent(new CustomEvent('invited', { detail: { email }, bubbles: true, composed: true }))\n this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }))\n } finally {\n this.working = false\n }\n }\n\n private async checkDefaultPassword() {\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 private async fetchRoles() {\n const response = await client.query({\n query: gql`\n query ($inherited: InheritedValueType) {\n roles(inherited: $inherited) {\n items {\n id\n name\n }\n }\n }\n `,\n variables: { inherited: InheritedValueType.Include },\n context: gqlContext()\n })\n\n this.roles = response.data?.roles?.items || []\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 private async onCall() {\n if (!this.callable) {\n return\n }\n\n const email = this.email.trim()\n\n if (this.inviting) {\n /*\n * 초대는 여기서 끝난다 — 역할은 초대장이 싣고 가고, 수락할 때 함께 붙는다. 아래의\n * 「넣고 나서 역할」 두 호출과 달리 한 번이다.\n */\n await this.sendInvitation(email)\n return\n }\n\n const username = this.creating ? this.usernameInput?.value.trim() || email : email\n\n if (this.creating && !this.nameInput?.value.trim()) {\n this.message = i18next.t('text.x is required', { x: i18next.t('field.name') })\n return\n }\n\n this.working = true\n\n try {\n const entered = this.creating ? await this.createAccount(email, username) : await this.addToDomain(username)\n\n if (!entered) {\n return\n }\n\n if (this.selected.size) {\n /*\n * 넣기와 역할 부여가 두 호출이다. 여기가 실패하면 그 사람은 역할 없이 들어와 있고,\n * 목록이 그것을 빨갛게 말한다 — 사라지지 않는다.\n *\n * `createUser` 도 역할을 받지만 **이 도메인 소유의 역할만** 붙인다. 상속 역할과 파트너\n * 역할을 조용히 버리므로, 고른 것을 그대로 주려면 이 호출을 쓴다.\n */\n 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.roles.map(role => ({ id: role.id })),\n selectedRoles: this.roles.filter(role => this.selected.has(role.id)).map(role => ({ id: role.id }))\n },\n context: gqlContext()\n })\n }\n\n this.dispatchEvent(new CustomEvent('user-invited', { detail: { username }, bubbles: true, composed: true }))\n } finally {\n this.working = false\n }\n }\n\n /** 이미 계정이 있는 사람 — 이 도메인에 바로 들어온다. */\n private async addToDomain(username: string): Promise<boolean> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($username: String!) {\n inviteUser(username: $username)\n }\n `,\n variables: { username },\n context: gqlContext()\n })\n\n if (response.errors) {\n this.message = response.errors[0]?.message || ''\n return false\n }\n\n return true\n }\n\n /**\n * 계정이 없는 사람 — 기본 비밀번호로 만든다.\n *\n * 역할은 여기서 안 넘긴다. `createUser` 는 **이 도메인 소유의 역할만** 붙이므로 상속·파트너\n * 역할이 조용히 빠진다. 부르는 쪽에서 `updateUserRoles` 로 한 번에 준다.\n */\n private async createAccount(email: string, username: string): Promise<boolean> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($user: NewUser!) {\n createUser(user: $user) {\n id\n }\n }\n `,\n variables: {\n user: {\n username,\n name: this.nameInput.value.trim(),\n email,\n userType: 'user'\n }\n },\n context: gqlContext()\n })\n\n if (response.errors) {\n this.message = response.errors[0]?.message || ''\n return false\n }\n\n return true\n }\n}\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { __decorate } from "tslib";
|
|
2
|
+
import { customElement } from 'lit/decorators.js';
|
|
3
|
+
import { AcceptInvitation } from '../../components/accept-invitation.js';
|
|
4
|
+
let AuthAcceptInvitation = class AuthAcceptInvitation extends AcceptInvitation {
|
|
5
|
+
};
|
|
6
|
+
AuthAcceptInvitation = __decorate([
|
|
7
|
+
customElement('auth-accept-invitation')
|
|
8
|
+
], AuthAcceptInvitation);
|
|
9
|
+
export { AuthAcceptInvitation };
|
|
10
|
+
//# sourceMappingURL=accept-invitation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accept-invitation.js","sourceRoot":"","sources":["../../../client/entries/auth/accept-invitation.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAA;AAGjE,IAAM,oBAAoB,GAA1B,MAAM,oBAAqB,SAAQ,gBAAgB;CAAG,CAAA;AAAhD,oBAAoB;IADhC,aAAa,CAAC,wBAAwB,CAAC;GAC3B,oBAAoB,CAA4B","sourcesContent":["import { customElement } from 'lit/decorators.js'\n\nimport { AcceptInvitation } from '../../components/accept-invitation.js'\n\n@customElement('auth-accept-invitation')\nexport class AuthAcceptInvitation extends AcceptInvitation {}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* The invitation list must not ask for the token.
|
|
5
|
+
*
|
|
6
|
+
* The token in an invitation mail is not a reference to a credential — it **is** the credential.
|
|
7
|
+
* Whoever holds it joins the domain, and for somebody with no account yet it creates one. It is
|
|
8
|
+
* not a GraphQL field on `Invitation` at all, for that reason; this holds the screen to the same
|
|
9
|
+
* line so that restoring the field later does not quietly put it in a grid cell.
|
|
10
|
+
*
|
|
11
|
+
* A credential in a grid cell is a credential in plain text beside a name — in every screenshot,
|
|
12
|
+
* on every shared display, for everyone who can open the screen. The people who *can* open this
|
|
13
|
+
* one are administrators, which is exactly who has it open on a projector.
|
|
14
|
+
*
|
|
15
|
+
* ⚠ Comments are stripped before searching. The prose above says `token` several times
|
|
16
|
+
* explaining why it is absent; searching the raw file would find the explanation and pass over a
|
|
17
|
+
* query that really asks for it.
|
|
18
|
+
*/
|
|
19
|
+
function source(name) {
|
|
20
|
+
const text = readFileSync(join(__dirname, name), 'utf-8');
|
|
21
|
+
return text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^[ \t]*\/\/.*$/gm, '');
|
|
22
|
+
}
|
|
23
|
+
describe('the invitation list query', () => {
|
|
24
|
+
it('does not ask for the token', () => {
|
|
25
|
+
expect(source('./invitations.ts')).not.toContain('token');
|
|
26
|
+
});
|
|
27
|
+
it('asks for the fields the columns render', () => {
|
|
28
|
+
const text = source('./invitations.ts');
|
|
29
|
+
for (const field of ['email', 'state', 'createdAt', 'expiresAt', 'acceptedAt', 'roles', 'creator']) {
|
|
30
|
+
expect(text).toContain(field);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
it('reads the state the server resolves, not the stored column', () => {
|
|
34
|
+
/*
|
|
35
|
+
* ⚠ `status` is what is written down; `state` is what the server answers after measuring a
|
|
36
|
+
* pending row against the clock. Showing `status` would call an invitation live for as long
|
|
37
|
+
* as nothing had been round to mark it expired.
|
|
38
|
+
*/
|
|
39
|
+
const text = source('./invitations.ts');
|
|
40
|
+
expect(text).toContain('state');
|
|
41
|
+
expect(text).not.toMatch(/name:\s*'status'/);
|
|
42
|
+
});
|
|
43
|
+
it('passes the grist options through, so paging and sorting reach the server', () => {
|
|
44
|
+
const text = source('./invitations.ts');
|
|
45
|
+
expect(text).toContain('$pagination: Pagination');
|
|
46
|
+
expect(text).toContain('$sortings: [Sorting!]');
|
|
47
|
+
expect(text).toContain('$filters: [Filter!]');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe('the accept page', () => {
|
|
51
|
+
function componentSource() {
|
|
52
|
+
return readFileSync(join(__dirname, '..', '..', 'components', 'accept-invitation.ts'), 'utf-8')
|
|
53
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
54
|
+
.replace(/^[ \t]*\/\/.*$/gm, '');
|
|
55
|
+
}
|
|
56
|
+
it('takes the token out of the address as well as reading it', () => {
|
|
57
|
+
/*
|
|
58
|
+
* ⚠ Reading the fragment is half of it. The fragment keeps the token out of server and proxy
|
|
59
|
+
* logs; replacing the address is what keeps it out of the browser history and off the screen.
|
|
60
|
+
* A page that read it and left it in the bar would undo the reason it is in the fragment.
|
|
61
|
+
*/
|
|
62
|
+
expect(componentSource()).toContain('replaceState');
|
|
63
|
+
});
|
|
64
|
+
it('never puts the token in a field the reader can see', () => {
|
|
65
|
+
const text = componentSource();
|
|
66
|
+
expect(text).not.toMatch(/\.value=\$\{[^}]*\btoken\b[^}]*\}/);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
//# sourceMappingURL=invitation-list-query.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invitation-list-query.test.js","sourceRoot":"","sources":["../../../client/pages/invitation/invitation-list-query.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAA;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAE3B;;;;;;;;;;;;;;;GAeG;AACH,SAAS,MAAM,CAAC,IAAY;IAC1B,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,CAAA;IAEzD,OAAO,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAA;AAC9E,CAAC;AAED,QAAQ,CAAC,2BAA2B,EAAE,GAAG,EAAE;IACzC,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;IAC3D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,IAAI,GAAG,MAAM,CAAC,kBAAkB,CAAC,CAAA;QAEvC,KAAK,MAAM,KAAK,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC;YACnG,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;QAC/B,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;QACpE;;;;WAIG;QACH,MAAM,IAAI,GAAG,MAAM,CAAC,kBAAkB,CAAC,CAAA;QAEvC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;QAC/B,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAA;IAC9C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,0EAA0E,EAAE,GAAG,EAAE;QAClF,MAAM,IAAI,GAAG,MAAM,CAAC,kBAAkB,CAAC,CAAA;QAEvC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,yBAAyB,CAAC,CAAA;QACjD,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,uBAAuB,CAAC,CAAA;QAC/C,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,qBAAqB,CAAC,CAAA;IAC/C,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,SAAS,eAAe;QACtB,OAAO,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,sBAAsB,CAAC,EAAE,OAAO,CAAC;aAC5F,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC;aAChC,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAA;IACpC,CAAC;IAED,EAAE,CAAC,0DAA0D,EAAE,GAAG,EAAE;QAClE;;;;WAIG;QACH,MAAM,CAAC,eAAe,EAAE,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAA;IACrD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,MAAM,IAAI,GAAG,eAAe,EAAE,CAAA;QAE9B,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,mCAAmC,CAAC,CAAA;IAC/D,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import { readFileSync } from 'fs'\nimport { join } from 'path'\n\n/**\n * The invitation list must not ask for the token.\n *\n * The token in an invitation mail is not a reference to a credential — it **is** the credential.\n * Whoever holds it joins the domain, and for somebody with no account yet it creates one. It is\n * not a GraphQL field on `Invitation` at all, for that reason; this holds the screen to the same\n * line so that restoring the field later does not quietly put it in a grid cell.\n *\n * A credential in a grid cell is a credential in plain text beside a name — in every screenshot,\n * on every shared display, for everyone who can open the screen. The people who *can* open this\n * one are administrators, which is exactly who has it open on a projector.\n *\n * ⚠ Comments are stripped before searching. The prose above says `token` several times\n * explaining why it is absent; searching the raw file would find the explanation and pass over a\n * query that really asks for it.\n */\nfunction source(name: string): string {\n const text = readFileSync(join(__dirname, name), 'utf-8')\n\n return text.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '').replace(/^[ \\t]*\\/\\/.*$/gm, '')\n}\n\ndescribe('the invitation list query', () => {\n it('does not ask for the token', () => {\n expect(source('./invitations.ts')).not.toContain('token')\n })\n\n it('asks for the fields the columns render', () => {\n const text = source('./invitations.ts')\n\n for (const field of ['email', 'state', 'createdAt', 'expiresAt', 'acceptedAt', 'roles', 'creator']) {\n expect(text).toContain(field)\n }\n })\n\n it('reads the state the server resolves, not the stored column', () => {\n /*\n * ⚠ `status` is what is written down; `state` is what the server answers after measuring a\n * pending row against the clock. Showing `status` would call an invitation live for as long\n * as nothing had been round to mark it expired.\n */\n const text = source('./invitations.ts')\n\n expect(text).toContain('state')\n expect(text).not.toMatch(/name:\\s*'status'/)\n })\n\n it('passes the grist options through, so paging and sorting reach the server', () => {\n const text = source('./invitations.ts')\n\n expect(text).toContain('$pagination: Pagination')\n expect(text).toContain('$sortings: [Sorting!]')\n expect(text).toContain('$filters: [Filter!]')\n })\n})\n\ndescribe('the accept page', () => {\n function componentSource(): string {\n return readFileSync(join(__dirname, '..', '..', 'components', 'accept-invitation.ts'), 'utf-8')\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')\n .replace(/^[ \\t]*\\/\\/.*$/gm, '')\n }\n\n it('takes the token out of the address as well as reading it', () => {\n /*\n * ⚠ Reading the fragment is half of it. The fragment keeps the token out of server and proxy\n * logs; replacing the address is what keeps it out of the browser history and off the screen.\n * A page that read it and left it in the bar would undo the reason it is in the fragment.\n */\n expect(componentSource()).toContain('replaceState')\n })\n\n it('never puts the token in a field the reader can see', () => {\n const text = componentSource()\n\n expect(text).not.toMatch(/\\.value=\\$\\{[^}]*\\btoken\\b[^}]*\\}/)\n })\n})\n"]}
|