@things-factory/auth-ui 10.1.16 → 10.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-client/components/secret-field.d.ts +35 -0
- package/dist-client/components/secret-field.js +149 -0
- package/dist-client/components/secret-field.js.map +1 -0
- package/dist-client/pages/app-binding/app-binding-list-query.test.d.ts +1 -0
- package/dist-client/pages/app-binding/app-binding-list-query.test.js +46 -0
- package/dist-client/pages/app-binding/app-binding-list-query.test.js.map +1 -0
- package/dist-client/pages/app-binding/app-binding.d.ts +1 -0
- package/dist-client/pages/app-binding/app-binding.js +8 -8
- package/dist-client/pages/app-binding/app-binding.js.map +1 -1
- package/dist-client/pages/app-binding/app-bindings.d.ts +63 -2
- package/dist-client/pages/app-binding/app-bindings.js +199 -117
- package/dist-client/pages/app-binding/app-bindings.js.map +1 -1
- package/dist-client/pages/appliance/appliance-list-query.test.d.ts +1 -0
- package/dist-client/pages/appliance/appliance-list-query.test.js +58 -0
- package/dist-client/pages/appliance/appliance-list-query.test.js.map +1 -0
- package/dist-client/pages/appliance/appliance.d.ts +1 -0
- package/dist-client/pages/appliance/appliance.js +2 -4
- package/dist-client/pages/appliance/appliance.js.map +1 -1
- package/dist-client/pages/appliance/home.d.ts +61 -2
- package/dist-client/pages/appliance/home.js +200 -122
- package/dist-client/pages/appliance/home.js.map +1 -1
- package/dist-client/pages/application/application.d.ts +1 -0
- package/dist-client/pages/application/application.js +10 -3
- package/dist-client/pages/application/application.js.map +1 -1
- package/dist-client/pages/application/applications.d.ts +61 -2
- package/dist-client/pages/application/applications.js +193 -128
- package/dist-client/pages/application/applications.js.map +1 -1
- package/dist-client/pages/application/register.js +81 -27
- package/dist-client/pages/application/register.js.map +1 -1
- package/dist-client/pages/auth-provider/auth-provider-management.d.ts +4 -2
- package/dist-client/pages/auth-provider/auth-provider-management.js +35 -6
- package/dist-client/pages/auth-provider/auth-provider-management.js.map +1 -1
- package/dist-client/pages/domain-owner/domain-owner-management.d.ts +2 -0
- package/dist-client/pages/domain-owner/domain-owner-management.js +34 -7
- package/dist-client/pages/domain-owner/domain-owner-management.js.map +1 -1
- package/dist-client/pages/secrets-are-not-printed.test.d.ts +1 -0
- package/dist-client/pages/secrets-are-not-printed.test.js +70 -0
- package/dist-client/pages/secrets-are-not-printed.test.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/application-status.d.ts +32 -0
- package/dist-client/utils/application-status.js +45 -0
- package/dist-client/utils/application-status.js.map +1 -0
- package/dist-client/utils/application-status.test.d.ts +1 -0
- package/dist-client/utils/application-status.test.js +45 -0
- package/dist-client/utils/application-status.test.js.map +1 -0
- package/dist-client/utils/form-values.d.ts +42 -0
- package/dist-client/utils/form-values.js +57 -0
- package/dist-client/utils/form-values.js.map +1 -0
- package/dist-client/utils/form-values.test.d.ts +1 -0
- package/dist-client/utils/form-values.test.js +62 -0
- package/dist-client/utils/form-values.test.js.map +1 -0
- package/dist-client/utils/list-result.d.ts +62 -0
- package/dist-client/utils/list-result.js +92 -0
- package/dist-client/utils/list-result.js.map +1 -0
- package/dist-client/utils/list-result.test.d.ts +1 -0
- package/dist-client/utils/list-result.test.js +160 -0
- package/dist-client/utils/list-result.test.js.map +1 -0
- package/dist-client/utils/secret-display.d.ts +51 -0
- package/dist-client/utils/secret-display.js +61 -0
- package/dist-client/utils/secret-display.js.map +1 -0
- package/dist-client/utils/secret-display.test.d.ts +1 -0
- package/dist-client/utils/secret-display.test.js +59 -0
- package/dist-client/utils/secret-display.test.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/translations/en.json +31 -0
- package/translations/ja.json +31 -0
- package/translations/ko.json +31 -0
- package/translations/ms.json +31 -0
- package/translations/zh.json +31 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import '@material/web/icon/icon.js';
|
|
2
|
+
import { LitElement } from 'lit';
|
|
3
|
+
declare const SecretField_base: (new (...args: any[]) => LitElement) & typeof LitElement;
|
|
4
|
+
/**
|
|
5
|
+
* A credential the reader is meant to copy once.
|
|
6
|
+
*
|
|
7
|
+
* ── Why this is not a readonly input ─────────────────────────────────────────
|
|
8
|
+
* The app binding and appliance detail screens printed refresh and access tokens into
|
|
9
|
+
* `<input readonly>` in plain text, from the moment the page opened. The reader needs the
|
|
10
|
+
* value exactly once — to paste it into the thing being set up — and for the rest of the
|
|
11
|
+
* time the screen is open it is just a credential sitting on a display.
|
|
12
|
+
*
|
|
13
|
+
* So the value is masked, copying does not require revealing it, and revealing is something
|
|
14
|
+
* the reader does on purpose. `utils/secret-display.ts` holds the rules, with tests: the mask
|
|
15
|
+
* is a fixed width so it does not leak the length, and an absent secret shows nothing rather
|
|
16
|
+
* than dots over a token nobody issued.
|
|
17
|
+
*
|
|
18
|
+
* ── Copy without reveal ──────────────────────────────────────────────────────
|
|
19
|
+
* The common case is "paste this into the other system", and it does not need the value on
|
|
20
|
+
* screen at all. Reveal exists for the other case — checking that the value matches what the
|
|
21
|
+
* other side already has — and the reader chooses it.
|
|
22
|
+
*
|
|
23
|
+
* `navigator.clipboard` needs a secure context. On a plain-http deployment it is absent, and
|
|
24
|
+
* the button says so instead of failing quietly, because a copy that silently did nothing
|
|
25
|
+
* would leave the reader pasting whatever was in the clipboard before.
|
|
26
|
+
*/
|
|
27
|
+
export declare class SecretField extends SecretField_base {
|
|
28
|
+
static styles: import("lit").CSSResult[];
|
|
29
|
+
/** The credential. Absent means none has been issued. */
|
|
30
|
+
value?: string;
|
|
31
|
+
private revealed;
|
|
32
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
33
|
+
private copy;
|
|
34
|
+
}
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { __decorate, __metadata } from "tslib";
|
|
2
|
+
import '@material/web/icon/icon.js';
|
|
3
|
+
import { css, html, LitElement } from 'lit';
|
|
4
|
+
import { customElement, property, state } from 'lit/decorators.js';
|
|
5
|
+
import { i18next, localize } from '@operato/i18n';
|
|
6
|
+
import { notify } from '@operato/layout';
|
|
7
|
+
import { hasSecret, shownSecret } from '../utils/secret-display.js';
|
|
8
|
+
/**
|
|
9
|
+
* A credential the reader is meant to copy once.
|
|
10
|
+
*
|
|
11
|
+
* ── Why this is not a readonly input ─────────────────────────────────────────
|
|
12
|
+
* The app binding and appliance detail screens printed refresh and access tokens into
|
|
13
|
+
* `<input readonly>` in plain text, from the moment the page opened. The reader needs the
|
|
14
|
+
* value exactly once — to paste it into the thing being set up — and for the rest of the
|
|
15
|
+
* time the screen is open it is just a credential sitting on a display.
|
|
16
|
+
*
|
|
17
|
+
* So the value is masked, copying does not require revealing it, and revealing is something
|
|
18
|
+
* the reader does on purpose. `utils/secret-display.ts` holds the rules, with tests: the mask
|
|
19
|
+
* is a fixed width so it does not leak the length, and an absent secret shows nothing rather
|
|
20
|
+
* than dots over a token nobody issued.
|
|
21
|
+
*
|
|
22
|
+
* ── Copy without reveal ──────────────────────────────────────────────────────
|
|
23
|
+
* The common case is "paste this into the other system", and it does not need the value on
|
|
24
|
+
* screen at all. Reveal exists for the other case — checking that the value matches what the
|
|
25
|
+
* other side already has — and the reader chooses it.
|
|
26
|
+
*
|
|
27
|
+
* `navigator.clipboard` needs a secure context. On a plain-http deployment it is absent, and
|
|
28
|
+
* the button says so instead of failing quietly, because a copy that silently did nothing
|
|
29
|
+
* would leave the reader pasting whatever was in the clipboard before.
|
|
30
|
+
*/
|
|
31
|
+
let SecretField = class SecretField extends localize(i18next)(LitElement) {
|
|
32
|
+
constructor() {
|
|
33
|
+
super(...arguments);
|
|
34
|
+
this.revealed = false;
|
|
35
|
+
}
|
|
36
|
+
static { this.styles = [
|
|
37
|
+
css `
|
|
38
|
+
:host {
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-direction: row;
|
|
41
|
+
align-items: center;
|
|
42
|
+
gap: var(--spacing-small, 4px);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
input {
|
|
46
|
+
flex: 1;
|
|
47
|
+
min-width: 0;
|
|
48
|
+
font-family: var(--theme-mono-font, monospace);
|
|
49
|
+
padding: var(--spacing-small, 4px) var(--spacing-medium, 8px);
|
|
50
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
51
|
+
border-radius: var(--md-sys-shape-corner-small, 5px);
|
|
52
|
+
background-color: var(--md-sys-color-surface);
|
|
53
|
+
color: var(--md-sys-color-on-surface);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
input[empty] {
|
|
57
|
+
font-family: inherit;
|
|
58
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
button {
|
|
62
|
+
display: inline-flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
padding: var(--spacing-small, 4px);
|
|
65
|
+
border: none;
|
|
66
|
+
border-radius: var(--md-sys-shape-corner-small, 5px);
|
|
67
|
+
background-color: transparent;
|
|
68
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
69
|
+
cursor: pointer;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
button:hover:not([disabled]) {
|
|
73
|
+
background-color: var(--md-sys-color-surface-variant);
|
|
74
|
+
color: var(--md-sys-color-on-surface);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/*
|
|
78
|
+
A button that cannot act says so before it is pressed. Leaving it live over an absent
|
|
79
|
+
token means the reader presses it, nothing happens, and they cannot tell a broken
|
|
80
|
+
screen from a credential that was never issued.
|
|
81
|
+
*/
|
|
82
|
+
button[disabled] {
|
|
83
|
+
opacity: 0.38;
|
|
84
|
+
cursor: default;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
md-icon {
|
|
88
|
+
--md-icon-size: 18px;
|
|
89
|
+
}
|
|
90
|
+
`
|
|
91
|
+
]; }
|
|
92
|
+
render() {
|
|
93
|
+
const present = hasSecret(this.value);
|
|
94
|
+
return html `
|
|
95
|
+
<input
|
|
96
|
+
type="text"
|
|
97
|
+
readonly
|
|
98
|
+
?empty=${!present}
|
|
99
|
+
.value=${present ? shownSecret(this.value, this.revealed) : i18next.t('text.not issued')}
|
|
100
|
+
/>
|
|
101
|
+
|
|
102
|
+
<button
|
|
103
|
+
?disabled=${!present}
|
|
104
|
+
title=${this.revealed ? i18next.t('button.hide') : i18next.t('button.reveal')}
|
|
105
|
+
@click=${() => (this.revealed = !this.revealed)}
|
|
106
|
+
>
|
|
107
|
+
<md-icon>${this.revealed ? 'visibility_off' : 'visibility'}</md-icon>
|
|
108
|
+
</button>
|
|
109
|
+
|
|
110
|
+
<button ?disabled=${!present} title=${i18next.t('button.copy')} @click=${this.copy}>
|
|
111
|
+
<md-icon>content_copy</md-icon>
|
|
112
|
+
</button>
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
async copy() {
|
|
116
|
+
if (!hasSecret(this.value)) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
/*
|
|
120
|
+
Absent outside a secure context. Saying so beats failing quietly — a copy that did
|
|
121
|
+
nothing leaves the reader pasting whatever was in the clipboard before, and blaming
|
|
122
|
+
the other system when it does not work.
|
|
123
|
+
*/
|
|
124
|
+
if (!navigator.clipboard?.writeText) {
|
|
125
|
+
notify({ level: 'error', message: i18next.t('text.copying needs a secure connection') });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
await navigator.clipboard.writeText(this.value);
|
|
130
|
+
notify({ message: i18next.t('text.copied') });
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
notify({ level: 'error', message: i18next.t('text.copying needs a secure connection') });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
__decorate([
|
|
138
|
+
property({ type: String }),
|
|
139
|
+
__metadata("design:type", String)
|
|
140
|
+
], SecretField.prototype, "value", void 0);
|
|
141
|
+
__decorate([
|
|
142
|
+
state(),
|
|
143
|
+
__metadata("design:type", Boolean)
|
|
144
|
+
], SecretField.prototype, "revealed", void 0);
|
|
145
|
+
SecretField = __decorate([
|
|
146
|
+
customElement('secret-field')
|
|
147
|
+
], SecretField);
|
|
148
|
+
export { SecretField };
|
|
149
|
+
//# sourceMappingURL=secret-field.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-field.js","sourceRoot":"","sources":["../../client/components/secret-field.ts"],"names":[],"mappings":";AAAA,OAAO,4BAA4B,CAAA;AAEnC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAElE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAExC,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAA;AAEnE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEI,IAAM,WAAW,GAAjB,MAAM,WAAY,SAAQ,QAAQ,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC;IAAvD;;QA6DY,aAAQ,GAAY,KAAK,CAAA;IAiD5C,CAAC;aA7GQ,WAAM,GAAG;QACd,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAqDF;KACF,AAvDY,CAuDZ;IAOD,MAAM;QACJ,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAErC,OAAO,IAAI,CAAA;;;;iBAIE,CAAC,OAAO;iBACR,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,iBAAiB,CAAC;;;;oBAI5E,CAAC,OAAO;gBACZ,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC;iBACpE,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC;;mBAEpC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,YAAY;;;0BAGxC,CAAC,OAAO,UAAU,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,WAAW,IAAI,CAAC,IAAI;;;KAGnF,CAAA;IACH,CAAC;IAEO,KAAK,CAAC,IAAI;QAChB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3B,OAAM;QACR,CAAC;QAED;;;;UAIE;QACF,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,SAAS,EAAE,CAAC;YACpC,MAAM,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,wCAAwC,CAAC,EAAE,CAAC,CAAA;YACxF,OAAM;QACR,CAAC;QAED,IAAI,CAAC;YACH,MAAM,SAAS,CAAC,SAAS,CAAC,SAAS,CAAC,IAAI,CAAC,KAAM,CAAC,CAAA;YAChD,MAAM,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,CAAC,CAAA;QAC/C,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,wCAAwC,CAAC,EAAE,CAAC,CAAA;QAC1F,CAAC;IACH,CAAC;;AAlD2B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;0CAAe;AAEzB;IAAhB,KAAK,EAAE;;6CAAkC;AA7D/B,WAAW;IADvB,aAAa,CAAC,cAAc,CAAC;GACjB,WAAW,CA8GvB","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { notify } from '@operato/layout'\n\nimport { hasSecret, shownSecret } from '../utils/secret-display.js'\n\n/**\n * A credential the reader is meant to copy once.\n *\n * ── Why this is not a readonly input ─────────────────────────────────────────\n * The app binding and appliance detail screens printed refresh and access tokens into\n * `<input readonly>` in plain text, from the moment the page opened. The reader needs the\n * value exactly once — to paste it into the thing being set up — and for the rest of the\n * time the screen is open it is just a credential sitting on a display.\n *\n * So the value is masked, copying does not require revealing it, and revealing is something\n * the reader does on purpose. `utils/secret-display.ts` holds the rules, with tests: the mask\n * is a fixed width so it does not leak the length, and an absent secret shows nothing rather\n * than dots over a token nobody issued.\n *\n * ── Copy without reveal ──────────────────────────────────────────────────────\n * The common case is \"paste this into the other system\", and it does not need the value on\n * screen at all. Reveal exists for the other case — checking that the value matches what the\n * other side already has — and the reader chooses it.\n *\n * `navigator.clipboard` needs a secure context. On a plain-http deployment it is absent, and\n * the button says so instead of failing quietly, because a copy that silently did nothing\n * would leave the reader pasting whatever was in the clipboard before.\n */\n@customElement('secret-field')\nexport class SecretField extends localize(i18next)(LitElement) {\n static styles = [\n css`\n :host {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: var(--spacing-small, 4px);\n }\n\n input {\n flex: 1;\n min-width: 0;\n font-family: var(--theme-mono-font, monospace);\n padding: var(--spacing-small, 4px) var(--spacing-medium, 8px);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: var(--md-sys-shape-corner-small, 5px);\n background-color: var(--md-sys-color-surface);\n color: var(--md-sys-color-on-surface);\n }\n\n input[empty] {\n font-family: inherit;\n color: var(--md-sys-color-on-surface-variant);\n }\n\n button {\n display: inline-flex;\n align-items: center;\n padding: var(--spacing-small, 4px);\n border: none;\n border-radius: var(--md-sys-shape-corner-small, 5px);\n background-color: transparent;\n color: var(--md-sys-color-on-surface-variant);\n cursor: pointer;\n }\n\n button:hover:not([disabled]) {\n background-color: var(--md-sys-color-surface-variant);\n color: var(--md-sys-color-on-surface);\n }\n\n /*\n A button that cannot act says so before it is pressed. Leaving it live over an absent\n token means the reader presses it, nothing happens, and they cannot tell a broken\n screen from a credential that was never issued.\n */\n button[disabled] {\n opacity: 0.38;\n cursor: default;\n }\n\n md-icon {\n --md-icon-size: 18px;\n }\n `\n ]\n\n /** The credential. Absent means none has been issued. */\n @property({ type: String }) value?: string\n\n @state() private revealed: boolean = false\n\n render() {\n const present = hasSecret(this.value)\n\n return html`\n <input\n type=\"text\"\n readonly\n ?empty=${!present}\n .value=${present ? shownSecret(this.value, this.revealed) : i18next.t('text.not issued')}\n />\n\n <button\n ?disabled=${!present}\n title=${this.revealed ? i18next.t('button.hide') : i18next.t('button.reveal')}\n @click=${() => (this.revealed = !this.revealed)}\n >\n <md-icon>${this.revealed ? 'visibility_off' : 'visibility'}</md-icon>\n </button>\n\n <button ?disabled=${!present} title=${i18next.t('button.copy')} @click=${this.copy}>\n <md-icon>content_copy</md-icon>\n </button>\n `\n }\n\n private async copy() {\n if (!hasSecret(this.value)) {\n return\n }\n\n /*\n Absent outside a secure context. Saying so beats failing quietly — a copy that did\n nothing leaves the reader pasting whatever was in the clipboard before, and blaming\n the other system when it does not work.\n */\n if (!navigator.clipboard?.writeText) {\n notify({ level: 'error', message: i18next.t('text.copying needs a secure connection') })\n return\n }\n\n try {\n await navigator.clipboard.writeText(this.value!)\n notify({ message: i18next.t('text.copied') })\n } catch (e) {\n notify({ level: 'error', message: i18next.t('text.copying needs a secure connection') })\n }\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* The bound-applications list must not ask for the refresh token.
|
|
5
|
+
*
|
|
6
|
+
* `AppBinding.refreshToken` is not a hash of anything. `oauth2-server.ts` writes the refresh
|
|
7
|
+
* token into the `password` column verbatim, and the field re-exposes that column under a
|
|
8
|
+
* friendlier name. Whoever holds it can exchange it for access tokens.
|
|
9
|
+
*
|
|
10
|
+
* The field carries its own `@privilege(category: "security", privilege: "query",
|
|
11
|
+
* domainOwnerGranted: true)`, so the server will not hand it to just anyone — but the people
|
|
12
|
+
* it *will* hand it to are the ones most likely to have this screen open on a shared display.
|
|
13
|
+
* A credential in a grid cell is a credential in plain text beside a name, in every
|
|
14
|
+
* screenshot and every shoulder-glance.
|
|
15
|
+
*
|
|
16
|
+
* ⚠ Comments are stripped before searching. The prose above names `refreshToken` four times
|
|
17
|
+
* explaining why it is absent; searching the raw file would find the explanation and pass
|
|
18
|
+
* over a query that really asks for it.
|
|
19
|
+
*/
|
|
20
|
+
function source(name) {
|
|
21
|
+
const text = readFileSync(join(__dirname, name), 'utf-8');
|
|
22
|
+
return text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^[ \t]*\/\/.*$/gm, '');
|
|
23
|
+
}
|
|
24
|
+
describe('the bound applications list query', () => {
|
|
25
|
+
it('does not ask for the refresh token', () => {
|
|
26
|
+
expect(source('./app-bindings.ts')).not.toContain('refreshToken');
|
|
27
|
+
});
|
|
28
|
+
it('asks for the fields the columns render', () => {
|
|
29
|
+
const text = source('./app-bindings.ts');
|
|
30
|
+
for (const field of ['name', 'email', 'status', 'scope', 'updatedAt', 'application']) {
|
|
31
|
+
expect(text).toContain(field);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
it('passes the grist options through, so paging and sorting reach the server', () => {
|
|
35
|
+
/*
|
|
36
|
+
* Worth asserting here in particular: the server paged before it filtered until
|
|
37
|
+
* dde62cd6a1, so this screen had no paging to pass through and the habit of not passing
|
|
38
|
+
* it could easily survive the fix.
|
|
39
|
+
*/
|
|
40
|
+
const text = source('./app-bindings.ts');
|
|
41
|
+
expect(text).toContain('$pagination: Pagination');
|
|
42
|
+
expect(text).toContain('$sortings: [Sorting!]');
|
|
43
|
+
expect(text).toContain('$filters: [Filter!]');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
//# sourceMappingURL=app-binding-list-query.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-binding-list-query.test.js","sourceRoot":"","sources":["../../../client/pages/app-binding/app-binding-list-query.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAA;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAE3B;;;;;;;;;;;;;;;;GAgBG;AACH,SAAS,MAAM,CAAC,IAAY;IAC1B,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,CAAA;IAEzD,OAAO,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAA;AAC9E,CAAC;AAED,QAAQ,CAAC,mCAAmC,EAAE,GAAG,EAAE;IACjD,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,cAAc,CAAC,CAAA;IACnE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,IAAI,GAAG,MAAM,CAAC,mBAAmB,CAAC,CAAA;QAExC,KAAK,MAAM,KAAK,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,aAAa,CAAC,EAAE,CAAC;YACrF,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;QAC/B,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,0EAA0E,EAAE,GAAG,EAAE;QAClF;;;;WAIG;QACH,MAAM,IAAI,GAAG,MAAM,CAAC,mBAAmB,CAAC,CAAA;QAExC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,yBAAyB,CAAC,CAAA;QACjD,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,uBAAuB,CAAC,CAAA;QAC/C,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,qBAAqB,CAAC,CAAA;IAC/C,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import { readFileSync } from 'fs'\nimport { join } from 'path'\n\n/**\n * The bound-applications list must not ask for the refresh token.\n *\n * `AppBinding.refreshToken` is not a hash of anything. `oauth2-server.ts` writes the refresh\n * token into the `password` column verbatim, and the field re-exposes that column under a\n * friendlier name. Whoever holds it can exchange it for access tokens.\n *\n * The field carries its own `@privilege(category: \"security\", privilege: \"query\",\n * domainOwnerGranted: true)`, so the server will not hand it to just anyone — but the people\n * it *will* hand it to are the ones most likely to have this screen open on a shared display.\n * A credential in a grid cell is a credential in plain text beside a name, in every\n * screenshot and every shoulder-glance.\n *\n * ⚠ Comments are stripped before searching. The prose above names `refreshToken` four times\n * explaining why it is absent; searching the raw file would find the explanation and pass\n * over a query that really asks for it.\n */\nfunction source(name: string): string {\n const text = readFileSync(join(__dirname, name), 'utf-8')\n\n return text.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '').replace(/^[ \\t]*\\/\\/.*$/gm, '')\n}\n\ndescribe('the bound applications list query', () => {\n it('does not ask for the refresh token', () => {\n expect(source('./app-bindings.ts')).not.toContain('refreshToken')\n })\n\n it('asks for the fields the columns render', () => {\n const text = source('./app-bindings.ts')\n\n for (const field of ['name', 'email', 'status', 'scope', 'updatedAt', 'application']) {\n expect(text).toContain(field)\n }\n })\n\n it('passes the grist options through, so paging and sorting reach the server', () => {\n /*\n * Worth asserting here in particular: the server paged before it filtered until\n * dde62cd6a1, so this screen had no paging to pass through and the habit of not passing\n * it could easily survive the fix.\n */\n const text = source('./app-bindings.ts')\n\n expect(text).toContain('$pagination: Pagination')\n expect(text).toContain('$sortings: [Sorting!]')\n expect(text).toContain('$filters: [Filter!]')\n })\n})\n"]}
|
|
@@ -6,7 +6,9 @@ import { css, html } from 'lit';
|
|
|
6
6
|
import { customElement, property, state } from 'lit/decorators.js';
|
|
7
7
|
import { asyncReplace } from 'lit/directives/async-replace.js';
|
|
8
8
|
import { client } from '@operato/graphql';
|
|
9
|
+
import { i18next } from '@operato/i18n';
|
|
9
10
|
import { navigate, PageView } from '@operato/shell';
|
|
11
|
+
import '../../components/secret-field.js';
|
|
10
12
|
import { parseJwt, sleep } from '@operato/utils';
|
|
11
13
|
let AppBinding = class AppBinding extends PageView {
|
|
12
14
|
static { this.styles = [
|
|
@@ -213,24 +215,22 @@ let AppBinding = class AppBinding extends PageView {
|
|
|
213
215
|
<legend>binding credential</legend>
|
|
214
216
|
<div field-2column>
|
|
215
217
|
<div field grid-span>
|
|
216
|
-
<label for="access-token"
|
|
217
|
-
<
|
|
218
|
-
<button button-in-field clipboard-copy @click=${e => e.preventDefault()}>copy</button>
|
|
218
|
+
<label for="access-token">${i18next.t('label.access token')}</label>
|
|
219
|
+
<secret-field id="access-token" .value=${this.accessToken || ''}></secret-field>
|
|
219
220
|
${accessTokenExp
|
|
220
221
|
? html `<div input-hint>
|
|
221
|
-
|
|
222
|
+
${i18next.t('text.expires at x', { x: new Date(accessTokenExp).toLocaleString() })} ·
|
|
222
223
|
${asyncReplace(this.expTimer(accessTokenExp))}
|
|
223
224
|
</div>`
|
|
224
225
|
: html ``}
|
|
225
226
|
</div>
|
|
226
227
|
|
|
227
228
|
<div field grid-span>
|
|
228
|
-
<label for="refresh-token"
|
|
229
|
-
<
|
|
230
|
-
<button button-in-field clipboard-copy @click=${e => e.preventDefault()}>copy</button>
|
|
229
|
+
<label for="refresh-token">${i18next.t('label.refresh token')}</label>
|
|
230
|
+
<secret-field id="refresh-token" .value=${binding.refreshToken || ''}></secret-field>
|
|
231
231
|
${refreshTokenExp
|
|
232
232
|
? html `<div input-hint>
|
|
233
|
-
|
|
233
|
+
${i18next.t('text.expires at x', { x: new Date(refreshTokenExp).toLocaleString() })} ·
|
|
234
234
|
${asyncReplace(this.expTimer(refreshTokenExp))}
|
|
235
235
|
</div>`
|
|
236
236
|
: html ``}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-binding.js","sourceRoot":"","sources":["../../../client/pages/app-binding/app-binding.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport Clipboard from 'clipboard'\nimport gql from 'graphql-tag'\nimport { css, html } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { asyncReplace } from 'lit/directives/async-replace.js'\nimport { client } from '@operato/graphql'\nimport { navigate, PageView } from '@operato/shell'\nimport { parseJwt, sleep } from '@operato/utils'\n\n@customElement('app-binding-page')\nclass AppBinding extends PageView { static styles = [\n css`\n :host { display: flex;\n flex-direction: column;\n overflow-y: auto;\n\n position: relative;\n\n background-color: var(--md-sys-color-background);\n padding: var(--spacing-large);\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 [icon] { position: absolute;\n top: 10px;\n right: 10px;\n\n max-width: 80px;\n }\n\n [icon] img { max-width: 100%;\n max-height: 100%;\n }\n\n [fieldset-container] { background-color: var(--md-sys-color-surface);\n margin: var(--spacing-large) 0 var(--spacing-medium) 0;\n padding: var(--spacing-medium);\n border-radius: var(--border-radius);\n box-shadow: var(--box-shadow);\n\n label { font: var(--label-font);\n color: var(--label-color, var(--md-sys-color-on-surface));\n text-transform: var(--label-text-transform);\n }\n input {\n background-color: var(--input-field-background, var(--md-sys-color-surface-container-highest));\n color: var(--input-field-color, var(--md-sys-color-on-surface));\n border: var(--border-dim-color);\n border-radius: var(--border-radius);\n margin: var(--input-margin);\n padding: var(--input-padding);\n font: var(--input-font);\n\n flex: 1;\n }\n select:focus,\n input:focus,\n button { outline: none;\n }\n form { max-width: var(--content-container-max-width);\n }\n }\n\n [fieldset-container] fieldset { margin: 0;\n margin-top: -15px;\n }\n\n fieldset { border-radius: var(--border-radius);\n border: var(--border-dim-color);\n margin: var(--fieldset-margin);\n padding: var(--fieldset-padding);\n }\n\n legend { padding: var(--legend-padding);\n font-weight: bold;\n color: var(--legend-color);\n }\n\n [field-2column] { display: grid;\n grid-template-columns: 1fr 1fr;\n grid-gap: 15px;\n }\n\n [field] { display: flex;\n flex-direction: column;\n position: relative;\n }\n\n [grid-span] { grid-column: span 2;\n }\n\n button,\n [button-in-field] { background-color: var(--button-background-color);\n border: var(--button-border);\n border-radius: var(--button-border-radius);\n padding: var(--button-padding);\n color: var(--button-color);\n font: var(--button-font);\n text-transform: var(--button-text-transform);\n\n margin: var(--spacing-medium) 0 var(--spacing-medium) var(--spacing-medium);\n float: right;\n text-decoration: none;\n }\n\n button:hover { border: var(--button-activ-border);\n box-shadow: var(--button-active-box-shadow);\n }\n\n [button-in-field] { border-radius: 0 var(--button-border-radius) var(--button-border-radius) 0;\n position: absolute;\n top: 12px;\n right: 0;\n max-height: 36px;\n }\n\n [input-hint] { font: var(--input-hint-font);\n color: var(--input-hint-color);\n }\n\n @media screen and (max-width: 480px) { [field] { grid-column: span 2;\n }\n }\n `\n ]\n\n @property({ type: Object }) appbinding: any\n @property({ type: String }) accessToken?: string\n\n @state() _refreshTokenInfo: any\n @state() _accessTokenInfo: any\n\n private clipboard?: Clipboard\n\n get context() { return { title: { icon: 'link',\n text: this.appbinding.name\n }\n }\n }\n\n render() { var binding = this.appbinding || {}\n var app = this.appbinding?.application || {}\n var refreshTokenExp = this._refreshTokenInfo?.exp\n var accessTokenExp = this._accessTokenInfo?.exp\n\n return html`\n <div>\n <h2><md-icon>link</md-icon> ${app.name}</h2>\n <p page-description>${app.description}</p>\n </div>\n\n ${app.icon\n ? html`\n <div icon>\n <img src=${app.icon} />\n </div>\n `\n : html``}\n\n <form>\n <div fieldset-container>\n <fieldset>\n <legend>application</legend>\n <div field-2column>\n <div field grid-span>\n <label for=\"app-name\">app name</label>\n <input id=\"app-name\" type=\"text\" .value=${app.name} readonly />\n </div>\n\n <div field grid-span>\n <label for=\"app-description\">description</label>\n <input id=\"app-description\" type=\"text\" .value=${app.description} readonly />\n </div>\n\n <div field>\n <label for=\"contact-email\">contact email</label>\n <input id=\"contact-email\" type=\"text\" .value=${app.email} readonly />\n </div>\n </div>\n </fieldset>\n </div>\n\n <div fieldset-container>\n <fieldset>\n <legend>binding</legend>\n <div field-2column>\n <div field grid-span>\n <label for=\"id\">id</label>\n <input id=\"id\" type=\"text\" .value=${binding.id} readonly />\n </div>\n\n <div field grid-span>\n <label for=\"name\">name</label>\n <input id=\"name\" type=\"text\" .value=${binding.email} readonly />\n </div>\n\n <div field>\n <label for=\"status\">status</label>\n <input id=\"status\" type=\"text\" .value=${binding.status} readonly />\n </div>\n\n <div field>\n <label for=\"scope\">scope</label>\n <input id=\"scope\" type=\"text\" .value=${binding.scope} readonly />\n </div>\n </div>\n </fieldset>\n </div>\n\n <div fieldset-container>\n <fieldset>\n <legend>binding credential</legend>\n <div field-2column>\n <div field grid-span>\n <label for=\"access-token\">access token</label>\n <input id=\"access-token\" type=\"text\" .value=${this.accessToken || ''} readonly />\n <button button-in-field clipboard-copy @click=${e => e.preventDefault()}>copy</button>\n ${accessTokenExp\n ? html`<div input-hint>\n expired in ${new Date(accessTokenExp).toLocaleString()} :\n ${asyncReplace(this.expTimer(accessTokenExp))}\n </div>`\n : html``}\n </div>\n\n <div field grid-span>\n <label for=\"refresh-token\">refresh token</label>\n <input id=\"refresh-token\" type=\"text\" .value=${binding.refreshToken || ''} readonly />\n <button button-in-field clipboard-copy @click=${e => e.preventDefault()}>copy</button>\n ${refreshTokenExp\n ? html`<div input-hint>\n expired in ${new Date(refreshTokenExp).toLocaleString()} :\n ${asyncReplace(this.expTimer(refreshTokenExp))}\n </div>`\n : html``}\n </div>\n </div>\n </fieldset>\n </div>\n\n <button @click=${this.deleteAppBinding.bind(this)}>delete this application binding</button>\n <button @click=${this.renewApplicationAccessToken.bind(this)}>renew access token</button>\n </form>\n `\n }\n\n async *expTimer(exp) { const DAY = 24 * 60 * 60\n const HOUR = 60 * 60\n const MIN = 60\n\n while (this.active) { var remain = Math.floor(Number(exp) - Date.now() / 1000)\n const days = Math.floor(remain / DAY)\n remain -= days * DAY\n const hours = Math.floor(remain / HOUR)\n remain -= hours * HOUR\n const mins = Math.floor(remain / MIN)\n const secs = remain - mins * MIN\n\n yield `${days} days ${hours} hours ${mins} mins ${secs} seconds remain`\n\n await sleep(1000)\n }\n }\n\n firstUpdated() { const copybuttons = this.renderRoot.querySelectorAll('[clipboard-copy]')\n\n this.clipboard = new Clipboard(copybuttons, { target: (trigger => trigger.parentElement.querySelector('input')) as any\n })\n }\n\n updated(changes) { /*\n * If this page properties are changed, this callback will be invoked.\n * This callback will be called back only when this page is activated.\n */\n if (changes.has('appbinding')) { const { refreshToken } = this.appbinding || {}\n this._refreshTokenInfo = refreshToken ? parseJwt(refreshToken) : {}\n }\n\n if (changes.has('accessToken')) { this._accessTokenInfo = this.accessToken ? parseJwt(this.accessToken) : {}\n }\n }\n\n async pageUpdated(changes, lifecycle, before) { if (this.active) { /*\n * this page is activated\n */\n await this.fetchAppBinding()\n } else { /* this page is deactivated */\n }\n }\n\n async fetchAppBinding() { const response = await client.query({ query: gql`\n query ($id: String!) { appBinding(id: $id) { id\n name\n description\n email\n scope\n status\n application { id\n name\n description\n email\n icon\n }\n refreshToken\n }\n }\n `,\n variables: { id: this.lifecycle.resourceId\n }\n })\n\n this.appbinding = response.data.appBinding\n }\n\n async deleteAppBinding(e) { e.preventDefault()\n\n const id = this.lifecycle.resourceId\n\n const response = await client.mutate({ mutation: gql`\n mutation ($id: String!) { deleteAppBinding(id: $id)\n }\n `,\n variables: { id\n }\n })\n\n const result = response.data.deleteAppBinding\n if (result) { console.log('delete sucess')\n navigate(`app-bindings`)\n } else { console.error('delete fail')\n }\n }\n\n async renewApplicationAccessToken(e) { e.preventDefault()\n\n const id = this.lifecycle.resourceId\n\n const response = await client.mutate({ mutation: gql`\n mutation ($id: String!) { renewApplicationAccessToken(id: $id) { accessToken\n refreshToken\n }\n }\n `,\n variables: { id\n }\n })\n\n if (response.errors) { console.error('renew application access token fail')\n } else { const { accessToken, refreshToken } = response.data.renewApplicationAccessToken\n this.accessToken = accessToken\n this.appbinding = { ...this.appbinding,\n refreshToken\n }\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"app-binding.js","sourceRoot":"","sources":["../../../client/pages/app-binding/app-binding.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport Clipboard from 'clipboard'\nimport gql from 'graphql-tag'\nimport { css, html } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { asyncReplace } from 'lit/directives/async-replace.js'\nimport { client } from '@operato/graphql'\nimport { i18next } from '@operato/i18n'\nimport { navigate, PageView } from '@operato/shell'\n\nimport '../../components/secret-field.js'\nimport { parseJwt, sleep } from '@operato/utils'\n\n@customElement('app-binding-page')\nclass AppBinding extends PageView { static styles = [\n css`\n :host { display: flex;\n flex-direction: column;\n overflow-y: auto;\n\n position: relative;\n\n background-color: var(--md-sys-color-background);\n padding: var(--spacing-large);\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 [icon] { position: absolute;\n top: 10px;\n right: 10px;\n\n max-width: 80px;\n }\n\n [icon] img { max-width: 100%;\n max-height: 100%;\n }\n\n [fieldset-container] { background-color: var(--md-sys-color-surface);\n margin: var(--spacing-large) 0 var(--spacing-medium) 0;\n padding: var(--spacing-medium);\n border-radius: var(--border-radius);\n box-shadow: var(--box-shadow);\n\n label { font: var(--label-font);\n color: var(--label-color, var(--md-sys-color-on-surface));\n text-transform: var(--label-text-transform);\n }\n input {\n background-color: var(--input-field-background, var(--md-sys-color-surface-container-highest));\n color: var(--input-field-color, var(--md-sys-color-on-surface));\n border: var(--border-dim-color);\n border-radius: var(--border-radius);\n margin: var(--input-margin);\n padding: var(--input-padding);\n font: var(--input-font);\n\n flex: 1;\n }\n select:focus,\n input:focus,\n button { outline: none;\n }\n form { max-width: var(--content-container-max-width);\n }\n }\n\n [fieldset-container] fieldset { margin: 0;\n margin-top: -15px;\n }\n\n fieldset { border-radius: var(--border-radius);\n border: var(--border-dim-color);\n margin: var(--fieldset-margin);\n padding: var(--fieldset-padding);\n }\n\n legend { padding: var(--legend-padding);\n font-weight: bold;\n color: var(--legend-color);\n }\n\n [field-2column] { display: grid;\n grid-template-columns: 1fr 1fr;\n grid-gap: 15px;\n }\n\n [field] { display: flex;\n flex-direction: column;\n position: relative;\n }\n\n [grid-span] { grid-column: span 2;\n }\n\n button,\n [button-in-field] { background-color: var(--button-background-color);\n border: var(--button-border);\n border-radius: var(--button-border-radius);\n padding: var(--button-padding);\n color: var(--button-color);\n font: var(--button-font);\n text-transform: var(--button-text-transform);\n\n margin: var(--spacing-medium) 0 var(--spacing-medium) var(--spacing-medium);\n float: right;\n text-decoration: none;\n }\n\n button:hover { border: var(--button-activ-border);\n box-shadow: var(--button-active-box-shadow);\n }\n\n [button-in-field] { border-radius: 0 var(--button-border-radius) var(--button-border-radius) 0;\n position: absolute;\n top: 12px;\n right: 0;\n max-height: 36px;\n }\n\n [input-hint] { font: var(--input-hint-font);\n color: var(--input-hint-color);\n }\n\n @media screen and (max-width: 480px) { [field] { grid-column: span 2;\n }\n }\n `\n ]\n\n @property({ type: Object }) appbinding: any\n @property({ type: String }) accessToken?: string\n\n @state() _refreshTokenInfo: any\n @state() _accessTokenInfo: any\n\n private clipboard?: Clipboard\n\n get context() { return { title: { icon: 'link',\n text: this.appbinding.name\n }\n }\n }\n\n render() { var binding = this.appbinding || {}\n var app = this.appbinding?.application || {}\n var refreshTokenExp = this._refreshTokenInfo?.exp\n var accessTokenExp = this._accessTokenInfo?.exp\n\n return html`\n <div>\n <h2><md-icon>link</md-icon> ${app.name}</h2>\n <p page-description>${app.description}</p>\n </div>\n\n ${app.icon\n ? html`\n <div icon>\n <img src=${app.icon} />\n </div>\n `\n : html``}\n\n <form>\n <div fieldset-container>\n <fieldset>\n <legend>application</legend>\n <div field-2column>\n <div field grid-span>\n <label for=\"app-name\">app name</label>\n <input id=\"app-name\" type=\"text\" .value=${app.name} readonly />\n </div>\n\n <div field grid-span>\n <label for=\"app-description\">description</label>\n <input id=\"app-description\" type=\"text\" .value=${app.description} readonly />\n </div>\n\n <div field>\n <label for=\"contact-email\">contact email</label>\n <input id=\"contact-email\" type=\"text\" .value=${app.email} readonly />\n </div>\n </div>\n </fieldset>\n </div>\n\n <div fieldset-container>\n <fieldset>\n <legend>binding</legend>\n <div field-2column>\n <div field grid-span>\n <label for=\"id\">id</label>\n <input id=\"id\" type=\"text\" .value=${binding.id} readonly />\n </div>\n\n <div field grid-span>\n <label for=\"name\">name</label>\n <input id=\"name\" type=\"text\" .value=${binding.email} readonly />\n </div>\n\n <div field>\n <label for=\"status\">status</label>\n <input id=\"status\" type=\"text\" .value=${binding.status} readonly />\n </div>\n\n <div field>\n <label for=\"scope\">scope</label>\n <input id=\"scope\" type=\"text\" .value=${binding.scope} readonly />\n </div>\n </div>\n </fieldset>\n </div>\n\n <div fieldset-container>\n <fieldset>\n <legend>binding credential</legend>\n <div field-2column>\n <div field grid-span>\n <label for=\"access-token\">${i18next.t('label.access token')}</label>\n <secret-field id=\"access-token\" .value=${this.accessToken || ''}></secret-field>\n ${accessTokenExp\n ? html`<div input-hint>\n ${i18next.t('text.expires at x', { x: new Date(accessTokenExp).toLocaleString() })} ·\n ${asyncReplace(this.expTimer(accessTokenExp))}\n </div>`\n : html``}\n </div>\n\n <div field grid-span>\n <label for=\"refresh-token\">${i18next.t('label.refresh token')}</label>\n <secret-field id=\"refresh-token\" .value=${binding.refreshToken || ''}></secret-field>\n ${refreshTokenExp\n ? html`<div input-hint>\n ${i18next.t('text.expires at x', { x: new Date(refreshTokenExp).toLocaleString() })} ·\n ${asyncReplace(this.expTimer(refreshTokenExp))}\n </div>`\n : html``}\n </div>\n </div>\n </fieldset>\n </div>\n\n <button @click=${this.deleteAppBinding.bind(this)}>delete this application binding</button>\n <button @click=${this.renewApplicationAccessToken.bind(this)}>renew access token</button>\n </form>\n `\n }\n\n async *expTimer(exp) { const DAY = 24 * 60 * 60\n const HOUR = 60 * 60\n const MIN = 60\n\n while (this.active) { var remain = Math.floor(Number(exp) - Date.now() / 1000)\n const days = Math.floor(remain / DAY)\n remain -= days * DAY\n const hours = Math.floor(remain / HOUR)\n remain -= hours * HOUR\n const mins = Math.floor(remain / MIN)\n const secs = remain - mins * MIN\n\n yield `${days} days ${hours} hours ${mins} mins ${secs} seconds remain`\n\n await sleep(1000)\n }\n }\n\n firstUpdated() { const copybuttons = this.renderRoot.querySelectorAll('[clipboard-copy]')\n\n this.clipboard = new Clipboard(copybuttons, { target: (trigger => trigger.parentElement.querySelector('input')) as any\n })\n }\n\n updated(changes) { /*\n * If this page properties are changed, this callback will be invoked.\n * This callback will be called back only when this page is activated.\n */\n if (changes.has('appbinding')) { const { refreshToken } = this.appbinding || {}\n this._refreshTokenInfo = refreshToken ? parseJwt(refreshToken) : {}\n }\n\n if (changes.has('accessToken')) { this._accessTokenInfo = this.accessToken ? parseJwt(this.accessToken) : {}\n }\n }\n\n async pageUpdated(changes, lifecycle, before) { if (this.active) { /*\n * this page is activated\n */\n await this.fetchAppBinding()\n } else { /* this page is deactivated */\n }\n }\n\n async fetchAppBinding() { const response = await client.query({ query: gql`\n query ($id: String!) { appBinding(id: $id) { id\n name\n description\n email\n scope\n status\n application { id\n name\n description\n email\n icon\n }\n refreshToken\n }\n }\n `,\n variables: { id: this.lifecycle.resourceId\n }\n })\n\n this.appbinding = response.data.appBinding\n }\n\n async deleteAppBinding(e) { e.preventDefault()\n\n const id = this.lifecycle.resourceId\n\n const response = await client.mutate({ mutation: gql`\n mutation ($id: String!) { deleteAppBinding(id: $id)\n }\n `,\n variables: { id\n }\n })\n\n const result = response.data.deleteAppBinding\n if (result) { console.log('delete sucess')\n navigate(`app-bindings`)\n } else { console.error('delete fail')\n }\n }\n\n async renewApplicationAccessToken(e) { e.preventDefault()\n\n const id = this.lifecycle.resourceId\n\n const response = await client.mutate({ mutation: gql`\n mutation ($id: String!) { renewApplicationAccessToken(id: $id) { accessToken\n refreshToken\n }\n }\n `,\n variables: { id\n }\n })\n\n if (response.errors) { console.error('renew application access token fail')\n } else { const { accessToken, refreshToken } = response.data.renewApplicationAccessToken\n this.accessToken = accessToken\n this.appbinding = { ...this.appbinding,\n refreshToken\n }\n }\n }\n}\n"]}
|
|
@@ -1,2 +1,63 @@
|
|
|
1
|
-
import '@material/web/
|
|
2
|
-
import '@
|
|
1
|
+
import '@material/web/icon/icon.js';
|
|
2
|
+
import '@operato/data-grist/ox-grist.js';
|
|
3
|
+
import '@operato/context/ox-context-page-toolbar.js';
|
|
4
|
+
import type { FetchOption } from '@operato/data-grist';
|
|
5
|
+
import { PageView } from '@operato/shell';
|
|
6
|
+
declare const AppBindings_base: (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
|
+
/**
|
|
13
|
+
* Applications bound to this domain.
|
|
14
|
+
*
|
|
15
|
+
* A binding is a `User` row with `userType: 'application'` — the account an application signs
|
|
16
|
+
* in as. The list shows who it is and what it may do; what it signs in *with* is elsewhere.
|
|
17
|
+
*
|
|
18
|
+
* ── The server answers honestly now ──────────────────────────────────────────
|
|
19
|
+
* Until dde62cd6a1 this query paged before it filtered: the database took a page of users of
|
|
20
|
+
* every type, JavaScript kept the applications among them, and the total reported how many
|
|
21
|
+
* survived that one page. A grid told there were three when there were thirty never offered a
|
|
22
|
+
* second page. Wiring paging up was pointless until that was fixed; it works now.
|
|
23
|
+
*
|
|
24
|
+
* ── ⚠ The refresh token is not asked for, and a test holds that ──────────────
|
|
25
|
+
* AppBinding's refresh token is the application's actual credential — oauth2-server.ts writes
|
|
26
|
+
* it into the password column verbatim, not hashed — and the field carries its own
|
|
27
|
+
* privilege directive for security:query.
|
|
28
|
+
*
|
|
29
|
+
* A credential that can be exchanged for access tokens does not belong in a grid cell, where
|
|
30
|
+
* it sits in plain text beside a name for anyone who can open the screen, and in any
|
|
31
|
+
* screenshot of it. `app-binding-list-query.test.ts` fails if the word appears here.
|
|
32
|
+
*/
|
|
33
|
+
export declare class AppBindings extends AppBindings_base {
|
|
34
|
+
static styles: import("lit").CSSResult[];
|
|
35
|
+
active: boolean;
|
|
36
|
+
config: any;
|
|
37
|
+
mode: 'CARD' | 'LIST' | 'GRID';
|
|
38
|
+
/** The last fetch did not answer. Distinct from answering with nothing. */
|
|
39
|
+
failed: boolean;
|
|
40
|
+
private grist;
|
|
41
|
+
get context(): {
|
|
42
|
+
title: string;
|
|
43
|
+
help: string;
|
|
44
|
+
search: {
|
|
45
|
+
handler: (search: string) => void;
|
|
46
|
+
value: string;
|
|
47
|
+
};
|
|
48
|
+
actions: {
|
|
49
|
+
title: string;
|
|
50
|
+
action: () => void;
|
|
51
|
+
icon: string;
|
|
52
|
+
}[];
|
|
53
|
+
toolbar: boolean;
|
|
54
|
+
};
|
|
55
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
56
|
+
pageInitialized(): Promise<void>;
|
|
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 {};
|