@crombolo/backoffice-host 0.13.2 → 0.14.4
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/assets/device.js +180 -0
- package/assets/login.js +89 -2
- package/assets/remote-approval.js +134 -0
- package/package.json +2 -2
- package/plugin/assistant/umbraco-package.json +1 -1
- package/plugin/branding/branding.js +9 -14
- package/plugin/branding/localization-en.js +11 -0
- package/plugin/branding/theme.css +2 -0
- package/plugin/localizations/ar.js +3 -39
- package/plugin/localizations/bs.js +3 -29
- package/plugin/localizations/cs.js +3 -24
- package/plugin/localizations/cy.js +3 -32
- package/plugin/localizations/da.js +3 -41
- package/plugin/localizations/de.js +3 -27
- package/plugin/localizations/es.js +3 -19
- package/plugin/localizations/fr.js +3 -26
- package/plugin/localizations/hr.js +3 -29
- package/plugin/localizations/it.js +3 -29
- package/plugin/localizations/ja.js +3 -11
- package/plugin/localizations/ko.js +3 -11
- package/plugin/localizations/nb.js +3 -18
- package/plugin/localizations/nl.js +3 -29
- package/plugin/localizations/pl.js +3 -11
- package/plugin/localizations/pt-br.js +0 -10
- package/plugin/localizations/pt.js +3 -39
- package/plugin/localizations/ru.js +3 -9
- package/plugin/localizations/sv.js +3 -20
- package/plugin/localizations/tr.js +3 -26
- package/plugin/localizations/uk.js +3 -9
- package/plugin/localizations/umbraco-package.json +22 -2
- package/plugin/localizations/vi.js +3 -42
- package/plugin/localizations/zh-tw.js +0 -6
- package/plugin/localizations/zh.js +0 -6
- package/plugin/seo/umbraco-package.json +1 -1
- package/plugin/sign-in/entry.js +20 -0
- package/plugin/sign-in/sign-in-dialog.js +127 -0
- package/plugin/sign-in/umbraco-package.json +15 -0
- package/plugin/signed-in-devices.js +90 -0
- package/plugin/two-factor-setup.js +147 -0
- package/plugin/umbraco-package.json +79 -27
- package/plugin/welcome-dashboard.js +2 -5
- package/plugin/workflow-designer.js +245 -170
- package/src/login-assets.ts +5 -2
- package/src/shell.ts +65 -1
- package/plugin/branding/sysinfo.js +0 -89
package/assets/device.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page a person approves a command-line sign-in on (story 107, RFC 8628).
|
|
3
|
+
* The `crombolo` command shows a code and opens this page; the person, already
|
|
4
|
+
* signed in here, checks the code matches and allows it — or declines.
|
|
5
|
+
*/
|
|
6
|
+
import { css, html, LitElement, nothing } from '@umbraco-cms/backoffice/external/lit'
|
|
7
|
+
import '@umbraco-cms/backoffice/external/uui'
|
|
8
|
+
|
|
9
|
+
const API = '/umbraco/management/api/v1/security/back-office'
|
|
10
|
+
|
|
11
|
+
class CromboloDeviceElement extends LitElement {
|
|
12
|
+
static properties = {
|
|
13
|
+
userCode: { type: String, attribute: 'user-code' },
|
|
14
|
+
logoImage: { type: String, attribute: 'logo-image' },
|
|
15
|
+
_pending: { state: true },
|
|
16
|
+
_error: { state: true },
|
|
17
|
+
_done: { state: true },
|
|
18
|
+
_busy: { state: true },
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
super()
|
|
23
|
+
this.userCode = ''
|
|
24
|
+
this.logoImage = ''
|
|
25
|
+
this._pending = undefined
|
|
26
|
+
this._error = ''
|
|
27
|
+
this._done = ''
|
|
28
|
+
this._busy = false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
connectedCallback() {
|
|
32
|
+
super.connectedCallback()
|
|
33
|
+
if (this.userCode) this.#look(this.userCode)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async #look(code) {
|
|
37
|
+
this._error = ''
|
|
38
|
+
this._busy = true
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch(`${API}/device/pending?user_code=${encodeURIComponent(code)}`, {
|
|
41
|
+
credentials: 'include',
|
|
42
|
+
})
|
|
43
|
+
if (response.ok) {
|
|
44
|
+
this._pending = await response.json()
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
const problem = await response.json().catch(() => ({}))
|
|
48
|
+
this._error = problem.detail ?? problem.title ?? 'That code is not waiting.'
|
|
49
|
+
} catch {
|
|
50
|
+
this._error = 'Could not reach the server.'
|
|
51
|
+
} finally {
|
|
52
|
+
this._busy = false
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async #decide(approve) {
|
|
57
|
+
this._busy = true
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetch(`${API}/device/decide`, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
credentials: 'include',
|
|
62
|
+
headers: { 'content-type': 'application/json' },
|
|
63
|
+
body: JSON.stringify({ userCode: this._pending.userCode, approve }),
|
|
64
|
+
})
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
this._error = 'That code is no longer waiting. Start the sign-in again.'
|
|
67
|
+
this._pending = undefined
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
this._done = approve
|
|
71
|
+
? 'Signed in. You can close this window and go back to your terminal.'
|
|
72
|
+
: 'Declined. Nothing was signed in.'
|
|
73
|
+
} catch {
|
|
74
|
+
this._error = 'Could not reach the server.'
|
|
75
|
+
} finally {
|
|
76
|
+
this._busy = false
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#submitCode(event) {
|
|
81
|
+
event.preventDefault()
|
|
82
|
+
const code = event.target.elements.code.value.trim()
|
|
83
|
+
if (code) this.#look(code)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#body() {
|
|
87
|
+
if (this._done) return html`<p id="notice" role="status">${this._done}</p>`
|
|
88
|
+
if (this._pending)
|
|
89
|
+
return html`
|
|
90
|
+
<p>
|
|
91
|
+
${this._pending.clientName ?? 'A command line'} is asking to sign in as you. Check that this
|
|
92
|
+
code matches the one it shows:
|
|
93
|
+
</p>
|
|
94
|
+
<p id="code">${this._pending.userCode}</p>
|
|
95
|
+
${this._error ? html`<p id="error" role="alert">${this._error}</p>` : nothing}
|
|
96
|
+
<div id="actions">
|
|
97
|
+
<uui-button look="primary" color="positive" label="Allow" .state=${this._busy ? 'waiting' : undefined}
|
|
98
|
+
@click=${() => this.#decide(true)}></uui-button>
|
|
99
|
+
<uui-button look="outline" label="Decline" @click=${() => this.#decide(false)}></uui-button>
|
|
100
|
+
</div>`
|
|
101
|
+
return html`
|
|
102
|
+
<form @submit=${this.#submitCode}>
|
|
103
|
+
<uui-form-layout-item>
|
|
104
|
+
<uui-label for="code" slot="label" required>The code your terminal shows</uui-label>
|
|
105
|
+
<uui-input id="code" name="code" autocomplete="off" required .value=${this.userCode ?? ''}></uui-input>
|
|
106
|
+
</uui-form-layout-item>
|
|
107
|
+
${this._error ? html`<p id="error" role="alert">${this._error}</p>` : nothing}
|
|
108
|
+
<uui-button type="submit" look="primary" label="Continue" .state=${this._busy ? 'waiting' : undefined}></uui-button>
|
|
109
|
+
</form>`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
render() {
|
|
113
|
+
return html`
|
|
114
|
+
<div id="layout">
|
|
115
|
+
<uui-box id="card">
|
|
116
|
+
${this.logoImage ? html`<img id="logo" src=${this.logoImage} alt="" />` : nothing}
|
|
117
|
+
<h1 class="uui-h3">Sign in from your terminal</h1>
|
|
118
|
+
${this.#body()}
|
|
119
|
+
</uui-box>
|
|
120
|
+
</div>
|
|
121
|
+
`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
static styles = css`
|
|
125
|
+
:host {
|
|
126
|
+
display: block;
|
|
127
|
+
min-height: 100vh;
|
|
128
|
+
background: var(--uui-color-surface-alt, #f4f4f4);
|
|
129
|
+
}
|
|
130
|
+
#layout {
|
|
131
|
+
display: flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
justify-content: center;
|
|
134
|
+
min-height: 100vh;
|
|
135
|
+
padding: 16px;
|
|
136
|
+
box-sizing: border-box;
|
|
137
|
+
}
|
|
138
|
+
#card {
|
|
139
|
+
width: 100%;
|
|
140
|
+
max-width: 420px;
|
|
141
|
+
}
|
|
142
|
+
#logo {
|
|
143
|
+
display: block;
|
|
144
|
+
height: 48px;
|
|
145
|
+
margin-bottom: var(--uui-size-space-4, 12px);
|
|
146
|
+
}
|
|
147
|
+
h1 {
|
|
148
|
+
margin: 0 0 var(--uui-size-space-5, 18px);
|
|
149
|
+
}
|
|
150
|
+
form {
|
|
151
|
+
display: flex;
|
|
152
|
+
flex-direction: column;
|
|
153
|
+
gap: var(--uui-size-space-4, 12px);
|
|
154
|
+
}
|
|
155
|
+
uui-input {
|
|
156
|
+
width: 100%;
|
|
157
|
+
}
|
|
158
|
+
#code {
|
|
159
|
+
font-family: monospace;
|
|
160
|
+
font-size: 1.75rem;
|
|
161
|
+
letter-spacing: 0.15em;
|
|
162
|
+
text-align: center;
|
|
163
|
+
margin: var(--uui-size-space-4, 12px) 0;
|
|
164
|
+
}
|
|
165
|
+
#actions {
|
|
166
|
+
display: flex;
|
|
167
|
+
gap: var(--uui-size-space-3, 9px);
|
|
168
|
+
}
|
|
169
|
+
#error {
|
|
170
|
+
margin: 0;
|
|
171
|
+
color: var(--uui-color-danger, #d42054);
|
|
172
|
+
}
|
|
173
|
+
#notice {
|
|
174
|
+
margin: 0;
|
|
175
|
+
color: var(--uui-color-positive, #0b8152);
|
|
176
|
+
}
|
|
177
|
+
`
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
customElements.define('crombolo-device', CromboloDeviceElement)
|
package/assets/login.js
CHANGED
|
@@ -39,6 +39,8 @@ class CromboloLoginElement extends LitElement {
|
|
|
39
39
|
logoImage: { type: String, attribute: 'logo-image-alternative' },
|
|
40
40
|
usernameIsEmail: { type: Boolean, attribute: 'username-is-email' },
|
|
41
41
|
allowPasswordReset: { type: Boolean, attribute: 'allow-password-reset' },
|
|
42
|
+
disableLocalLogin: { type: Boolean, attribute: 'disable-local-login' },
|
|
43
|
+
externalProviders: { type: Array, attribute: 'external-providers' },
|
|
42
44
|
_busy: { state: true },
|
|
43
45
|
_error: { state: true },
|
|
44
46
|
_view: { state: true },
|
|
@@ -51,6 +53,8 @@ class CromboloLoginElement extends LitElement {
|
|
|
51
53
|
this.logoImage = ''
|
|
52
54
|
this.usernameIsEmail = true
|
|
53
55
|
this.allowPasswordReset = false
|
|
56
|
+
this.disableLocalLogin = false
|
|
57
|
+
this.externalProviders = []
|
|
54
58
|
this._busy = false
|
|
55
59
|
this._error = ''
|
|
56
60
|
this._notice = ''
|
|
@@ -63,6 +67,9 @@ class CromboloLoginElement extends LitElement {
|
|
|
63
67
|
? { kind: 'reset', user: params.get('userId'), code: params.get('resetCode') }
|
|
64
68
|
: undefined
|
|
65
69
|
this._view = this._link ? 'checking' : 'login'
|
|
70
|
+
// An identity provider's sign-in that came back refused says why.
|
|
71
|
+
const external = params.get('externalLoginError')
|
|
72
|
+
if (external) this._error = external
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
connectedCallback() {
|
|
@@ -182,7 +189,9 @@ class CromboloLoginElement extends LitElement {
|
|
|
182
189
|
return
|
|
183
190
|
}
|
|
184
191
|
if (response.status === 402) {
|
|
185
|
-
|
|
192
|
+
const challenge = await response.json().catch(() => ({}))
|
|
193
|
+
this._twoFactorProvider = challenge.enabledTwoFactorProviderNames?.[0]
|
|
194
|
+
this._view = 'two-factor'
|
|
186
195
|
} else if (response.status === 403) {
|
|
187
196
|
this._error = 'This account is locked or not approved.'
|
|
188
197
|
} else if (response.status === 401) {
|
|
@@ -197,6 +206,57 @@ class CromboloLoginElement extends LitElement {
|
|
|
197
206
|
}
|
|
198
207
|
}
|
|
199
208
|
|
|
209
|
+
async #verifyTwoFactor(event) {
|
|
210
|
+
event.preventDefault()
|
|
211
|
+
if (this._busy) return
|
|
212
|
+
const code = event.target.elements.code.value.trim()
|
|
213
|
+
if (!code) return
|
|
214
|
+
this._busy = true
|
|
215
|
+
this._error = ''
|
|
216
|
+
try {
|
|
217
|
+
const response = await this.#post('/security/back-office/verify-2fa', {
|
|
218
|
+
provider: this._twoFactorProvider,
|
|
219
|
+
code,
|
|
220
|
+
})
|
|
221
|
+
if (response.ok) {
|
|
222
|
+
window.location.href = localTarget(this.returnUrl, '/umbraco')
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
if (response.status === 401) {
|
|
226
|
+
this._view = 'login'
|
|
227
|
+
this._error = 'That took too long. Sign in again.'
|
|
228
|
+
} else if (response.status === 403) {
|
|
229
|
+
this._view = 'login'
|
|
230
|
+
this._error = 'This account is locked or not approved.'
|
|
231
|
+
} else {
|
|
232
|
+
this._error = 'That code is not right. Try the next one.'
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
this._error = 'Could not reach the server.'
|
|
236
|
+
} finally {
|
|
237
|
+
this._busy = false
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
#twoFactorForm() {
|
|
242
|
+
return html`
|
|
243
|
+
<h1 class="uui-h3">Enter your code</h1>
|
|
244
|
+
<p>Enter the six-digit code from your authenticator app, or one of your recovery codes.</p>
|
|
245
|
+
<form @submit=${this.#verifyTwoFactor}>
|
|
246
|
+
<uui-form-layout-item>
|
|
247
|
+
<uui-label for="code" slot="label" required>Code</uui-label>
|
|
248
|
+
<uui-input id="code" name="code" autocomplete="one-time-code" required ?disabled=${this._busy}></uui-input>
|
|
249
|
+
</uui-form-layout-item>
|
|
250
|
+
${this.#messages()}
|
|
251
|
+
<uui-button type="submit" look="primary" color="positive" .state=${this._busy ? 'waiting' : undefined} label="Verify"></uui-button>
|
|
252
|
+
<uui-button look="link" label="Back to sign in" @click=${() => {
|
|
253
|
+
this._error = ''
|
|
254
|
+
this._view = 'login'
|
|
255
|
+
}}></uui-button>
|
|
256
|
+
</form>
|
|
257
|
+
`
|
|
258
|
+
}
|
|
259
|
+
|
|
200
260
|
#messages() {
|
|
201
261
|
return html`${this._notice ? html`<p id="notice" role="status">${this._notice}</p>` : nothing}
|
|
202
262
|
${this._error ? html`<p id="error" role="alert">${this._error}</p>` : nothing}`
|
|
@@ -245,7 +305,9 @@ class CromboloLoginElement extends LitElement {
|
|
|
245
305
|
? this.#newPasswordForm()
|
|
246
306
|
: this._view === 'forgot'
|
|
247
307
|
? this.#resetRequestForm()
|
|
248
|
-
: this
|
|
308
|
+
: this._view === 'two-factor'
|
|
309
|
+
? this.#twoFactorForm()
|
|
310
|
+
: this.#loginForm()
|
|
249
311
|
return html`
|
|
250
312
|
<div id="layout">
|
|
251
313
|
<uui-box id="card">
|
|
@@ -256,7 +318,25 @@ class CromboloLoginElement extends LitElement {
|
|
|
256
318
|
`
|
|
257
319
|
}
|
|
258
320
|
|
|
321
|
+
/** A button per backoffice identity provider, each returning here signed in. */
|
|
322
|
+
#providers() {
|
|
323
|
+
if (!this.externalProviders?.length) return nothing
|
|
324
|
+
return html`<div id="providers">
|
|
325
|
+
${this.externalProviders.map(
|
|
326
|
+
(provider) =>
|
|
327
|
+
html`<uui-button look="outline" href=${provider.url} label=${`Sign in with ${provider.name}`}>Sign in with ${provider.name}</uui-button>`,
|
|
328
|
+
)}
|
|
329
|
+
</div>`
|
|
330
|
+
}
|
|
331
|
+
|
|
259
332
|
#loginForm() {
|
|
333
|
+
// With local sign-in off, the providers are the only way in.
|
|
334
|
+
if (this.disableLocalLogin)
|
|
335
|
+
return html`
|
|
336
|
+
<h1 class="uui-h3">Sign in</h1>
|
|
337
|
+
${this.#messages()}
|
|
338
|
+
${this.#providers()}
|
|
339
|
+
`
|
|
260
340
|
return html`
|
|
261
341
|
<h1 class="uui-h3">Sign in</h1>
|
|
262
342
|
<form @submit=${this.#submit}>
|
|
@@ -301,6 +381,7 @@ class CromboloLoginElement extends LitElement {
|
|
|
301
381
|
: nothing
|
|
302
382
|
}
|
|
303
383
|
</form>
|
|
384
|
+
${this.#providers()}
|
|
304
385
|
`
|
|
305
386
|
}
|
|
306
387
|
|
|
@@ -342,6 +423,12 @@ class CromboloLoginElement extends LitElement {
|
|
|
342
423
|
margin: 0;
|
|
343
424
|
color: var(--uui-color-danger, #d42054);
|
|
344
425
|
}
|
|
426
|
+
#providers {
|
|
427
|
+
display: flex;
|
|
428
|
+
flex-direction: column;
|
|
429
|
+
gap: var(--uui-size-space-3, 9px);
|
|
430
|
+
margin-top: var(--uui-size-space-5, 18px);
|
|
431
|
+
}
|
|
345
432
|
#notice {
|
|
346
433
|
margin: 0;
|
|
347
434
|
color: var(--uui-color-positive, #0b8152);
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page a push to a protected environment is approved on (story 107). The
|
|
3
|
+
* person who pushed shares its link; someone else allowed to push here looks at
|
|
4
|
+
* who sent it and when, and approves or declines it.
|
|
5
|
+
*/
|
|
6
|
+
import { css, html, LitElement, nothing } from '@umbraco-cms/backoffice/external/lit'
|
|
7
|
+
import '@umbraco-cms/backoffice/external/uui'
|
|
8
|
+
|
|
9
|
+
const API = '/umbraco/remote/v1/jobs'
|
|
10
|
+
|
|
11
|
+
class CromboloRemoteApprovalElement extends LitElement {
|
|
12
|
+
static properties = {
|
|
13
|
+
job: { type: String },
|
|
14
|
+
logoImage: { type: String, attribute: 'logo-image' },
|
|
15
|
+
_job: { state: true },
|
|
16
|
+
_error: { state: true },
|
|
17
|
+
_busy: { state: true },
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
constructor() {
|
|
21
|
+
super()
|
|
22
|
+
this.job = ''
|
|
23
|
+
this.logoImage = ''
|
|
24
|
+
this._job = undefined
|
|
25
|
+
this._error = ''
|
|
26
|
+
this._busy = false
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
connectedCallback() {
|
|
30
|
+
super.connectedCallback()
|
|
31
|
+
this.#load()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async #load() {
|
|
35
|
+
const response = await fetch(`${API}/${encodeURIComponent(this.job)}`, {
|
|
36
|
+
credentials: 'include',
|
|
37
|
+
})
|
|
38
|
+
const body = await response.json().catch(() => ({}))
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
this._error = body.title ?? 'There is no such push.'
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
this._job = body
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async #decide(approve) {
|
|
47
|
+
this._busy = true
|
|
48
|
+
this._error = ''
|
|
49
|
+
const response = await fetch(
|
|
50
|
+
`${API}/${encodeURIComponent(this.job)}/${approve ? 'approve' : 'decline'}`,
|
|
51
|
+
{
|
|
52
|
+
method: 'POST',
|
|
53
|
+
credentials: 'include',
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
const body = await response.json().catch(() => ({}))
|
|
57
|
+
this._busy = false
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
this._error = body.title ?? 'That could not be done.'
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
this._job = body
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#body() {
|
|
66
|
+
const job = this._job
|
|
67
|
+
if (!job)
|
|
68
|
+
return this._error ? html`<p id="error">${this._error}</p>` : html`<uui-loader></uui-loader>`
|
|
69
|
+
const when = new Date(job.createdAt).toLocaleString()
|
|
70
|
+
if (job.state !== 'approval-required')
|
|
71
|
+
return html`<p>
|
|
72
|
+
${job.state === 'refused' ? (job.problem?.title ?? 'This push was declined.') : 'This push has been decided.'}
|
|
73
|
+
</p>`
|
|
74
|
+
return html`
|
|
75
|
+
<p>
|
|
76
|
+
<strong>${job.requestedBy}</strong> wants to replace <strong>${job.environment}</strong> with a copy of their
|
|
77
|
+
site, sent ${when}. Everything on ${job.environment} that is not in it goes to the recycle bin, and a backup is
|
|
78
|
+
taken first.
|
|
79
|
+
</p>
|
|
80
|
+
${this._error ? html`<p id="error" role="alert">${this._error}</p>` : nothing}
|
|
81
|
+
<div id="actions">
|
|
82
|
+
<uui-button look="primary" color="positive" label="Approve" .state=${this._busy ? 'waiting' : undefined}
|
|
83
|
+
@click=${() => this.#decide(true)}></uui-button>
|
|
84
|
+
<uui-button look="outline" color="danger" label="Decline" @click=${() => this.#decide(false)}></uui-button>
|
|
85
|
+
</div>`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
render() {
|
|
89
|
+
return html`<div id="layout">
|
|
90
|
+
<uui-box id="card">
|
|
91
|
+
${this.logoImage ? html`<img id="logo" src=${this.logoImage} alt="" />` : nothing}
|
|
92
|
+
<h1 class="uui-h3">Approve a push</h1>
|
|
93
|
+
${this.#body()}
|
|
94
|
+
</uui-box>
|
|
95
|
+
</div>`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
static styles = css`
|
|
99
|
+
:host {
|
|
100
|
+
display: block;
|
|
101
|
+
min-height: 100vh;
|
|
102
|
+
background: var(--uui-color-surface-alt, #f4f4f4);
|
|
103
|
+
}
|
|
104
|
+
#layout {
|
|
105
|
+
display: flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
justify-content: center;
|
|
108
|
+
min-height: 100vh;
|
|
109
|
+
padding: 16px;
|
|
110
|
+
box-sizing: border-box;
|
|
111
|
+
}
|
|
112
|
+
#card {
|
|
113
|
+
width: 100%;
|
|
114
|
+
max-width: 480px;
|
|
115
|
+
}
|
|
116
|
+
#logo {
|
|
117
|
+
display: block;
|
|
118
|
+
height: 48px;
|
|
119
|
+
margin-bottom: var(--uui-size-space-4, 12px);
|
|
120
|
+
}
|
|
121
|
+
h1 {
|
|
122
|
+
margin: 0 0 var(--uui-size-space-5, 18px);
|
|
123
|
+
}
|
|
124
|
+
#actions {
|
|
125
|
+
display: flex;
|
|
126
|
+
gap: var(--uui-size-space-3, 9px);
|
|
127
|
+
}
|
|
128
|
+
#error {
|
|
129
|
+
color: var(--uui-color-danger, #d42054);
|
|
130
|
+
}
|
|
131
|
+
`
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
customElements.define('crombolo-remote-approval', CromboloRemoteApprovalElement)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crombolo/backoffice-host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.4",
|
|
4
4
|
"description": "Serves the Umbraco backoffice SPA: shell HTML, import map and package manifests.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Crombolo",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"access": "public"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@crombolo/core": "0.
|
|
34
|
+
"@crombolo/core": "0.14.4",
|
|
35
35
|
"@crombolo/backoffice-dist": "18.2.0"
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -1,25 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The two places the vendored client names the product in code rather than in a
|
|
3
|
-
* string table
|
|
3
|
+
* string table.
|
|
4
4
|
*
|
|
5
|
-
* `view.controller.js` builds the page title with a hardcoded
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* So it is corrected as it is written. The suffix is the site's own name, which
|
|
11
|
-
* the shell states in `<meta name="application-name">` — so a site called
|
|
12
|
-
* something other than Crombolo gets its own name rather than a second hardcoded
|
|
13
|
-
* one.
|
|
5
|
+
* `view.controller.js` builds the page title with a hardcoded suffix, which
|
|
6
|
+
* `vendor:backoffice` rebrands to Crombolo. The suffix should be the site's own
|
|
7
|
+
* name, which the shell states in `<meta name="application-name">` — so a site
|
|
8
|
+
* called something other than Crombolo gets its own name rather than ours.
|
|
14
9
|
*
|
|
15
10
|
* Not read from `document.title`, although the shell puts the name there too.
|
|
16
11
|
* This module loads after the client has started, and by then the client may
|
|
17
|
-
* have written a title of its own. Taking `Content |
|
|
18
|
-
* name makes every correction end in ` |
|
|
12
|
+
* have written a title of its own. Taking `Content | Crombolo` for the site's
|
|
13
|
+
* name makes every correction end in ` | Crombolo` again: the observer below
|
|
19
14
|
* then answers its own change, forever, and the page never gets its main thread
|
|
20
15
|
* back.
|
|
21
16
|
*/
|
|
22
|
-
const CLIENT_SUFFIX = ' |
|
|
17
|
+
const CLIENT_SUFFIX = ' | Crombolo'
|
|
23
18
|
|
|
24
19
|
/** The site's name as the shell stated it. */
|
|
25
20
|
export function brandName(doc = document) {
|
|
@@ -70,7 +65,7 @@ function correctLogoLink() {
|
|
|
70
65
|
if (!anchor) return false
|
|
71
66
|
anchor.href = 'https://github.com/kineco-au/crombolo-cms'
|
|
72
67
|
anchor.textContent = 'Crombolo on GitHub'
|
|
73
|
-
const image = logo.shadowRoot.querySelector('img
|
|
68
|
+
const image = logo.shadowRoot.querySelector('img#logo')
|
|
74
69
|
if (image) image.alt = brandName()
|
|
75
70
|
return true
|
|
76
71
|
}
|
|
@@ -46,6 +46,9 @@ export default {
|
|
|
46
46
|
"Hello there and welcome to Crombolo! In just 1 minute you'll be good to go, we just need you to setup a password and add a picture for your avatar.",
|
|
47
47
|
userinviteExpiredMessage:
|
|
48
48
|
'Welcome to Crombolo! Unfortunately your invite has expired. Please contact your administrator to get a new one.',
|
|
49
|
+
|
|
50
|
+
// The heading over the remote permissions (story 107) in a user group.
|
|
51
|
+
'permissionsEntityGroup_crombolo-remote': 'Moving whole sites',
|
|
49
52
|
},
|
|
50
53
|
general: {
|
|
51
54
|
umbracoInfo: 'Crombolo info',
|
|
@@ -106,6 +109,14 @@ export default {
|
|
|
106
109
|
searchUmbracoIssues: 'Search Crombolo issues',
|
|
107
110
|
searchUmbracoIssuesOnGithub: 'Search Crombolo issues on GitHub',
|
|
108
111
|
},
|
|
112
|
+
redirectUrls: {
|
|
113
|
+
// Followed by `CROMBOLO_TRACK_REDIRECTS`, which `vendor:backoffice` puts in
|
|
114
|
+
// place of the .NET setting; it tracks when true, the inverse of Umbraco's.
|
|
115
|
+
disableUrlTrackerInstruction:
|
|
116
|
+
"Redirect URL tracking is set in the site's environment. To turn it off, set this variable to false:",
|
|
117
|
+
enableUrlTrackerInstruction:
|
|
118
|
+
"Redirect URL tracking is set in the site's environment. To turn it on, set this variable to true, or remove it:",
|
|
119
|
+
},
|
|
109
120
|
defaultdialogs: {
|
|
110
121
|
linkYourConfirm:
|
|
111
122
|
'You are about to link your Crombolo and {0} accounts and you will be redirected to {0} to complete the process.',
|
|
@@ -68,6 +68,8 @@
|
|
|
68
68
|
--uui-color-header-surface: var(--crombolo-orange);
|
|
69
69
|
--uui-color-header-contrast: rgb(84 50 31 / 80%);
|
|
70
70
|
--uui-color-header-contrast-emphasis: var(--crombolo-cocoa);
|
|
71
|
+
/* The active section's marker, dark so it stands out from the white submenu below. */
|
|
72
|
+
--crombolo-header-current: var(--crombolo-orange-text);
|
|
71
73
|
|
|
72
74
|
/* Pure white panels on a warm off-white page, so cards still separate. */
|
|
73
75
|
--uui-color-background: var(--crombolo-cream);
|
|
@@ -3,45 +3,6 @@
|
|
|
3
3
|
// plugin/branding/localization-en.js curates, and "content type" where
|
|
4
4
|
// Umbraco says "document type".
|
|
5
5
|
export default {
|
|
6
|
-
"login": {
|
|
7
|
-
"instruction": "سجل الدخول إلى Crombolo"
|
|
8
|
-
},
|
|
9
|
-
"user": {
|
|
10
|
-
"changePasswordDescription": "يمكنك تغيير كلمة مرورك للوصول إلى واجهة Crombolo من خلال ملء النموذج أدناه والنقر على زر 'تغيير كلمة المرور'",
|
|
11
|
-
"createUserHelp": "قم بإنشاء مستخدمين جدد لمنحهم الوصول إلى Crombolo. عند إنشاء مستخدم جديد، سيتم توليد كلمة مرور يمكنك مشاركتها مع المستخدم.",
|
|
12
|
-
"inviteUserHelp": "ادعُ مستخدمين جدد لمنحهم الوصول إلى Crombolo. سيتم إرسال بريد إلكتروني دعوة إلى المستخدم مع معلومات حول كيفية تسجيل الدخول إلى Crombolo. تدوم الدعوات لمدة 72 ساعة.",
|
|
13
|
-
"noConsole": "تعطيل الوصول إلى Crombolo",
|
|
14
|
-
"userCreatedSuccessHelp": "تم إنشاء المستخدم الجديد بنجاح. لتسجيل الدخول إلى Crombolo استخدم كلمة المرور أدناه.",
|
|
15
|
-
"userInvitedSuccessHelp": "تم إرسال دعوة إلى المستخدم الجديد مع تفاصيل حول كيفية تسجيل الدخول إلى Crombolo.",
|
|
16
|
-
"userinviteWelcomeMessage": "مرحباً بك في Crombolo! في دقيقة واحدة فقط ستكون جاهزاً، نحن فقط بحاجة منك لتعيين كلمة مرور وإضافة صورة لملفك الشخصي.",
|
|
17
|
-
"userinviteExpiredMessage": "مرحباً بك في Crombolo! للأسف انتهت صلاحية دعوتك. يرجى الاتصال بالمسؤول الخاص بك واطلب منهم إعادة إرسالها.",
|
|
18
|
-
"documentType": "نوع المحتوى"
|
|
19
|
-
},
|
|
20
|
-
"general": {
|
|
21
|
-
"umbracoInfo": "معلومات Crombolo"
|
|
22
|
-
},
|
|
23
|
-
"visuallyHiddenTexts": {
|
|
24
|
-
"searchOverlayTitle": "بحث في مكتب Crombolo الخلفي"
|
|
25
|
-
},
|
|
26
|
-
"languages": {
|
|
27
|
-
"defaultLanguageHelp": "يمكن لموقع Crombolo أن يحتوي فقط على لغة افتراضية واحدة."
|
|
28
|
-
},
|
|
29
|
-
"paste": {
|
|
30
|
-
"errorMessage": "النص الذي تحاول لصقه يحتوي على أحرف خاصة أو تنسيق. قد يكون ذلك بسبب نسخ النص من Microsoft Word. يمكن لـ Crombolo إزالة الأحرف الخاصة أو التنسيق تلقائيًا، بحيث يكون المحتوى الملصق أكثر ملاءمة للويب.\n"
|
|
31
|
-
},
|
|
32
|
-
"dashboard": {
|
|
33
|
-
"nothinghappens": "إذا لم يفتح Crombolo، قد تحتاج إلى السماح بالنوافذ المنبثقة من هذا الموقع"
|
|
34
|
-
},
|
|
35
|
-
"logViewer": {
|
|
36
|
-
"searchUmbracoSource": "البحث في مصدر Crombolo",
|
|
37
|
-
"searchWithinUmbracoSourceCodeOnGithub": "البحث ضمن كود مصدر Crombolo على Github",
|
|
38
|
-
"searchUmbracoIssues": "البحث في قضايا Crombolo",
|
|
39
|
-
"searchUmbracoIssuesOnGithub": "البحث في قضايا Crombolo على Github"
|
|
40
|
-
},
|
|
41
|
-
"defaultdialogs": {
|
|
42
|
-
"linkYourConfirm": "أنت على وشك ربط حسابك في Crombolo وحسابك في {0} وسيتم توجيهك إلى {0} للتأكيد.",
|
|
43
|
-
"unLinkYourConfirm": "أنت على وشك إلغاء ربط حسابك في Crombolo وحسابك في {0} وسيتم تسجيل خروجك."
|
|
44
|
-
},
|
|
45
6
|
"actions": {
|
|
46
7
|
"changeDocType": "تغيير نوع المحتوى",
|
|
47
8
|
"exportDocumentType": "تصدير نوع المحتوى",
|
|
@@ -87,6 +48,9 @@ export default {
|
|
|
87
48
|
"treeHeaders": {
|
|
88
49
|
"documentTypes": "أنواع المحتوى"
|
|
89
50
|
},
|
|
51
|
+
"user": {
|
|
52
|
+
"documentType": "نوع المحتوى"
|
|
53
|
+
},
|
|
90
54
|
"references": {
|
|
91
55
|
"labelUsedByDocumentTypes": "مستشهد به من قبل أنواع المحتوى التالية"
|
|
92
56
|
},
|
|
@@ -3,35 +3,6 @@
|
|
|
3
3
|
// plugin/branding/localization-en.js curates, and "content type" where
|
|
4
4
|
// Umbraco says "document type".
|
|
5
5
|
export default {
|
|
6
|
-
"user": {
|
|
7
|
-
"changePasswordDescription": "Možete promijeniti lozinku za pristup Crombolo backofficeu popunjavanjem\n izađite iz donjeg obrasca i kliknite na dugme 'Promijeni lozinku'\n ",
|
|
8
|
-
"createUserHelp": "Kreirajte nove korisnike da im date pristup Crombolo. Kada se novom korisniku kreira lozinka\n će biti generisano koje možete podijeliti s korisnikom.\n ",
|
|
9
|
-
"inviteUserHelp": "Pozovite nove korisnike da im daju pristup Crombolo. E-mail sa pozivom će biti poslan na\n korisnik s informacijama o tome kako se prijaviti na Crombolo. Pozivnice traju 72 sata.\n ",
|
|
10
|
-
"noConsole": "Onemogućite pristup Crombolo-u",
|
|
11
|
-
"userCreatedSuccessHelp": "Novi korisnik je uspješno kreiran. Za prijavu na Crombolo koristite\n lozinka ispod.\n ",
|
|
12
|
-
"userInvitedSuccessHelp": "Novom korisniku je poslana pozivnica s detaljima o tome kako se prijaviti\n Crombolo.\n ",
|
|
13
|
-
"userinviteWelcomeMessage": "Pozdrav i dobrodošli u Crombolo! Za samo 1 minut možete krenuti, mi samo trebate postaviti lozinku.",
|
|
14
|
-
"userinviteExpiredMessage": "Dobrodošli u Crombolo! Nažalost, vaš poziv je istekao. Molimo kontaktirajte svoju\n administratora i zamolite ih da ga ponovo pošalju.\n ",
|
|
15
|
-
"documentType": "Tip sadržaja"
|
|
16
|
-
},
|
|
17
|
-
"general": {
|
|
18
|
-
"umbracoInfo": "Crombolo info"
|
|
19
|
-
},
|
|
20
|
-
"languages": {
|
|
21
|
-
"defaultLanguageHelp": "Crombolo stranica može imati samo jedan zadani jezik."
|
|
22
|
-
},
|
|
23
|
-
"paste": {
|
|
24
|
-
"errorMessage": "Tekst koji pokušavate zalijepiti sadrži posebne znakove ili formatiranje. Ovo bi moglo biti\n uzrokovano kopiranjem teksta iz programa Microsoft Word. Crombolo može automatski ukloniti posebne znakove ili formatiranje, tako da\n zalijepljeni sadržaj će biti prikladniji za web.\n "
|
|
25
|
-
},
|
|
26
|
-
"dashboard": {
|
|
27
|
-
"nothinghappens": "Ako se Crombolo ne otvara, možda ćete morati dozvoliti iskačuće prozore sa ove stranice"
|
|
28
|
-
},
|
|
29
|
-
"logViewer": {
|
|
30
|
-
"searchUmbracoSource": "Pretraži Crombolo Source",
|
|
31
|
-
"searchWithinUmbracoSourceCodeOnGithub": "Pretraži Crombolo source code on Github-u",
|
|
32
|
-
"searchUmbracoIssues": "Pretraži Crombolo Issues",
|
|
33
|
-
"searchUmbracoIssuesOnGithub": "Pretraži Crombolo Issues na Github-u"
|
|
34
|
-
},
|
|
35
6
|
"actions": {
|
|
36
7
|
"changeDocType": "Promijeni Tip Sadržaja",
|
|
37
8
|
"exportDocumentType": "Izvezi Tip Sadržaja",
|
|
@@ -76,6 +47,9 @@ export default {
|
|
|
76
47
|
"treeHeaders": {
|
|
77
48
|
"documentTypes": "Tipovi sadržaja"
|
|
78
49
|
},
|
|
50
|
+
"user": {
|
|
51
|
+
"documentType": "Tip sadržaja"
|
|
52
|
+
},
|
|
79
53
|
"references": {
|
|
80
54
|
"labelUsedByDocumentTypes": "Koristi se u tipovima sadržaja"
|
|
81
55
|
},
|