@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.
Files changed (98) hide show
  1. package/dist-client/components/app-binding-detail.d.ts +46 -0
  2. package/dist-client/components/app-binding-detail.js +384 -0
  3. package/dist-client/components/app-binding-detail.js.map +1 -0
  4. package/dist-client/components/appliance-editor.d.ts +37 -0
  5. package/dist-client/components/appliance-editor.js +489 -0
  6. package/dist-client/components/appliance-editor.js.map +1 -0
  7. package/dist-client/components/application-editor.d.ts +31 -0
  8. package/dist-client/components/application-editor.js +491 -0
  9. package/dist-client/components/application-editor.js.map +1 -0
  10. package/dist-client/components/profile-component.js +28 -11
  11. package/dist-client/components/profile-component.js.map +1 -1
  12. package/dist-client/components/secret-field.d.ts +35 -0
  13. package/dist-client/components/secret-field.js +149 -0
  14. package/dist-client/components/secret-field.js.map +1 -0
  15. package/dist-client/constants/application-type-parity.test.js +62 -0
  16. package/dist-client/constants/application-type-parity.test.js.map +1 -0
  17. package/dist-client/constants/application.d.ts +17 -7
  18. package/dist-client/constants/application.js +18 -8
  19. package/dist-client/constants/application.js.map +1 -1
  20. package/dist-client/pages/app-binding/app-binding-list-query.test.js +46 -0
  21. package/dist-client/pages/app-binding/app-binding-list-query.test.js.map +1 -0
  22. package/dist-client/pages/app-binding/app-bindings.d.ts +65 -2
  23. package/dist-client/pages/app-binding/app-bindings.js +217 -117
  24. package/dist-client/pages/app-binding/app-bindings.js.map +1 -1
  25. package/dist-client/pages/appliance/appliance-list-query.test.d.ts +1 -0
  26. package/dist-client/pages/appliance/appliance-list-query.test.js +58 -0
  27. package/dist-client/pages/appliance/appliance-list-query.test.js.map +1 -0
  28. package/dist-client/pages/appliance/home.d.ts +63 -2
  29. package/dist-client/pages/appliance/home.js +219 -123
  30. package/dist-client/pages/appliance/home.js.map +1 -1
  31. package/dist-client/pages/application/applications.d.ts +63 -2
  32. package/dist-client/pages/application/applications.js +212 -129
  33. package/dist-client/pages/application/applications.js.map +1 -1
  34. package/dist-client/pages/auth-provider/auth-provider-management.d.ts +4 -2
  35. package/dist-client/pages/auth-provider/auth-provider-management.js +35 -6
  36. package/dist-client/pages/auth-provider/auth-provider-management.js.map +1 -1
  37. package/dist-client/pages/domain-owner/domain-owner-management.d.ts +2 -0
  38. package/dist-client/pages/domain-owner/domain-owner-management.js +34 -7
  39. package/dist-client/pages/domain-owner/domain-owner-management.js.map +1 -1
  40. package/dist-client/pages/secrets-are-not-printed.test.d.ts +1 -0
  41. package/dist-client/pages/secrets-are-not-printed.test.js +88 -0
  42. package/dist-client/pages/secrets-are-not-printed.test.js.map +1 -0
  43. package/dist-client/route.js +0 -15
  44. package/dist-client/route.js.map +1 -1
  45. package/dist-client/tsconfig.tsbuildinfo +1 -1
  46. package/dist-client/utils/application-status.d.ts +32 -0
  47. package/dist-client/utils/application-status.js +45 -0
  48. package/dist-client/utils/application-status.js.map +1 -0
  49. package/dist-client/utils/application-status.test.d.ts +1 -0
  50. package/dist-client/utils/application-status.test.js +45 -0
  51. package/dist-client/utils/application-status.test.js.map +1 -0
  52. package/dist-client/utils/form-values.d.ts +71 -0
  53. package/dist-client/utils/form-values.js +102 -0
  54. package/dist-client/utils/form-values.js.map +1 -0
  55. package/dist-client/utils/form-values.test.d.ts +1 -0
  56. package/dist-client/utils/form-values.test.js +120 -0
  57. package/dist-client/utils/form-values.test.js.map +1 -0
  58. package/dist-client/utils/list-result.d.ts +62 -0
  59. package/dist-client/utils/list-result.js +92 -0
  60. package/dist-client/utils/list-result.js.map +1 -0
  61. package/dist-client/utils/list-result.test.d.ts +1 -0
  62. package/dist-client/utils/list-result.test.js +160 -0
  63. package/dist-client/utils/list-result.test.js.map +1 -0
  64. package/dist-client/utils/secret-display.d.ts +51 -0
  65. package/dist-client/utils/secret-display.js +61 -0
  66. package/dist-client/utils/secret-display.js.map +1 -0
  67. package/dist-client/utils/secret-display.test.d.ts +1 -0
  68. package/dist-client/utils/secret-display.test.js +59 -0
  69. package/dist-client/utils/secret-display.test.js.map +1 -0
  70. package/dist-client/utils/webauthn-verification.d.ts +56 -0
  71. package/dist-client/utils/webauthn-verification.js +79 -0
  72. package/dist-client/utils/webauthn-verification.js.map +1 -0
  73. package/dist-client/utils/webauthn-verification.test.d.ts +1 -0
  74. package/dist-client/utils/webauthn-verification.test.js +79 -0
  75. package/dist-client/utils/webauthn-verification.test.js.map +1 -0
  76. package/dist-server/tsconfig.tsbuildinfo +1 -1
  77. package/package.json +3 -3
  78. package/things-factory.config.js +0 -5
  79. package/translations/en.json +56 -0
  80. package/translations/ja.json +57 -0
  81. package/translations/ko.json +56 -0
  82. package/translations/ms.json +57 -0
  83. package/translations/zh.json +57 -0
  84. package/dist-client/pages/app-binding/app-binding.d.ts +0 -1
  85. package/dist-client/pages/app-binding/app-binding.js +0 -371
  86. package/dist-client/pages/app-binding/app-binding.js.map +0 -1
  87. package/dist-client/pages/appliance/appliance.d.ts +0 -1
  88. package/dist-client/pages/appliance/appliance.js +0 -392
  89. package/dist-client/pages/appliance/appliance.js.map +0 -1
  90. package/dist-client/pages/appliance/register.js +0 -166
  91. package/dist-client/pages/appliance/register.js.map +0 -1
  92. package/dist-client/pages/application/application.d.ts +0 -1
  93. package/dist-client/pages/application/application.js +0 -377
  94. package/dist-client/pages/application/application.js.map +0 -1
  95. package/dist-client/pages/application/register.js +0 -192
  96. package/dist-client/pages/application/register.js.map +0 -1
  97. /package/dist-client/{pages/appliance/register.d.ts → constants/application-type-parity.test.d.ts} +0 -0
  98. /package/dist-client/pages/{application/register.d.ts → app-binding/app-binding-list-query.test.d.ts} +0 -0
@@ -1,149 +1,249 @@
1
1
  import { __decorate, __metadata } from "tslib";
2
- import '@material/web/button/elevated-button.js';
3
- import '@material/web/button/outlined-button.js';
2
+ import '@material/web/icon/icon.js';
3
+ import '@operato/data-grist/ox-grist.js';
4
+ import '@operato/context/ox-context-page-toolbar.js';
4
5
  import gql from 'graphql-tag';
5
6
  import { css, html } from 'lit';
6
- import { customElement, property } from 'lit/decorators.js';
7
- import { client } from '@operato/graphql';
7
+ import { customElement, property, query } from 'lit/decorators.js';
8
+ import { client, gqlContext } from '@operato/graphql';
9
+ import { i18next, localize } from '@operato/i18n';
10
+ import { openPopup } from '@operato/layout';
8
11
  import { navigate, PageView } from '@operato/shell';
9
- let AppBindings = class AppBindings extends PageView {
12
+ import { CommonHeaderStyles, CommonGristStyles, ScrollbarStyles } from '@operato/styles';
13
+ import { isMobileDevice } from '@operato/utils';
14
+ import { p13n } from '@operato/p13n';
15
+ import '../../components/app-binding-detail.js';
16
+ import { listOutcome, pageFromResponse } from '../../utils/list-result.js';
17
+ /**
18
+ * Applications bound to this domain.
19
+ *
20
+ * A binding is a `User` row with `userType: 'application'` — the account an application signs
21
+ * in as. The list shows who it is and what it may do; what it signs in *with* is elsewhere.
22
+ *
23
+ * ── The server answers honestly now ──────────────────────────────────────────
24
+ * Until dde62cd6a1 this query paged before it filtered: the database took a page of users of
25
+ * every type, JavaScript kept the applications among them, and the total reported how many
26
+ * survived that one page. A grid told there were three when there were thirty never offered a
27
+ * second page. Wiring paging up was pointless until that was fixed; it works now.
28
+ *
29
+ * ── ⚠ The refresh token is not asked for, and a test holds that ──────────────
30
+ * AppBinding's refresh token is the application's actual credential — oauth2-server.ts writes
31
+ * it into the password column verbatim, not hashed — and the field carries its own
32
+ * privilege directive for security:query.
33
+ *
34
+ * A credential that can be exchanged for access tokens does not belong in a grid cell, where
35
+ * it sits in plain text beside a name for anyone who can open the screen, and in any
36
+ * screenshot of it. `app-binding-list-query.test.ts` fails if the word appears here.
37
+ */
38
+ let AppBindings = class AppBindings extends p13n(localize(i18next)(PageView)) {
10
39
  constructor() {
11
40
  super(...arguments);
12
- this.appBindings = [];
41
+ this.active = false;
42
+ this.mode = isMobileDevice() ? 'CARD' : 'GRID';
43
+ /** The last fetch did not answer. Distinct from answering with nothing. */
44
+ this.failed = false;
13
45
  }
14
46
  static { this.styles = [
47
+ CommonGristStyles,
48
+ CommonHeaderStyles,
49
+ ScrollbarStyles,
15
50
  css `
16
- :host { background-color: var(--md-sys-color-background);
17
- padding: var(--spacing-large);
18
-
19
- overflow: auto;
20
- }
21
-
22
- md-elevated-button { text-transform: capitalize;
23
- }
24
-
25
- md-outlined-button { float: right;
26
- margin-top: var(--spacing-medium);
27
- text-transform: capitalize;
28
- }
29
-
30
- h2 { margin: var(--title-margin);
31
- font: var(--title-font);
32
- color: var(--title-text-color);
33
- }
34
-
35
- [page-description] { margin: var(--page-description-margin);
36
- font: var(--page-description-font);
37
- color: var(--page-description-color);
38
- }
39
-
40
- table { width: 100%;
41
- margin: var(--spacing-large) 0;
42
- border-collapse: collapse;
43
- }
44
-
45
- tr { background-color: var(--tr-background-color);
46
- }
47
-
48
- tr:nth-child(odd) { background-color: var(--tr-background-odd-color);
49
- }
50
-
51
- tr:hover { background-color: var(--tr-background-hover-color);
52
- }
53
-
54
- th { border-top: var(--th-border-top);
55
- border-bottom: var(--td-border-bottom);
56
- padding: var(--th-padding);
57
-
58
- font: var(--th-font);
59
- color: var(--th-color);
60
- text-transform: var(--th-text-transform);
61
- text-align: left;
62
- }
63
-
64
- td { padding: var(--td-padding);
65
- border-bottom: var(--td-border-bottom);
66
- font: var(--td-font);
67
- color: var(--td-color);
68
- }
69
-
70
- td a { color: var(--md-sys-color-primary);
71
- font: bold 16px var(--theme-font);
72
-
73
- display: block;
74
- text-decoration: none;
75
- }
76
-
77
- .text-align-center { text-align: center;
78
- }
79
-
80
- .text-align-right { text-align: right;
81
- }
51
+ :host {
52
+ display: flex;
53
+ flex-direction: column;
54
+ overflow: hidden;
55
+ }
56
+
57
+ ox-grist {
58
+ overflow-y: auto;
59
+ flex: 1;
60
+ }
61
+
62
+ [failed] {
63
+ padding: var(--spacing-medium);
64
+ color: var(--md-sys-color-error);
65
+ font: var(--label-font, inherit);
66
+ }
82
67
  `
83
68
  ]; }
84
69
  get context() {
85
- return { title: `application bindings`,
86
- help: 'auth/application'
70
+ return {
71
+ title: i18next.t('text.bound applications'),
72
+ help: 'auth/application',
73
+ search: {
74
+ handler: (search) => {
75
+ this.grist.searchText = search;
76
+ },
77
+ value: this.grist?.searchText || ''
78
+ },
79
+ actions: [
80
+ {
81
+ title: i18next.t('text.applications'),
82
+ action: () => navigate('applications'),
83
+ icon: 'apps'
84
+ }
85
+ ],
86
+ toolbar: false
87
87
  };
88
88
  }
89
89
  render() {
90
- var appBindings = this.appBindings || [];
91
90
  return html `
92
- <div>
93
- <h2>Bound Applications</h2>
94
- <p page-description>Bound Applications goes here</p>
95
- </div>
96
-
97
- <table>
98
- <tr>
99
- <th>app name</th>
100
- <th>Scope</th>
101
- <th>status</th>
102
- </tr>
103
- ${appBindings.map(appBinding => html `
104
- <tr>
105
- <td>
106
- <a href=${`app-binding/${appBinding.id}`}>${appBinding.name}</a>
107
- ${appBinding.description}
108
- </td>
109
- <td>${appBinding.scope}</td>
110
- <td>${appBinding.status}</td>
111
- </tr>
112
- `)}
113
- </table>
114
- <md-elevated-button @click=${e => navigate('applications')}>registered applications ..</md-elevated-button>
91
+ <ox-grist
92
+ .mode=${this.mode}
93
+ .config=${this.config}
94
+ .fetchHandler=${this.fetchHandler.bind(this)}
95
+ .personalConfigProvider=${this.getPagePreferenceProvider('ox-grist')}
96
+ >
97
+ <div slot="headroom" class="header">
98
+ <ox-context-page-toolbar class="actions" .context=${this.context}></ox-context-page-toolbar>
99
+ </div>
100
+ </ox-grist>
101
+
102
+ ${this.failed ? html `<div failed>${i18next.t('text.the list could not be loaded')}</div>` : ''}
115
103
  `;
116
104
  }
117
- async pageUpdated(changes, lifecycle, before) {
105
+ async pageInitialized() {
106
+ this.config = {
107
+ rows: { appendable: false, selectable: { multiple: false } },
108
+ columns: [
109
+ { type: 'gutter', gutterName: 'sequence' },
110
+ {
111
+ type: 'gutter',
112
+ gutterName: 'button',
113
+ icon: 'chevron_right',
114
+ title: i18next.t('button.detail'),
115
+ handlers: {
116
+ click: (_columns, _data, _column, record) => {
117
+ if (record?.id) {
118
+ this.openDetail(record.id);
119
+ }
120
+ }
121
+ }
122
+ },
123
+ {
124
+ type: 'string',
125
+ name: 'name',
126
+ header: i18next.t('field.name'),
127
+ record: { editable: false },
128
+ sortable: true,
129
+ width: 180
130
+ },
131
+ {
132
+ type: 'string',
133
+ name: 'applicationName',
134
+ header: i18next.t('label.application'),
135
+ record: { editable: false },
136
+ width: 180
137
+ },
138
+ {
139
+ type: 'string',
140
+ name: 'email',
141
+ header: i18next.t('field.email'),
142
+ record: { editable: false },
143
+ sortable: true,
144
+ width: 220
145
+ },
146
+ {
147
+ type: 'string',
148
+ name: 'scope',
149
+ header: i18next.t('field.scope'),
150
+ record: { editable: false },
151
+ width: 220
152
+ },
153
+ {
154
+ type: 'string',
155
+ name: 'status',
156
+ header: i18next.t('field.status'),
157
+ record: { align: 'center', editable: false },
158
+ sortable: true,
159
+ width: 130
160
+ },
161
+ {
162
+ type: 'datetime',
163
+ name: 'updatedAt',
164
+ header: i18next.t('field.updated at'),
165
+ sortable: true,
166
+ width: 180
167
+ }
168
+ ]
169
+ };
170
+ }
171
+ /*
172
+ * A binding is not authored — it is what an application ends up with after the OAuth flow —
173
+ * so this popup shows one and offers the two things a person can do to it. `app-binding/{id}`
174
+ * was a route; nothing outside auth-ui linked to it.
175
+ */
176
+ openDetail(appBindingId) {
177
+ const popup = openPopup(html `<app-binding-detail
178
+ .appBindingId=${appBindingId}
179
+ @app-binding-changed=${() => this.grist.fetch()}
180
+ @close=${() => popup.close()}
181
+ ></app-binding-detail>`, {
182
+ backdrop: true,
183
+ size: 'medium',
184
+ title: i18next.t('text.bound application')
185
+ });
186
+ }
187
+ async pageUpdated(_changes, _lifecycle) {
118
188
  if (this.active) {
119
- this.appBindings = (await this.fetchAppBindings()).items;
189
+ await this.updateComplete;
190
+ this.grist.fetch();
120
191
  }
121
192
  }
122
- async fetchAppBindings() {
123
- const response = await client.query({ query: gql `
124
- query { appBindings { items { id
193
+ async fetchHandler({ page, limit, sortings = [], filters = [] }) {
194
+ const response = await client.query({
195
+ query: gql `
196
+ query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {
197
+ appBindings(filters: $filters, pagination: $pagination, sortings: $sortings) {
198
+ items {
199
+ id
125
200
  name
126
- application { id
127
- name
128
- }
129
- scope
201
+ email
130
202
  status
131
- }
203
+ scope
204
+ updatedAt
205
+ application {
206
+ id
207
+ name
208
+ }
209
+ }
132
210
  total
133
- }
134
- }
135
- `
136
- });
137
- if (!response.errors) {
138
- return response.data.appBindings;
211
+ }
139
212
  }
213
+ `,
214
+ variables: { filters, pagination: { page, limit }, sortings },
215
+ context: gqlContext()
216
+ });
217
+ const { items, total, failed } = listOutcome(pageFromResponse(response, 'appBindings'));
218
+ this.failed = failed;
219
+ return {
220
+ total,
221
+ records: items.map((item) => ({ ...item, applicationName: item.application?.name || '' }))
222
+ };
140
223
  }
141
224
  };
142
225
  __decorate([
143
- property({ type: Array }),
144
- __metadata("design:type", Array)
145
- ], AppBindings.prototype, "appBindings", void 0);
226
+ property({ type: Boolean }),
227
+ __metadata("design:type", Boolean)
228
+ ], AppBindings.prototype, "active", void 0);
229
+ __decorate([
230
+ property({ type: Object }),
231
+ __metadata("design:type", Object)
232
+ ], AppBindings.prototype, "config", void 0);
233
+ __decorate([
234
+ property({ type: String }),
235
+ __metadata("design:type", String)
236
+ ], AppBindings.prototype, "mode", void 0);
237
+ __decorate([
238
+ property({ type: Boolean }),
239
+ __metadata("design:type", Boolean)
240
+ ], AppBindings.prototype, "failed", void 0);
241
+ __decorate([
242
+ query('ox-grist'),
243
+ __metadata("design:type", Function)
244
+ ], AppBindings.prototype, "grist", void 0);
146
245
  AppBindings = __decorate([
147
246
  customElement('app-bindings-page')
148
247
  ], AppBindings);
248
+ export { AppBindings };
149
249
  //# sourceMappingURL=app-bindings.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"app-bindings.js","sourceRoot":"","sources":["../../../client/pages/app-binding/app-bindings.ts"],"names":[],"mappings":";AAAA,OAAO,yCAAyC,CAAA;AAChD,OAAO,yCAAyC,CAAA;AAEhD,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAA;AAC/B,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAC3D,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAGnD,IAAM,WAAW,GAAjB,MAAM,WAAY,SAAQ,QAAQ;IAAlC;;QAuE6B,gBAAW,GAAU,EAAE,CAAA;IA4DpD,CAAC;aAnI6C,WAAM,GAAG;QACnD,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAmEF;KACF,AArEiD,CAqEjD;IAID,IAAI,OAAO;QAAS,OAAO,EAAQ,KAAK,EAAE,sBAAsB;YAC5D,IAAI,EAAE,kBAAkB;SAC5B,CAAA;IACD,CAAC;IAEA,MAAM;QAAS,IAAI,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,EAAE,CAAA;QAErD,OAAO,IAAI,CAAA;;;;;;;;;;;;UAYL,WAAW,CAAC,GAAG,CACf,UAAU,CAAC,EAAE,CAAC,IAAI,CAAA;;;0BAGF,eAAe,UAAU,CAAC,EAAE,EAAE,IAAI,UAAU,CAAC,IAAI;kBACzD,UAAU,CAAC,WAAW;;oBAEpB,UAAU,CAAC,KAAK;oBAChB,UAAU,CAAC,MAAM;;WAE1B,CACF;;mCAE0B,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,cAAc,CAAC;KAC3D,CAAA;IACJ,CAAC;IAEA,KAAK,CAAC,WAAW,CAAC,OAAO,EAAE,SAAS,EAAE,MAAM;QAAQ,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAAO,IAAI,CAAC,WAAW,GAAG,CAAC,MAAM,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC,KAAK,CAAA;QACtI,CAAC;IACD,CAAC;IAEA,KAAK,CAAC,gBAAgB;QAAS,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,EAAQ,KAAK,EAAE,GAAG,CAAA;;;;;;;;;;;;OAYhF;SACL,CAAC,CAAA;QAEC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;YAAO,OAAO,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAA;QACjE,CAAC;IACD,CAAC;;AA3D2B;IAA1B,QAAQ,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;;gDAAwB;AAvE9C,WAAW;IADhB,aAAa,CAAC,mBAAmB,CAAC;GAC7B,WAAW,CAmIhB","sourcesContent":["import '@material/web/button/elevated-button.js'\nimport '@material/web/button/outlined-button.js'\n\nimport gql from 'graphql-tag'\nimport { css, html } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport { client } from '@operato/graphql'\nimport { navigate, PageView } from '@operato/shell'\n\n@customElement('app-bindings-page')\nclass AppBindings extends PageView { static styles = [\n css`\n :host { background-color: var(--md-sys-color-background);\n padding: var(--spacing-large);\n\n overflow: auto;\n }\n\n md-elevated-button { text-transform: capitalize;\n }\n\n md-outlined-button { float: right;\n margin-top: var(--spacing-medium);\n text-transform: capitalize;\n }\n\n h2 { margin: var(--title-margin);\n font: var(--title-font);\n color: var(--title-text-color);\n }\n\n [page-description] { margin: var(--page-description-margin);\n font: var(--page-description-font);\n color: var(--page-description-color);\n }\n\n table { width: 100%;\n margin: var(--spacing-large) 0;\n border-collapse: collapse;\n }\n\n tr { background-color: var(--tr-background-color);\n }\n\n tr:nth-child(odd) { background-color: var(--tr-background-odd-color);\n }\n\n tr:hover { background-color: var(--tr-background-hover-color);\n }\n\n th { border-top: var(--th-border-top);\n border-bottom: var(--td-border-bottom);\n padding: var(--th-padding);\n\n font: var(--th-font);\n color: var(--th-color);\n text-transform: var(--th-text-transform);\n text-align: left;\n }\n\n td { padding: var(--td-padding);\n border-bottom: var(--td-border-bottom);\n font: var(--td-font);\n color: var(--td-color);\n }\n\n td a { color: var(--md-sys-color-primary);\n font: bold 16px var(--theme-font);\n\n display: block;\n text-decoration: none;\n }\n\n .text-align-center { text-align: center;\n }\n\n .text-align-right { text-align: right;\n }\n `\n ]\n\n @property({ type: Array }) appBindings: any[] = []\n\n get context() { return { title: `application bindings`,\n help: 'auth/application'\n }\n }\n\n render() { var appBindings = this.appBindings || []\n\n return html`\n <div>\n <h2>Bound Applications</h2>\n <p page-description>Bound Applications goes here</p>\n </div>\n\n <table>\n <tr>\n <th>app name</th>\n <th>Scope</th>\n <th>status</th>\n </tr>\n ${appBindings.map(\n appBinding => html`\n <tr>\n <td>\n <a href=${`app-binding/${appBinding.id}`}>${appBinding.name}</a>\n ${appBinding.description}\n </td>\n <td>${appBinding.scope}</td>\n <td>${appBinding.status}</td>\n </tr>\n `\n )}\n </table>\n <md-elevated-button @click=${e => navigate('applications')}>registered applications ..</md-elevated-button>\n `\n }\n\n async pageUpdated(changes, lifecycle, before) { if (this.active) { this.appBindings = (await this.fetchAppBindings()).items\n }\n }\n\n async fetchAppBindings() { const response = await client.query({ query: gql`\n query { appBindings { items { id\n name\n application { id\n name\n }\n scope\n status\n }\n total\n }\n }\n `\n })\n\n if (!response.errors) { return response.data.appBindings\n }\n }\n}\n"]}
1
+ {"version":3,"file":"app-bindings.js","sourceRoot":"","sources":["../../../client/pages/app-binding/app-bindings.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport '@operato/data-grist/ox-grist.js'\nimport '@operato/context/ox-context-page-toolbar.js'\n\nimport gql from 'graphql-tag'\nimport { css, html } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\n\nimport type { DataGrist, FetchOption } from '@operato/data-grist'\nimport { client, gqlContext } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { openPopup } from '@operato/layout'\nimport { navigate, PageView } from '@operato/shell'\nimport { CommonHeaderStyles, CommonGristStyles, ScrollbarStyles } from '@operato/styles'\nimport { isMobileDevice } from '@operato/utils'\nimport { p13n } from '@operato/p13n'\n\nimport '../../components/app-binding-detail.js'\nimport { listOutcome, pageFromResponse } from '../../utils/list-result.js'\n\n/**\n * Applications bound to this domain.\n *\n * A binding is a `User` row with `userType: 'application'` — the account an application signs\n * in as. The list shows who it is and what it may do; what it signs in *with* is elsewhere.\n *\n * ── The server answers honestly now ──────────────────────────────────────────\n * Until dde62cd6a1 this query paged before it filtered: the database took a page of users of\n * every type, JavaScript kept the applications among them, and the total reported how many\n * survived that one page. A grid told there were three when there were thirty never offered a\n * second page. Wiring paging up was pointless until that was fixed; it works now.\n *\n * ── ⚠ The refresh token is not asked for, and a test holds that ──────────────\n * AppBinding's refresh token is the application's actual credential — oauth2-server.ts writes\n * it into the password column verbatim, not hashed — and the field carries its own\n * privilege directive for security:query.\n *\n * A credential that can be exchanged for access tokens does not belong in a grid cell, where\n * it sits in plain text beside a name for anyone who can open the screen, and in any\n * screenshot of it. `app-binding-list-query.test.ts` fails if the word appears here.\n */\n@customElement('app-bindings-page')\nexport class AppBindings extends p13n(localize(i18next)(PageView)) {\n static styles = [\n CommonGristStyles,\n CommonHeaderStyles,\n ScrollbarStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n }\n\n ox-grist {\n overflow-y: auto;\n flex: 1;\n }\n\n [failed] {\n padding: var(--spacing-medium);\n color: var(--md-sys-color-error);\n font: var(--label-font, inherit);\n }\n `\n ]\n\n @property({ type: Boolean }) active: boolean = false\n @property({ type: Object }) config: any\n @property({ type: String }) mode: 'CARD' | 'LIST' | 'GRID' = isMobileDevice() ? 'CARD' : 'GRID'\n\n /** The last fetch did not answer. Distinct from answering with nothing. */\n @property({ type: Boolean }) failed: boolean = false\n\n @query('ox-grist') private grist!: DataGrist\n\n get context() {\n return {\n title: i18next.t('text.bound applications'),\n help: 'auth/application',\n search: {\n handler: (search: string) => {\n this.grist.searchText = search\n },\n value: this.grist?.searchText || ''\n },\n actions: [\n {\n title: i18next.t('text.applications'),\n action: () => navigate('applications'),\n icon: 'apps'\n }\n ],\n toolbar: false\n }\n }\n\n render() {\n return html`\n <ox-grist\n .mode=${this.mode}\n .config=${this.config}\n .fetchHandler=${this.fetchHandler.bind(this)}\n .personalConfigProvider=${this.getPagePreferenceProvider('ox-grist')!}\n >\n <div slot=\"headroom\" class=\"header\">\n <ox-context-page-toolbar class=\"actions\" .context=${this.context}></ox-context-page-toolbar>\n </div>\n </ox-grist>\n\n ${this.failed ? html`<div failed>${i18next.t('text.the list could not be loaded')}</div>` : ''}\n `\n }\n\n async pageInitialized() {\n this.config = {\n rows: { appendable: false, selectable: { multiple: false } },\n columns: [\n { type: 'gutter', gutterName: 'sequence' },\n {\n type: 'gutter',\n gutterName: 'button',\n icon: 'chevron_right',\n title: i18next.t('button.detail'),\n handlers: {\n click: (_columns: any, _data: any, _column: any, record: any) => {\n if (record?.id) {\n this.openDetail(record.id)\n }\n }\n }\n },\n {\n type: 'string',\n name: 'name',\n header: i18next.t('field.name'),\n record: { editable: false },\n sortable: true,\n width: 180\n },\n {\n type: 'string',\n name: 'applicationName',\n header: i18next.t('label.application'),\n record: { editable: false },\n width: 180\n },\n {\n type: 'string',\n name: 'email',\n header: i18next.t('field.email'),\n record: { editable: false },\n sortable: true,\n width: 220\n },\n {\n type: 'string',\n name: 'scope',\n header: i18next.t('field.scope'),\n record: { editable: false },\n width: 220\n },\n {\n type: 'string',\n name: 'status',\n header: i18next.t('field.status'),\n record: { align: 'center', editable: false },\n sortable: true,\n width: 130\n },\n {\n type: 'datetime',\n name: 'updatedAt',\n header: i18next.t('field.updated at'),\n sortable: true,\n width: 180\n }\n ]\n }\n }\n\n /*\n * A binding is not authored — it is what an application ends up with after the OAuth flow —\n * so this popup shows one and offers the two things a person can do to it. `app-binding/{id}`\n * was a route; nothing outside auth-ui linked to it.\n */\n private openDetail(appBindingId: string) {\n const popup = openPopup(\n html`<app-binding-detail\n .appBindingId=${appBindingId}\n @app-binding-changed=${() => this.grist.fetch()}\n @close=${() => popup.close()}\n ></app-binding-detail>`,\n {\n backdrop: true,\n size: 'medium',\n title: i18next.t('text.bound application')\n }\n )\n }\n\n async pageUpdated(_changes: any, _lifecycle: any) {\n if (this.active) {\n await this.updateComplete\n this.grist.fetch()\n }\n }\n\n async fetchHandler({ page, limit, sortings = [], filters = [] }: FetchOption) {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n appBindings(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items {\n id\n name\n email\n status\n scope\n updatedAt\n application {\n id\n name\n }\n }\n total\n }\n }\n `,\n variables: { filters, pagination: { page, limit }, sortings },\n context: gqlContext()\n })\n\n const { items, total, failed } = listOutcome(pageFromResponse(response, 'appBindings'))\n\n this.failed = failed\n\n return {\n total,\n records: items.map((item: any) => ({ ...item, applicationName: item.application?.name || '' }))\n }\n }\n}\n"]}
@@ -0,0 +1,58 @@
1
+ import { readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ /**
4
+ * A list query must not ask for a field that carries its own privilege gate.
5
+ *
6
+ * `Appliance.accessToken` is declared with
7
+ * `@privilege(category: "security", privilege: "query", domainOwnerGranted: true)` on the
8
+ * field. A viewer without that privilege who asks for it gets the rows **and** an error
9
+ * about the one field.
10
+ *
11
+ * ⚠ An earlier version of this comment said that would black the whole list out. It would
12
+ * have, until `pageFromResponse` learned to read the error's path and keep a page whose only
13
+ * complaint points at a value inside it. That reasoning is gone; the rule stands on its own
14
+ * ground instead:
15
+ *
16
+ * A token in a list is a token shown to everyone who may open the list, sitting in a grid
17
+ * cell, in plain text, next to a name. Nothing about the appliance list needs it, and the
18
+ * screens that do need it ask for it one record at a time. It is one word in a query and
19
+ * nothing else in the build objects to it, which is why a test holds it rather than a
20
+ * comment.
21
+ *
22
+ * ⚠ The comments are stripped before searching. The doc comment above names `accessToken`
23
+ * three times; searching the raw file would find the prose and pass over a query that
24
+ * really does ask for it. That is the defect this repository produced eight times in one
25
+ * day — a measurement that reads the same words as the thing it measures.
26
+ */
27
+ function source(name) {
28
+ const text = readFileSync(join(__dirname, name), 'utf-8');
29
+ return text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^[ \t]*\/\/.*$/gm, '');
30
+ }
31
+ /** The fields Appliance declares with no gate of their own. */
32
+ const UNGATED = ['name', 'serialNo', 'brand', 'model', 'netmask', 'description', 'updatedAt'];
33
+ describe('the appliance list query', () => {
34
+ it('does not ask for the access token', () => {
35
+ expect(source('./home.ts')).not.toContain('accessToken');
36
+ });
37
+ it('asks for the fields the columns render', () => {
38
+ /*
39
+ * The other half. A column whose field is not requested renders empty for every row,
40
+ * which reads as "this appliance has no serial number" rather than "nobody asked".
41
+ */
42
+ const text = source('./home.ts');
43
+ for (const field of UNGATED) {
44
+ expect(text).toContain(field);
45
+ }
46
+ });
47
+ it('passes the grist options through, so paging and sorting reach the server', () => {
48
+ /*
49
+ * The screen this replaced fetched everything on every visit and sorted nothing. The
50
+ * server takes ListParam; forgetting to pass it is invisible until the table is long.
51
+ */
52
+ const text = source('./home.ts');
53
+ expect(text).toContain('$pagination: Pagination');
54
+ expect(text).toContain('$sortings: [Sorting!]');
55
+ expect(text).toContain('$filters: [Filter!]');
56
+ });
57
+ });
58
+ //# sourceMappingURL=appliance-list-query.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"appliance-list-query.test.js","sourceRoot":"","sources":["../../../client/pages/appliance/appliance-list-query.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAA;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAE3B;;;;;;;;;;;;;;;;;;;;;;;GAuBG;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,+DAA+D;AAC/D,MAAM,OAAO,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,WAAW,CAAC,CAAA;AAE7F,QAAQ,CAAC,0BAA0B,EAAE,GAAG,EAAE;IACxC,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC3C,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,aAAa,CAAC,CAAA;IAC1D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD;;;WAGG;QACH,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAAC,CAAA;QAEhC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,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;;;WAGG;QACH,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAAC,CAAA;QAEhC,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 * A list query must not ask for a field that carries its own privilege gate.\n *\n * `Appliance.accessToken` is declared with\n * `@privilege(category: \"security\", privilege: \"query\", domainOwnerGranted: true)` on the\n * field. A viewer without that privilege who asks for it gets the rows **and** an error\n * about the one field.\n *\n * ⚠ An earlier version of this comment said that would black the whole list out. It would\n * have, until `pageFromResponse` learned to read the error's path and keep a page whose only\n * complaint points at a value inside it. That reasoning is gone; the rule stands on its own\n * ground instead:\n *\n * A token in a list is a token shown to everyone who may open the list, sitting in a grid\n * cell, in plain text, next to a name. Nothing about the appliance list needs it, and the\n * screens that do need it ask for it one record at a time. It is one word in a query and\n * nothing else in the build objects to it, which is why a test holds it rather than a\n * comment.\n *\n * ⚠ The comments are stripped before searching. The doc comment above names `accessToken`\n * three times; searching the raw file would find the prose and pass over a query that\n * really does ask for it. That is the defect this repository produced eight times in one\n * day — a measurement that reads the same words as the thing it measures.\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\n/** The fields Appliance declares with no gate of their own. */\nconst UNGATED = ['name', 'serialNo', 'brand', 'model', 'netmask', 'description', 'updatedAt']\n\ndescribe('the appliance list query', () => {\n it('does not ask for the access token', () => {\n expect(source('./home.ts')).not.toContain('accessToken')\n })\n\n it('asks for the fields the columns render', () => {\n /*\n * The other half. A column whose field is not requested renders empty for every row,\n * which reads as \"this appliance has no serial number\" rather than \"nobody asked\".\n */\n const text = source('./home.ts')\n\n for (const field of UNGATED) {\n expect(text).toContain(field)\n }\n })\n\n it('passes the grist options through, so paging and sorting reach the server', () => {\n /*\n * The screen this replaced fetched everything on every visit and sorted nothing. The\n * server takes ListParam; forgetting to pass it is invisible until the table is long.\n */\n const text = source('./home.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,63 @@
1
- import '@material/web/button/elevated-button.js';
2
- import '@material/web/button/outlined-button.js';
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/appliance-editor.js';
7
+ declare const Appliances_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
+ * Registered appliances.
15
+ *
16
+ * ── What this screen looked like before ──────────────────────────────────────
17
+ * A hand-written table with no paging, sorting or search, and three columns that were not
18
+ * data: "API health" printed OK, "Installs" printed 1, "status" printed draft, identical on
19
+ * every row. An appliance that had never checked in read exactly like one reporting fine.
20
+ * Those are gone — Appliance declares none of the three — and the columns here are the
21
+ * fields it does declare.
22
+ *
23
+ * ── ⚠ The access token is not asked for, and a test holds that ──────────────
24
+ * `Appliance.accessToken` carries its own
25
+ * `@privilege(category: "security", privilege: "query", domainOwnerGranted: true)`. A list
26
+ * is the wrong place for it regardless of who may read it: a token in a grid cell is a token
27
+ * in plain text beside a name, visible to everyone who can open the screen and to anyone
28
+ * looking over their shoulder. The screens that genuinely need it ask one record at a time.
29
+ *
30
+ * `appliance-list-query.test.ts` fails if the word reappears here.
31
+ */
32
+ export declare class Appliances extends Appliances_base {
33
+ static styles: import("lit").CSSResult[];
34
+ active: boolean;
35
+ config: any;
36
+ mode: 'CARD' | 'LIST' | 'GRID';
37
+ /** The last fetch did not answer. Distinct from answering with nothing. */
38
+ failed: boolean;
39
+ private grist;
40
+ get context(): {
41
+ title: string;
42
+ help: string;
43
+ search: {
44
+ handler: (search: string) => void;
45
+ value: string;
46
+ };
47
+ actions: {
48
+ title: string;
49
+ action: () => void;
50
+ icon: string;
51
+ }[];
52
+ toolbar: boolean;
53
+ };
54
+ render(): import("lit-html").TemplateResult<1>;
55
+ pageInitialized(): Promise<void>;
56
+ private openEditor;
57
+ pageUpdated(_changes: any, _lifecycle: any): Promise<void>;
58
+ fetchHandler({ page, limit, sortings, filters }: FetchOption): Promise<{
59
+ total: number;
60
+ records: any[];
61
+ }>;
62
+ }
63
+ export {};