@things-factory/auth-ui 10.1.16 → 10.1.18
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/app-binding-detail.d.ts +46 -0
- package/dist-client/components/app-binding-detail.js +384 -0
- package/dist-client/components/app-binding-detail.js.map +1 -0
- package/dist-client/components/appliance-editor.d.ts +37 -0
- package/dist-client/components/appliance-editor.js +489 -0
- package/dist-client/components/appliance-editor.js.map +1 -0
- package/dist-client/components/application-editor.d.ts +31 -0
- package/dist-client/components/application-editor.js +491 -0
- package/dist-client/components/application-editor.js.map +1 -0
- package/dist-client/components/profile-component.js +28 -11
- package/dist-client/components/profile-component.js.map +1 -1
- package/dist-client/components/secret-field.d.ts +35 -0
- package/dist-client/components/secret-field.js +149 -0
- package/dist-client/components/secret-field.js.map +1 -0
- package/dist-client/constants/application-type-parity.test.js +62 -0
- package/dist-client/constants/application-type-parity.test.js.map +1 -0
- package/dist-client/constants/application.d.ts +17 -7
- package/dist-client/constants/application.js +18 -8
- package/dist-client/constants/application.js.map +1 -1
- package/dist-client/pages/app-binding/app-binding-list-query.test.js +46 -0
- package/dist-client/pages/app-binding/app-binding-list-query.test.js.map +1 -0
- package/dist-client/pages/app-binding/app-bindings.d.ts +65 -2
- package/dist-client/pages/app-binding/app-bindings.js +217 -117
- package/dist-client/pages/app-binding/app-bindings.js.map +1 -1
- package/dist-client/pages/appliance/appliance-list-query.test.d.ts +1 -0
- package/dist-client/pages/appliance/appliance-list-query.test.js +58 -0
- package/dist-client/pages/appliance/appliance-list-query.test.js.map +1 -0
- package/dist-client/pages/appliance/home.d.ts +63 -2
- package/dist-client/pages/appliance/home.js +219 -123
- package/dist-client/pages/appliance/home.js.map +1 -1
- package/dist-client/pages/application/applications.d.ts +63 -2
- package/dist-client/pages/application/applications.js +212 -129
- package/dist-client/pages/application/applications.js.map +1 -1
- package/dist-client/pages/auth-provider/auth-provider-management.d.ts +4 -2
- package/dist-client/pages/auth-provider/auth-provider-management.js +35 -6
- package/dist-client/pages/auth-provider/auth-provider-management.js.map +1 -1
- package/dist-client/pages/domain-owner/domain-owner-management.d.ts +2 -0
- package/dist-client/pages/domain-owner/domain-owner-management.js +34 -7
- package/dist-client/pages/domain-owner/domain-owner-management.js.map +1 -1
- package/dist-client/pages/secrets-are-not-printed.test.d.ts +1 -0
- package/dist-client/pages/secrets-are-not-printed.test.js +88 -0
- package/dist-client/pages/secrets-are-not-printed.test.js.map +1 -0
- package/dist-client/route.js +0 -15
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/application-status.d.ts +32 -0
- package/dist-client/utils/application-status.js +45 -0
- package/dist-client/utils/application-status.js.map +1 -0
- package/dist-client/utils/application-status.test.d.ts +1 -0
- package/dist-client/utils/application-status.test.js +45 -0
- package/dist-client/utils/application-status.test.js.map +1 -0
- package/dist-client/utils/form-values.d.ts +71 -0
- package/dist-client/utils/form-values.js +102 -0
- package/dist-client/utils/form-values.js.map +1 -0
- package/dist-client/utils/form-values.test.d.ts +1 -0
- package/dist-client/utils/form-values.test.js +120 -0
- package/dist-client/utils/form-values.test.js.map +1 -0
- package/dist-client/utils/list-result.d.ts +62 -0
- package/dist-client/utils/list-result.js +92 -0
- package/dist-client/utils/list-result.js.map +1 -0
- package/dist-client/utils/list-result.test.d.ts +1 -0
- package/dist-client/utils/list-result.test.js +160 -0
- package/dist-client/utils/list-result.test.js.map +1 -0
- package/dist-client/utils/secret-display.d.ts +51 -0
- package/dist-client/utils/secret-display.js +61 -0
- package/dist-client/utils/secret-display.js.map +1 -0
- package/dist-client/utils/secret-display.test.d.ts +1 -0
- package/dist-client/utils/secret-display.test.js +59 -0
- package/dist-client/utils/secret-display.test.js.map +1 -0
- package/dist-client/utils/webauthn-verification.d.ts +56 -0
- package/dist-client/utils/webauthn-verification.js +79 -0
- package/dist-client/utils/webauthn-verification.js.map +1 -0
- package/dist-client/utils/webauthn-verification.test.d.ts +1 -0
- package/dist-client/utils/webauthn-verification.test.js +79 -0
- package/dist-client/utils/webauthn-verification.test.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/things-factory.config.js +0 -5
- package/translations/en.json +56 -0
- package/translations/ja.json +57 -0
- package/translations/ko.json +56 -0
- package/translations/ms.json +57 -0
- package/translations/zh.json +57 -0
- package/dist-client/pages/app-binding/app-binding.d.ts +0 -1
- package/dist-client/pages/app-binding/app-binding.js +0 -371
- package/dist-client/pages/app-binding/app-binding.js.map +0 -1
- package/dist-client/pages/appliance/appliance.d.ts +0 -1
- package/dist-client/pages/appliance/appliance.js +0 -392
- package/dist-client/pages/appliance/appliance.js.map +0 -1
- package/dist-client/pages/appliance/register.js +0 -166
- package/dist-client/pages/appliance/register.js.map +0 -1
- package/dist-client/pages/application/application.d.ts +0 -1
- package/dist-client/pages/application/application.js +0 -377
- package/dist-client/pages/application/application.js.map +0 -1
- package/dist-client/pages/application/register.js +0 -192
- package/dist-client/pages/application/register.js.map +0 -1
- /package/dist-client/{pages/appliance/register.d.ts → constants/application-type-parity.test.d.ts} +0 -0
- /package/dist-client/pages/{application/register.d.ts → app-binding/app-binding-list-query.test.d.ts} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profile-component.js","sourceRoot":"","sources":["../../client/components/profile-component.ts"],"names":[],"mappings":"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","sourcesContent":["import '@operato/i18n/ox-i18n.js'\nimport '@operato/i18n/ox-i18n-selector.js'\nimport './change-password.js'\nimport './delete-user-popup.js'\nimport './my-login-history.js'\n\nimport { gql } from 'graphql-tag'\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\nimport { startRegistration } from '@simplewebauthn/browser'\n\nimport { client } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { notify, openPopup } from '@operato/layout'\nimport { FormFieldStyles } from '@operato/styles'\nimport { auth, getLanguages } from '@things-factory/auth-base/dist-client'\n\nconst isAvailableWebauthn = 'PublicKeyCredential' in window\n@customElement('profile-component')\nexport class ProfileComponent extends localize(i18next)(LitElement) {\n static styles = [\n FormFieldStyles,\n css`\n :host {\n display: block;\n background-color: var(--md-sys-color-background);\n padding: 15px 0;\n }\n .wrap {\n max-width: var(--profile-wrap-max-width, 400px);\n margin: auto;\n display: grid;\n grid-template-columns: 1fr 1fr;\n }\n\n * {\n box-sizing: border-box;\n }\n\n *:focus {\n outline: none;\n }\n\n /* 색·테두리·모서리·포커스·placeholder 는 FormFieldStyles 가 담당하고, 여기서는 치수만 둔다 */\n input {\n margin: var(--spacing-small) 0;\n padding: 9px;\n border-radius: var(--border-radius);\n font: var(--auth-input-field-font);\n width: var(--auth-input-field-width);\n }\n\n .user {\n background: url(/assets/images/icon-profile.png) center top no-repeat;\n margin: var(--profile-icon-margin);\n padding: 180px 20px 20px 20px;\n color: var(--md-sys-color-secondary);\n font: var(--header-bar-title);\n text-align: center;\n }\n\n hr {\n width: 100%;\n /* rgba(0,0,0,.1) 은 다크 배경에서 보이지 않는다 */\n border: dotted 1px var(--md-sys-color-outline-variant);\n }\n\n .wrap * {\n grid-column: span 2;\n }\n\n label {\n font: bold 14px var(--theme-font);\n color: var(--md-sys-color-primary);\n text-transform: capitalize;\n grid-column: 1;\n }\n\n .wrap *.inline {\n grid-column: unset;\n }\n\n /*\n * 언어 select 는 ox-i18n-selector 의 shadow root 안에 있어 위 input 규칙이 닿지 않는다.\n * 높이가 어긋나지 않도록 인풋과 **같은 값**을 토큰으로 넘긴다(padding 9px, 16px 글자).\n */\n ox-i18n-selector {\n --i18n-selector-field-width: var(--auth-input-field-width);\n /* 여백은 호스트에만 준다 — 내부에도 주면 두 번 더해져 인풋보다 벌어진다 */\n --i18n-selector-field-margin: 0;\n --i18n-selector-field-padding: 9px;\n --i18n-selector-field-border-radius: var(--border-radius);\n --i18n-selector-field-font: var(--auth-input-field-font);\n /* ox-i18n-selector 가 font 뒤에 font-size(기본 15px)를 다시 선언하므로 크기도 넘겨야 한다 */\n --i18n-selector-field-font-size: 16px;\n --i18n-selector-field-border: var(--input-field-border, 1px solid var(--md-sys-color-outline));\n --i18n-selector-field-background-color: var(\n --input-field-background,\n var(--md-sys-color-surface-container-highest)\n );\n --i18n-selector-field-color: var(--input-field-color, var(--md-sys-color-on-surface));\n margin: var(--spacing-small) 0;\n }\n\n md-text-button {\n text-transform: capitalize;\n }\n\n footer {\n padding: 20px;\n text-align: center;\n }\n\n footer p {\n font-size: 14px;\n margin-bottom: 5px;\n color: var(--md-sys-color-on-background);\n }\n\n footer a {\n color: var(--md-sys-color-primary);\n text-decoration: none;\n font-weight: bold;\n }\n `\n ]\n\n @property({ type: String }) username?: string\n @property({ type: String }) email?: string\n @property({ type: String }) name?: string\n\n @state() languages: { code: string; display: string }[] = []\n @state() passwordRule: {\n lowerCase?: boolean\n upperCase?: boolean\n digit?: boolean\n specialCharacter?: boolean\n allowRepeat?: boolean\n useTightPattern?: boolean\n useLoosePattern?: boolean\n tightCharacterLength?: number\n looseCharacterLength?: number\n } = {}\n\n @query('#username') usernameEl!: HTMLInputElement\n @query('#name') nameEl!: HTMLInputElement\n @query('#email') emailEl!: HTMLInputElement\n @query('#locale') localeEl!: HTMLInputElement\n\n async connectedCallback(): Promise<void> {\n super.connectedCallback()\n\n const response = await client.query({\n query: gql`\n query {\n passwordRule {\n lowerCase\n upperCase\n digit\n specialCharacter\n allowRepeat\n useTightPattern\n useLoosePattern\n tightCharacterLength\n looseCharacterLength\n }\n }\n `\n })\n\n this.passwordRule = response.data\n }\n\n async firstUpdated() {\n auth.on('profile', ({ credential }) => {\n this.setCredential(credential)\n })\n\n this.setCredential(auth.credential)\n\n this.languages = await getLanguages()\n }\n\n setCredential(credential) {\n if (credential) {\n this.username = credential.username\n this.name = credential.name\n this.email = credential.email\n } else {\n this.username = ''\n this.name = ''\n this.email = ''\n }\n }\n\n render() {\n return html`\n <div class=\"wrap\">\n <div class=\"user\">${this.username || ''}</div>\n\n <label for=\"username\"><ox-i18n slot=\"title\" msgid=\"label.user-id\"></ox-i18n></label>\n <input id=\"username\" @change=${e => this.onUsernameChanged(e.target.value)} .value=${this.username || ''} />\n\n <hr />\n\n <label for=\"name\"><ox-i18n slot=\"title\" msgid=\"label.name\"></ox-i18n></label>\n <input id=\"name\" @change=${e => this.onNameChanged(e.target.value)} .value=${this.name || ''} />\n\n <hr />\n\n <label for=\"email\"><ox-i18n slot=\"title\" msgid=\"label.email\"></ox-i18n></label>\n <input id=\"email\" type=\"email\" @change=${e => this.onEmailChanged(e.target.value)} .value=${this.email || ''} />\n\n <hr />\n\n <label for=\"locale\"><ox-i18n slot=\"title\" msgid=\"label.language\"></ox-i18n></label>\n <ox-i18n-selector\n id=\"locale\"\n @change=${e => this.onLocaleChanged(e.detail)}\n value=${i18next.language || 'en-US'}\n .languages=${this.languages}\n ></ox-i18n-selector>\n\n <hr />\n\n <label for=\"change-password\">\n <ox-i18n msgid=\"label.password\"></ox-i18n>\n </label>\n\n <change-password id=\"change-password\" .passwordRule=${this.passwordRule}></change-password>\n\n ${\n isAvailableWebauthn\n ? html`\n <md-text-button @click=${() => this.registerUser()}\n >${i18next.t('button.security-key registration')}</md-text-button\n >\n `\n : nothing\n }\n\n <footer>\n <p>\n ${i18next.t('text.click login history')}\n <a href=\"javascript:void(0);\" @click=${this.openLoginHistory.bind(this)}\n >${i18next.t('label.login_history')}</a\n >\n </p>\n </footer>\n </div>\n `\n }\n\n async onUsernameChanged(username) {\n if (!username) return\n\n var oldUsername = this.username\n\n try {\n const message = await auth.updateProfile({\n username\n })\n\n notify({\n level: 'info',\n message\n })\n } catch (e: any) {\n this.usernameEl.value = oldUsername || ''\n\n notify({\n level: 'error',\n message: 'message' in e ? e.message : e\n })\n }\n }\n\n async onNameChanged(name) {\n if (!name) return\n\n var oldName = this.name\n\n try {\n const message = await auth.updateProfile({\n name\n })\n\n notify({\n level: 'info',\n message\n })\n } catch (e: any) {\n this.nameEl.value = oldName || ''\n\n notify({\n level: 'error',\n message: 'message' in e ? e.message : e\n })\n }\n }\n\n async onEmailChanged(email) {\n if (!email) return\n\n var oldEmail = this.email\n\n try {\n const message = await auth.updateProfile({\n email\n })\n\n notify({\n level: 'info',\n message\n })\n } catch (e: any) {\n this.emailEl.value = oldEmail || ''\n\n notify({\n level: 'error',\n message: 'message' in e ? e.message : e\n })\n }\n }\n\n async onLocaleChanged(value) {\n if (!value) return\n\n var oldLocale = i18next.language\n\n try {\n const message = await auth.updateProfile({\n locale: value\n })\n\n i18next.changeLanguage(value)\n\n notify({\n level: 'info',\n message\n })\n } catch (e: any) {\n this.localeEl.value = oldLocale\n\n notify({\n level: 'error',\n message: 'message' in e ? e.message : e\n })\n }\n }\n\n openLoginHistory() {\n openPopup(html` <my-login-history></my-login-history> `, {\n title: i18next.t('label.login_history')\n })\n }\n\n deleteUser() {\n openPopup(html` <delete-user-popup></delete-user-popup> `, {\n title: i18next.t('label.delete account')\n })\n }\n\n async registerUser() {\n try {\n const options = await fetch('/auth/register-webauthn/challenge').then(res => res.json())\n const attResp = await startRegistration(options)\n const verification = await fetch('/auth/verify-registration', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify(attResp)\n }).then(res => res.json())\n\n if (verification.verified) {\n notify({\n level: 'info',\n message: i18next.t('text.user credential registered successfully')\n })\n } else {\n console.error(await verification.text())\n\n notify({\n level: 'error',\n message: i18next.t('error.user credential registeration not allowed')\n })\n }\n } catch (error) {\n notify({\n level: 'error',\n message: i18next.t('error.user credential registeration failed')\n })\n }\n }\n\n get applicationMeta() {\n var iconLink: HTMLLinkElement | null = document.querySelector('link[rel=\"application-icon\"]')\n var titleMeta: HTMLMetaElement | null = document.querySelector('meta[name=\"application-name\"]')\n var descriptionMeta: HTMLMetaElement | null = document.querySelector('meta[name=\"application-description\"]')\n\n return {\n icon: iconLink?.href || '',\n title: titleMeta?.content || 'Things Factory',\n description: descriptionMeta?.content || 'Reimagining Software'\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"profile-component.js","sourceRoot":"","sources":["../../client/components/profile-component.ts"],"names":[],"mappings":"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","sourcesContent":["import '@operato/i18n/ox-i18n.js'\nimport '@operato/i18n/ox-i18n-selector.js'\nimport './change-password.js'\nimport './delete-user-popup.js'\nimport './my-login-history.js'\n\nimport { gql } from 'graphql-tag'\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\nimport { startRegistration } from '@simplewebauthn/browser'\n\nimport { client } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { notify, openPopup } from '@operato/layout'\nimport { FormFieldStyles } from '@operato/styles'\nimport { auth, getLanguages } from '@things-factory/auth-base/dist-client'\n\nimport { readVerification } from '../utils/webauthn-verification.js'\n\nconst isAvailableWebauthn = 'PublicKeyCredential' in window\n@customElement('profile-component')\nexport class ProfileComponent extends localize(i18next)(LitElement) {\n static styles = [\n FormFieldStyles,\n css`\n :host {\n display: block;\n background-color: var(--md-sys-color-background);\n padding: 15px 0;\n }\n .wrap {\n max-width: var(--profile-wrap-max-width, 400px);\n margin: auto;\n display: grid;\n grid-template-columns: 1fr 1fr;\n }\n\n * {\n box-sizing: border-box;\n }\n\n *:focus {\n outline: none;\n }\n\n /* 색·테두리·모서리·포커스·placeholder 는 FormFieldStyles 가 담당하고, 여기서는 치수만 둔다 */\n input {\n margin: var(--spacing-small) 0;\n padding: 9px;\n border-radius: var(--border-radius);\n font: var(--auth-input-field-font);\n width: var(--auth-input-field-width);\n }\n\n .user {\n background: url(/assets/images/icon-profile.png) center top no-repeat;\n margin: var(--profile-icon-margin);\n padding: 180px 20px 20px 20px;\n color: var(--md-sys-color-secondary);\n font: var(--header-bar-title);\n text-align: center;\n }\n\n hr {\n width: 100%;\n /* rgba(0,0,0,.1) 은 다크 배경에서 보이지 않는다 */\n border: dotted 1px var(--md-sys-color-outline-variant);\n }\n\n .wrap * {\n grid-column: span 2;\n }\n\n label {\n font: bold 14px var(--theme-font);\n color: var(--md-sys-color-primary);\n text-transform: capitalize;\n grid-column: 1;\n }\n\n .wrap *.inline {\n grid-column: unset;\n }\n\n /*\n * 언어 select 는 ox-i18n-selector 의 shadow root 안에 있어 위 input 규칙이 닿지 않는다.\n * 높이가 어긋나지 않도록 인풋과 **같은 값**을 토큰으로 넘긴다(padding 9px, 16px 글자).\n */\n ox-i18n-selector {\n --i18n-selector-field-width: var(--auth-input-field-width);\n /* 여백은 호스트에만 준다 — 내부에도 주면 두 번 더해져 인풋보다 벌어진다 */\n --i18n-selector-field-margin: 0;\n --i18n-selector-field-padding: 9px;\n --i18n-selector-field-border-radius: var(--border-radius);\n --i18n-selector-field-font: var(--auth-input-field-font);\n /* ox-i18n-selector 가 font 뒤에 font-size(기본 15px)를 다시 선언하므로 크기도 넘겨야 한다 */\n --i18n-selector-field-font-size: 16px;\n --i18n-selector-field-border: var(--input-field-border, 1px solid var(--md-sys-color-outline));\n --i18n-selector-field-background-color: var(\n --input-field-background,\n var(--md-sys-color-surface-container-highest)\n );\n --i18n-selector-field-color: var(--input-field-color, var(--md-sys-color-on-surface));\n margin: var(--spacing-small) 0;\n }\n\n md-text-button {\n text-transform: capitalize;\n }\n\n footer {\n padding: 20px;\n text-align: center;\n }\n\n footer p {\n font-size: 14px;\n margin-bottom: 5px;\n color: var(--md-sys-color-on-background);\n }\n\n footer a {\n color: var(--md-sys-color-primary);\n text-decoration: none;\n font-weight: bold;\n }\n `\n ]\n\n @property({ type: String }) username?: string\n @property({ type: String }) email?: string\n @property({ type: String }) name?: string\n\n @state() languages: { code: string; display: string }[] = []\n @state() passwordRule: {\n lowerCase?: boolean\n upperCase?: boolean\n digit?: boolean\n specialCharacter?: boolean\n allowRepeat?: boolean\n useTightPattern?: boolean\n useLoosePattern?: boolean\n tightCharacterLength?: number\n looseCharacterLength?: number\n } = {}\n\n @query('#username') usernameEl!: HTMLInputElement\n @query('#name') nameEl!: HTMLInputElement\n @query('#email') emailEl!: HTMLInputElement\n @query('#locale') localeEl!: HTMLInputElement\n\n async connectedCallback(): Promise<void> {\n super.connectedCallback()\n\n const response = await client.query({\n query: gql`\n query {\n passwordRule {\n lowerCase\n upperCase\n digit\n specialCharacter\n allowRepeat\n useTightPattern\n useLoosePattern\n tightCharacterLength\n looseCharacterLength\n }\n }\n `\n })\n\n this.passwordRule = response.data\n }\n\n async firstUpdated() {\n auth.on('profile', ({ credential }) => {\n this.setCredential(credential)\n })\n\n this.setCredential(auth.credential)\n\n this.languages = await getLanguages()\n }\n\n setCredential(credential) {\n if (credential) {\n this.username = credential.username\n this.name = credential.name\n this.email = credential.email\n } else {\n this.username = ''\n this.name = ''\n this.email = ''\n }\n }\n\n render() {\n return html`\n <div class=\"wrap\">\n <div class=\"user\">${this.username || ''}</div>\n\n <label for=\"username\"><ox-i18n slot=\"title\" msgid=\"label.user-id\"></ox-i18n></label>\n <input id=\"username\" @change=${e => this.onUsernameChanged(e.target.value)} .value=${this.username || ''} />\n\n <hr />\n\n <label for=\"name\"><ox-i18n slot=\"title\" msgid=\"label.name\"></ox-i18n></label>\n <input id=\"name\" @change=${e => this.onNameChanged(e.target.value)} .value=${this.name || ''} />\n\n <hr />\n\n <label for=\"email\"><ox-i18n slot=\"title\" msgid=\"label.email\"></ox-i18n></label>\n <input id=\"email\" type=\"email\" @change=${e => this.onEmailChanged(e.target.value)} .value=${this.email || ''} />\n\n <hr />\n\n <label for=\"locale\"><ox-i18n slot=\"title\" msgid=\"label.language\"></ox-i18n></label>\n <ox-i18n-selector\n id=\"locale\"\n @change=${e => this.onLocaleChanged(e.detail)}\n value=${i18next.language || 'en-US'}\n .languages=${this.languages}\n ></ox-i18n-selector>\n\n <hr />\n\n <label for=\"change-password\">\n <ox-i18n msgid=\"label.password\"></ox-i18n>\n </label>\n\n <change-password id=\"change-password\" .passwordRule=${this.passwordRule}></change-password>\n\n ${\n isAvailableWebauthn\n ? html`\n <md-text-button @click=${() => this.registerUser()}\n >${i18next.t('button.security-key registration')}</md-text-button\n >\n `\n : nothing\n }\n\n <footer>\n <p>\n ${i18next.t('text.click login history')}\n <a href=\"javascript:void(0);\" @click=${this.openLoginHistory.bind(this)}\n >${i18next.t('label.login_history')}</a\n >\n </p>\n </footer>\n </div>\n `\n }\n\n async onUsernameChanged(username) {\n if (!username) return\n\n var oldUsername = this.username\n\n try {\n const message = await auth.updateProfile({\n username\n })\n\n notify({\n level: 'info',\n message\n })\n } catch (e: any) {\n this.usernameEl.value = oldUsername || ''\n\n notify({\n level: 'error',\n message: 'message' in e ? e.message : e\n })\n }\n }\n\n async onNameChanged(name) {\n if (!name) return\n\n var oldName = this.name\n\n try {\n const message = await auth.updateProfile({\n name\n })\n\n notify({\n level: 'info',\n message\n })\n } catch (e: any) {\n this.nameEl.value = oldName || ''\n\n notify({\n level: 'error',\n message: 'message' in e ? e.message : e\n })\n }\n }\n\n async onEmailChanged(email) {\n if (!email) return\n\n var oldEmail = this.email\n\n try {\n const message = await auth.updateProfile({\n email\n })\n\n notify({\n level: 'info',\n message\n })\n } catch (e: any) {\n this.emailEl.value = oldEmail || ''\n\n notify({\n level: 'error',\n message: 'message' in e ? e.message : e\n })\n }\n }\n\n async onLocaleChanged(value) {\n if (!value) return\n\n var oldLocale = i18next.language\n\n try {\n const message = await auth.updateProfile({\n locale: value\n })\n\n i18next.changeLanguage(value)\n\n notify({\n level: 'info',\n message\n })\n } catch (e: any) {\n this.localeEl.value = oldLocale\n\n notify({\n level: 'error',\n message: 'message' in e ? e.message : e\n })\n }\n }\n\n openLoginHistory() {\n openPopup(html` <my-login-history></my-login-history> `, {\n title: i18next.t('label.login_history')\n })\n }\n\n deleteUser() {\n openPopup(html` <delete-user-popup></delete-user-popup> `, {\n title: i18next.t('label.delete account')\n })\n }\n\n /*\n * ⚠ The three endings here are not interchangeable, and the screen used to give the same one\n * for all of them.\n *\n * registered the server confirmed it\n * refused the server said no — usually this credential is already registered\n * failed we never got an answer: the browser refused, the device was cancelled,\n * the network dropped\n *\n * `await verification.text()` on an already-parsed body threw a TypeError that the catch\n * below swallowed, so **refused arrived as failed** and the refusal message was unreachable\n * code. `readVerification` works out which ending it is without being able to throw; see\n * `utils/webauthn-verification.ts`.\n */\n async registerUser() {\n try {\n const options = await fetch('/auth/register-webauthn/challenge').then(res => res.json())\n const attResp = await startRegistration(options)\n\n const verification = await readVerification(\n await fetch('/auth/verify-registration', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify(attResp)\n })\n )\n\n if (verification.outcome === 'registered') {\n notify({\n level: 'info',\n message: i18next.t('text.user credential registered successfully')\n })\n\n return\n }\n\n /* The server's own words, in English, for whoever reads the console — not for the screen. */\n console.error('passkey registration refused:', verification.reason)\n\n notify({\n level: 'error',\n message: i18next.t('error.user credential registration not allowed')\n })\n } catch (error) {\n /* Never reached the server, or never got past the device. A different thing to say. */\n console.error('passkey registration failed:', error)\n\n notify({\n level: 'error',\n message: i18next.t('error.user credential registration failed')\n })\n }\n }\n\n get applicationMeta() {\n var iconLink: HTMLLinkElement | null = document.querySelector('link[rel=\"application-icon\"]')\n var titleMeta: HTMLMetaElement | null = document.querySelector('meta[name=\"application-name\"]')\n var descriptionMeta: HTMLMetaElement | null = document.querySelector('meta[name=\"application-description\"]')\n\n return {\n icon: iconLink?.href || '',\n title: titleMeta?.content || 'Things Factory',\n description: descriptionMeta?.content || 'Reimagining Software'\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import '@material/web/icon/icon.js';
|
|
2
|
+
import { LitElement } from 'lit';
|
|
3
|
+
declare const SecretField_base: (new (...args: any[]) => LitElement) & typeof LitElement;
|
|
4
|
+
/**
|
|
5
|
+
* A credential the reader is meant to copy once.
|
|
6
|
+
*
|
|
7
|
+
* ── Why this is not a readonly input ─────────────────────────────────────────
|
|
8
|
+
* The app binding and appliance detail screens printed refresh and access tokens into
|
|
9
|
+
* `<input readonly>` in plain text, from the moment the page opened. The reader needs the
|
|
10
|
+
* value exactly once — to paste it into the thing being set up — and for the rest of the
|
|
11
|
+
* time the screen is open it is just a credential sitting on a display.
|
|
12
|
+
*
|
|
13
|
+
* So the value is masked, copying does not require revealing it, and revealing is something
|
|
14
|
+
* the reader does on purpose. `utils/secret-display.ts` holds the rules, with tests: the mask
|
|
15
|
+
* is a fixed width so it does not leak the length, and an absent secret shows nothing rather
|
|
16
|
+
* than dots over a token nobody issued.
|
|
17
|
+
*
|
|
18
|
+
* ── Copy without reveal ──────────────────────────────────────────────────────
|
|
19
|
+
* The common case is "paste this into the other system", and it does not need the value on
|
|
20
|
+
* screen at all. Reveal exists for the other case — checking that the value matches what the
|
|
21
|
+
* other side already has — and the reader chooses it.
|
|
22
|
+
*
|
|
23
|
+
* `navigator.clipboard` needs a secure context. On a plain-http deployment it is absent, and
|
|
24
|
+
* the button says so instead of failing quietly, because a copy that silently did nothing
|
|
25
|
+
* would leave the reader pasting whatever was in the clipboard before.
|
|
26
|
+
*/
|
|
27
|
+
export declare class SecretField extends SecretField_base {
|
|
28
|
+
static styles: import("lit").CSSResult[];
|
|
29
|
+
/** The credential. Absent means none has been issued. */
|
|
30
|
+
value?: string;
|
|
31
|
+
private revealed;
|
|
32
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
33
|
+
private copy;
|
|
34
|
+
}
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { __decorate, __metadata } from "tslib";
|
|
2
|
+
import '@material/web/icon/icon.js';
|
|
3
|
+
import { css, html, LitElement } from 'lit';
|
|
4
|
+
import { customElement, property, state } from 'lit/decorators.js';
|
|
5
|
+
import { i18next, localize } from '@operato/i18n';
|
|
6
|
+
import { notify } from '@operato/layout';
|
|
7
|
+
import { hasSecret, shownSecret } from '../utils/secret-display.js';
|
|
8
|
+
/**
|
|
9
|
+
* A credential the reader is meant to copy once.
|
|
10
|
+
*
|
|
11
|
+
* ── Why this is not a readonly input ─────────────────────────────────────────
|
|
12
|
+
* The app binding and appliance detail screens printed refresh and access tokens into
|
|
13
|
+
* `<input readonly>` in plain text, from the moment the page opened. The reader needs the
|
|
14
|
+
* value exactly once — to paste it into the thing being set up — and for the rest of the
|
|
15
|
+
* time the screen is open it is just a credential sitting on a display.
|
|
16
|
+
*
|
|
17
|
+
* So the value is masked, copying does not require revealing it, and revealing is something
|
|
18
|
+
* the reader does on purpose. `utils/secret-display.ts` holds the rules, with tests: the mask
|
|
19
|
+
* is a fixed width so it does not leak the length, and an absent secret shows nothing rather
|
|
20
|
+
* than dots over a token nobody issued.
|
|
21
|
+
*
|
|
22
|
+
* ── Copy without reveal ──────────────────────────────────────────────────────
|
|
23
|
+
* The common case is "paste this into the other system", and it does not need the value on
|
|
24
|
+
* screen at all. Reveal exists for the other case — checking that the value matches what the
|
|
25
|
+
* other side already has — and the reader chooses it.
|
|
26
|
+
*
|
|
27
|
+
* `navigator.clipboard` needs a secure context. On a plain-http deployment it is absent, and
|
|
28
|
+
* the button says so instead of failing quietly, because a copy that silently did nothing
|
|
29
|
+
* would leave the reader pasting whatever was in the clipboard before.
|
|
30
|
+
*/
|
|
31
|
+
let SecretField = class SecretField extends localize(i18next)(LitElement) {
|
|
32
|
+
constructor() {
|
|
33
|
+
super(...arguments);
|
|
34
|
+
this.revealed = false;
|
|
35
|
+
}
|
|
36
|
+
static { this.styles = [
|
|
37
|
+
css `
|
|
38
|
+
:host {
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-direction: row;
|
|
41
|
+
align-items: center;
|
|
42
|
+
gap: var(--spacing-small, 4px);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
input {
|
|
46
|
+
flex: 1;
|
|
47
|
+
min-width: 0;
|
|
48
|
+
font-family: var(--theme-mono-font, monospace);
|
|
49
|
+
padding: var(--spacing-small, 4px) var(--spacing-medium, 8px);
|
|
50
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
51
|
+
border-radius: var(--md-sys-shape-corner-small, 5px);
|
|
52
|
+
background-color: var(--md-sys-color-surface);
|
|
53
|
+
color: var(--md-sys-color-on-surface);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
input[empty] {
|
|
57
|
+
font-family: inherit;
|
|
58
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
button {
|
|
62
|
+
display: inline-flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
padding: var(--spacing-small, 4px);
|
|
65
|
+
border: none;
|
|
66
|
+
border-radius: var(--md-sys-shape-corner-small, 5px);
|
|
67
|
+
background-color: transparent;
|
|
68
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
69
|
+
cursor: pointer;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
button:hover:not([disabled]) {
|
|
73
|
+
background-color: var(--md-sys-color-surface-variant);
|
|
74
|
+
color: var(--md-sys-color-on-surface);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/*
|
|
78
|
+
A button that cannot act says so before it is pressed. Leaving it live over an absent
|
|
79
|
+
token means the reader presses it, nothing happens, and they cannot tell a broken
|
|
80
|
+
screen from a credential that was never issued.
|
|
81
|
+
*/
|
|
82
|
+
button[disabled] {
|
|
83
|
+
opacity: 0.38;
|
|
84
|
+
cursor: default;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
md-icon {
|
|
88
|
+
--md-icon-size: 18px;
|
|
89
|
+
}
|
|
90
|
+
`
|
|
91
|
+
]; }
|
|
92
|
+
render() {
|
|
93
|
+
const present = hasSecret(this.value);
|
|
94
|
+
return html `
|
|
95
|
+
<input
|
|
96
|
+
type="text"
|
|
97
|
+
readonly
|
|
98
|
+
?empty=${!present}
|
|
99
|
+
.value=${present ? shownSecret(this.value, this.revealed) : i18next.t('text.not issued')}
|
|
100
|
+
/>
|
|
101
|
+
|
|
102
|
+
<button
|
|
103
|
+
?disabled=${!present}
|
|
104
|
+
title=${this.revealed ? i18next.t('button.hide') : i18next.t('button.reveal')}
|
|
105
|
+
@click=${() => (this.revealed = !this.revealed)}
|
|
106
|
+
>
|
|
107
|
+
<md-icon>${this.revealed ? 'visibility_off' : 'visibility'}</md-icon>
|
|
108
|
+
</button>
|
|
109
|
+
|
|
110
|
+
<button ?disabled=${!present} title=${i18next.t('button.copy')} @click=${this.copy}>
|
|
111
|
+
<md-icon>content_copy</md-icon>
|
|
112
|
+
</button>
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
async copy() {
|
|
116
|
+
if (!hasSecret(this.value)) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
/*
|
|
120
|
+
Absent outside a secure context. Saying so beats failing quietly — a copy that did
|
|
121
|
+
nothing leaves the reader pasting whatever was in the clipboard before, and blaming
|
|
122
|
+
the other system when it does not work.
|
|
123
|
+
*/
|
|
124
|
+
if (!navigator.clipboard?.writeText) {
|
|
125
|
+
notify({ level: 'error', message: i18next.t('text.copying needs a secure connection') });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
await navigator.clipboard.writeText(this.value);
|
|
130
|
+
notify({ message: i18next.t('text.copied') });
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
notify({ level: 'error', message: i18next.t('text.copying needs a secure connection') });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
__decorate([
|
|
138
|
+
property({ type: String }),
|
|
139
|
+
__metadata("design:type", String)
|
|
140
|
+
], SecretField.prototype, "value", void 0);
|
|
141
|
+
__decorate([
|
|
142
|
+
state(),
|
|
143
|
+
__metadata("design:type", Boolean)
|
|
144
|
+
], SecretField.prototype, "revealed", void 0);
|
|
145
|
+
SecretField = __decorate([
|
|
146
|
+
customElement('secret-field')
|
|
147
|
+
], SecretField);
|
|
148
|
+
export { SecretField };
|
|
149
|
+
//# sourceMappingURL=secret-field.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-field.js","sourceRoot":"","sources":["../../client/components/secret-field.ts"],"names":[],"mappings":";AAAA,OAAO,4BAA4B,CAAA;AAEnC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAElE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAExC,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAA;AAEnE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEI,IAAM,WAAW,GAAjB,MAAM,WAAY,SAAQ,QAAQ,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC;IAAvD;;QA6DY,aAAQ,GAAY,KAAK,CAAA;IAiD5C,CAAC;aA7GQ,WAAM,GAAG;QACd,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAqDF;KACF,AAvDY,CAuDZ;IAOD,MAAM;QACJ,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAErC,OAAO,IAAI,CAAA;;;;iBAIE,CAAC,OAAO;iBACR,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,iBAAiB,CAAC;;;;oBAI5E,CAAC,OAAO;gBACZ,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC;iBACpE,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC;;mBAEpC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,YAAY;;;0BAGxC,CAAC,OAAO,UAAU,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,WAAW,IAAI,CAAC,IAAI;;;KAGnF,CAAA;IACH,CAAC;IAEO,KAAK,CAAC,IAAI;QAChB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3B,OAAM;QACR,CAAC;QAED;;;;UAIE;QACF,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,SAAS,EAAE,CAAC;YACpC,MAAM,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,wCAAwC,CAAC,EAAE,CAAC,CAAA;YACxF,OAAM;QACR,CAAC;QAED,IAAI,CAAC;YACH,MAAM,SAAS,CAAC,SAAS,CAAC,SAAS,CAAC,IAAI,CAAC,KAAM,CAAC,CAAA;YAChD,MAAM,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,CAAC,CAAA;QAC/C,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,wCAAwC,CAAC,EAAE,CAAC,CAAA;QAC1F,CAAC;IACH,CAAC;;AAlD2B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;0CAAe;AAEzB;IAAhB,KAAK,EAAE;;6CAAkC;AA7D/B,WAAW;IADvB,aAAa,CAAC,cAAc,CAAC;GACjB,WAAW,CA8GvB","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { notify } from '@operato/layout'\n\nimport { hasSecret, shownSecret } from '../utils/secret-display.js'\n\n/**\n * A credential the reader is meant to copy once.\n *\n * ── Why this is not a readonly input ─────────────────────────────────────────\n * The app binding and appliance detail screens printed refresh and access tokens into\n * `<input readonly>` in plain text, from the moment the page opened. The reader needs the\n * value exactly once — to paste it into the thing being set up — and for the rest of the\n * time the screen is open it is just a credential sitting on a display.\n *\n * So the value is masked, copying does not require revealing it, and revealing is something\n * the reader does on purpose. `utils/secret-display.ts` holds the rules, with tests: the mask\n * is a fixed width so it does not leak the length, and an absent secret shows nothing rather\n * than dots over a token nobody issued.\n *\n * ── Copy without reveal ──────────────────────────────────────────────────────\n * The common case is \"paste this into the other system\", and it does not need the value on\n * screen at all. Reveal exists for the other case — checking that the value matches what the\n * other side already has — and the reader chooses it.\n *\n * `navigator.clipboard` needs a secure context. On a plain-http deployment it is absent, and\n * the button says so instead of failing quietly, because a copy that silently did nothing\n * would leave the reader pasting whatever was in the clipboard before.\n */\n@customElement('secret-field')\nexport class SecretField extends localize(i18next)(LitElement) {\n static styles = [\n css`\n :host {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: var(--spacing-small, 4px);\n }\n\n input {\n flex: 1;\n min-width: 0;\n font-family: var(--theme-mono-font, monospace);\n padding: var(--spacing-small, 4px) var(--spacing-medium, 8px);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: var(--md-sys-shape-corner-small, 5px);\n background-color: var(--md-sys-color-surface);\n color: var(--md-sys-color-on-surface);\n }\n\n input[empty] {\n font-family: inherit;\n color: var(--md-sys-color-on-surface-variant);\n }\n\n button {\n display: inline-flex;\n align-items: center;\n padding: var(--spacing-small, 4px);\n border: none;\n border-radius: var(--md-sys-shape-corner-small, 5px);\n background-color: transparent;\n color: var(--md-sys-color-on-surface-variant);\n cursor: pointer;\n }\n\n button:hover:not([disabled]) {\n background-color: var(--md-sys-color-surface-variant);\n color: var(--md-sys-color-on-surface);\n }\n\n /*\n A button that cannot act says so before it is pressed. Leaving it live over an absent\n token means the reader presses it, nothing happens, and they cannot tell a broken\n screen from a credential that was never issued.\n */\n button[disabled] {\n opacity: 0.38;\n cursor: default;\n }\n\n md-icon {\n --md-icon-size: 18px;\n }\n `\n ]\n\n /** The credential. Absent means none has been issued. */\n @property({ type: String }) value?: string\n\n @state() private revealed: boolean = false\n\n render() {\n const present = hasSecret(this.value)\n\n return html`\n <input\n type=\"text\"\n readonly\n ?empty=${!present}\n .value=${present ? shownSecret(this.value, this.revealed) : i18next.t('text.not issued')}\n />\n\n <button\n ?disabled=${!present}\n title=${this.revealed ? i18next.t('button.hide') : i18next.t('button.reveal')}\n @click=${() => (this.revealed = !this.revealed)}\n >\n <md-icon>${this.revealed ? 'visibility_off' : 'visibility'}</md-icon>\n </button>\n\n <button ?disabled=${!present} title=${i18next.t('button.copy')} @click=${this.copy}>\n <md-icon>content_copy</md-icon>\n </button>\n `\n }\n\n private async copy() {\n if (!hasSecret(this.value)) {\n return\n }\n\n /*\n Absent outside a secure context. Saying so beats failing quietly — a copy that did\n nothing leaves the reader pasting whatever was in the clipboard before, and blaming\n the other system when it does not work.\n */\n if (!navigator.clipboard?.writeText) {\n notify({ level: 'error', message: i18next.t('text.copying needs a secure connection') })\n return\n }\n\n try {\n await navigator.clipboard.writeText(this.value!)\n notify({ message: i18next.t('text.copied') })\n } catch (e) {\n notify({ level: 'error', message: i18next.t('text.copying needs a secure connection') })\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { APPLICATION_TYPES } from './application';
|
|
4
|
+
/**
|
|
5
|
+
* The client's list of application types is a copy of the server's enum, and it had drifted.
|
|
6
|
+
*
|
|
7
|
+
* `ApplicationType` declares six members; `APPLICATION_TYPES` named five. `SFTP` was added on
|
|
8
|
+
* the server and never here, so:
|
|
9
|
+
*
|
|
10
|
+
* - an SFTP application could not be chosen from the dropdown, and
|
|
11
|
+
* - an existing SFTP application opened with **no option selected**, so pressing save
|
|
12
|
+
* changed its type to whatever the browser had picked for it.
|
|
13
|
+
*
|
|
14
|
+
* The client cannot import the enum — a client file that reaches into `server/` moves the
|
|
15
|
+
* whole build — so this test reads the declaration as text instead. That is enough: the thing
|
|
16
|
+
* that goes wrong is a member being added on one side only, and a member cannot be added
|
|
17
|
+
* without appearing in that block.
|
|
18
|
+
*
|
|
19
|
+
* ⚠ The measurement counts **the enum's own members**, not every mention of the word. An
|
|
20
|
+
* earlier habit of grepping for a bare identifier counts this file's prose, the import lines
|
|
21
|
+
* and the field declarations that merely use the type.
|
|
22
|
+
*/
|
|
23
|
+
/** Members declared inside `export enum ApplicationType { … }`, in declaration order. */
|
|
24
|
+
function serverApplicationTypes() {
|
|
25
|
+
const source = readFileSync(join(__dirname, '../../../auth-base/server/service/application/application.ts'), 'utf-8');
|
|
26
|
+
const block = source.match(/export enum ApplicationType \{([^}]*)\}/);
|
|
27
|
+
if (!block) {
|
|
28
|
+
throw new Error('ApplicationType is no longer declared as `export enum ApplicationType { … }` in ' +
|
|
29
|
+
'auth-base/server/service/application/application.ts. This test reads that block as ' +
|
|
30
|
+
'text; point it at wherever the enum lives now rather than deleting it.');
|
|
31
|
+
}
|
|
32
|
+
return Array.from(block[1].matchAll(/^\s*([A-Z0-9_]+)\s*=/gm)).map(match => match[1]);
|
|
33
|
+
}
|
|
34
|
+
describe('the application types the client offers', () => {
|
|
35
|
+
it('finds the enum it is supposed to mirror', () => {
|
|
36
|
+
/*
|
|
37
|
+
* Guards the reading itself. A regex that stops matching would otherwise make every other
|
|
38
|
+
* case below compare an empty list against an empty list and pass in silence.
|
|
39
|
+
*/
|
|
40
|
+
const types = serverApplicationTypes();
|
|
41
|
+
expect(types.length).toBeGreaterThan(1);
|
|
42
|
+
expect(types).toContain('OTHERS');
|
|
43
|
+
});
|
|
44
|
+
it('offers exactly what the server declares', () => {
|
|
45
|
+
expect([...APPLICATION_TYPES].sort()).toEqual([...serverApplicationTypes()].sort());
|
|
46
|
+
});
|
|
47
|
+
it('offers the type whose absence started this', () => {
|
|
48
|
+
/* SFTP is the member that was on the server and not here. Named so the case is readable. */
|
|
49
|
+
expect(APPLICATION_TYPES).toContain('SFTP');
|
|
50
|
+
});
|
|
51
|
+
it('offers the server default, so a new application can be left as it is', () => {
|
|
52
|
+
/*
|
|
53
|
+
* `Application.type` declares `default: ApplicationType.OTHERS`. The editor preselects it,
|
|
54
|
+
* which it cannot do if the option is missing.
|
|
55
|
+
*/
|
|
56
|
+
expect(APPLICATION_TYPES).toContain('OTHERS');
|
|
57
|
+
});
|
|
58
|
+
it('names each type once', () => {
|
|
59
|
+
expect(new Set(APPLICATION_TYPES).size).toEqual(APPLICATION_TYPES.length);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
//# sourceMappingURL=application-type-parity.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"application-type-parity.test.js","sourceRoot":"","sources":["../../client/constants/application-type-parity.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAA;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAE3B,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAEjD;;;;;;;;;;;;;;;;;;GAkBG;AAEH,yFAAyF;AACzF,SAAS,sBAAsB;IAC7B,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,8DAA8D,CAAC,EAAE,OAAO,CAAC,CAAA;IAErH,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,yCAAyC,CAAC,CAAA;IAErE,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,kFAAkF;YAChF,qFAAqF;YACrF,wEAAwE,CAC3E,CAAA;IACH,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,wBAAwB,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;AACvF,CAAC;AAED,QAAQ,CAAC,yCAAyC,EAAE,GAAG,EAAE;IACvD,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD;;;WAGG;QACH,MAAM,KAAK,GAAG,sBAAsB,EAAE,CAAA;QAEtC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAA;QACvC,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAA;IACnC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,MAAM,CAAC,CAAC,GAAG,iBAAiB,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,sBAAsB,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAA;IACrF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACpD,4FAA4F;QAC5F,MAAM,CAAC,iBAAiB,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IAC7C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sEAAsE,EAAE,GAAG,EAAE;QAC9E;;;WAGG;QACH,MAAM,CAAC,iBAAiB,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAA;IAC/C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sBAAsB,EAAE,GAAG,EAAE;QAC9B,MAAM,CAAC,IAAI,GAAG,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAA;IAC3E,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import { readFileSync } from 'fs'\nimport { join } from 'path'\n\nimport { APPLICATION_TYPES } from './application'\n\n/**\n * The client's list of application types is a copy of the server's enum, and it had drifted.\n *\n * `ApplicationType` declares six members; `APPLICATION_TYPES` named five. `SFTP` was added on\n * the server and never here, so:\n *\n * - an SFTP application could not be chosen from the dropdown, and\n * - an existing SFTP application opened with **no option selected**, so pressing save\n * changed its type to whatever the browser had picked for it.\n *\n * The client cannot import the enum — a client file that reaches into `server/` moves the\n * whole build — so this test reads the declaration as text instead. That is enough: the thing\n * that goes wrong is a member being added on one side only, and a member cannot be added\n * without appearing in that block.\n *\n * ⚠ The measurement counts **the enum's own members**, not every mention of the word. An\n * earlier habit of grepping for a bare identifier counts this file's prose, the import lines\n * and the field declarations that merely use the type.\n */\n\n/** Members declared inside `export enum ApplicationType { … }`, in declaration order. */\nfunction serverApplicationTypes(): string[] {\n const source = readFileSync(join(__dirname, '../../../auth-base/server/service/application/application.ts'), 'utf-8')\n\n const block = source.match(/export enum ApplicationType \\{([^}]*)\\}/)\n\n if (!block) {\n throw new Error(\n 'ApplicationType is no longer declared as `export enum ApplicationType { … }` in ' +\n 'auth-base/server/service/application/application.ts. This test reads that block as ' +\n 'text; point it at wherever the enum lives now rather than deleting it.'\n )\n }\n\n return Array.from(block[1].matchAll(/^\\s*([A-Z0-9_]+)\\s*=/gm)).map(match => match[1])\n}\n\ndescribe('the application types the client offers', () => {\n it('finds the enum it is supposed to mirror', () => {\n /*\n * Guards the reading itself. A regex that stops matching would otherwise make every other\n * case below compare an empty list against an empty list and pass in silence.\n */\n const types = serverApplicationTypes()\n\n expect(types.length).toBeGreaterThan(1)\n expect(types).toContain('OTHERS')\n })\n\n it('offers exactly what the server declares', () => {\n expect([...APPLICATION_TYPES].sort()).toEqual([...serverApplicationTypes()].sort())\n })\n\n it('offers the type whose absence started this', () => {\n /* SFTP is the member that was on the server and not here. Named so the case is readable. */\n expect(APPLICATION_TYPES).toContain('SFTP')\n })\n\n it('offers the server default, so a new application can be left as it is', () => {\n /*\n * `Application.type` declares `default: ApplicationType.OTHERS`. The editor preselects it,\n * which it cannot do if the option is missing.\n */\n expect(APPLICATION_TYPES).toContain('OTHERS')\n })\n\n it('names each type once', () => {\n expect(new Set(APPLICATION_TYPES).size).toEqual(APPLICATION_TYPES.length)\n })\n})\n"]}
|
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The application types a person can choose from.
|
|
3
|
+
*
|
|
4
|
+
* ── This is a copy, and it had already drifted ──────────────────────────────
|
|
5
|
+
* The list belongs to the server: `ApplicationType` in
|
|
6
|
+
* `auth-base/server/service/application/application.ts`. The client cannot import it — a
|
|
7
|
+
* client file that reaches into `server/` drags the whole build across — so this is a copy,
|
|
8
|
+
* and a copy is something that goes stale quietly.
|
|
9
|
+
*
|
|
10
|
+
* It had. The server declared six types; this said five. `SFTP` was added on the server and
|
|
11
|
+
* never here, so an SFTP application could not be chosen, and an existing one showed up in the
|
|
12
|
+
* dropdown as nothing selected at all.
|
|
13
|
+
*
|
|
14
|
+
* `application-type-parity.test.ts` reads the server file as text and fails when the two lists
|
|
15
|
+
* part again. There was no `APPLICATION_TYPE` map beside this one worth keeping — it had no
|
|
16
|
+
* callers and was the older of the two spellings.
|
|
17
|
+
*/
|
|
8
18
|
export declare const APPLICATION_TYPES: string[];
|
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The application types a person can choose from.
|
|
3
|
+
*
|
|
4
|
+
* ── This is a copy, and it had already drifted ──────────────────────────────
|
|
5
|
+
* The list belongs to the server: `ApplicationType` in
|
|
6
|
+
* `auth-base/server/service/application/application.ts`. The client cannot import it — a
|
|
7
|
+
* client file that reaches into `server/` drags the whole build across — so this is a copy,
|
|
8
|
+
* and a copy is something that goes stale quietly.
|
|
9
|
+
*
|
|
10
|
+
* It had. The server declared six types; this said five. `SFTP` was added on the server and
|
|
11
|
+
* never here, so an SFTP application could not be chosen, and an existing one showed up in the
|
|
12
|
+
* dropdown as nothing selected at all.
|
|
13
|
+
*
|
|
14
|
+
* `application-type-parity.test.ts` reads the server file as text and fails when the two lists
|
|
15
|
+
* part again. There was no `APPLICATION_TYPE` map beside this one worth keeping — it had no
|
|
16
|
+
* callers and was the older of the two spellings.
|
|
17
|
+
*/
|
|
18
|
+
export const APPLICATION_TYPES = ['SELLERCRAFT', 'XILNEX', 'MMS', 'XERO', 'OTHERS', 'SFTP'];
|
|
9
19
|
//# sourceMappingURL=application.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"application.js","sourceRoot":"","sources":["../../client/constants/application.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,
|
|
1
|
+
{"version":3,"file":"application.js","sourceRoot":"","sources":["../../client/constants/application.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAA","sourcesContent":["/**\n * The application types a person can choose from.\n *\n * ── This is a copy, and it had already drifted ──────────────────────────────\n * The list belongs to the server: `ApplicationType` in\n * `auth-base/server/service/application/application.ts`. The client cannot import it — a\n * client file that reaches into `server/` drags the whole build across — so this is a copy,\n * and a copy is something that goes stale quietly.\n *\n * It had. The server declared six types; this said five. `SFTP` was added on the server and\n * never here, so an SFTP application could not be chosen, and an existing one showed up in the\n * dropdown as nothing selected at all.\n *\n * `application-type-parity.test.ts` reads the server file as text and fails when the two lists\n * part again. There was no `APPLICATION_TYPE` map beside this one worth keeping — it had no\n * callers and was the older of the two spellings.\n */\nexport const APPLICATION_TYPES = ['SELLERCRAFT', 'XILNEX', 'MMS', 'XERO', 'OTHERS', 'SFTP']\n"]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* The bound-applications list must not ask for the refresh token.
|
|
5
|
+
*
|
|
6
|
+
* `AppBinding.refreshToken` is not a hash of anything. `oauth2-server.ts` writes the refresh
|
|
7
|
+
* token into the `password` column verbatim, and the field re-exposes that column under a
|
|
8
|
+
* friendlier name. Whoever holds it can exchange it for access tokens.
|
|
9
|
+
*
|
|
10
|
+
* The field carries its own `@privilege(category: "security", privilege: "query",
|
|
11
|
+
* domainOwnerGranted: true)`, so the server will not hand it to just anyone — but the people
|
|
12
|
+
* it *will* hand it to are the ones most likely to have this screen open on a shared display.
|
|
13
|
+
* A credential in a grid cell is a credential in plain text beside a name, in every
|
|
14
|
+
* screenshot and every shoulder-glance.
|
|
15
|
+
*
|
|
16
|
+
* ⚠ Comments are stripped before searching. The prose above names `refreshToken` four times
|
|
17
|
+
* explaining why it is absent; searching the raw file would find the explanation and pass
|
|
18
|
+
* over a query that really asks for it.
|
|
19
|
+
*/
|
|
20
|
+
function source(name) {
|
|
21
|
+
const text = readFileSync(join(__dirname, name), 'utf-8');
|
|
22
|
+
return text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^[ \t]*\/\/.*$/gm, '');
|
|
23
|
+
}
|
|
24
|
+
describe('the bound applications list query', () => {
|
|
25
|
+
it('does not ask for the refresh token', () => {
|
|
26
|
+
expect(source('./app-bindings.ts')).not.toContain('refreshToken');
|
|
27
|
+
});
|
|
28
|
+
it('asks for the fields the columns render', () => {
|
|
29
|
+
const text = source('./app-bindings.ts');
|
|
30
|
+
for (const field of ['name', 'email', 'status', 'scope', 'updatedAt', 'application']) {
|
|
31
|
+
expect(text).toContain(field);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
it('passes the grist options through, so paging and sorting reach the server', () => {
|
|
35
|
+
/*
|
|
36
|
+
* Worth asserting here in particular: the server paged before it filtered until
|
|
37
|
+
* dde62cd6a1, so this screen had no paging to pass through and the habit of not passing
|
|
38
|
+
* it could easily survive the fix.
|
|
39
|
+
*/
|
|
40
|
+
const text = source('./app-bindings.ts');
|
|
41
|
+
expect(text).toContain('$pagination: Pagination');
|
|
42
|
+
expect(text).toContain('$sortings: [Sorting!]');
|
|
43
|
+
expect(text).toContain('$filters: [Filter!]');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
//# sourceMappingURL=app-binding-list-query.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-binding-list-query.test.js","sourceRoot":"","sources":["../../../client/pages/app-binding/app-binding-list-query.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAA;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAE3B;;;;;;;;;;;;;;;;GAgBG;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,mCAAmC,EAAE,GAAG,EAAE;IACjD,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,cAAc,CAAC,CAAA;IACnE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,IAAI,GAAG,MAAM,CAAC,mBAAmB,CAAC,CAAA;QAExC,KAAK,MAAM,KAAK,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,aAAa,CAAC,EAAE,CAAC;YACrF,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;QAC/B,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,0EAA0E,EAAE,GAAG,EAAE;QAClF;;;;WAIG;QACH,MAAM,IAAI,GAAG,MAAM,CAAC,mBAAmB,CAAC,CAAA;QAExC,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","sourcesContent":["import { readFileSync } from 'fs'\nimport { join } from 'path'\n\n/**\n * The bound-applications list must not ask for the refresh token.\n *\n * `AppBinding.refreshToken` is not a hash of anything. `oauth2-server.ts` writes the refresh\n * token into the `password` column verbatim, and the field re-exposes that column under a\n * friendlier name. Whoever holds it can exchange it for access tokens.\n *\n * The field carries its own `@privilege(category: \"security\", privilege: \"query\",\n * domainOwnerGranted: true)`, so the server will not hand it to just anyone — but the people\n * it *will* hand it to are the ones most likely to have this screen open on a shared display.\n * A credential in a grid cell is a credential in plain text beside a name, in every\n * screenshot and every shoulder-glance.\n *\n * ⚠ Comments are stripped before searching. The prose above names `refreshToken` four times\n * explaining why it is absent; searching the raw file would find the explanation and pass\n * over a 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 bound applications list query', () => {\n it('does not ask for the refresh token', () => {\n expect(source('./app-bindings.ts')).not.toContain('refreshToken')\n })\n\n it('asks for the fields the columns render', () => {\n const text = source('./app-bindings.ts')\n\n for (const field of ['name', 'email', 'status', 'scope', 'updatedAt', 'application']) {\n expect(text).toContain(field)\n }\n })\n\n it('passes the grist options through, so paging and sorting reach the server', () => {\n /*\n * Worth asserting here in particular: the server paged before it filtered until\n * dde62cd6a1, so this screen had no paging to pass through and the habit of not passing\n * it could easily survive the fix.\n */\n const text = source('./app-bindings.ts')\n\n expect(text).toContain('$pagination: Pagination')\n expect(text).toContain('$sortings: [Sorting!]')\n expect(text).toContain('$filters: [Filter!]')\n })\n})\n"]}
|
|
@@ -1,2 +1,65 @@
|
|
|
1
|
-
import '@material/web/
|
|
2
|
-
import '@
|
|
1
|
+
import '@material/web/icon/icon.js';
|
|
2
|
+
import '@operato/data-grist/ox-grist.js';
|
|
3
|
+
import '@operato/context/ox-context-page-toolbar.js';
|
|
4
|
+
import type { FetchOption } from '@operato/data-grist';
|
|
5
|
+
import { PageView } from '@operato/shell';
|
|
6
|
+
import '../../components/app-binding-detail.js';
|
|
7
|
+
declare const AppBindings_base: (new (...args: any[]) => {
|
|
8
|
+
__preferenceProviders: {
|
|
9
|
+
[element: string]: import("@operato/p13n").PagePreferenceProvider;
|
|
10
|
+
};
|
|
11
|
+
getPagePreferenceProvider(element: string): import("@operato/p13n").PagePreferenceProvider | undefined;
|
|
12
|
+
}) & (new (...args: any[]) => import("lit").LitElement) & typeof PageView;
|
|
13
|
+
/**
|
|
14
|
+
* Applications bound to this domain.
|
|
15
|
+
*
|
|
16
|
+
* A binding is a `User` row with `userType: 'application'` — the account an application signs
|
|
17
|
+
* in as. The list shows who it is and what it may do; what it signs in *with* is elsewhere.
|
|
18
|
+
*
|
|
19
|
+
* ── The server answers honestly now ──────────────────────────────────────────
|
|
20
|
+
* Until dde62cd6a1 this query paged before it filtered: the database took a page of users of
|
|
21
|
+
* every type, JavaScript kept the applications among them, and the total reported how many
|
|
22
|
+
* survived that one page. A grid told there were three when there were thirty never offered a
|
|
23
|
+
* second page. Wiring paging up was pointless until that was fixed; it works now.
|
|
24
|
+
*
|
|
25
|
+
* ── ⚠ The refresh token is not asked for, and a test holds that ──────────────
|
|
26
|
+
* AppBinding's refresh token is the application's actual credential — oauth2-server.ts writes
|
|
27
|
+
* it into the password column verbatim, not hashed — and the field carries its own
|
|
28
|
+
* privilege directive for security:query.
|
|
29
|
+
*
|
|
30
|
+
* A credential that can be exchanged for access tokens does not belong in a grid cell, where
|
|
31
|
+
* it sits in plain text beside a name for anyone who can open the screen, and in any
|
|
32
|
+
* screenshot of it. `app-binding-list-query.test.ts` fails if the word appears here.
|
|
33
|
+
*/
|
|
34
|
+
export declare class AppBindings extends AppBindings_base {
|
|
35
|
+
static styles: import("lit").CSSResult[];
|
|
36
|
+
active: boolean;
|
|
37
|
+
config: any;
|
|
38
|
+
mode: 'CARD' | 'LIST' | 'GRID';
|
|
39
|
+
/** The last fetch did not answer. Distinct from answering with nothing. */
|
|
40
|
+
failed: boolean;
|
|
41
|
+
private grist;
|
|
42
|
+
get context(): {
|
|
43
|
+
title: string;
|
|
44
|
+
help: string;
|
|
45
|
+
search: {
|
|
46
|
+
handler: (search: string) => void;
|
|
47
|
+
value: string;
|
|
48
|
+
};
|
|
49
|
+
actions: {
|
|
50
|
+
title: string;
|
|
51
|
+
action: () => void;
|
|
52
|
+
icon: string;
|
|
53
|
+
}[];
|
|
54
|
+
toolbar: boolean;
|
|
55
|
+
};
|
|
56
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
57
|
+
pageInitialized(): Promise<void>;
|
|
58
|
+
private openDetail;
|
|
59
|
+
pageUpdated(_changes: any, _lifecycle: any): Promise<void>;
|
|
60
|
+
fetchHandler({ page, limit, sortings, filters }: FetchOption): Promise<{
|
|
61
|
+
total: number;
|
|
62
|
+
records: any[];
|
|
63
|
+
}>;
|
|
64
|
+
}
|
|
65
|
+
export {};
|