@things-factory/auth-ui 10.1.41 → 10.1.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist-client/components/abstract-password-reset.d.ts +6 -0
  2. package/dist-client/components/abstract-password-reset.js +16 -1
  3. package/dist-client/components/abstract-password-reset.js.map +1 -1
  4. package/dist-client/components/accept-invitation.js +2 -2
  5. package/dist-client/components/accept-invitation.js.map +1 -1
  6. package/dist-client/components/auth-badge.d.ts +20 -0
  7. package/dist-client/components/auth-badge.js +50 -0
  8. package/dist-client/components/auth-badge.js.map +1 -0
  9. package/dist-client/components/auth-badge.test.js +37 -0
  10. package/dist-client/components/auth-badge.test.js.map +1 -0
  11. package/dist-client/components/create-domain-popup-theme.test.d.ts +1 -0
  12. package/dist-client/components/create-domain-popup-theme.test.js +16 -0
  13. package/dist-client/components/create-domain-popup-theme.test.js.map +1 -0
  14. package/dist-client/components/create-domain-popup.js +3 -1
  15. package/dist-client/components/create-domain-popup.js.map +1 -1
  16. package/dist-client/entries/auth/verify.d.ts +25 -0
  17. package/dist-client/entries/auth/verify.js +81 -0
  18. package/dist-client/entries/auth/verify.js.map +1 -0
  19. package/dist-client/pages/role/role-list-page.d.ts +8 -0
  20. package/dist-client/pages/role/role-list-page.js +73 -24
  21. package/dist-client/pages/role/role-list-page.js.map +1 -1
  22. package/dist-client/pages/role/role-template-gap.test.d.ts +1 -0
  23. package/dist-client/pages/role/role-template-gap.test.js +30 -0
  24. package/dist-client/pages/role/role-template-gap.test.js.map +1 -0
  25. package/dist-client/pages/user/user-list-page.d.ts +1 -0
  26. package/dist-client/pages/user/user-list-page.js +6 -21
  27. package/dist-client/pages/user/user-list-page.js.map +1 -1
  28. package/dist-client/tsconfig.tsbuildinfo +1 -1
  29. package/dist-client/utils/{invitation-token.d.ts → link-token.d.ts} +5 -4
  30. package/dist-client/utils/{invitation-token.js → link-token.js} +6 -5
  31. package/dist-client/utils/link-token.js.map +1 -0
  32. package/dist-client/utils/link-token.test.d.ts +1 -0
  33. package/dist-client/utils/{invitation-token.test.js → link-token.test.js} +17 -17
  34. package/dist-client/utils/link-token.test.js.map +1 -0
  35. package/package.json +5 -5
  36. package/translations/en.json +5 -0
  37. package/translations/ja.json +5 -0
  38. package/translations/ko.json +5 -0
  39. package/translations/ms.json +5 -0
  40. package/translations/zh.json +5 -0
  41. package/dist-client/utils/invitation-token.js.map +0 -1
  42. package/dist-client/utils/invitation-token.test.js.map +0 -1
  43. /package/dist-client/{utils/invitation-token.test.d.ts → components/auth-badge.test.d.ts} +0 -0
@@ -14,6 +14,12 @@ export declare abstract class AbstractPasswordReset extends AbstractAuthPage {
14
14
  private passwordPattern;
15
15
  private passwordHelp;
16
16
  abstract get submitButtonLabel(): string;
17
+ /**
18
+ * The mailed link is `…#<token>`: the browser does not send what follows `#`, so the token stays
19
+ * out of server and proxy logs. Taken out of the address bar here as well — `replaceState`, so
20
+ * going back does not put it on screen.
21
+ */
22
+ connectedCallback(): void;
17
23
  render(): import("lit-html").TemplateResult<1>;
18
24
  updated(changed: any): void;
19
25
  languageUpdated(): void;
@@ -12,6 +12,7 @@ import { i18next } from '@operato/i18n';
12
12
  import { AUTH_STYLE_SIGN } from '../auth-style-sign.js';
13
13
  import { generatePasswordPatternHelp, generatePasswordPatternRegExp } from '../utils/password-rule.js';
14
14
  import { AbstractAuthPage } from './abstract-auth-page.js';
15
+ import { readLinkToken } from '../utils/link-token.js';
15
16
  import { EntryControlSizeStyles } from '../entry-control-size-styles.js';
16
17
  export class AbstractPasswordReset extends AbstractAuthPage {
17
18
  constructor() {
@@ -45,6 +46,19 @@ export class AbstractPasswordReset extends AbstractAuthPage {
45
46
  `,
46
47
  AUTH_STYLE_SIGN
47
48
  ]; }
49
+ /**
50
+ * The mailed link is `…#<token>`: the browser does not send what follows `#`, so the token stays
51
+ * out of server and proxy logs. Taken out of the address bar here as well — `replaceState`, so
52
+ * going back does not put it on screen.
53
+ */
54
+ connectedCallback() {
55
+ super.connectedCallback();
56
+ const { token, clean } = readLinkToken(window.location.href);
57
+ this.token = token;
58
+ if (clean && clean !== window.location.href) {
59
+ window.history.replaceState(null, '', clean);
60
+ }
61
+ }
48
62
  render() {
49
63
  var { icon, title, description } = this.applicationMeta;
50
64
  const { disableUserFavoredLanguage, languages } = this.data || {};
@@ -126,7 +140,8 @@ export class AbstractPasswordReset extends AbstractAuthPage {
126
140
  }
127
141
  updated(changed) {
128
142
  super.updated(changed);
129
- if (changed.has('data')) {
143
+ /* A refused post renders the form again with the token it carried, in the page, not the address. */
144
+ if (changed.has('data') && this.data?.token) {
130
145
  this.token = this.data.token;
131
146
  }
132
147
  if (changed.has('message')) {
@@ -1 +1 @@
1
- {"version":3,"file":"abstract-password-reset.js","sourceRoot":"","sources":["../../client/components/abstract-password-reset.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'\n\nimport '@operato/i18n/ox-i18n.js'\nimport '@operato/i18n/ox-i18n-selector.js'\nimport '@operato/layout/ox-snack-bar.js'\n\nimport '../components/profile-component.js'\n\nimport { css, html, nothing } from 'lit'\nimport { property, query } from 'lit/decorators.js'\n\nimport { i18next } from '@operato/i18n'\n\nimport { AUTH_STYLE_SIGN } from '../auth-style-sign.js'\nimport { generatePasswordPatternHelp, generatePasswordPatternRegExp } from '../utils/password-rule.js'\nimport { AbstractAuthPage } from './abstract-auth-page.js'\nimport { EntryControlSizeStyles } from '../entry-control-size-styles.js'\n\nexport abstract class AbstractPasswordReset extends AbstractAuthPage {\n static styles = [\n EntryControlSizeStyles,\n css`\n :host {\n position: relative;\n overflow: hidden;\n\n display: flex;\n flex-direction: row;\n\n width: 100vw;\n height: 100vh;\n height: 100dvh;\n }\n\n @media print {\n :host {\n width: 100%;\n height: 100%;\n min-height: 100vh;\n min-height: 100dvh;\n }\n }\n `,\n AUTH_STYLE_SIGN\n ]\n\n @property({ type: Object }) data: any\n @property({ type: String }) token?: string\n\n @query('#confirm-password') confirmPass!: HTMLElement\n\n private passwordPattern: string = ''\n private passwordHelp: string = ''\n\n abstract get submitButtonLabel(): string\n\n render() {\n var { icon, title, description } = this.applicationMeta\n const { disableUserFavoredLanguage, languages } = this.data || {}\n\n return html`\n <div class=\"wrap\">\n <div class=\"auth-brand\">\n <img src=${icon} />\n <strong class=\"name\">${title}</strong>\n <span class=\"welcome-msg\">${description}</span>\n </div>\n\n <div class=\"auth-form\">\n <h3><ox-i18n msgid=\"title.${this.title}\"></ox-i18n></h3>\n <form\n id=\"form\"\n action=\"${this.actionUrl}\"\n method=\"post\"\n @keypress=${e => {\n if (e.key == 'Enter') this._onSubmit(e)\n }}\n >\n <input name=\"token\" type=\"hidden\" .value=${this.token || ''} required />\n <div class=\"field\">\n <md-filled-text-field\n name=\"password\"\n type=\"password\"\n label=${String(i18next.t('label.password'))}\n pattern=${this.passwordPattern || ''}\n supporting-text=${this.passwordHelp}\n autocomplete=\"off\"\n @input=${e => {\n var val = e.target.value\n this.confirmPass.setAttribute('pattern', val.replace(/[-[\\]{}()*+?.,\\\\^$|#\\s]/g, '[$&]'))\n }}\n required\n ><md-icon slot=\"leading-icon\">password</md-icon></md-filled-text-field\n >\n </div>\n\n <div class=\"field\">\n <md-filled-text-field\n id=\"confirm-password\"\n name=\"confirm-password\"\n type=\"password\"\n label=${String(i18next.t('field.confirm password'))}\n autocomplete=\"off\"\n required\n ><md-icon slot=\"leading-icon\">password</md-icon></md-filled-text-field\n >\n </div>\n\n <md-elevated-button id=\"submit-button\" type=\"button\" @click=${e => this._onSubmit(e)}>\n <ox-i18n msgid=\"${this.submitButtonLabel}\"></ox-i18n>\n </md-elevated-button>\n\n ${!disableUserFavoredLanguage\n ? html` <div id=\"locale-area\">\n <label for=\"locale-selector\"><md-icon>language</md-icon></label>\n <ox-i18n-selector\n id=\"locale-selector\"\n value=${i18next.language || 'en-US'}\n .languages=${languages}\n @change=${e => {\n var locale = e.detail\n if (!locale) return\n\n i18next.changeLanguage(locale)\n }}\n ></ox-i18n-selector>\n </div>`\n : nothing}\n </form>\n </div>\n </div>\n\n <ox-snack-bar id=\"snackbar\" level=\"error\" .message=${this.message}></ox-snack-bar>\n `\n }\n\n updated(changed) {\n super.updated(changed)\n\n if (changed.has('data')) {\n this.token = this.data.token\n }\n\n if (changed.has('message')) {\n if (!this.message) {\n this.hideSnackbar()\n } else {\n this.showSnackbar({\n timer: -1\n })\n }\n }\n }\n\n languageUpdated() {\n this.passwordPattern = generatePasswordPatternRegExp(this.data.passwordRule).source\n this.passwordHelp = generatePasswordPatternHelp(this.data.passwordRule)\n }\n\n async submit() {\n this.formEl.submit()\n }\n}\n"]}
1
+ {"version":3,"file":"abstract-password-reset.js","sourceRoot":"","sources":["../../client/components/abstract-password-reset.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'\n\nimport '@operato/i18n/ox-i18n.js'\nimport '@operato/i18n/ox-i18n-selector.js'\nimport '@operato/layout/ox-snack-bar.js'\n\nimport '../components/profile-component.js'\n\nimport { css, html, nothing } from 'lit'\nimport { property, query } from 'lit/decorators.js'\n\nimport { i18next } from '@operato/i18n'\n\nimport { AUTH_STYLE_SIGN } from '../auth-style-sign.js'\nimport { generatePasswordPatternHelp, generatePasswordPatternRegExp } from '../utils/password-rule.js'\nimport { AbstractAuthPage } from './abstract-auth-page.js'\nimport { readLinkToken } from '../utils/link-token.js'\nimport { EntryControlSizeStyles } from '../entry-control-size-styles.js'\n\nexport abstract class AbstractPasswordReset extends AbstractAuthPage {\n static styles = [\n EntryControlSizeStyles,\n css`\n :host {\n position: relative;\n overflow: hidden;\n\n display: flex;\n flex-direction: row;\n\n width: 100vw;\n height: 100vh;\n height: 100dvh;\n }\n\n @media print {\n :host {\n width: 100%;\n height: 100%;\n min-height: 100vh;\n min-height: 100dvh;\n }\n }\n `,\n AUTH_STYLE_SIGN\n ]\n\n @property({ type: Object }) data: any\n @property({ type: String }) token?: string\n\n @query('#confirm-password') confirmPass!: HTMLElement\n\n private passwordPattern: string = ''\n private passwordHelp: string = ''\n\n abstract get submitButtonLabel(): string\n\n /**\n * The mailed link is `…#<token>`: the browser does not send what follows `#`, so the token stays\n * out of server and proxy logs. Taken out of the address bar here as well — `replaceState`, so\n * going back does not put it on screen.\n */\n connectedCallback() {\n super.connectedCallback()\n\n const { token, clean } = readLinkToken(window.location.href)\n\n this.token = token\n\n if (clean && clean !== window.location.href) {\n window.history.replaceState(null, '', clean)\n }\n }\n\n render() {\n var { icon, title, description } = this.applicationMeta\n const { disableUserFavoredLanguage, languages } = this.data || {}\n\n return html`\n <div class=\"wrap\">\n <div class=\"auth-brand\">\n <img src=${icon} />\n <strong class=\"name\">${title}</strong>\n <span class=\"welcome-msg\">${description}</span>\n </div>\n\n <div class=\"auth-form\">\n <h3><ox-i18n msgid=\"title.${this.title}\"></ox-i18n></h3>\n <form\n id=\"form\"\n action=\"${this.actionUrl}\"\n method=\"post\"\n @keypress=${e => {\n if (e.key == 'Enter') this._onSubmit(e)\n }}\n >\n <input name=\"token\" type=\"hidden\" .value=${this.token || ''} required />\n <div class=\"field\">\n <md-filled-text-field\n name=\"password\"\n type=\"password\"\n label=${String(i18next.t('label.password'))}\n pattern=${this.passwordPattern || ''}\n supporting-text=${this.passwordHelp}\n autocomplete=\"off\"\n @input=${e => {\n var val = e.target.value\n this.confirmPass.setAttribute('pattern', val.replace(/[-[\\]{}()*+?.,\\\\^$|#\\s]/g, '[$&]'))\n }}\n required\n ><md-icon slot=\"leading-icon\">password</md-icon></md-filled-text-field\n >\n </div>\n\n <div class=\"field\">\n <md-filled-text-field\n id=\"confirm-password\"\n name=\"confirm-password\"\n type=\"password\"\n label=${String(i18next.t('field.confirm password'))}\n autocomplete=\"off\"\n required\n ><md-icon slot=\"leading-icon\">password</md-icon></md-filled-text-field\n >\n </div>\n\n <md-elevated-button id=\"submit-button\" type=\"button\" @click=${e => this._onSubmit(e)}>\n <ox-i18n msgid=\"${this.submitButtonLabel}\"></ox-i18n>\n </md-elevated-button>\n\n ${!disableUserFavoredLanguage\n ? html` <div id=\"locale-area\">\n <label for=\"locale-selector\"><md-icon>language</md-icon></label>\n <ox-i18n-selector\n id=\"locale-selector\"\n value=${i18next.language || 'en-US'}\n .languages=${languages}\n @change=${e => {\n var locale = e.detail\n if (!locale) return\n\n i18next.changeLanguage(locale)\n }}\n ></ox-i18n-selector>\n </div>`\n : nothing}\n </form>\n </div>\n </div>\n\n <ox-snack-bar id=\"snackbar\" level=\"error\" .message=${this.message}></ox-snack-bar>\n `\n }\n\n updated(changed) {\n super.updated(changed)\n\n /* A refused post renders the form again with the token it carried, in the page, not the address. */\n if (changed.has('data') && this.data?.token) {\n this.token = this.data.token\n }\n\n if (changed.has('message')) {\n if (!this.message) {\n this.hideSnackbar()\n } else {\n this.showSnackbar({\n timer: -1\n })\n }\n }\n }\n\n languageUpdated() {\n this.passwordPattern = generatePasswordPatternRegExp(this.data.passwordRule).source\n this.passwordHelp = generatePasswordPatternHelp(this.data.passwordRule)\n }\n\n async submit() {\n this.formEl.submit()\n }\n}\n"]}
@@ -11,7 +11,7 @@ import { i18next } from '@operato/i18n';
11
11
  import { AUTH_STYLE_SIGN } from '../auth-style-sign.js';
12
12
  import { EntryControlSizeStyles } from '../entry-control-size-styles.js';
13
13
  import { AbstractAuthPage } from './abstract-auth-page.js';
14
- import { readInvitationToken } from '../utils/invitation-token.js';
14
+ import { readLinkToken } from '../utils/link-token.js';
15
15
  /**
16
16
  * Accepting an invitation.
17
17
  *
@@ -71,7 +71,7 @@ export class AcceptInvitation extends AbstractAuthPage {
71
71
  }
72
72
  connectedCallback() {
73
73
  super.connectedCallback();
74
- const { token, clean } = readInvitationToken(window.location.href);
74
+ const { token, clean } = readLinkToken(window.location.href);
75
75
  this.token = token;
76
76
  /*
77
77
  * Taken out of the address bar and out of this history entry. `replaceState` rather than
@@ -1 +1 @@
1
- {"version":3,"file":"accept-invitation.js","sourceRoot":"","sources":["../../client/components/accept-invitation.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'\n\nimport '@operato/i18n/ox-i18n.js'\nimport '@operato/i18n/ox-i18n-selector.js'\nimport '@operato/layout/ox-snack-bar.js'\n\nimport { css, html, nothing } from 'lit'\nimport { property } from 'lit/decorators.js'\n\nimport { i18next } from '@operato/i18n'\n\nimport { AUTH_STYLE_SIGN } from '../auth-style-sign.js'\nimport { EntryControlSizeStyles } from '../entry-control-size-styles.js'\nimport { AbstractAuthPage } from './abstract-auth-page.js'\nimport { readInvitationToken } from '../utils/invitation-token.js'\n\n/**\n * Accepting an invitation.\n *\n * ── Why the token is not in the address ─────────────────────────────────────\n * The link is `…/accept-invitation#<token>`, and a browser does not send what follows the `#`.\n * So the request that loads this page carries nothing, and the token stays out of server access\n * logs, proxy logs and `Referer`. This page reads it from the fragment and then clears it from\n * the address bar, so it does not sit in the browser's history either.\n *\n * The one place it can still travel is the mail being forwarded, and nothing about where the\n * token sits can prevent that. So the address it was sent to is asked for as well: holding the\n * link is not enough on its own.\n *\n * ── Why the password fields only appear after a try ─────────────────────────\n * Whether an address already has an account is not this page's to announce. It sends what it\n * has, and the server answers `invitation-needs-a-password` when it needs one — so the extra\n * fields appear for the person who is actually creating an account, and nobody learns anything\n * by loading the page.\n */\nexport class AcceptInvitation extends AbstractAuthPage {\n static styles = [\n EntryControlSizeStyles,\n css`\n :host {\n position: relative;\n overflow: hidden;\n\n display: flex;\n flex-direction: row;\n\n width: 100vw;\n height: 100vh;\n height: 100dvh;\n }\n\n .note {\n margin: 0 0 var(--spacing-medium) 0;\n font-size: 0.85rem;\n line-height: 1.5;\n color: var(--md-sys-color-on-surface-variant);\n }\n `,\n AUTH_STYLE_SIGN\n ]\n\n get pageName() {\n return 'accept invitation'\n }\n\n /*\n * ⚠ `/auth/…` — the server router carries that prefix. This read `/accept-invitation` until\n * 2026-09-24, which is no route; see the note beside `ACCEPT_INVITATION_PATH` in auth-base.\n */\n get actionUrl() {\n return '/auth/accept-invitation'\n }\n\n get title() {\n return 'accept invitation'\n }\n\n /** Read from the fragment on connect. Empty when the link was opened without one. */\n @property({ type: String }) token?: string\n\n /** The server asked for a password, so this person is creating an account. */\n @property({ type: Boolean }) needsPassword?: boolean\n\n /** Joined. The page stops being a form and says where to go. */\n @property({ type: Boolean }) joined?: boolean\n\n @property({ type: Boolean }) busy?: boolean\n\n connectedCallback() {\n super.connectedCallback()\n\n const { token, clean } = readInvitationToken(window.location.href)\n\n this.token = token\n\n /*\n * Taken out of the address bar and out of this history entry. `replaceState` rather than\n * `pushState`: going back should not put the token back on screen.\n */\n if (clean && clean !== window.location.href) {\n window.history.replaceState(null, '', clean)\n }\n }\n\n render() {\n const { icon, title, description } = this.applicationMeta\n const { disableUserFavoredLanguage, languages } = this.data || {}\n\n return html`\n <div class=\"wrap\">\n <div class=\"auth-brand\">\n <img src=${icon} />\n <strong class=\"name\">${title}</strong>\n <span class=\"welcome-msg\">${description}</span>\n </div>\n\n <div class=\"auth-form\">\n <h3><ox-i18n msgid=\"title.accept invitation\"></ox-i18n></h3>\n\n ${this.joined ? this.renderDone() : this.renderForm(disableUserFavoredLanguage, languages)}\n </div>\n </div>\n\n <ox-snack-bar id=\"snackbar\" level=\"error\" .message=${this.message}></ox-snack-bar>\n `\n }\n\n private renderDone() {\n return html`\n <p class=\"note\">${i18next.t('text.you have joined. sign in to continue')}</p>\n <md-elevated-button @click=${() => (window.location.href = '/auth/signin')}>\n <ox-i18n msgid=\"button.sign in\"></ox-i18n>\n </md-elevated-button>\n `\n }\n\n private renderForm(disableUserFavoredLanguage: boolean, languages: any) {\n return html`\n <form id=\"form\" @keypress=${(e: KeyboardEvent) => e.key === 'Enter' && this.accept()}>\n <p class=\"note\">${i18next.t('text.confirm the address this invitation was sent to')}</p>\n\n <div class=\"field\">\n <md-filled-text-field\n name=\"email\"\n type=\"email\"\n label=${String(i18next.t('field.email'))}\n autocomplete=\"email\"\n required\n ><md-icon slot=\"leading-icon\">mail</md-icon></md-filled-text-field\n >\n </div>\n\n ${this.token\n ? nothing\n : html`\n <div class=\"field\">\n <md-filled-text-field\n name=\"token\"\n label=${String(i18next.t('label.invitation code'))}\n supporting-text=${String(i18next.t('text.the code is printed in the invitation email'))}\n autocomplete=\"off\"\n required\n ><md-icon slot=\"leading-icon\">key</md-icon></md-filled-text-field\n >\n </div>\n `}\n ${this.needsPassword ? this.renderNewAccount() : nothing}\n\n <md-elevated-button id=\"submit-button\" type=\"button\" ?disabled=${this.busy} @click=${() => this.accept()}>\n <ox-i18n msgid=\"button.accept invitation\"></ox-i18n>\n </md-elevated-button>\n\n ${!disableUserFavoredLanguage\n ? html`<div id=\"locale-area\">\n <label for=\"locale-selector\"><md-icon>language</md-icon></label>\n <ox-i18n-selector\n id=\"locale-selector\"\n value=${i18next.language || 'en-US'}\n .languages=${languages}\n @change=${(e: CustomEvent) => e.detail && i18next.changeLanguage(e.detail as any)}\n ></ox-i18n-selector>\n </div>`\n : nothing}\n </form>\n `\n }\n\n private renderNewAccount() {\n return html`\n <p class=\"note\">${i18next.t('text.choose a password to finish joining')}</p>\n\n <div class=\"field\">\n <md-filled-text-field name=\"name\" label=${String(i18next.t('field.name'))} autocomplete=\"name\"\n ><md-icon slot=\"leading-icon\">person</md-icon></md-filled-text-field\n >\n </div>\n\n <div class=\"field\">\n <md-filled-text-field\n name=\"password\"\n type=\"password\"\n label=${String(i18next.t('label.password'))}\n autocomplete=\"new-password\"\n required\n ><md-icon slot=\"leading-icon\">password</md-icon></md-filled-text-field\n >\n </div>\n `\n }\n\n /*\n * ⚠ Not called `valueOf`. Overriding `Object.prototype.valueOf` with a different signature\n * makes the class type unassignable to `Object`, and every `@property` decorator on it then\n * fails to resolve — with an error that talks about decorators and never mentions the method.\n */\n private fieldValue(name: string): string {\n return (this.renderRoot.querySelector(`[name=\"${name}\"]`) as any)?.value?.trim() || ''\n }\n\n /*\n * The base class calls this to commit the page. It does not submit the form element: the\n * answer here is read rather than navigated to, because a refusal can mean \"now ask for a\n * password\" instead of \"stop\".\n */\n async submit() {\n await this.accept()\n }\n\n private async accept() {\n if (this.busy) {\n return\n }\n\n this.busy = true\n this.message = ''\n\n try {\n const response = await fetch(this.actionUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n token: this.token || this.fieldValue('token'),\n email: this.fieldValue('email'),\n name: this.fieldValue('name'),\n password: this.fieldValue('password')\n })\n })\n\n if (response.ok) {\n this.joined = true\n return\n }\n\n const refusal = await response.json().catch(() => ({}))\n\n /*\n * The one refusal that is not the end of the attempt — it means this person has no account\n * yet, which is exactly who this page is for.\n */\n if (refusal?.refusal?.code === 'invitation-needs-a-password' || refusal?.code === 'invitation-needs-a-password') {\n this.needsPassword = true\n this.message = String(i18next.t('text.choose a password to finish joining'))\n return\n }\n\n this.message = refusal?.message || String(i18next.t('text.this invitation cannot be used'))\n } catch (e) {\n this.message = String(i18next.t('text.this invitation cannot be used'))\n } finally {\n this.busy = false\n }\n }\n\n updated(changed: any) {\n super.updated(changed)\n\n if (changed.has('message')) {\n if (!this.message) {\n this.hideSnackbar()\n } else {\n this.showSnackbar({ timer: -1 })\n }\n }\n }\n}\n"]}
1
+ {"version":3,"file":"accept-invitation.js","sourceRoot":"","sources":["../../client/components/accept-invitation.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'\n\nimport '@operato/i18n/ox-i18n.js'\nimport '@operato/i18n/ox-i18n-selector.js'\nimport '@operato/layout/ox-snack-bar.js'\n\nimport { css, html, nothing } from 'lit'\nimport { property } from 'lit/decorators.js'\n\nimport { i18next } from '@operato/i18n'\n\nimport { AUTH_STYLE_SIGN } from '../auth-style-sign.js'\nimport { EntryControlSizeStyles } from '../entry-control-size-styles.js'\nimport { AbstractAuthPage } from './abstract-auth-page.js'\nimport { readLinkToken } from '../utils/link-token.js'\n\n/**\n * Accepting an invitation.\n *\n * ── Why the token is not in the address ─────────────────────────────────────\n * The link is `…/accept-invitation#<token>`, and a browser does not send what follows the `#`.\n * So the request that loads this page carries nothing, and the token stays out of server access\n * logs, proxy logs and `Referer`. This page reads it from the fragment and then clears it from\n * the address bar, so it does not sit in the browser's history either.\n *\n * The one place it can still travel is the mail being forwarded, and nothing about where the\n * token sits can prevent that. So the address it was sent to is asked for as well: holding the\n * link is not enough on its own.\n *\n * ── Why the password fields only appear after a try ─────────────────────────\n * Whether an address already has an account is not this page's to announce. It sends what it\n * has, and the server answers `invitation-needs-a-password` when it needs one — so the extra\n * fields appear for the person who is actually creating an account, and nobody learns anything\n * by loading the page.\n */\nexport class AcceptInvitation extends AbstractAuthPage {\n static styles = [\n EntryControlSizeStyles,\n css`\n :host {\n position: relative;\n overflow: hidden;\n\n display: flex;\n flex-direction: row;\n\n width: 100vw;\n height: 100vh;\n height: 100dvh;\n }\n\n .note {\n margin: 0 0 var(--spacing-medium) 0;\n font-size: 0.85rem;\n line-height: 1.5;\n color: var(--md-sys-color-on-surface-variant);\n }\n `,\n AUTH_STYLE_SIGN\n ]\n\n get pageName() {\n return 'accept invitation'\n }\n\n /*\n * ⚠ `/auth/…` — the server router carries that prefix. This read `/accept-invitation` until\n * 2026-09-24, which is no route; see the note beside `ACCEPT_INVITATION_PATH` in auth-base.\n */\n get actionUrl() {\n return '/auth/accept-invitation'\n }\n\n get title() {\n return 'accept invitation'\n }\n\n /** Read from the fragment on connect. Empty when the link was opened without one. */\n @property({ type: String }) token?: string\n\n /** The server asked for a password, so this person is creating an account. */\n @property({ type: Boolean }) needsPassword?: boolean\n\n /** Joined. The page stops being a form and says where to go. */\n @property({ type: Boolean }) joined?: boolean\n\n @property({ type: Boolean }) busy?: boolean\n\n connectedCallback() {\n super.connectedCallback()\n\n const { token, clean } = readLinkToken(window.location.href)\n\n this.token = token\n\n /*\n * Taken out of the address bar and out of this history entry. `replaceState` rather than\n * `pushState`: going back should not put the token back on screen.\n */\n if (clean && clean !== window.location.href) {\n window.history.replaceState(null, '', clean)\n }\n }\n\n render() {\n const { icon, title, description } = this.applicationMeta\n const { disableUserFavoredLanguage, languages } = this.data || {}\n\n return html`\n <div class=\"wrap\">\n <div class=\"auth-brand\">\n <img src=${icon} />\n <strong class=\"name\">${title}</strong>\n <span class=\"welcome-msg\">${description}</span>\n </div>\n\n <div class=\"auth-form\">\n <h3><ox-i18n msgid=\"title.accept invitation\"></ox-i18n></h3>\n\n ${this.joined ? this.renderDone() : this.renderForm(disableUserFavoredLanguage, languages)}\n </div>\n </div>\n\n <ox-snack-bar id=\"snackbar\" level=\"error\" .message=${this.message}></ox-snack-bar>\n `\n }\n\n private renderDone() {\n return html`\n <p class=\"note\">${i18next.t('text.you have joined. sign in to continue')}</p>\n <md-elevated-button @click=${() => (window.location.href = '/auth/signin')}>\n <ox-i18n msgid=\"button.sign in\"></ox-i18n>\n </md-elevated-button>\n `\n }\n\n private renderForm(disableUserFavoredLanguage: boolean, languages: any) {\n return html`\n <form id=\"form\" @keypress=${(e: KeyboardEvent) => e.key === 'Enter' && this.accept()}>\n <p class=\"note\">${i18next.t('text.confirm the address this invitation was sent to')}</p>\n\n <div class=\"field\">\n <md-filled-text-field\n name=\"email\"\n type=\"email\"\n label=${String(i18next.t('field.email'))}\n autocomplete=\"email\"\n required\n ><md-icon slot=\"leading-icon\">mail</md-icon></md-filled-text-field\n >\n </div>\n\n ${this.token\n ? nothing\n : html`\n <div class=\"field\">\n <md-filled-text-field\n name=\"token\"\n label=${String(i18next.t('label.invitation code'))}\n supporting-text=${String(i18next.t('text.the code is printed in the invitation email'))}\n autocomplete=\"off\"\n required\n ><md-icon slot=\"leading-icon\">key</md-icon></md-filled-text-field\n >\n </div>\n `}\n ${this.needsPassword ? this.renderNewAccount() : nothing}\n\n <md-elevated-button id=\"submit-button\" type=\"button\" ?disabled=${this.busy} @click=${() => this.accept()}>\n <ox-i18n msgid=\"button.accept invitation\"></ox-i18n>\n </md-elevated-button>\n\n ${!disableUserFavoredLanguage\n ? html`<div id=\"locale-area\">\n <label for=\"locale-selector\"><md-icon>language</md-icon></label>\n <ox-i18n-selector\n id=\"locale-selector\"\n value=${i18next.language || 'en-US'}\n .languages=${languages}\n @change=${(e: CustomEvent) => e.detail && i18next.changeLanguage(e.detail as any)}\n ></ox-i18n-selector>\n </div>`\n : nothing}\n </form>\n `\n }\n\n private renderNewAccount() {\n return html`\n <p class=\"note\">${i18next.t('text.choose a password to finish joining')}</p>\n\n <div class=\"field\">\n <md-filled-text-field name=\"name\" label=${String(i18next.t('field.name'))} autocomplete=\"name\"\n ><md-icon slot=\"leading-icon\">person</md-icon></md-filled-text-field\n >\n </div>\n\n <div class=\"field\">\n <md-filled-text-field\n name=\"password\"\n type=\"password\"\n label=${String(i18next.t('label.password'))}\n autocomplete=\"new-password\"\n required\n ><md-icon slot=\"leading-icon\">password</md-icon></md-filled-text-field\n >\n </div>\n `\n }\n\n /*\n * ⚠ Not called `valueOf`. Overriding `Object.prototype.valueOf` with a different signature\n * makes the class type unassignable to `Object`, and every `@property` decorator on it then\n * fails to resolve — with an error that talks about decorators and never mentions the method.\n */\n private fieldValue(name: string): string {\n return (this.renderRoot.querySelector(`[name=\"${name}\"]`) as any)?.value?.trim() || ''\n }\n\n /*\n * The base class calls this to commit the page. It does not submit the form element: the\n * answer here is read rather than navigated to, because a refusal can mean \"now ask for a\n * password\" instead of \"stop\".\n */\n async submit() {\n await this.accept()\n }\n\n private async accept() {\n if (this.busy) {\n return\n }\n\n this.busy = true\n this.message = ''\n\n try {\n const response = await fetch(this.actionUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n token: this.token || this.fieldValue('token'),\n email: this.fieldValue('email'),\n name: this.fieldValue('name'),\n password: this.fieldValue('password')\n })\n })\n\n if (response.ok) {\n this.joined = true\n return\n }\n\n const refusal = await response.json().catch(() => ({}))\n\n /*\n * The one refusal that is not the end of the attempt — it means this person has no account\n * yet, which is exactly who this page is for.\n */\n if (refusal?.refusal?.code === 'invitation-needs-a-password' || refusal?.code === 'invitation-needs-a-password') {\n this.needsPassword = true\n this.message = String(i18next.t('text.choose a password to finish joining'))\n return\n }\n\n this.message = refusal?.message || String(i18next.t('text.this invitation cannot be used'))\n } catch (e) {\n this.message = String(i18next.t('text.this invitation cannot be used'))\n } finally {\n this.busy = false\n }\n }\n\n updated(changed: any) {\n super.updated(changed)\n\n if (changed.has('message')) {\n if (!this.message) {\n this.hideSnackbar()\n } else {\n this.showSnackbar({ timer: -1 })\n }\n }\n }\n}\n"]}
@@ -0,0 +1,20 @@
1
+ import { LitElement } from 'lit';
2
+ /**
3
+ * A summary badge that carries its own look - for grid cells.
4
+ *
5
+ * The role and user lists drew their badges as `<span class="badge">` and styled `.badge` in the
6
+ * page. A grid cell renders inside the grid field's own shadow root (`ox-grid-field` ·
7
+ * `ox-list-field`), where page CSS does not reach, so every badge came out as plain text
8
+ * [확인함 2026-09-24 :3500 role list: background transparent, radius 0, padding 0]. The two that
9
+ * mattered most were the ones that said something waits on the reader - "grants nothing" and a
10
+ * locked account - and they looked like any other word in the row.
11
+ *
12
+ * Colour only where something waits on the reader (`attention`); the default state is quiet
13
+ * (`quiet`). Two lists in one package use it, so it lives here.
14
+ */
15
+ export declare class AuthBadge extends LitElement {
16
+ static styles: import("lit").CSSResult;
17
+ /** `attention` when something waits on the reader, `quiet` for the default state, none otherwise. */
18
+ tone?: 'attention' | 'quiet';
19
+ render(): import("lit-html").TemplateResult<1>;
20
+ }
@@ -0,0 +1,50 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import { LitElement, css, html } from 'lit';
3
+ import { customElement, property } from 'lit/decorators.js';
4
+ /**
5
+ * A summary badge that carries its own look - for grid cells.
6
+ *
7
+ * The role and user lists drew their badges as `<span class="badge">` and styled `.badge` in the
8
+ * page. A grid cell renders inside the grid field's own shadow root (`ox-grid-field` ·
9
+ * `ox-list-field`), where page CSS does not reach, so every badge came out as plain text
10
+ * [확인함 2026-09-24 :3500 role list: background transparent, radius 0, padding 0]. The two that
11
+ * mattered most were the ones that said something waits on the reader - "grants nothing" and a
12
+ * locked account - and they looked like any other word in the row.
13
+ *
14
+ * Colour only where something waits on the reader (`attention`); the default state is quiet
15
+ * (`quiet`). Two lists in one package use it, so it lives here.
16
+ */
17
+ let AuthBadge = class AuthBadge extends LitElement {
18
+ static { this.styles = css `
19
+ :host {
20
+ display: inline-block;
21
+ padding: 2px 8px;
22
+ border-radius: var(--md-sys-shape-corner-small, 6px);
23
+ font-size: 0.78rem;
24
+ line-height: 1.4;
25
+ white-space: nowrap;
26
+ background-color: var(--md-sys-color-surface-variant);
27
+ color: var(--md-sys-color-on-surface-variant);
28
+ }
29
+ :host([tone='attention']) {
30
+ background-color: var(--md-sys-color-error-container);
31
+ color: var(--md-sys-color-on-error-container);
32
+ }
33
+ :host([tone='quiet']) {
34
+ background-color: transparent;
35
+ color: var(--md-sys-color-outline);
36
+ }
37
+ `; }
38
+ render() {
39
+ return html `<slot></slot>`;
40
+ }
41
+ };
42
+ __decorate([
43
+ property({ reflect: true }),
44
+ __metadata("design:type", String)
45
+ ], AuthBadge.prototype, "tone", void 0);
46
+ AuthBadge = __decorate([
47
+ customElement('auth-badge')
48
+ ], AuthBadge);
49
+ export { AuthBadge };
50
+ //# sourceMappingURL=auth-badge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-badge.js","sourceRoot":"","sources":["../../client/components/auth-badge.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAE3D;;;;;;;;;;;;GAYG;AAEI,IAAM,SAAS,GAAf,MAAM,SAAU,SAAQ,UAAU;aAChC,WAAM,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;GAmBlB,AAnBY,CAmBZ;IAKD,MAAM;QACJ,OAAO,IAAI,CAAA,eAAe,CAAA;IAC5B,CAAC;;AAJ4B;IAA5B,QAAQ,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;;uCAA6B;AAvB9C,SAAS;IADrB,aAAa,CAAC,YAAY,CAAC;GACf,SAAS,CA4BrB","sourcesContent":["import { LitElement, css, html } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\n/**\n * A summary badge that carries its own look - for grid cells.\n *\n * The role and user lists drew their badges as `<span class=\"badge\">` and styled `.badge` in the\n * page. A grid cell renders inside the grid field's own shadow root (`ox-grid-field` ·\n * `ox-list-field`), where page CSS does not reach, so every badge came out as plain text\n * [확인함 2026-09-24 :3500 role list: background transparent, radius 0, padding 0]. The two that\n * mattered most were the ones that said something waits on the reader - \"grants nothing\" and a\n * locked account - and they looked like any other word in the row.\n *\n * Colour only where something waits on the reader (`attention`); the default state is quiet\n * (`quiet`). Two lists in one package use it, so it lives here.\n */\n@customElement('auth-badge')\nexport class AuthBadge extends LitElement {\n static styles = css`\n :host {\n display: inline-block;\n padding: 2px 8px;\n border-radius: var(--md-sys-shape-corner-small, 6px);\n font-size: 0.78rem;\n line-height: 1.4;\n white-space: nowrap;\n background-color: var(--md-sys-color-surface-variant);\n color: var(--md-sys-color-on-surface-variant);\n }\n :host([tone='attention']) {\n background-color: var(--md-sys-color-error-container);\n color: var(--md-sys-color-on-error-container);\n }\n :host([tone='quiet']) {\n background-color: transparent;\n color: var(--md-sys-color-outline);\n }\n `\n\n /** `attention` when something waits on the reader, `quiet` for the default state, none otherwise. */\n @property({ reflect: true }) tone?: 'attention' | 'quiet'\n\n render() {\n return html`<slot></slot>`\n }\n}\n"]}
@@ -0,0 +1,37 @@
1
+ import { readdirSync, readFileSync, statSync } from 'fs';
2
+ import path from 'path';
3
+ /**
4
+ * Badges in grid cells carry their own look.
5
+ *
6
+ * A grid cell renders inside the grid field's shadow root, where page CSS does not reach. The role
7
+ * and user lists drew `<span class="badge">` and styled `.badge` in the page, so every badge came out
8
+ * as plain text - "grants nothing" and a locked account looked like any other word
9
+ * [확인함 2026-09-24 :3500]. `<auth-badge>` styles itself.
10
+ */
11
+ const CLIENT = path.resolve(__dirname, '..');
12
+ function sources(dir, found = []) {
13
+ for (const entry of readdirSync(dir)) {
14
+ const full = path.join(dir, entry);
15
+ if (statSync(full).isDirectory())
16
+ sources(full, found);
17
+ else if (full.endsWith('.ts') && !full.endsWith('.test.ts'))
18
+ found.push(full);
19
+ }
20
+ return found;
21
+ }
22
+ /** Code only - a comment that quotes the old markup is not a use of it. */
23
+ const withoutComments = (text) => text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
24
+ describe('badges in auth-ui', () => {
25
+ it('★ no screen draws a page-styled badge span that a grid cell would leave unstyled', () => {
26
+ const offenders = sources(CLIENT)
27
+ .filter(file => /class="badge"/.test(withoutComments(readFileSync(file, 'utf-8'))))
28
+ .map(file => path.relative(CLIENT, file));
29
+ expect(offenders).toEqual([]);
30
+ });
31
+ it.each(['pages/role/role-list-page.ts', 'pages/user/user-list-page.ts'])('★ %s loads the badge it draws', file => {
32
+ const source = readFileSync(path.join(CLIENT, file), 'utf-8');
33
+ expect(source).toMatch(/<auth-badge/);
34
+ expect(source).toContain("import '../../components/auth-badge.js'");
35
+ });
36
+ });
37
+ //# sourceMappingURL=auth-badge.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-badge.test.js","sourceRoot":"","sources":["../../client/components/auth-badge.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAA;AACxD,OAAO,IAAI,MAAM,MAAM,CAAA;AAEvB;;;;;;;GAOG;AAEH,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,CAAA;AAE5C,SAAS,OAAO,CAAC,GAAW,EAAE,QAAkB,EAAE;IAChD,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC;QACrC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;QAClC,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE;YAAE,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;aACjD,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC/E,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,2EAA2E;AAC3E,MAAM,eAAe,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAA;AAE5G,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,EAAE,CAAC,kFAAkF,EAAE,GAAG,EAAE;QAC1F,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC;aAC9B,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,eAAe,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;aAClF,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAA;QAC3C,MAAM,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IAC/B,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,IAAI,CAAC,CAAC,8BAA8B,EAAE,8BAA8B,CAAC,CAAC,CAAC,+BAA+B,EAAE,IAAI,CAAC,EAAE;QAChH,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,CAAA;QAC7D,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,CAAA;QACrC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,yCAAyC,CAAC,CAAA;IACrE,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import { readdirSync, readFileSync, statSync } from 'fs'\nimport path from 'path'\n\n/**\n * Badges in grid cells carry their own look.\n *\n * A grid cell renders inside the grid field's shadow root, where page CSS does not reach. The role\n * and user lists drew `<span class=\"badge\">` and styled `.badge` in the page, so every badge came out\n * as plain text - \"grants nothing\" and a locked account looked like any other word\n * [확인함 2026-09-24 :3500]. `<auth-badge>` styles itself.\n */\n\nconst CLIENT = path.resolve(__dirname, '..')\n\nfunction sources(dir: string, found: string[] = []): string[] {\n for (const entry of readdirSync(dir)) {\n const full = path.join(dir, entry)\n if (statSync(full).isDirectory()) sources(full, found)\n else if (full.endsWith('.ts') && !full.endsWith('.test.ts')) found.push(full)\n }\n return found\n}\n\n/** Code only - a comment that quotes the old markup is not a use of it. */\nconst withoutComments = (text: string) => text.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '').replace(/^\\s*\\/\\/.*$/gm, '')\n\ndescribe('badges in auth-ui', () => {\n it('★ no screen draws a page-styled badge span that a grid cell would leave unstyled', () => {\n const offenders = sources(CLIENT)\n .filter(file => /class=\"badge\"/.test(withoutComments(readFileSync(file, 'utf-8'))))\n .map(file => path.relative(CLIENT, file))\n expect(offenders).toEqual([])\n })\n\n it.each(['pages/role/role-list-page.ts', 'pages/user/user-list-page.ts'])('★ %s loads the badge it draws', file => {\n const source = readFileSync(path.join(CLIENT, file), 'utf-8')\n expect(source).toMatch(/<auth-badge/)\n expect(source).toContain(\"import '../../components/auth-badge.js'\")\n })\n})\n"]}
@@ -0,0 +1,16 @@
1
+ import { readFileSync } from 'fs';
2
+ import path from 'path';
3
+ /**
4
+ * The create-domain popup marks an invalid name in the theme's error colours. It used a fixed
5
+ * #fce6e6 behind text that follows the theme, which in the dark theme was light text on pink.
6
+ */
7
+ const source = readFileSync(path.join(__dirname, 'create-domain-popup.ts'), 'utf-8');
8
+ describe('the invalid-name mark in create-domain-popup', () => {
9
+ it('★ uses the error container pair, not a fixed colour', () => {
10
+ const rule = source.slice(source.indexOf('input.checkValidName {'), source.indexOf('}', source.indexOf('input.checkValidName {')));
11
+ expect(rule).toMatch(/background-color:\s*var\(--md-sys-color-error-container\)/);
12
+ expect(rule).toMatch(/color:\s*var\(--md-sys-color-on-error-container\)/);
13
+ expect(rule).not.toMatch(/#[0-9a-f]{3,6}/i);
14
+ });
15
+ });
16
+ //# sourceMappingURL=create-domain-popup-theme.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-domain-popup-theme.test.js","sourceRoot":"","sources":["../../client/components/create-domain-popup-theme.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAA;AACjC,OAAO,IAAI,MAAM,MAAM,CAAA;AAEvB;;;GAGG;AACH,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,wBAAwB,CAAC,EAAE,OAAO,CAAC,CAAA;AAEpF,QAAQ,CAAC,8CAA8C,EAAE,GAAG,EAAE;IAC5D,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,wBAAwB,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,OAAO,CAAC,wBAAwB,CAAC,CAAC,CAAC,CAAA;QAClI,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,2DAA2D,CAAC,CAAA;QACjF,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,mDAAmD,CAAC,CAAA;QACzE,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAA;IAC7C,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import { readFileSync } from 'fs'\nimport path from 'path'\n\n/**\n * The create-domain popup marks an invalid name in the theme's error colours. It used a fixed\n * #fce6e6 behind text that follows the theme, which in the dark theme was light text on pink.\n */\nconst source = readFileSync(path.join(__dirname, 'create-domain-popup.ts'), 'utf-8')\n\ndescribe('the invalid-name mark in create-domain-popup', () => {\n it('★ uses the error container pair, not a fixed colour', () => {\n const rule = source.slice(source.indexOf('input.checkValidName {'), source.indexOf('}', source.indexOf('input.checkValidName {')))\n expect(rule).toMatch(/background-color:\\s*var\\(--md-sys-color-error-container\\)/)\n expect(rule).toMatch(/color:\\s*var\\(--md-sys-color-on-error-container\\)/)\n expect(rule).not.toMatch(/#[0-9a-f]{3,6}/i)\n })\n})\n"]}
@@ -24,8 +24,10 @@ let CreateDomainPopup = class CreateDomainPopup extends localize(i18next)(LitEle
24
24
  padding: var(--spacing-large);
25
25
  }
26
26
 
27
+ /* A name that is not valid - the theme's error pair, so the text stays readable in the dark theme. */
27
28
  input.checkValidName {
28
- background-color: #fce6e6;
29
+ background-color: var(--md-sys-color-error-container);
30
+ color: var(--md-sys-color-on-error-container);
29
31
  }
30
32
 
31
33
  label {
@@ -1 +1 @@
1
- {"version":3,"file":"create-domain-popup.js","sourceRoot":"","sources":["../../client/components/create-domain-popup.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, LitElement } from 'lit'\nimport { customElement, query } from 'lit/decorators.js'\n\nimport { client } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { OxPrompt } from '@operato/popup/ox-prompt.js'\nimport { CommonHeaderStyles, ScrollbarStyles } from '@operato/styles'\n\n@customElement('create-domain-popup')\nclass CreateDomainPopup extends localize(i18next)(LitElement) {\n static styles = [\n ScrollbarStyles,\n CommonHeaderStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n background-color: var(--md-sys-color-background);\n overflow: auto;\n }\n\n form {\n flex: 1;\n padding: var(--spacing-large);\n }\n\n input.checkValidName {\n background-color: #fce6e6;\n }\n\n label {\n display: flex;\n flex-direction: column;\n\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 border: var(--border-dim-color);\n border-radius: var(--border-radius);\n margin: var(--input-margin);\n padding: var(--input-padding);\n background-color: var(--md-sys-color-surface);\n font: var(--input-font);\n }\n\n [field] {\n grid-column: span 2;\n }\n `\n ]\n\n @query('input[name=\"name\"]') nameInput!: HTMLInputElement\n\n render() {\n return html`\n <form>\n <div field grid-span>\n <label\n >${i18next.t('label.x name', { x: i18next.t('label.domain') })}<input\n type=\"text\"\n name=\"name\"\n @input=${this.checkValidation}\n autofocus\n /></label>\n </div>\n\n <div field grid-span>\n <label>${i18next.t('label.description')}<input type=\"text\" name=\"description\" /></label>\n </div>\n </form>\n\n <div class=\"footer\">\n <div filler></div>\n <button @click=${e => this.onCreateDomain()} done><md-icon>add</md-icon>${i18next.t('button.create')}</button>\n </div>\n `\n }\n\n firstUpdated() {\n this.nameInput.focus() // autofocus\n }\n\n get inputData() {\n return this.renderRoot.querySelectorAll('input')\n }\n\n checkValidation(e) {\n const currentInput = e.currentTarget\n const regExp = /^[a-zA-Z ]+$/\n\n if (!regExp.test(currentInput.value)) {\n currentInput.classList.add('checkValidName')\n } else {\n currentInput.classList.remove('checkValidName')\n }\n }\n\n async onCreateDomain() {\n const domainInput: { [prop: string]: string } = {}\n this.inputData.forEach(data => (domainInput[data.name] = data.value))\n const regExp = /^[a-zA-z0-9- ]+$/\n\n if (!regExp.test(domainInput.name)) {\n return this.showToast(i18next.t('error: domain name should consist only of letters or dashes'))\n }\n\n const response = await client.mutate({\n mutation: gql`\n mutation domainRegister($domainInput: DomainGeneratorInput!) {\n domainRegister(domainInput: $domainInput) {\n id\n name\n }\n }\n `,\n variables: { domainInput }\n })\n\n if (!response.errors) {\n await OxPrompt.open({\n type: 'success',\n title: i18next.t('text.completed'),\n text: i18next.t('text.x_created_successfully', { x: i18next.t('label.domain') }),\n confirmButton: { text: i18next.t('button.confirm') }\n })\n\n history.back()\n\n this.dispatchEvent(new CustomEvent('fetch-data'))\n }\n }\n\n showToast(message) {\n document.dispatchEvent(new CustomEvent('notify', { detail: { message } }))\n }\n}\n"]}
1
+ {"version":3,"file":"create-domain-popup.js","sourceRoot":"","sources":["../../client/components/create-domain-popup.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, LitElement } from 'lit'\nimport { customElement, query } from 'lit/decorators.js'\n\nimport { client } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { OxPrompt } from '@operato/popup/ox-prompt.js'\nimport { CommonHeaderStyles, ScrollbarStyles } from '@operato/styles'\n\n@customElement('create-domain-popup')\nclass CreateDomainPopup extends localize(i18next)(LitElement) {\n static styles = [\n ScrollbarStyles,\n CommonHeaderStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n background-color: var(--md-sys-color-background);\n overflow: auto;\n }\n\n form {\n flex: 1;\n padding: var(--spacing-large);\n }\n\n /* A name that is not valid - the theme's error pair, so the text stays readable in the dark theme. */\n input.checkValidName {\n background-color: var(--md-sys-color-error-container);\n color: var(--md-sys-color-on-error-container);\n }\n\n label {\n display: flex;\n flex-direction: column;\n\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 border: var(--border-dim-color);\n border-radius: var(--border-radius);\n margin: var(--input-margin);\n padding: var(--input-padding);\n background-color: var(--md-sys-color-surface);\n font: var(--input-font);\n }\n\n [field] {\n grid-column: span 2;\n }\n `\n ]\n\n @query('input[name=\"name\"]') nameInput!: HTMLInputElement\n\n render() {\n return html`\n <form>\n <div field grid-span>\n <label\n >${i18next.t('label.x name', { x: i18next.t('label.domain') })}<input\n type=\"text\"\n name=\"name\"\n @input=${this.checkValidation}\n autofocus\n /></label>\n </div>\n\n <div field grid-span>\n <label>${i18next.t('label.description')}<input type=\"text\" name=\"description\" /></label>\n </div>\n </form>\n\n <div class=\"footer\">\n <div filler></div>\n <button @click=${e => this.onCreateDomain()} done><md-icon>add</md-icon>${i18next.t('button.create')}</button>\n </div>\n `\n }\n\n firstUpdated() {\n this.nameInput.focus() // autofocus\n }\n\n get inputData() {\n return this.renderRoot.querySelectorAll('input')\n }\n\n checkValidation(e) {\n const currentInput = e.currentTarget\n const regExp = /^[a-zA-Z ]+$/\n\n if (!regExp.test(currentInput.value)) {\n currentInput.classList.add('checkValidName')\n } else {\n currentInput.classList.remove('checkValidName')\n }\n }\n\n async onCreateDomain() {\n const domainInput: { [prop: string]: string } = {}\n this.inputData.forEach(data => (domainInput[data.name] = data.value))\n const regExp = /^[a-zA-z0-9- ]+$/\n\n if (!regExp.test(domainInput.name)) {\n return this.showToast(i18next.t('error: domain name should consist only of letters or dashes'))\n }\n\n const response = await client.mutate({\n mutation: gql`\n mutation domainRegister($domainInput: DomainGeneratorInput!) {\n domainRegister(domainInput: $domainInput) {\n id\n name\n }\n }\n `,\n variables: { domainInput }\n })\n\n if (!response.errors) {\n await OxPrompt.open({\n type: 'success',\n title: i18next.t('text.completed'),\n text: i18next.t('text.x_created_successfully', { x: i18next.t('label.domain') }),\n confirmButton: { text: i18next.t('button.confirm') }\n })\n\n history.back()\n\n this.dispatchEvent(new CustomEvent('fetch-data'))\n }\n }\n\n showToast(message) {\n document.dispatchEvent(new CustomEvent('notify', { detail: { message } }))\n }\n}\n"]}
@@ -0,0 +1,25 @@
1
+ import '@material/web/button/elevated-button.js';
2
+ import '@material/web/textfield/filled-text-field.js';
3
+ import '@operato/i18n/ox-i18n.js';
4
+ import '../../components/profile-component.js';
5
+ import { AbstractAuthPage } from '../../components/abstract-auth-page.js';
6
+ /**
7
+ * The page the sign-up confirmation mail links to — `/auth/verify#<token>`.
8
+ *
9
+ * Opening the link used to confirm the address (`GET /auth/verify/<token>`), so a mail scanner that
10
+ * fetched it confirmed it too, and the token sat in every access log it passed. Now the token rides
11
+ * after `#`, this page takes it out of the address bar, and the confirmation is the person's press
12
+ * of the button — a POST.
13
+ *
14
+ * A link whose fragment is missing or mangled shows a box for the code instead of a dead button.
15
+ */
16
+ export declare class AuthVerify extends AbstractAuthPage {
17
+ token?: string;
18
+ get pageName(): string;
19
+ get actionUrl(): string;
20
+ connectedCallback(): void;
21
+ firstUpdated(): void;
22
+ get formfields(): import("lit-html").TemplateResult<1>;
23
+ get links(): import("lit-html").TemplateResult<1>;
24
+ submit(): Promise<void>;
25
+ }
@@ -0,0 +1,81 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import '@material/web/button/elevated-button.js';
3
+ import '@material/web/textfield/filled-text-field.js';
4
+ import '@operato/i18n/ox-i18n.js';
5
+ import '../../components/profile-component.js';
6
+ import { html } from 'lit';
7
+ import { customElement, property } from 'lit/decorators.js';
8
+ import { i18next } from '@operato/i18n';
9
+ import { AbstractAuthPage } from '../../components/abstract-auth-page.js';
10
+ import { readLinkToken } from '../../utils/link-token.js';
11
+ /**
12
+ * The page the sign-up confirmation mail links to — `/auth/verify#<token>`.
13
+ *
14
+ * Opening the link used to confirm the address (`GET /auth/verify/<token>`), so a mail scanner that
15
+ * fetched it confirmed it too, and the token sat in every access log it passed. Now the token rides
16
+ * after `#`, this page takes it out of the address bar, and the confirmation is the person's press
17
+ * of the button — a POST.
18
+ *
19
+ * A link whose fragment is missing or mangled shows a box for the code instead of a dead button.
20
+ */
21
+ let AuthVerify = class AuthVerify extends AbstractAuthPage {
22
+ get pageName() {
23
+ return 'activate account';
24
+ }
25
+ get actionUrl() {
26
+ return '/auth/verify';
27
+ }
28
+ connectedCallback() {
29
+ super.connectedCallback();
30
+ const { token, clean } = readLinkToken(window.location.href);
31
+ this.token = token;
32
+ /* `replaceState`, not `pushState`: going back should not put the token back on screen. */
33
+ if (clean && clean !== window.location.href) {
34
+ window.history.replaceState(null, '', clean);
35
+ }
36
+ }
37
+ firstUpdated() {
38
+ /* The base focuses the first text field, and with a token in hand there is none. */
39
+ if (!this.token) {
40
+ super.firstUpdated();
41
+ }
42
+ }
43
+ get formfields() {
44
+ return html `
45
+ ${this.token
46
+ ? html `<input name="token" type="hidden" .value=${this.token} />`
47
+ : html `
48
+ <div class="field">
49
+ <md-filled-text-field
50
+ name="token"
51
+ type="text"
52
+ label=${String(i18next.t('field.code'))}
53
+ autocomplete="off"
54
+ autocapitalize="off"
55
+ required
56
+ ><md-icon slot="leading-icon">key</md-icon></md-filled-text-field
57
+ >
58
+ </div>
59
+ `}
60
+
61
+ <md-elevated-button class="ui" type="button" @click=${e => this._onSubmit(e)}>
62
+ <ox-i18n msgid="button.activate"></ox-i18n>
63
+ </md-elevated-button>
64
+ `;
65
+ }
66
+ get links() {
67
+ return html ``;
68
+ }
69
+ async submit() {
70
+ this.formEl.submit();
71
+ }
72
+ };
73
+ __decorate([
74
+ property({ type: String }),
75
+ __metadata("design:type", String)
76
+ ], AuthVerify.prototype, "token", void 0);
77
+ AuthVerify = __decorate([
78
+ customElement('auth-verify')
79
+ ], AuthVerify);
80
+ export { AuthVerify };
81
+ //# sourceMappingURL=verify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../../../client/entries/auth/verify.ts"],"names":[],"mappings":";AAAA,OAAO,yCAAyC,CAAA;AAChD,OAAO,8CAA8C,CAAA;AAErD,OAAO,0BAA0B,CAAA;AACjC,OAAO,uCAAuC,CAAA;AAE9C,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,CAAA;AAC1B,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAE3D,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAEvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,wCAAwC,CAAA;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAEzD;;;;;;;;;GASG;AAEI,IAAM,UAAU,GAAhB,MAAM,UAAW,SAAQ,gBAAgB;IAG9C,IAAI,QAAQ;QACV,OAAO,kBAAkB,CAAA;IAC3B,CAAC;IAED,IAAI,SAAS;QACX,OAAO,cAAc,CAAA;IACvB,CAAC;IAED,iBAAiB;QACf,KAAK,CAAC,iBAAiB,EAAE,CAAA;QAEzB,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;QAE5D,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;QAElB,0FAA0F;QAC1F,IAAI,KAAK,IAAI,KAAK,KAAK,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;YAC5C,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,EAAE,EAAE,EAAE,KAAK,CAAC,CAAA;QAC9C,CAAC;IACH,CAAC;IAED,YAAY;QACV,oFAAoF;QACpF,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YAChB,KAAK,CAAC,YAAY,EAAE,CAAA;QACtB,CAAC;IACH,CAAC;IAED,IAAI,UAAU;QACZ,OAAO,IAAI,CAAA;QACP,IAAI,CAAC,KAAK;YACV,CAAC,CAAC,IAAI,CAAA,4CAA4C,IAAI,CAAC,KAAK,KAAK;YACjE,CAAC,CAAC,IAAI,CAAA;;;;;wBAKU,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;;;;;;;WAO5C;;4DAEiD,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;;;KAG7E,CAAA;IACH,CAAC;IAED,IAAI,KAAK;QACP,OAAO,IAAI,CAAA,EAAE,CAAA;IACf,CAAC;IAED,KAAK,CAAC,MAAM;QACV,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAA;IACtB,CAAC;CACF,CAAA;AA7D6B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;yCAAe;AAD/B,UAAU;IADtB,aAAa,CAAC,aAAa,CAAC;GAChB,UAAU,CA8DtB","sourcesContent":["import '@material/web/button/elevated-button.js'\nimport '@material/web/textfield/filled-text-field.js'\n\nimport '@operato/i18n/ox-i18n.js'\nimport '../../components/profile-component.js'\n\nimport { html } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { i18next } from '@operato/i18n'\n\nimport { AbstractAuthPage } from '../../components/abstract-auth-page.js'\nimport { readLinkToken } from '../../utils/link-token.js'\n\n/**\n * The page the sign-up confirmation mail links to — `/auth/verify#<token>`.\n *\n * Opening the link used to confirm the address (`GET /auth/verify/<token>`), so a mail scanner that\n * fetched it confirmed it too, and the token sat in every access log it passed. Now the token rides\n * after `#`, this page takes it out of the address bar, and the confirmation is the person's press\n * of the button — a POST.\n *\n * A link whose fragment is missing or mangled shows a box for the code instead of a dead button.\n */\n@customElement('auth-verify')\nexport class AuthVerify extends AbstractAuthPage {\n @property({ type: String }) token?: string\n\n get pageName() {\n return 'activate account'\n }\n\n get actionUrl() {\n return '/auth/verify'\n }\n\n connectedCallback() {\n super.connectedCallback()\n\n const { token, clean } = readLinkToken(window.location.href)\n\n this.token = token\n\n /* `replaceState`, not `pushState`: going back should not put the token back on screen. */\n if (clean && clean !== window.location.href) {\n window.history.replaceState(null, '', clean)\n }\n }\n\n firstUpdated() {\n /* The base focuses the first text field, and with a token in hand there is none. */\n if (!this.token) {\n super.firstUpdated()\n }\n }\n\n get formfields() {\n return html`\n ${this.token\n ? html`<input name=\"token\" type=\"hidden\" .value=${this.token} />`\n : html`\n <div class=\"field\">\n <md-filled-text-field\n name=\"token\"\n type=\"text\"\n label=${String(i18next.t('field.code'))}\n autocomplete=\"off\"\n autocapitalize=\"off\"\n required\n ><md-icon slot=\"leading-icon\">key</md-icon></md-filled-text-field\n >\n </div>\n `}\n\n <md-elevated-button class=\"ui\" type=\"button\" @click=${e => this._onSubmit(e)}>\n <ox-i18n msgid=\"button.activate\"></ox-i18n>\n </md-elevated-button>\n `\n }\n\n get links() {\n return html``\n }\n\n async submit() {\n this.formEl.submit()\n }\n}\n"]}
@@ -1,4 +1,6 @@
1
+ import '@material/web/button/text-button.js';
1
2
  import '@material/web/icon/icon.js';
3
+ import '../../components/auth-badge.js';
2
4
  import '@operato/data-grist';
3
5
  import '@operato/oops/ox-oops-note.js';
4
6
  import type { CSSResultGroup } from 'lit';
@@ -211,6 +213,12 @@ export declare class RoleListPage extends RoleListPageBase {
211
213
  /** 템플릿에서 — 이름은 이 공장이 정한 것을 그대로 보낸다. */
212
214
  createFromTemplate(templateId: string, name: string, separately?: boolean): any;
213
215
  private confirmStandingSeparately;
216
+ private templateGapCell;
217
+ /**
218
+ * 빠진 권한을 더한다 — **무엇을 더하는지 먼저 보인다.** 권한을 여는 일이라 그 목록을 읽고 누른다.
219
+ * 더하는 길은 템플릿 고르개와 같다(`createRoleFromTemplate` 가 같은 이름의 역할을 채운다).
220
+ */
221
+ private addFromTemplate;
214
222
  /** 백지에서 — 권한 없이 선다. 목록의 권한 수 칸이 0 을 빨갛게 말하므로 다음 걸음이 보인다. */
215
223
  createBlankRole(name: string, description: string): Promise<any>;
216
224
  /**