@things-factory/auth-ui 10.1.192 → 10.1.198

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 (51) hide show
  1. package/dist-client/{entries/auth/activate-page.test.js → activate-page.test.js} +4 -1
  2. package/dist-client/activate-page.test.js.map +1 -0
  3. package/dist-client/bootstrap.js +4 -1
  4. package/dist-client/bootstrap.js.map +1 -1
  5. package/dist-client/components/my-login-history.js +14 -10
  6. package/dist-client/components/my-login-history.js.map +1 -1
  7. package/dist-client/components/role-template-picker.d.ts +5 -0
  8. package/dist-client/components/role-template-picker.js +41 -5
  9. package/dist-client/components/role-template-picker.js.map +1 -1
  10. package/dist-client/components/user-status-renderer.d.ts +9 -0
  11. package/dist-client/components/user-status-renderer.js +36 -0
  12. package/dist-client/components/user-status-renderer.js.map +1 -0
  13. package/dist-client/entity360/person-360.d.ts +13 -0
  14. package/dist-client/entity360/person-360.js +96 -0
  15. package/dist-client/entity360/person-360.js.map +1 -1
  16. package/dist-client/entries-hold-only-pages.test.d.ts +1 -0
  17. package/dist-client/entries-hold-only-pages.test.js +25 -0
  18. package/dist-client/entries-hold-only-pages.test.js.map +1 -0
  19. package/dist-client/index.d.ts +1 -0
  20. package/dist-client/index.js +14 -1
  21. package/dist-client/index.js.map +1 -1
  22. package/dist-client/pages/audit/audit-page.d.ts +46 -0
  23. package/dist-client/pages/audit/audit-page.js +417 -0
  24. package/dist-client/pages/audit/audit-page.js.map +1 -0
  25. package/dist-client/pages/role/role-privilege-page.d.ts +38 -0
  26. package/dist-client/pages/role/role-privilege-page.js +441 -5
  27. package/dist-client/pages/role/role-privilege-page.js.map +1 -1
  28. package/dist-client/pages/role/role-task-review.test.d.ts +1 -0
  29. package/dist-client/pages/role/role-task-review.test.js +49 -0
  30. package/dist-client/pages/role/role-task-review.test.js.map +1 -0
  31. package/dist-client/pages/user/user-list-page.js +1 -31
  32. package/dist-client/pages/user/user-list-page.js.map +1 -1
  33. package/dist-client/route.js +3 -0
  34. package/dist-client/route.js.map +1 -1
  35. package/dist-client/tsconfig.tsbuildinfo +1 -1
  36. package/dist-client/utils/audit-words.d.ts +30 -0
  37. package/dist-client/utils/audit-words.js +57 -0
  38. package/dist-client/utils/audit-words.js.map +1 -0
  39. package/dist-client/utils/audit-words.test.d.ts +1 -0
  40. package/dist-client/utils/audit-words.test.js +47 -0
  41. package/dist-client/utils/audit-words.test.js.map +1 -0
  42. package/package.json +4 -4
  43. package/things-factory.config.js +1 -0
  44. package/translations/en.json +68 -1
  45. package/translations/es.json +68 -1
  46. package/translations/ja.json +68 -1
  47. package/translations/ko.json +68 -1
  48. package/translations/ms.json +68 -1
  49. package/translations/zh.json +68 -1
  50. package/dist-client/entries/auth/activate-page.test.js.map +0 -1
  51. /package/dist-client/{entries/auth/activate-page.test.d.ts → activate-page.test.d.ts} +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"person-360.js","sourceRoot":"","sources":["../../client/entity360/person-360.ts"],"names":[],"mappings":"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","sourcesContent":["import gql from 'graphql-tag'\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { client } from '@operato/graphql'\nimport { i18next } from '@operato/i18n'\nimport {\n readEntity360Door,\n registerEntity360Kind,\n registerEntity360Section,\n type Entity360DoorAnswer,\n type Entity360Subject\n} from '@operato/layout'\n\n/**\n * The person 360 (ADR-0067, design/ui/entity360-person.md): a `User` value anywhere - a grid's\n * `updater`, a request's author - opens it. auth-ui declares the kind because the account is its\n * fact; organization and worklist hang their own sections on it with their own doors.\n *\n * This section is identity and the roles held on this site. `userRoles` answers this domain's roles\n * only (and its parent's) - `User.roles` answers every domain's and must not be used here.\n */\n\nexport function registerPersonKind() {\n registerEntity360Kind({\n kind: 'person',\n typenames: ['User'],\n title: ({ value }) => value.name || value.email || '',\n context: ({ value }) => (value.name && value.email ? value.email : '')\n })\n\n registerEntity360Section('person', {\n name: 'roles',\n order: 30,\n render: subject => html`<person-roles-section .subject=${subject}></person-roles-section>`\n })\n}\n\n@customElement('person-roles-section')\nexport class PersonRolesSection extends LitElement {\n static styles = css`\n ul {\n margin: 0;\n padding: 0;\n list-style: none;\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n }\n li {\n padding: 2px 8px;\n border-radius: var(--md-sys-shape-corner-small, 8px);\n background: var(--md-sys-color-surface-container-high);\n color: var(--md-sys-color-on-surface);\n font: 13px/1.5 sans-serif;\n }\n `\n\n @property({ attribute: false }) subject?: Entity360Subject\n @state() answer?: Entity360DoorAnswer<{ id: string; name: string }[]>\n\n updated(changed: Map<string, unknown>) {\n if (changed.has('subject')) this.read()\n }\n\n private async read() {\n const id = this.subject?.id\n this.answer = undefined\n if (!id) return\n\n const response = await client.query({\n query: gql`\n query ($userId: String!) {\n userRoles(userId: $userId) {\n id\n name\n assigned\n }\n }\n `,\n variables: { userId: id }\n })\n\n /* A later subject may have arrived while this one was being read. */\n if (this.subject?.id !== id) return\n this.answer = readEntity360Door(response, data => (data?.userRoles || []).filter((role: any) => role.assigned))\n }\n\n render() {\n const answer = this.answer\n return html`<ox-entity360-section\n .heading=${i18next.t('field.roles', { defaultValue: 'roles' })}\n .state=${answer?.state ?? 'loading'}\n .message=${answer?.message ?? ''}\n >\n <ul>\n ${(answer?.data || []).map(role => html`<li>${role.name}</li>`)}\n </ul>\n </ox-entity360-section>`\n }\n}\n"]}
1
+ {"version":3,"file":"person-360.js","sourceRoot":"","sources":["../../client/entity360/person-360.ts"],"names":[],"mappings":"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","sourcesContent":["import gql from 'graphql-tag'\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { client } from '@operato/graphql'\nimport { i18next } from '@operato/i18n'\n\nimport { lastDays, recordWords } from '../utils/audit-words.js'\nimport {\n readEntity360Door,\n registerEntity360Kind,\n registerEntity360Section,\n type Entity360DoorAnswer,\n type Entity360Subject\n} from '@operato/layout'\n\n/**\n * The person 360 (ADR-0067, design/ui/entity360-person.md): a `User` value anywhere - a grid's\n * `updater`, a request's author - opens it. auth-ui declares the kind because the account is its\n * fact; organization and worklist hang their own sections on it with their own doors.\n *\n * This section is identity and the roles held on this site. `userRoles` answers this domain's roles\n * only (and its parent's) - `User.roles` answers every domain's and must not be used here.\n */\n\nexport function registerPersonKind() {\n registerEntity360Kind({\n kind: 'person',\n typenames: ['User'],\n title: ({ value }) => value.name || value.email || '',\n context: ({ value }) => (value.name && value.email ? value.email : '')\n })\n\n registerEntity360Section('person', {\n name: 'roles',\n order: 30,\n render: subject => html`<person-roles-section .subject=${subject}></person-roles-section>`\n })\n}\n\n/** The person's last requests in this domain - for those who hold `audit:query` (ADR-0141 §6.1). */\nexport function registerPersonAuditSection() {\n registerEntity360Section('person', {\n name: 'audit',\n order: 40,\n render: subject => html`<person-audit-section .subject=${subject}></person-audit-section>`\n })\n}\n\nconst AUDIT_SHOWN = 10\n\n@customElement('person-audit-section')\nexport class PersonAuditSection extends LitElement {\n static styles = css`\n ul {\n margin: 0;\n padding: 0;\n list-style: none;\n display: grid;\n grid-template-columns: max-content 1fr;\n gap: 4px 12px;\n font: 13px/1.5 sans-serif;\n color: var(--md-sys-color-on-surface);\n }\n li {\n display: contents;\n }\n time {\n color: var(--md-sys-color-on-surface-variant);\n font-variant-numeric: tabular-nums;\n }\n `\n\n @property({ attribute: false }) subject?: Entity360Subject\n @state() answer?: Entity360DoorAnswer<{ at: string; what: string }[]>\n\n updated(changed: Map<string, unknown>) {\n if (changed.has('subject')) this.read()\n }\n\n private async read() {\n const id = this.subject?.id\n this.answer = undefined\n if (!id) return\n\n const { from, to } = lastDays(30)\n const response = await client.query({\n query: gql`\n query ($filter: AuditRecordFilter!) {\n auditRecords(filter: $filter) {\n items {\n request {\n createdAt\n }\n actions {\n kind\n door\n decision\n outcome\n code\n count\n detail\n }\n }\n }\n }\n `,\n variables: { filter: { from: from.toISOString(), to: to.toISOString(), actorId: id, limit: AUDIT_SHOWN } },\n fetchPolicy: 'no-cache'\n })\n\n if (this.subject?.id !== id) return\n const t = i18next.t.bind(i18next)\n this.answer = readEntity360Door(response, data =>\n (data?.auditRecords?.items || [])\n .map((item: any) => ({ at: item.request.createdAt, what: recordWords(t, item.actions) }))\n .filter((line: any) => line.what)\n )\n }\n\n render() {\n const answer = this.answer\n return html`<ox-entity360-section\n .heading=${i18next.t('audit.title')}\n .state=${answer?.state ?? 'loading'}\n .message=${answer?.message ?? ''}\n >\n <ul>\n ${(answer?.data || []).map(line => html`<li><time>${new Date(line.at).toLocaleString()}</time><span>${line.what}</span></li>`)}\n </ul>\n </ox-entity360-section>`\n }\n}\n\n@customElement('person-roles-section')\nexport class PersonRolesSection extends LitElement {\n static styles = css`\n ul {\n margin: 0;\n padding: 0;\n list-style: none;\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n }\n li {\n padding: 2px 8px;\n border-radius: var(--md-sys-shape-corner-small, 8px);\n background: var(--md-sys-color-surface-container-high);\n color: var(--md-sys-color-on-surface);\n font: 13px/1.5 sans-serif;\n }\n `\n\n @property({ attribute: false }) subject?: Entity360Subject\n @state() answer?: Entity360DoorAnswer<{ id: string; name: string }[]>\n\n updated(changed: Map<string, unknown>) {\n if (changed.has('subject')) this.read()\n }\n\n private async read() {\n const id = this.subject?.id\n this.answer = undefined\n if (!id) return\n\n const response = await client.query({\n query: gql`\n query ($userId: String!) {\n userRoles(userId: $userId) {\n id\n name\n assigned\n }\n }\n `,\n variables: { userId: id }\n })\n\n /* A later subject may have arrived while this one was being read. */\n if (this.subject?.id !== id) return\n this.answer = readEntity360Door(response, data => (data?.userRoles || []).filter((role: any) => role.assigned))\n }\n\n render() {\n const answer = this.answer\n return html`<ox-entity360-section\n .heading=${i18next.t('field.roles', { defaultValue: 'roles' })}\n .state=${answer?.state ?? 'loading'}\n .message=${answer?.message ?? ''}\n >\n <ul>\n ${(answer?.data || []).map(role => html`<li>${role.name}</li>`)}\n </ul>\n </ox-entity360-section>`\n }\n}\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ import { readdirSync, statSync } from 'fs';
2
+ import { join } from 'path';
3
+ /**
4
+ * Nothing but pages lives under `entries/`.
5
+ *
6
+ * The builder takes every `.js` under a module's `dist-client/entries/` as a webpack entry
7
+ * (`builder/controller/entries-builder.js`), and the client tsconfig compiles every file here. A
8
+ * test placed beside its page was compiled, published in 10.1.192, and made an entry: the
9
+ * production image's `build:app` failed resolving the test's `fs`. The local build runs tsc only,
10
+ * so it did not see it.
11
+ */
12
+ const ENTRIES = join(__dirname, 'entries');
13
+ function filesUnder(dir) {
14
+ return readdirSync(dir).flatMap(entry => {
15
+ const path = join(dir, entry);
16
+ return statSync(path).isDirectory() ? filesUnder(path) : [path];
17
+ });
18
+ }
19
+ it('no test is placed under entries/', () => {
20
+ const tests = filesUnder(ENTRIES)
21
+ .filter(path => /\.(test|spec)\.[jt]s$/.test(path))
22
+ .map(path => path.slice(ENTRIES.length + 1));
23
+ expect(tests).toEqual([]);
24
+ });
25
+ //# sourceMappingURL=entries-hold-only-pages.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entries-hold-only-pages.test.js","sourceRoot":"","sources":["../client/entries-hold-only-pages.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAA;AAC1C,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAE3B;;;;;;;;GAQG;AAEH,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;AAE1C,SAAS,UAAU,CAAC,GAAW;IAC7B,OAAO,WAAW,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE;QACtC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;QAE7B,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;IACjE,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;IAC1C,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC;SAC9B,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;SAClD,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAA;IAE9C,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;AAC3B,CAAC,CAAC,CAAA","sourcesContent":["import { readdirSync, statSync } from 'fs'\nimport { join } from 'path'\n\n/**\n * Nothing but pages lives under `entries/`.\n *\n * The builder takes every `.js` under a module's `dist-client/entries/` as a webpack entry\n * (`builder/controller/entries-builder.js`), and the client tsconfig compiles every file here. A\n * test placed beside its page was compiled, published in 10.1.192, and made an entry: the\n * production image's `build:app` failed resolving the test's `fs`. The local build runs tsc only,\n * so it did not see it.\n */\n\nconst ENTRIES = join(__dirname, 'entries')\n\nfunction filesUnder(dir: string): string[] {\n return readdirSync(dir).flatMap(entry => {\n const path = join(dir, entry)\n\n return statSync(path).isDirectory() ? filesUnder(path) : [path]\n })\n}\n\nit('no test is placed under entries/', () => {\n const tests = filesUnder(ENTRIES)\n .filter(path => /\\.(test|spec)\\.[jt]s$/.test(path))\n .map(path => path.slice(ENTRIES.length + 1))\n\n expect(tests).toEqual([])\n})\n"]}
@@ -11,5 +11,6 @@ export type AuthManagementMenuOptions = {
11
11
  authProvider?: boolean;
12
12
  domain?: boolean;
13
13
  attribute?: boolean;
14
+ audit?: boolean;
14
15
  };
15
16
  export declare function setAuthManagementMenus(options?: any): Promise<void>;
@@ -8,7 +8,7 @@ export * from './components/role-selector.js';
8
8
  export * from './components/domain-switch.js';
9
9
  export * from './components/invite-user.js';
10
10
  export async function setAuthManagementMenus(options) {
11
- const { role = true, appliance = true, application = true, user = true, authProvider = true, domain = true, attribute = true } = options || {};
11
+ const { role = true, appliance = true, application = true, user = true, authProvider = true, domain = true, attribute = true, audit = true } = options || {};
12
12
  if (await hasPrivilege({ privilege: 'mutation', category: 'user', domainOwnerGranted: true, superUserGranted: true })) {
13
13
  role &&
14
14
  store.dispatch({
@@ -66,6 +66,19 @@ export async function setAuthManagementMenus(options) {
66
66
  }
67
67
  });
68
68
  }
69
+ /* The audit asks its own privilege - the domain owner does not pass on ownership (ADR-0141 §5). */
70
+ if (audit && (await hasPrivilege({ privilege: 'query', category: 'audit', superUserGranted: true }))) {
71
+ store.dispatch({
72
+ type: ADD_MORENDA,
73
+ morenda: {
74
+ icon: html ` <md-icon>policy</md-icon> `,
75
+ name: html ` <ox-i18n msgid="audit.title"></ox-i18n> `,
76
+ action: () => {
77
+ navigate('audit');
78
+ }
79
+ }
80
+ });
81
+ }
69
82
  if (await hasPrivilege({ domainOwnerGranted: true, superUserGranted: true })) {
70
83
  domain &&
71
84
  store.dispatch({
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../client/index.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@operato/i18n/ox-i18n.js'\n\nimport { html } from 'lit-html'\n\nimport { navigate, store } from '@operato/shell'\nimport { ADD_MORENDA } from '@things-factory/more-base/client'\nimport { hasPrivilege } from '@things-factory/auth-base/dist-client'\n\nexport * from './components/role-selector.js'\nexport * from './components/domain-switch.js'\nexport * from './components/invite-user.js'\n\nexport type AuthManagementMenuOptions = {\n role?: boolean\n appliance?: boolean\n application?: boolean\n user?: boolean\n authProvider?: boolean\n domain?: boolean\n attribute?: boolean\n}\n\nexport async function setAuthManagementMenus(options?: any) {\n const {\n role = true,\n appliance = true,\n application = true,\n user = true,\n authProvider = true,\n domain = true,\n attribute = true\n } = options || {}\n\n if (\n await hasPrivilege({ privilege: 'mutation', category: 'user', domainOwnerGranted: true, superUserGranted: true })\n ) {\n role &&\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>how_to_reg</md-icon> `,\n name: html` <ox-i18n msgid=\"text.role_management\"></ox-i18n> `,\n action: () => {\n navigate('roles')\n }\n }\n })\n\n appliance &&\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>devices</md-icon> `,\n name: html` <ox-i18n msgid=\"text.appliance\"></ox-i18n> `,\n action: () => {\n navigate('appliance-home')\n }\n }\n })\n\n application &&\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>apps</md-icon> `,\n name: html` <ox-i18n msgid=\"text.application management\"></ox-i18n> `,\n action: () => {\n navigate('applications')\n }\n }\n })\n\n user &&\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>people</md-icon> `,\n name: html` <ox-i18n msgid=\"text.user management\"></ox-i18n> `,\n action: () => {\n navigate('users')\n }\n }\n })\n\n authProvider &&\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>id_card</md-icon> `,\n name: html` <ox-i18n msgid=\"text.auth-provider management\"></ox-i18n> `,\n action: () => {\n navigate('auth-providers')\n }\n }\n })\n }\n\n if (await hasPrivilege({ domainOwnerGranted: true, superUserGranted: true })) {\n domain &&\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>supervised_user_circle</md-icon> `,\n name: html` <ox-i18n msgid=\"text.domain owners\"></ox-i18n> `,\n action: () => {\n navigate('domain-owners')\n }\n }\n })\n }\n\n if (await hasPrivilege({ superUserGranted: true })) {\n domain &&\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>business</md-icon> `,\n name: html` <ox-i18n msgid=\"text.domain management\"></ox-i18n> `,\n action: () => {\n navigate('domains')\n }\n }\n })\n\n attribute &&\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>dataset</md-icon> `,\n name: html` <ox-i18n msgid=\"text.attribute management\"></ox-i18n> `,\n action: () => {\n navigate('attributes')\n }\n }\n })\n }\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../client/index.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@operato/i18n/ox-i18n.js'\n\nimport { html } from 'lit-html'\n\nimport { navigate, store } from '@operato/shell'\nimport { ADD_MORENDA } from '@things-factory/more-base/client'\nimport { hasPrivilege } from '@things-factory/auth-base/dist-client'\n\nexport * from './components/role-selector.js'\nexport * from './components/domain-switch.js'\nexport * from './components/invite-user.js'\n\nexport type AuthManagementMenuOptions = {\n role?: boolean\n appliance?: boolean\n application?: boolean\n user?: boolean\n authProvider?: boolean\n domain?: boolean\n attribute?: boolean\n audit?: boolean\n}\n\nexport async function setAuthManagementMenus(options?: any) {\n const {\n role = true,\n appliance = true,\n application = true,\n user = true,\n authProvider = true,\n domain = true,\n attribute = true,\n audit = true\n } = options || {}\n\n if (\n await hasPrivilege({ privilege: 'mutation', category: 'user', domainOwnerGranted: true, superUserGranted: true })\n ) {\n role &&\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>how_to_reg</md-icon> `,\n name: html` <ox-i18n msgid=\"text.role_management\"></ox-i18n> `,\n action: () => {\n navigate('roles')\n }\n }\n })\n\n appliance &&\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>devices</md-icon> `,\n name: html` <ox-i18n msgid=\"text.appliance\"></ox-i18n> `,\n action: () => {\n navigate('appliance-home')\n }\n }\n })\n\n application &&\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>apps</md-icon> `,\n name: html` <ox-i18n msgid=\"text.application management\"></ox-i18n> `,\n action: () => {\n navigate('applications')\n }\n }\n })\n\n user &&\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>people</md-icon> `,\n name: html` <ox-i18n msgid=\"text.user management\"></ox-i18n> `,\n action: () => {\n navigate('users')\n }\n }\n })\n\n authProvider &&\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>id_card</md-icon> `,\n name: html` <ox-i18n msgid=\"text.auth-provider management\"></ox-i18n> `,\n action: () => {\n navigate('auth-providers')\n }\n }\n })\n }\n\n /* The audit asks its own privilege - the domain owner does not pass on ownership (ADR-0141 §5). */\n if (audit && (await hasPrivilege({ privilege: 'query', category: 'audit', superUserGranted: true }))) {\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>policy</md-icon> `,\n name: html` <ox-i18n msgid=\"audit.title\"></ox-i18n> `,\n action: () => {\n navigate('audit')\n }\n }\n })\n }\n\n if (await hasPrivilege({ domainOwnerGranted: true, superUserGranted: true })) {\n domain &&\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>supervised_user_circle</md-icon> `,\n name: html` <ox-i18n msgid=\"text.domain owners\"></ox-i18n> `,\n action: () => {\n navigate('domain-owners')\n }\n }\n })\n }\n\n if (await hasPrivilege({ superUserGranted: true })) {\n domain &&\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>business</md-icon> `,\n name: html` <ox-i18n msgid=\"text.domain management\"></ox-i18n> `,\n action: () => {\n navigate('domains')\n }\n }\n })\n\n attribute &&\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>dataset</md-icon> `,\n name: html` <ox-i18n msgid=\"text.attribute management\"></ox-i18n> `,\n action: () => {\n navigate('attributes')\n }\n }\n })\n }\n}\n"]}
@@ -0,0 +1,46 @@
1
+ import '@material/web/icon/icon.js';
2
+ import '@operato/data-grist';
3
+ import type { CSSResultGroup } from 'lit';
4
+ import { PageView } from '@operato/shell';
5
+ import type { FetchOption } from '@operato/data-grist';
6
+ declare const AuditPageBase: (new (...args: any[]) => {
7
+ __preferenceProviders: {
8
+ [element: string]: import("@operato/p13n").PagePreferenceProvider;
9
+ };
10
+ getPagePreferenceProvider(element: string): import("@operato/p13n").PagePreferenceProvider | undefined;
11
+ }) & (new (...args: any[]) => import("lit").LitElement) & typeof PageView;
12
+ export declare class AuditPage extends AuditPageBase {
13
+ static styles: CSSResultGroup;
14
+ mode: 'CARD' | 'GRID' | 'LIST';
15
+ private view;
16
+ private days;
17
+ private from;
18
+ private to;
19
+ private actor?;
20
+ private door?;
21
+ private outcome;
22
+ private health?;
23
+ private grist;
24
+ get context(): {
25
+ title: string;
26
+ help: string;
27
+ filter: {
28
+ handler: () => void;
29
+ };
30
+ };
31
+ render(): import("lit-html").TemplateResult<1>;
32
+ private renderControls;
33
+ private renderHealth;
34
+ private show;
35
+ private lastDays;
36
+ private pick;
37
+ private narrow;
38
+ pageUpdated(_changes: any, _lifecycle: any): Promise<void>;
39
+ private readHealth;
40
+ private configOf;
41
+ fetchHandler({ page, limit }: FetchOption): Promise<{
42
+ total: any;
43
+ records: any;
44
+ }>;
45
+ }
46
+ export {};
@@ -0,0 +1,417 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import '@material/web/icon/icon.js';
3
+ import '@operato/data-grist';
4
+ import gql from 'graphql-tag';
5
+ import { css, html, nothing } from 'lit';
6
+ import { customElement, property, query, state } from 'lit/decorators.js';
7
+ import { keyed } from 'lit/directives/keyed.js';
8
+ import { client } from '@operato/graphql';
9
+ import { i18next, localize } from '@operato/i18n';
10
+ import { p13n } from '@operato/p13n';
11
+ import { PageView } from '@operato/shell';
12
+ import { ButtonStyles, CommonGristStyles, CommonHeaderStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles';
13
+ import { isMobileDevice } from '@operato/utils';
14
+ import { statusRenderer } from '../../components/user-status-renderer.js';
15
+ import { doorWords, lastDays, recordWords } from '../../utils/audit-words.js';
16
+ const PERIODS = [7, 30, 90];
17
+ const VIEWS = ['people', 'doors', 'records'];
18
+ const OUTCOMES = ['', 'denied', 'error', 'failed'];
19
+ const AuditPageBase = p13n(localize(i18next)(PageView));
20
+ function dateInputValue(date) {
21
+ const pad = (n) => String(n).padStart(2, '0');
22
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
23
+ }
24
+ let AuditPage = class AuditPage extends AuditPageBase {
25
+ constructor() {
26
+ super(...arguments);
27
+ this.mode = isMobileDevice() ? 'LIST' : 'GRID';
28
+ this.view = 'people';
29
+ this.days = 30;
30
+ this.from = lastDays(30).from;
31
+ this.to = new Date();
32
+ this.outcome = '';
33
+ }
34
+ static { this.styles = [
35
+ WorkSurfacePageStyles,
36
+ ScrollbarStyles,
37
+ CommonGristStyles,
38
+ CommonHeaderStyles,
39
+ ButtonStyles,
40
+ css `
41
+ .filters {
42
+ flex-wrap: wrap;
43
+ }
44
+
45
+ [group] {
46
+ display: flex;
47
+ align-items: center;
48
+ gap: var(--spacing-tiny, 2px);
49
+ }
50
+
51
+ button[aria-pressed='true'] {
52
+ background-color: var(--md-sys-color-secondary-container);
53
+ color: var(--md-sys-color-on-secondary-container);
54
+ }
55
+
56
+ input[type='date'] {
57
+ height: var(--context-action-height, 28px);
58
+ box-sizing: border-box;
59
+ border: 1px solid var(--md-sys-color-outline-variant);
60
+ border-radius: var(--md-sys-shape-corner-small, 4px);
61
+ background: var(--md-sys-color-surface-container-lowest);
62
+ color: var(--md-sys-color-on-surface);
63
+ font: inherit;
64
+ font-size: var(--ui-t-body, 13px);
65
+ padding: 0 var(--spacing-small, 4px);
66
+ }
67
+
68
+ [narrowed] {
69
+ display: flex;
70
+ align-items: center;
71
+ gap: var(--spacing-small, 4px);
72
+ font-size: var(--ui-t-body, 13px);
73
+ color: var(--md-sys-color-on-surface);
74
+ }
75
+
76
+ [health] {
77
+ margin-left: auto;
78
+ font-size: var(--ui-t-body, 13px);
79
+ color: var(--ui-crit, var(--md-sys-color-error));
80
+ }
81
+ `
82
+ ]; }
83
+ get context() {
84
+ return {
85
+ title: i18next.t('audit.title'),
86
+ help: 'auth/audit',
87
+ filter: {
88
+ handler: () => this.grist.toggleHeadroom()
89
+ }
90
+ };
91
+ }
92
+ render() {
93
+ return html `${keyed(this.view, html `<ox-grist class="page-fill" .mode=${this.mode} .config=${this.configOf(this.view)} .fetchHandler=${this.fetchHandler.bind(this)}>
94
+ <div slot="headroom" class="header">
95
+ <div class="filters">${this.renderControls()}</div>
96
+ </div>
97
+ </ox-grist>`)}`;
98
+ }
99
+ renderControls() {
100
+ const t = i18next.t.bind(i18next);
101
+ const pressed = (on) => (on ? 'true' : 'false');
102
+ return html `
103
+ <div group>
104
+ ${VIEWS.map(view => html `<button quiet aria-pressed=${pressed(this.view === view)} @click=${() => this.show(view)}>${t(`audit.view.${view}`)}</button>`)}
105
+ </div>
106
+
107
+ <div group>
108
+ ${PERIODS.map(days => html `<button quiet aria-pressed=${pressed(this.days === days)} @click=${() => this.lastDays(days)}>${t('audit.period.days', { count: days })}</button>`)}
109
+ <input type="date" aria-label=${t('audit.period.from')} .value=${dateInputValue(this.from)} @change=${(e) => this.pick('from', e)} />
110
+ <span>~</span>
111
+ <input type="date" aria-label=${t('audit.period.to')} .value=${dateInputValue(this.to)} @change=${(e) => this.pick('to', e)} />
112
+ </div>
113
+
114
+ ${this.view === 'records'
115
+ ? html `<div group>
116
+ ${OUTCOMES.map(outcome => html `<button quiet aria-pressed=${pressed(this.outcome === outcome)} @click=${() => this.narrow({ outcome })}>${t(outcome ? `audit.outcome.${outcome}` : 'audit.outcome.all')}</button>`)}
117
+ </div>`
118
+ : nothing}
119
+ ${this.view === 'records' && this.actor
120
+ ? html `<span narrowed>${this.actor.name}<button quiet small @click=${() => this.narrow({ actor: undefined })}>${t('audit.button.all-people')}</button></span>`
121
+ : nothing}
122
+ ${this.view === 'records' && this.door
123
+ ? html `<span narrowed>${doorWords(t, this.door)}<button quiet small @click=${() => this.narrow({ door: undefined })}>${t('audit.button.all-privileges')}</button></span>`
124
+ : nothing}
125
+ ${this.renderHealth()}
126
+ `;
127
+ }
128
+ /* Only what is wrong is said: failures, or that they could not be counted. Never a 0 for "unknown". */
129
+ renderHealth() {
130
+ const health = this.health;
131
+ if (health === 'unknown')
132
+ return html `<span health>${i18next.t('audit.health.unknown')}</span>`;
133
+ if (!health?.failures)
134
+ return nothing;
135
+ return html `<span health>${i18next.t('audit.health.failures', { count: health.failures, since: new Date(health.since).toLocaleString() })}</span>`;
136
+ }
137
+ show(view) {
138
+ this.view = view;
139
+ }
140
+ lastDays(days) {
141
+ const { from, to } = lastDays(days);
142
+ this.days = days;
143
+ this.from = from;
144
+ this.to = to;
145
+ this.grist?.fetch();
146
+ }
147
+ pick(which, event) {
148
+ const value = event.target.value;
149
+ if (!value)
150
+ return;
151
+ const [year, month, day] = value.split('-').map(Number);
152
+ this[which] = which === 'from' ? new Date(year, month - 1, day, 0, 0, 0) : new Date(year, month - 1, day, 23, 59, 59, 999);
153
+ this.days = null;
154
+ this.grist?.fetch();
155
+ }
156
+ narrow(change) {
157
+ Object.assign(this, change);
158
+ if (this.view !== 'records')
159
+ this.view = 'records';
160
+ else
161
+ this.grist?.fetch();
162
+ }
163
+ async pageUpdated(_changes, _lifecycle) {
164
+ if (this.active) {
165
+ await this.updateComplete;
166
+ this.grist?.fetch();
167
+ this.readHealth();
168
+ }
169
+ }
170
+ async readHealth() {
171
+ try {
172
+ const response = await client.query({ query: gql `{ auditHealth { failures since } }`, fetchPolicy: 'no-cache' });
173
+ this.health = response.errors?.length || !response.data?.auditHealth ? 'unknown' : response.data.auditHealth;
174
+ }
175
+ catch {
176
+ this.health = 'unknown';
177
+ }
178
+ }
179
+ configOf(view) {
180
+ const t = i18next.t.bind(i18next);
181
+ const number = (name, header, click) => ({
182
+ type: 'integer',
183
+ name,
184
+ header,
185
+ record: { align: 'right', editable: false },
186
+ width: 90,
187
+ ...(click ? { handlers: { click: (_c, _d, _col, record) => record && click(record) } } : {})
188
+ });
189
+ if (view === 'people') {
190
+ const person = (record) => ({ id: record.id, name: record.name || record.email });
191
+ return {
192
+ rows: { appendable: false, selectable: false },
193
+ pagination: { infinite: true },
194
+ columns: [
195
+ { type: 'gutter', gutterName: 'sequence' },
196
+ {
197
+ type: 'string',
198
+ name: 'name',
199
+ header: t('field.name'),
200
+ record: { editable: false },
201
+ width: 140,
202
+ handlers: { click: (_c, _d, _col, record) => record && this.narrow({ actor: person(record), door: undefined, outcome: '' }) }
203
+ },
204
+ { type: 'string', name: 'email', header: t('field.email'), record: { editable: false }, width: 200 },
205
+ { type: 'string', name: 'roles', header: t('field.roles'), record: { editable: false }, width: 200 },
206
+ { type: 'string', name: 'status', header: t('field.status'), record: { editable: false, align: 'center', renderer: statusRenderer(() => 0) }, width: 80 },
207
+ { type: 'datetime', name: 'lastCheckinAt', header: t('audit.field.last-checkin'), record: { editable: false }, width: 160 },
208
+ number('checkins', t('audit.field.checkins')),
209
+ number('failedSignins', t('audit.field.failed-signins'), record => this.narrow({ actor: person(record), door: undefined, outcome: 'failed' })),
210
+ number('granted', t('audit.field.granted'), record => this.narrow({ actor: person(record), door: undefined, outcome: '' })),
211
+ number('denied', t('audit.field.denied'), record => this.narrow({ actor: person(record), door: undefined, outcome: 'denied' }))
212
+ ]
213
+ };
214
+ }
215
+ if (view === 'doors') {
216
+ return {
217
+ rows: { appendable: false, selectable: false },
218
+ pagination: { infinite: true },
219
+ columns: [
220
+ { type: 'gutter', gutterName: 'sequence' },
221
+ {
222
+ type: 'string',
223
+ name: 'doorWords',
224
+ header: t('audit.field.privilege'),
225
+ record: { editable: false },
226
+ width: 240,
227
+ handlers: { click: (_c, _d, _col, record) => record && this.narrow({ door: record.door, actor: undefined, outcome: '' }) }
228
+ },
229
+ number('people', t('audit.field.people')),
230
+ number('granted', t('audit.field.granted'), record => this.narrow({ door: record.door, actor: undefined, outcome: '' })),
231
+ number('denied', t('audit.field.denied'), record => this.narrow({ door: record.door, actor: undefined, outcome: 'denied' }))
232
+ ]
233
+ };
234
+ }
235
+ return {
236
+ rows: { appendable: false, selectable: false },
237
+ pagination: { pages: [50, 100, 200] },
238
+ columns: [
239
+ { type: 'gutter', gutterName: 'sequence' },
240
+ { type: 'datetime', name: 'createdAt', header: t('audit.field.time'), record: { editable: false }, width: 160 },
241
+ { type: 'string', name: 'actorName', header: t('audit.field.actor'), record: { editable: false }, width: 120 },
242
+ { type: 'string', name: 'operation', header: t('audit.field.request'), record: { editable: false }, width: 200 },
243
+ { type: 'string', name: 'what', header: t('audit.field.actions'), record: { editable: false }, width: 360 },
244
+ { type: 'string', name: 'ip', header: t('audit.field.address'), record: { editable: false }, width: 120 },
245
+ { type: 'integer', name: 'durationMs', header: t('audit.field.duration'), record: { editable: false, align: 'right' }, width: 80 }
246
+ ]
247
+ };
248
+ }
249
+ async fetchHandler({ page, limit }) {
250
+ const t = i18next.t.bind(i18next);
251
+ const period = { from: this.from.toISOString(), to: this.to.toISOString() };
252
+ if (this.view === 'people') {
253
+ const response = await client.query({
254
+ query: gql `
255
+ query ($from: DateTimeISO!, $to: DateTimeISO!) {
256
+ auditPeople(from: $from, to: $to) {
257
+ user {
258
+ id
259
+ name
260
+ email
261
+ status
262
+ }
263
+ roles
264
+ lastCheckinAt
265
+ checkins
266
+ failedSignins
267
+ granted
268
+ denied
269
+ }
270
+ }
271
+ `,
272
+ variables: period,
273
+ fetchPolicy: 'no-cache'
274
+ });
275
+ const people = response.data?.auditPeople || [];
276
+ const records = people
277
+ .map((one) => ({
278
+ ...one,
279
+ id: one.user.id,
280
+ name: one.user.name,
281
+ email: one.user.email,
282
+ roles: one.roles.join(', '),
283
+ status: one.user.status
284
+ }))
285
+ .sort((a, b) => String(b.lastCheckinAt || '').localeCompare(String(a.lastCheckinAt || '')));
286
+ return { total: records.length, records };
287
+ }
288
+ if (this.view === 'doors') {
289
+ const response = await client.query({
290
+ query: gql `
291
+ query ($from: DateTimeISO!, $to: DateTimeISO!) {
292
+ auditDoors(from: $from, to: $to) {
293
+ door
294
+ people
295
+ granted
296
+ denied
297
+ }
298
+ }
299
+ `,
300
+ variables: period,
301
+ fetchPolicy: 'no-cache'
302
+ });
303
+ /* The doors used come first; the rest keep the server's order. */
304
+ const used = (one) => one.granted + one.denied;
305
+ const records = (response.data?.auditDoors || [])
306
+ .map((one) => ({ ...one, doorWords: doorWords(t, one.door) }))
307
+ .sort((a, b) => used(b) - used(a));
308
+ return { total: records.length, records };
309
+ }
310
+ const response = await client.query({
311
+ query: gql `
312
+ query ($filter: AuditRecordFilter!) {
313
+ auditRecords(filter: $filter) {
314
+ items {
315
+ request {
316
+ id
317
+ createdAt
318
+ actor {
319
+ id
320
+ name
321
+ email
322
+ }
323
+ ip
324
+ channel
325
+ operationType
326
+ operationName
327
+ fields
328
+ route
329
+ durationMs
330
+ }
331
+ actions {
332
+ kind
333
+ door
334
+ decision
335
+ outcome
336
+ code
337
+ count
338
+ objectId
339
+ detail
340
+ }
341
+ }
342
+ total
343
+ }
344
+ }
345
+ `,
346
+ variables: {
347
+ filter: {
348
+ ...period,
349
+ actorId: this.actor?.id,
350
+ door: this.door,
351
+ outcome: this.outcome || undefined,
352
+ page,
353
+ limit
354
+ }
355
+ },
356
+ fetchPolicy: 'no-cache'
357
+ });
358
+ const list = response.data?.auditRecords;
359
+ return {
360
+ total: list?.total || 0,
361
+ records: (list?.items || []).map(({ request, actions }) => ({
362
+ id: request.id,
363
+ createdAt: request.createdAt,
364
+ actorName: request.actor?.name || request.actor?.email || '',
365
+ what: recordWords(t, actions),
366
+ operation: request.channel === 'graphql' ? [request.operationType, request.fields || request.operationName].filter(Boolean).join(' ') : request.route || '',
367
+ ip: request.ip,
368
+ durationMs: request.durationMs
369
+ }))
370
+ };
371
+ }
372
+ };
373
+ __decorate([
374
+ property({ type: String }),
375
+ __metadata("design:type", String)
376
+ ], AuditPage.prototype, "mode", void 0);
377
+ __decorate([
378
+ state(),
379
+ __metadata("design:type", String)
380
+ ], AuditPage.prototype, "view", void 0);
381
+ __decorate([
382
+ state(),
383
+ __metadata("design:type", Object)
384
+ ], AuditPage.prototype, "days", void 0);
385
+ __decorate([
386
+ state(),
387
+ __metadata("design:type", Date)
388
+ ], AuditPage.prototype, "from", void 0);
389
+ __decorate([
390
+ state(),
391
+ __metadata("design:type", Date)
392
+ ], AuditPage.prototype, "to", void 0);
393
+ __decorate([
394
+ state(),
395
+ __metadata("design:type", Object)
396
+ ], AuditPage.prototype, "actor", void 0);
397
+ __decorate([
398
+ state(),
399
+ __metadata("design:type", String)
400
+ ], AuditPage.prototype, "door", void 0);
401
+ __decorate([
402
+ state(),
403
+ __metadata("design:type", String)
404
+ ], AuditPage.prototype, "outcome", void 0);
405
+ __decorate([
406
+ state(),
407
+ __metadata("design:type", Object)
408
+ ], AuditPage.prototype, "health", void 0);
409
+ __decorate([
410
+ query('ox-grist'),
411
+ __metadata("design:type", Function)
412
+ ], AuditPage.prototype, "grist", void 0);
413
+ AuditPage = __decorate([
414
+ customElement('audit-page')
415
+ ], AuditPage);
416
+ export { AuditPage };
417
+ //# sourceMappingURL=audit-page.js.map