@things-factory/auth-ui 8.0.0-beta.9 → 8.0.0

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 (100) hide show
  1. package/client/auth-style-sign.ts +194 -0
  2. package/client/bootstrap.ts +51 -0
  3. package/client/components/abstract-auth-page.ts +301 -0
  4. package/client/components/abstract-password-reset.ts +168 -0
  5. package/client/components/abstract-sign.ts +127 -0
  6. package/client/components/change-password.ts +153 -0
  7. package/client/components/contact-us.ts +113 -0
  8. package/client/components/create-domain-popup.ts +141 -0
  9. package/client/components/create-role.ts +123 -0
  10. package/client/components/create-user.ts +95 -0
  11. package/client/components/credential-manager.ts +64 -0
  12. package/client/components/delete-user-popup.ts +117 -0
  13. package/client/components/domain-switch.ts +127 -0
  14. package/client/components/invite-customer.ts +104 -0
  15. package/client/components/invite-user.ts +96 -0
  16. package/client/components/my-login-history.ts +101 -0
  17. package/client/components/ownership-transfer-popup.ts +110 -0
  18. package/client/components/partner-info-card.ts +89 -0
  19. package/client/components/partner-role-editor.ts +153 -0
  20. package/client/components/profile-component.ts +332 -0
  21. package/client/components/role-edit-form.ts +92 -0
  22. package/client/components/role-privilege-editor.ts +267 -0
  23. package/client/components/role-selector.ts +102 -0
  24. package/client/components/user-role-editor.ts +499 -0
  25. package/client/constants/application.ts +9 -0
  26. package/client/constants/index.ts +1 -0
  27. package/client/entries/auth/activate.ts +272 -0
  28. package/client/entries/auth/checkin.ts +190 -0
  29. package/client/entries/auth/forgot-password.ts +103 -0
  30. package/client/entries/auth/reset-password.ts +22 -0
  31. package/client/entries/auth/result.ts +193 -0
  32. package/client/entries/auth/signin.ts +18 -0
  33. package/client/entries/auth/signup.ts +109 -0
  34. package/client/entries/auth/unlock-user.ts +22 -0
  35. package/client/entries/oauth2/oauth2-decision-error-page.ts +50 -0
  36. package/client/entries/oauth2/oauth2-decision-page.ts +196 -0
  37. package/client/entries/public/home.ts +246 -0
  38. package/client/index.ts +124 -0
  39. package/client/pages/app-binding/app-binding.ts +423 -0
  40. package/client/pages/app-binding/app-bindings.ts +171 -0
  41. package/client/pages/appliance/appliance.ts +452 -0
  42. package/client/pages/appliance/home.ts +177 -0
  43. package/client/pages/appliance/register.ts +183 -0
  44. package/client/pages/application/application.ts +428 -0
  45. package/client/pages/application/applications.ts +182 -0
  46. package/client/pages/application/register.ts +211 -0
  47. package/client/pages/attribute/attribute-set-item-list.ts +237 -0
  48. package/client/pages/attribute/attribute-set-management.ts +282 -0
  49. package/client/pages/auth-provider/auth-provider-management.ts +381 -0
  50. package/client/pages/domain/domain-management.ts +410 -0
  51. package/client/pages/partner/partner-management.ts +112 -0
  52. package/client/pages/profile.ts +32 -0
  53. package/client/pages/role/role-management.ts +134 -0
  54. package/client/pages/user/user-management.ts +224 -0
  55. package/client/route.ts +67 -0
  56. package/client/themes/auth-theme.css +65 -0
  57. package/client/utils/password-rule.ts +37 -0
  58. package/dist-client/components/abstract-auth-page.js +10 -10
  59. package/dist-client/components/abstract-auth-page.js.map +1 -1
  60. package/dist-client/components/abstract-password-reset.d.ts +2 -1
  61. package/dist-client/components/abstract-password-reset.js +14 -7
  62. package/dist-client/components/abstract-password-reset.js.map +1 -1
  63. package/dist-client/components/abstract-sign.js +11 -12
  64. package/dist-client/components/abstract-sign.js.map +1 -1
  65. package/dist-client/components/contact-us.d.ts +1 -1
  66. package/dist-client/components/contact-us.js +7 -10
  67. package/dist-client/components/contact-us.js.map +1 -1
  68. package/dist-client/components/create-user.js +5 -28
  69. package/dist-client/components/create-user.js.map +1 -1
  70. package/dist-client/components/invite-user.js +11 -19
  71. package/dist-client/components/invite-user.js.map +1 -1
  72. package/dist-client/components/ownership-transfer-popup.js +3 -3
  73. package/dist-client/components/ownership-transfer-popup.js.map +1 -1
  74. package/dist-client/components/profile-component.d.ts +1 -5
  75. package/dist-client/components/profile-component.js +4 -64
  76. package/dist-client/components/profile-component.js.map +1 -1
  77. package/dist-client/components/role-privilege-editor.js +1 -2
  78. package/dist-client/components/role-privilege-editor.js.map +1 -1
  79. package/dist-client/components/user-role-editor.js +18 -18
  80. package/dist-client/components/user-role-editor.js.map +1 -1
  81. package/dist-client/entries/auth/checkin.js +1 -1
  82. package/dist-client/entries/auth/checkin.js.map +1 -1
  83. package/dist-client/entries/auth/forgot-password.js +2 -11
  84. package/dist-client/entries/auth/forgot-password.js.map +1 -1
  85. package/dist-client/entries/auth/signup.js +7 -13
  86. package/dist-client/entries/auth/signup.js.map +1 -1
  87. package/dist-client/index.js +1 -1
  88. package/dist-client/index.js.map +1 -1
  89. package/dist-client/pages/user/user-management.d.ts +1 -5
  90. package/dist-client/pages/user/user-management.js +7 -6
  91. package/dist-client/pages/user/user-management.js.map +1 -1
  92. package/dist-client/tsconfig.tsbuildinfo +1 -1
  93. package/dist-server/tsconfig.tsbuildinfo +1 -1
  94. package/package.json +12 -12
  95. package/server/index.ts +0 -0
  96. package/translations/en.json +2 -6
  97. package/translations/ja.json +2 -6
  98. package/translations/ko.json +2 -6
  99. package/translations/ms.json +2 -6
  100. package/translations/zh.json +2 -6
@@ -0,0 +1,95 @@
1
+ import '@material/web/button/outlined-button.js'
2
+ import '@material/web/textfield/filled-text-field.js'
3
+
4
+ import { css, html, LitElement } from 'lit'
5
+ import { customElement, query } from 'lit/decorators.js'
6
+
7
+ import { i18next } from '@operato/i18n'
8
+
9
+ @customElement('create-user')
10
+ class CreateUser extends LitElement {
11
+ static styles = css`
12
+ :host {
13
+ --md-text-field-fill-color: var(--md-sys-color-on-primary);
14
+ background-color: var(--md-sys-color-surface);
15
+ margin: var(--spacing-large) 0;
16
+ padding: var(--spacing-large);
17
+ border-radius: var(--border-radius);
18
+ box-shadow: var(--box-shadow);
19
+
20
+ display: grid;
21
+ grid-template-columns: 1fr 2fr auto;
22
+ gap: 5px 15px;
23
+ clear: both;
24
+ max-width: var(--input-container-max-width);
25
+
26
+ align-items: center;
27
+ }
28
+
29
+ md-outlined-button {
30
+ margin: var(--input-margin);
31
+ }
32
+
33
+ @media screen and (max-width: 480px) {
34
+ :host {
35
+ grid-template-columns: 1fr 1fr;
36
+ }
37
+
38
+ md-outlined-button {
39
+ grid-column: span 2;
40
+
41
+ margin: var(--input-margin);
42
+ }
43
+ }
44
+ `
45
+
46
+ @query('[name=name]') nameInput!: HTMLInputElement
47
+ @query('[name=email]') emailInput!: HTMLInputElement
48
+
49
+ render() {
50
+ return html`
51
+ <md-filled-text-field
52
+ type="text"
53
+ name="name"
54
+ label=${String(i18next.t('label.x name', { x: i18next.t('label.user') }))}
55
+ ></md-filled-text-field>
56
+
57
+ <md-filled-text-field type="email" name="email" label=${String(i18next.t('field.email'))}></md-filled-text-field>
58
+
59
+ <md-outlined-button @click=${this.onCreateUser.bind(this)}
60
+ >${String(i18next.t('button.create'))}</md-outlined-button
61
+ >
62
+ `
63
+ }
64
+
65
+ async onCreateUser() {
66
+ try {
67
+ if (!this.emailInput.checkValidity()) {
68
+ throw new Error(i18next.t('error.not valid pattern of type', { type: 'e-mail' }))
69
+ }
70
+
71
+ if (!this.nameInput.value) {
72
+ throw new Error(i18next.t('error.value is empty', { value: 'name' }))
73
+ }
74
+
75
+ const user = {
76
+ name: this.nameInput.value.trim(),
77
+ email: this.emailInput.value.trim()
78
+ }
79
+
80
+ await this.dispatchEvent(new CustomEvent('create-user', { detail: user }))
81
+
82
+ this.nameInput.value = ''
83
+ this.emailInput.value = ''
84
+ } catch (e: any) {
85
+ document.dispatchEvent(
86
+ new CustomEvent('notify', {
87
+ detail: {
88
+ level: 'error',
89
+ message: 'message' in e ? e.message : e
90
+ }
91
+ })
92
+ )
93
+ }
94
+ }
95
+ }
@@ -0,0 +1,64 @@
1
+ import gql from 'graphql-tag'
2
+ import { LitElement, html, css } from 'lit'
3
+ import { customElement, property } from 'lit/decorators.js'
4
+
5
+ import { client } from '@operato/graphql'
6
+
7
+ @customElement('credential-manager')
8
+ export class CredentialManager extends LitElement {
9
+ @property({ type: Array }) credentials: {
10
+ credentialId: string
11
+ }[] = []
12
+
13
+ static styles = css`
14
+ div {
15
+ margin: 20px;
16
+ }
17
+ button {
18
+ margin: 5px;
19
+ }
20
+ `
21
+
22
+ connectedCallback() {
23
+ super.connectedCallback()
24
+ this.fetchCredentials()
25
+ }
26
+
27
+ async fetchCredentials() {
28
+ const response = await fetch('/auth/credentials', {
29
+ method: 'GET',
30
+ credentials: 'include'
31
+ })
32
+ this.credentials = await response.json()
33
+ }
34
+
35
+ async deleteCredential(credentialId: string) {
36
+ const response = await fetch(`/credentials/${credentialId}`, {
37
+ method: 'DELETE',
38
+ credentials: 'include'
39
+ })
40
+ if (response.ok) {
41
+ this.fetchCredentials()
42
+ } else {
43
+ console.error('Failed to delete credential')
44
+ }
45
+ }
46
+
47
+ render() {
48
+ return html`
49
+ <div>
50
+ <h2>Manage Your WebAuthn Credentials</h2>
51
+ <ul>
52
+ ${this.credentials.map(
53
+ credential => html`
54
+ <li>
55
+ ${credential.credentialId}
56
+ <button @click=${() => this.deleteCredential(credential.credentialId)}>Delete</button>
57
+ </li>
58
+ `
59
+ )}
60
+ </ul>
61
+ </div>
62
+ `
63
+ }
64
+ }
@@ -0,0 +1,117 @@
1
+ import '@operato/i18n/ox-i18n.js'
2
+
3
+ import { css, html, LitElement } from 'lit'
4
+ import { customElement } from 'lit/decorators.js'
5
+
6
+ import { i18next, localize } from '@operato/i18n'
7
+ import { notify } from '@operato/layout'
8
+ import { auth } from '@things-factory/auth-base/dist-client/auth.js'
9
+
10
+ @customElement('delete-user-popup')
11
+ export class DeleteUserPopup extends localize(i18next)(LitElement) {
12
+ static styles = [
13
+ css`
14
+ :host {
15
+ display: flex;
16
+ flex-direction: column;
17
+ color: var(--popup-content-color);
18
+ background-color: var(--popup-content-background-color);
19
+ padding: var(--popup-content-padding);
20
+ }
21
+
22
+ * {
23
+ box-sizing: border-box;
24
+ }
25
+ *:focus {
26
+ outline: none;
27
+ }
28
+
29
+ label {
30
+ font: bold 14px var(--theme-font);
31
+ color: var(--md-sys-color-primary);
32
+ }
33
+
34
+ input {
35
+ border: var(--change-password-field-border);
36
+ border-radius: var(--change-password-field-border-radius);
37
+ padding: var(--change-password-field-padding);
38
+
39
+ font: var(--change-password-field-font);
40
+ width: var(--change-password-field-width);
41
+ }
42
+ input:focus {
43
+ border: 1px solid var(--focus-background-color);
44
+ }
45
+
46
+ div.field {
47
+ padding-bottom: 10px;
48
+ }
49
+
50
+ ::placeholder {
51
+ font-size: 0.8rem;
52
+ text-transform: capitalize;
53
+ }
54
+
55
+ button {
56
+ background-color: var(--status-danger-color, #d14946);
57
+ margin: 2px 2px 10px 2px;
58
+ height: var(--button-height, 28px);
59
+ color: var(--button-color, #fff);
60
+ font: var(--button-font);
61
+ border-radius: var(--button-radius, 5px);
62
+ border: var(--button-border, 1px solid transparent);
63
+ line-height: 1.5;
64
+ }
65
+ button:hover,
66
+ button:active {
67
+ background-color: var(--button-active-background-color, #22a6a7);
68
+ border: var(--button-active-border);
69
+ }
70
+ `
71
+ ]
72
+
73
+ render() {
74
+ return html`
75
+ <h1><ox-i18n msgid="label.delete account"></ox-i18n></h1>
76
+ <span><ox-i18n msgid="text.delete account warning message"></ox-i18n></span>
77
+ <form @submit=${e => this.submit(e)}>
78
+ <div class="field">
79
+ <label for="email"><ox-i18n msgid="label.email"></ox-i18n></label>
80
+ <input id="email" type="email" name="email" autocapitalize="off" required />
81
+ <label for="password"><ox-i18n msgid="label.password"></ox-i18n></label>
82
+ <input id="password" type="password" name="password" required />
83
+ </div>
84
+
85
+ <button class="ui button" type="submit"><ox-i18n msgid="label.delete account"></ox-i18n></button>
86
+ </form>
87
+ `
88
+ }
89
+
90
+ async submit(e: Event) {
91
+ e.preventDefault()
92
+
93
+ const form = e.target as HTMLFormElement
94
+
95
+ var params = {}
96
+ new FormData(form).forEach((value, key) => {
97
+ params[key] = value
98
+ })
99
+
100
+ try {
101
+ const message = await auth.deleteUser(params)
102
+ notify({
103
+ level: 'info',
104
+ message
105
+ })
106
+
107
+ auth.signout()
108
+ } catch (e: any) {
109
+ form.reset()
110
+
111
+ notify({
112
+ level: 'error',
113
+ message: 'message' in e ? e.message : e
114
+ })
115
+ }
116
+ }
117
+ }
@@ -0,0 +1,127 @@
1
+ import '@material/web/icon/icon.js'
2
+
3
+ import { css, html, LitElement, nothing } from 'lit'
4
+ import { customElement, property } from 'lit/decorators.js'
5
+ import { connect } from 'pwa-helpers/connect-mixin'
6
+
7
+ import { store } from '@operato/shell'
8
+ import { ScrollbarStyles } from '@operato/styles'
9
+
10
+ @customElement('domain-switch')
11
+ export class DomainSwitch extends connect(store)(LitElement) {
12
+ static styles = [
13
+ ScrollbarStyles,
14
+ css`
15
+ :host {
16
+ display: flex;
17
+ max-width: 100%;
18
+ background-color: var(--md-sys-color-primary-container, rgb(215 231 241));
19
+ border-bottom: var(--border-dim-color);
20
+ }
21
+
22
+ :host * {
23
+ vertical-align: middle;
24
+ }
25
+
26
+ div {
27
+ flex: 1;
28
+ display: flex;
29
+ flex-direction: row;
30
+ padding: var(--spacing-small);
31
+ }
32
+
33
+ md-icon {
34
+ background-color: var(--md-sys-color-primary, rgb(46 121 190));
35
+ margin-right: var(--spacing-small);
36
+ padding: 2px;
37
+ border-radius: 50%;
38
+ font-size: var(--fontsize-large);
39
+ color: var(--md-sys-color-on-primary, rgba(255,255,255,.9));
40
+ }
41
+ span,
42
+ select {
43
+ flex: 1;
44
+ color: var(--md-sys-color-on-primary-container, rgb(50 66 82));
45
+ font: bold 14px/20px var(--theme-font);
46
+ }
47
+
48
+ select {
49
+ border: none;
50
+ background-color: transparent;
51
+ }
52
+
53
+ select:focus {
54
+ outline: 0;
55
+ }
56
+ :host([dark]) {
57
+ background-color: rgba(0, 0, 0, 0.2);
58
+ padding: 0 !important;
59
+ border-bottom: none;
60
+ }
61
+ :host([dark]) md-icon {
62
+ background-color: var(--secondary-text-color);
63
+ margin: 1px 4px 0px 0px;
64
+ padding: 1px 2px;
65
+ border-radius: 50%;
66
+ line-height: 19px;
67
+ }
68
+ :host([dark]) span,
69
+ :host([dark]) select {
70
+ color: var(--md-sys-color-on-primary);
71
+ font: bold 13px/13px var(--theme-font);
72
+ }
73
+ :host([dark]) option {
74
+ background-color: var(--primary-color, #585858);
75
+ color: var(--md-sys-color-on-primary, #fff);
76
+ }
77
+ :host([dark]) span {
78
+ line-height: 23px;
79
+ }
80
+
81
+ :host([rounded-corner]) {
82
+ height: 30px;
83
+ border-radius: 20px;
84
+ border: var(--border-dim-color);
85
+ }
86
+ :host([rounded-corner]) div {
87
+ padding: var(--spacing-small) var(--spacing-medium);
88
+ }
89
+ `
90
+ ]
91
+
92
+ @property({ type: Array }) domains: any[] = []
93
+ @property({ type: Object }) domain: any
94
+ @property({ type: String, attribute: true }) attrname: string = 'name'
95
+ @property({ type: String, attribute: true }) icon?: string
96
+
97
+ render() {
98
+ const domains = this.domains || []
99
+ const domain = this.domain || {}
100
+ const attrname = this.attrname || 'name'
101
+
102
+ return html`
103
+ <div>
104
+ ${this.icon ? html`<md-icon>${this.icon}</md-icon>` : nothing}
105
+ ${domains.length <= 1
106
+ ? html` <span>${domains[0]?.[attrname] || domain.name}</span> `
107
+ : html`
108
+ <select
109
+ .value=${domain.subdomain}
110
+ @change=${e => (window.location.pathname = `/auth/checkin/${e.target.value}`)}
111
+ >
112
+ ${domains.map(
113
+ d => html`
114
+ <option .value=${d.subdomain} ?selected=${d.subdomain == domain.subdomain}>${d[attrname]}</option>
115
+ `
116
+ )}
117
+ </select>
118
+ `}
119
+ </div>
120
+ `
121
+ }
122
+
123
+ stateChanged(state) {
124
+ this.domains = state.app.domains
125
+ this.domain = state.app.domain
126
+ }
127
+ }
@@ -0,0 +1,104 @@
1
+ import gql from 'graphql-tag'
2
+ import { css, html, LitElement } from 'lit'
3
+ import { customElement, property, query } from 'lit/decorators.js'
4
+
5
+ import { client, gqlContext } from '@operato/graphql'
6
+ import { i18next, localize } from '@operato/i18n'
7
+ import { OxPrompt } from '@operato/popup/ox-prompt.js'
8
+
9
+ @customElement('invite-customer')
10
+ class InviteCustomer extends localize(i18next)(LitElement) {
11
+ static styles = [
12
+ css`
13
+ input {
14
+ border: var(--border-dim-color);
15
+ border-radius: var(--border-radius);
16
+ margin: var(--input-margin);
17
+ padding: var(--input-padding);
18
+ min-width: 250px;
19
+ font: var(--input-font);
20
+ }
21
+ md-outlined-button {
22
+ margin: var(--input-margin);
23
+ }
24
+ @media screen and (max-width: 480px) {
25
+ div {
26
+ display: grid;
27
+ }
28
+ }
29
+ `
30
+ ]
31
+
32
+ @property({ type: Array }) customers: any[] = []
33
+
34
+ @query('input#customer-name') customerNameInput!: HTMLInputElement
35
+
36
+ render() {
37
+ return html`
38
+ <div>
39
+ <input id="customer-name" required />
40
+ <md-outlined-button @click=${this.invite.bind(this)}>
41
+ <md-icon slot="icon">group_add</md-icon>
42
+ ${String(i18next.t('label.invite customer'))}
43
+ </md-outlined-button>
44
+ </div>
45
+ `
46
+ }
47
+
48
+ async invite() {
49
+ try {
50
+ if (!this.customerNameInput.value)
51
+ throw new Error(i18next.t('error.value is empty', { value: i18next.t('field.name') }))
52
+
53
+ if (this.customers.find(c => c.name?.toLowerCase() === this.customerNameInput.value.toLowerCase())) {
54
+ throw new Error(
55
+ i18next.t('error.x already exists in y', { x: this.customerNameInput.value, y: i18next.t('field.customer') })
56
+ )
57
+ }
58
+
59
+ if (
60
+ await OxPrompt.open({
61
+ title: i18next.t('text.are_you_sure'),
62
+ text: i18next.t('text.do_you_want_to_invite_x', { x: i18next.t(`label.partner`) }),
63
+ confirmButton: { text: i18next.t('button.confirm') },
64
+ cancelButton: { text: i18next.t('button.cancel') }
65
+ })
66
+ ) {
67
+ const customerDomainName = this.customerNameInput.value
68
+
69
+ const response = await client.mutate({
70
+ mutation: gql`
71
+ mutation inviteCustomer($customerDomainName: String!) {
72
+ inviteCustomer(customerDomainName: $customerDomainName)
73
+ }
74
+ `,
75
+ variables: { customerDomainName },
76
+ context: gqlContext()
77
+ })
78
+
79
+ if (!response.errors) {
80
+ const answer = this.dispatchEvent(new CustomEvent('invitationCompleted'))
81
+
82
+ if (answer) {
83
+ await OxPrompt.open({
84
+ type: 'success',
85
+ title: i18next.t('text.completed'),
86
+ confirmButton: { text: i18next.t('button.confirm') }
87
+ })
88
+ }
89
+
90
+ this.customerNameInput.value = ''
91
+ }
92
+ }
93
+ } catch (e: any) {
94
+ document.dispatchEvent(
95
+ new CustomEvent('notify', {
96
+ detail: {
97
+ level: 'error',
98
+ message: 'message' in e ? e.message : e
99
+ }
100
+ })
101
+ )
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,96 @@
1
+ import './user-role-editor'
2
+
3
+ import gql from 'graphql-tag'
4
+ import { css, html, LitElement } from 'lit'
5
+ import { customElement, query } from 'lit/decorators.js'
6
+
7
+ import { client, gqlContext } from '@operato/graphql'
8
+ import { i18next, localize } from '@operato/i18n'
9
+ import { OxPrompt } from '@operato/popup/ox-prompt.js'
10
+
11
+ @customElement('invite-user')
12
+ class InviteUser extends localize(i18next)(LitElement) {
13
+ static styles = css`
14
+ :host {
15
+ display: grid;
16
+ }
17
+
18
+ input {
19
+ flex: 1;
20
+
21
+ border: var(--border-dim-color);
22
+ border-radius: var(--border-radius);
23
+ margin: var(--input-margin);
24
+ padding: var(--input-padding);
25
+ min-width: 250px;
26
+ font: var(--input-font);
27
+ }
28
+
29
+ md-outlined-button {
30
+ margin: var(--input-margin);
31
+ text-transform: capitalize;
32
+ }
33
+ `
34
+
35
+ @query('input[name=email]') emailInput!: HTMLInputElement
36
+
37
+ render() {
38
+ return html`
39
+ <input name="email" type="email" required name="invite-email" autocapitalize="off" />
40
+ <md-outlined-button @click=${this.invite.bind(this)}>
41
+ <md-icon slot="icon">group_add</md-icon>${String(i18next.t('label.invite user'))}
42
+ </md-outlined-button>
43
+ `
44
+ }
45
+
46
+ async invite() {
47
+ try {
48
+ if (!this.emailInput.checkValidity()) {
49
+ throw new Error(i18next.t('error.not valid pattern of type', { type: 'e-mail' }))
50
+ }
51
+
52
+ if (
53
+ await OxPrompt.open({
54
+ title: i18next.t('text.are_you_sure'),
55
+ text: i18next.t('text.do_you_want_to_invite_x', { x: i18next.t(`label.user`) }),
56
+ confirmButton: { text: i18next.t('button.confirm') },
57
+ cancelButton: { text: i18next.t('button.cancel') }
58
+ })
59
+ ) {
60
+ await this.inviteUser(this.emailInput.value)
61
+
62
+ this.dispatchEvent(new CustomEvent('invitationCompleted'))
63
+ }
64
+ } catch (e: any) {
65
+ document.dispatchEvent(
66
+ new CustomEvent('notify', {
67
+ detail: {
68
+ level: 'error',
69
+ message: 'message' in e ? e.message : e
70
+ }
71
+ })
72
+ )
73
+ }
74
+ }
75
+
76
+ async inviteUser(email) {
77
+ const response = await client.mutate({
78
+ mutation: gql`
79
+ mutation inviteUser($email: EmailAddress!) {
80
+ inviteUser(email: $email)
81
+ }
82
+ `,
83
+ variables: { email },
84
+ context: gqlContext()
85
+ })
86
+
87
+ if (!response.errors) {
88
+ await OxPrompt.open({
89
+ title: i18next.t('text.completed'),
90
+ confirmButton: { text: i18next.t('button.confirm') }
91
+ })
92
+
93
+ this.emailInput.value = ''
94
+ }
95
+ }
96
+ }
@@ -0,0 +1,101 @@
1
+ import '@operato/data-grist'
2
+
3
+ import gql from 'graphql-tag'
4
+ import { css, html, LitElement } from 'lit'
5
+ import { customElement, property } from 'lit/decorators.js'
6
+
7
+ import { client } from '@operato/graphql'
8
+ import { i18next } from '@operato/i18n'
9
+ import { isMobileDevice } from '@operato/utils'
10
+
11
+ @customElement('my-login-history')
12
+ class MyLoginHistory extends LitElement {
13
+ static styles = css`
14
+ :host {
15
+ display: flex;
16
+ flex-direction: column;
17
+ background-color: var(--md-sys-color-background);
18
+ padding: var(--spacing-large);
19
+ overflow: auto;
20
+ }
21
+ ox-grist {
22
+ flex: 1;
23
+ }
24
+ `
25
+
26
+ @property({ type: Array }) histories: any[] = []
27
+ @property({ type: Number }) limit?: number
28
+
29
+ render() {
30
+ if (!this.histories?.length) return html``
31
+
32
+ const config = {
33
+ rows: { appendable: false },
34
+ pagination: { infinite: true },
35
+ columns: [
36
+ { type: 'gutter', gutterName: 'sequence' },
37
+ {
38
+ type: 'object',
39
+ name: 'accessDomain',
40
+ header: i18next.t('field.domain'),
41
+ record: { editable: false },
42
+ width: 200
43
+ },
44
+ {
45
+ type: 'datetime',
46
+ name: 'accessedAt',
47
+ header: i18next.t('field.accessed-at'),
48
+ record: { editable: false },
49
+ width: 200
50
+ },
51
+ {
52
+ type: 'string',
53
+ name: 'accessorIp',
54
+ header: i18next.t('field.ip_address'),
55
+ record: { editable: false },
56
+ width: 200
57
+ }
58
+ ]
59
+ }
60
+
61
+ return html`
62
+ <ox-grist
63
+ .mode=${isMobileDevice() ? 'LIST' : 'GRID'}
64
+ .config=${config}
65
+ .data="${{ records: this.histories }}"
66
+ ></ox-grist>
67
+ `
68
+ }
69
+
70
+ firstUpdated() {
71
+ this.fetchLoginHistories()
72
+ }
73
+
74
+ async fetchLoginHistories() {
75
+ try {
76
+ const response = await client.query({
77
+ query: gql`
78
+ query myLoginHistories($limit: Float!) {
79
+ myLoginHistories(limit: $limit) {
80
+ accessDomain {
81
+ name
82
+ }
83
+ accessorIp
84
+ accessedAt
85
+ }
86
+ }
87
+ `,
88
+ variables: { limit: this.limit || 10 }
89
+ })
90
+
91
+ if (response.errors?.length) return
92
+ this.histories = response.data.myLoginHistories
93
+ } catch (e: any) {
94
+ this.showToast('message' in e ? e.message : e)
95
+ }
96
+ }
97
+
98
+ showToast(message) {
99
+ document.dispatchEvent(new CustomEvent('notify', { detail: { message } }))
100
+ }
101
+ }