@crombolo/backoffice-host 0.13.2 → 0.14.1
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/login.js +89 -2
- 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 +8 -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/two-factor-setup.js +147 -0
- package/plugin/umbraco-package.json +27 -27
- package/plugin/welcome-dashboard.js +2 -5
- package/plugin/workflow-designer.js +245 -170
- package/src/shell.ts +4 -1
- package/plugin/branding/sysinfo.js +0 -89
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dialog shown when a session ends while the backoffice is open: crombolo's
|
|
3
|
+
* own, laid out as the login page is. Signing in opens the login page in a
|
|
4
|
+
* pop-up, as the client's dialog does, so what was on screen stays put.
|
|
5
|
+
*
|
|
6
|
+
* Closes once whichever comes first reports the session back: the sign-in it
|
|
7
|
+
* started, or another tab signing in.
|
|
8
|
+
*/
|
|
9
|
+
import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth'
|
|
10
|
+
import { css, html, nothing } from '@umbraco-cms/backoffice/external/lit'
|
|
11
|
+
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'
|
|
12
|
+
import { UMB_SERVER_CONTEXT } from '@umbraco-cms/backoffice/server'
|
|
13
|
+
|
|
14
|
+
/** The identity provider the server's authorize endpoint knows; nothing displays it. */
|
|
15
|
+
const PROVIDER = 'Umbraco'
|
|
16
|
+
const LOGO = '/umbraco/management/api/v1/security/back-office/graphics/login-logo-alternative'
|
|
17
|
+
|
|
18
|
+
export class CromboloSignInDialogElement extends UmbModalBaseElement {
|
|
19
|
+
static properties = {
|
|
20
|
+
_logo: { state: true },
|
|
21
|
+
_busy: { state: true },
|
|
22
|
+
_error: { state: true },
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#auth
|
|
26
|
+
#closed = false
|
|
27
|
+
|
|
28
|
+
constructor() {
|
|
29
|
+
super()
|
|
30
|
+
this.consumeContext(UMB_SERVER_CONTEXT, (server) => {
|
|
31
|
+
this._logo = `${server?.getServerUrl() ?? ''}${LOGO}`
|
|
32
|
+
})
|
|
33
|
+
this.consumeContext(UMB_AUTH_CONTEXT, (auth) => {
|
|
34
|
+
this.#auth = auth
|
|
35
|
+
this.observe(auth?.isAuthorized, (authorized) => {
|
|
36
|
+
if (authorized) this.#close()
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get #timedOut() {
|
|
42
|
+
return this.data?.userLoginState === 'timedOut'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#close() {
|
|
46
|
+
if (this.#closed) return
|
|
47
|
+
this.#closed = true
|
|
48
|
+
this.value = { success: true }
|
|
49
|
+
this._submitModal()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async #signIn() {
|
|
53
|
+
if (!this.#auth) return
|
|
54
|
+
this._busy = true
|
|
55
|
+
this._error = undefined
|
|
56
|
+
try {
|
|
57
|
+
// A pop-up when the session timed out, so the page underneath survives;
|
|
58
|
+
// otherwise the whole window goes to the login page.
|
|
59
|
+
await this.#auth.makeAuthorizationRequest(PROVIDER, !this.#timedOut)
|
|
60
|
+
if (this.#auth.getIsAuthorized()) this.#close()
|
|
61
|
+
} catch {
|
|
62
|
+
this._error = 'Signing in did not complete. Try again.'
|
|
63
|
+
} finally {
|
|
64
|
+
this._busy = false
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
render() {
|
|
69
|
+
return html`<div id="layout">
|
|
70
|
+
<uui-box id="card">
|
|
71
|
+
${this._logo ? html`<img id="logo" src=${this._logo} alt="" />` : nothing}
|
|
72
|
+
<h1 class="uui-h3">${this.#timedOut ? 'Your session has ended' : 'Sign in'}</h1>
|
|
73
|
+
${this.#timedOut ? html`<p>Sign in again to carry on where you left off.</p>` : nothing}
|
|
74
|
+
${this._error ? html`<p id="error">${this._error}</p>` : nothing}
|
|
75
|
+
<uui-button
|
|
76
|
+
look="primary"
|
|
77
|
+
color="positive"
|
|
78
|
+
label="Sign in"
|
|
79
|
+
.state=${this._busy ? 'waiting' : undefined}
|
|
80
|
+
@click=${() => this.#signIn()}
|
|
81
|
+
></uui-button>
|
|
82
|
+
</uui-box>
|
|
83
|
+
</div>`
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static styles = css`
|
|
87
|
+
:host {
|
|
88
|
+
display: block;
|
|
89
|
+
width: 100vw;
|
|
90
|
+
height: 100vh;
|
|
91
|
+
background: var(--uui-color-surface-alt, #f4f4f4);
|
|
92
|
+
}
|
|
93
|
+
#layout {
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: center;
|
|
96
|
+
justify-content: center;
|
|
97
|
+
height: 100%;
|
|
98
|
+
padding: 16px;
|
|
99
|
+
box-sizing: border-box;
|
|
100
|
+
}
|
|
101
|
+
#card {
|
|
102
|
+
width: 100%;
|
|
103
|
+
max-width: 400px;
|
|
104
|
+
}
|
|
105
|
+
#logo {
|
|
106
|
+
display: block;
|
|
107
|
+
height: 48px;
|
|
108
|
+
margin-bottom: var(--uui-size-space-4, 12px);
|
|
109
|
+
}
|
|
110
|
+
h1 {
|
|
111
|
+
margin: 0 0 var(--uui-size-space-4, 12px);
|
|
112
|
+
}
|
|
113
|
+
p {
|
|
114
|
+
margin: 0 0 var(--uui-size-space-5, 18px);
|
|
115
|
+
}
|
|
116
|
+
#error {
|
|
117
|
+
color: var(--uui-color-danger, #d42054);
|
|
118
|
+
}
|
|
119
|
+
uui-button {
|
|
120
|
+
width: 100%;
|
|
121
|
+
}
|
|
122
|
+
`
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
customElements.define('crombolo-sign-in-dialog', CromboloSignInDialogElement)
|
|
126
|
+
|
|
127
|
+
export default CromboloSignInDialogElement
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../backoffice-dist/dist/umbraco-package-schema.json",
|
|
3
|
+
"name": "Crombolo Sign-in Dialog",
|
|
4
|
+
"id": "Crombolo.SignInDialog",
|
|
5
|
+
"version": "0.14.1",
|
|
6
|
+
"allowPublicAccess": true,
|
|
7
|
+
"extensions": [
|
|
8
|
+
{
|
|
9
|
+
"type": "appEntryPoint",
|
|
10
|
+
"alias": "Crombolo.AppEntryPoint.SignInDialog",
|
|
11
|
+
"name": "Crombolo Sign-in Dialog",
|
|
12
|
+
"js": "/crombolo-plugin/sign-in/entry.js"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sets up an authenticator app for the signed-in user (story 032): the QR code,
|
|
3
|
+
* the first code to prove the app has it, then the recovery codes, shown once.
|
|
4
|
+
* Umbraco's own setup screen throws the recovery codes away, hence this one.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { UmbCurrentUserRepository } from '@umbraco-cms/backoffice/current-user'
|
|
8
|
+
import { css, html, nothing } from '@umbraco-cms/backoffice/external/lit'
|
|
9
|
+
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
|
|
10
|
+
|
|
11
|
+
const endpoint = (providerName) =>
|
|
12
|
+
`${(document.querySelector('base')?.getAttribute('href') ?? '/umbraco/').replace(/\/$/, '')}/management/api/v1/user/current/2fa/${encodeURIComponent(providerName)}`
|
|
13
|
+
|
|
14
|
+
export default class CromboloTwoFactorSetupElement extends UmbLitElement {
|
|
15
|
+
static properties = {
|
|
16
|
+
providerName: { attribute: false },
|
|
17
|
+
displayName: { attribute: false },
|
|
18
|
+
close: { attribute: false },
|
|
19
|
+
_setup: { state: true },
|
|
20
|
+
_error: { state: true },
|
|
21
|
+
_codes: { state: true },
|
|
22
|
+
_busy: { state: true },
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
constructor() {
|
|
26
|
+
super()
|
|
27
|
+
this.providerName = ''
|
|
28
|
+
this.displayName = ''
|
|
29
|
+
this.close = () => {}
|
|
30
|
+
this._setup = undefined
|
|
31
|
+
this._error = undefined
|
|
32
|
+
this._codes = undefined
|
|
33
|
+
this._busy = false
|
|
34
|
+
this.#repository = new UmbCurrentUserRepository(this)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#repository
|
|
38
|
+
|
|
39
|
+
async firstUpdated() {
|
|
40
|
+
const response = await fetch(endpoint(this.providerName), {
|
|
41
|
+
credentials: 'include',
|
|
42
|
+
headers: { accept: 'application/json' },
|
|
43
|
+
})
|
|
44
|
+
const body = await response.json().catch(() => ({}))
|
|
45
|
+
if (!response.ok || !body.secret) {
|
|
46
|
+
this._error = body.detail ?? 'Two-factor sign-in cannot be set up right now.'
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
this._setup = body
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async #submit(event) {
|
|
53
|
+
event.preventDefault()
|
|
54
|
+
const code = new FormData(event.target).get('code')?.toString().trim()
|
|
55
|
+
if (!code) return
|
|
56
|
+
this._busy = true
|
|
57
|
+
this._error = undefined
|
|
58
|
+
const response = await fetch(endpoint(this.providerName), {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
credentials: 'include',
|
|
61
|
+
headers: { accept: 'application/json', 'content-type': 'application/json' },
|
|
62
|
+
body: JSON.stringify({ code, secret: this._setup.secret }),
|
|
63
|
+
})
|
|
64
|
+
const body = await response.json().catch(() => ({}))
|
|
65
|
+
this._busy = false
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
this._error = body.detail ?? 'That code is not right. Try the next one.'
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
this._codes = body.recoveryCodes ?? []
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async #done() {
|
|
74
|
+
await this.#repository.requestMfaLoginProviders()
|
|
75
|
+
this.close()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#copy() {
|
|
79
|
+
navigator.clipboard?.writeText(this._codes.join('\n'))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
render() {
|
|
83
|
+
return html`<umb-body-layout headline=${this.displayName || 'Authenticator app'}>
|
|
84
|
+
${this._codes ? this.#renderCodes() : this.#renderSetup()}
|
|
85
|
+
</umb-body-layout>`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#renderSetup() {
|
|
89
|
+
if (!this._setup)
|
|
90
|
+
return this._error
|
|
91
|
+
? html`<p class="error">${this._error}</p>
|
|
92
|
+
<div slot="actions"><uui-button label="Close" @click=${() => this.close()}></uui-button></div>`
|
|
93
|
+
: html`<uui-loader-bar></uui-loader-bar>`
|
|
94
|
+
return html`<form id="setup" @submit=${this.#submit} novalidate>
|
|
95
|
+
<p>Scan this code with your authenticator app, then enter the six-digit code it shows.</p>
|
|
96
|
+
<img class="qr" src=${this._setup.qrCodeSetupImageUrl} alt="QR code for your authenticator app" />
|
|
97
|
+
<p class="secret">Or enter this key by hand: <code>${this._setup.secret}</code></p>
|
|
98
|
+
<uui-form-layout-item>
|
|
99
|
+
<uui-label slot="label" for="code" required>Code from the app</uui-label>
|
|
100
|
+
<uui-input id="code" name="code" inputmode="numeric" autocomplete="one-time-code" label="Code from the app" required></uui-input>
|
|
101
|
+
</uui-form-layout-item>
|
|
102
|
+
${this._error ? html`<p class="error">${this._error}</p>` : nothing}
|
|
103
|
+
</form>
|
|
104
|
+
<div slot="actions">
|
|
105
|
+
<uui-button label="Cancel" @click=${() => this.close()}></uui-button>
|
|
106
|
+
<uui-button type="submit" form="setup" look="primary" color="positive" label="Turn on" .state=${this._busy ? 'waiting' : undefined}
|
|
107
|
+
@click=${() => this.renderRoot.querySelector('#setup')?.requestSubmit()}></uui-button>
|
|
108
|
+
</div>`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#renderCodes() {
|
|
112
|
+
return html`<p>Two-factor sign-in is on. Keep these recovery codes somewhere safe: each one
|
|
113
|
+
signs you in once if you lose your phone. They will not be shown again.</p>
|
|
114
|
+
<ul class="codes">${this._codes.map((code) => html`<li><code>${code}</code></li>`)}</ul>
|
|
115
|
+
<div slot="actions">
|
|
116
|
+
<uui-button label="Copy codes" @click=${this.#copy}></uui-button>
|
|
117
|
+
<uui-button look="primary" label="Done" @click=${this.#done}></uui-button>
|
|
118
|
+
</div>`
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
static styles = css`
|
|
122
|
+
.qr {
|
|
123
|
+
display: block;
|
|
124
|
+
width: 200px;
|
|
125
|
+
height: 200px;
|
|
126
|
+
margin: var(--uui-size-space-4) auto;
|
|
127
|
+
background: #fff;
|
|
128
|
+
}
|
|
129
|
+
.secret code,
|
|
130
|
+
.codes code {
|
|
131
|
+
font-family: monospace;
|
|
132
|
+
word-break: break-all;
|
|
133
|
+
}
|
|
134
|
+
.codes {
|
|
135
|
+
display: grid;
|
|
136
|
+
grid-template-columns: repeat(2, 1fr);
|
|
137
|
+
gap: var(--uui-size-space-2);
|
|
138
|
+
padding: 0;
|
|
139
|
+
list-style: none;
|
|
140
|
+
}
|
|
141
|
+
.error {
|
|
142
|
+
color: var(--uui-color-danger);
|
|
143
|
+
}
|
|
144
|
+
`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
customElements.define('crombolo-two-factor-setup', CromboloTwoFactorSetupElement)
|
|
@@ -2,9 +2,17 @@
|
|
|
2
2
|
"$schema": "../../backoffice-dist/dist/umbraco-package-schema.json",
|
|
3
3
|
"name": "Crombolo",
|
|
4
4
|
"id": "Crombolo.Core",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.14.1",
|
|
6
6
|
"allowPublicAccess": false,
|
|
7
7
|
"extensions": [
|
|
8
|
+
{
|
|
9
|
+
"type": "mfaLoginProvider",
|
|
10
|
+
"alias": "Crombolo.MfaLoginProvider.Authenticator",
|
|
11
|
+
"name": "Crombolo Authenticator App",
|
|
12
|
+
"forProviderName": "UmbracoUserAppAuthenticator",
|
|
13
|
+
"element": "/crombolo-plugin/two-factor-setup.js",
|
|
14
|
+
"meta": { "label": "Authenticator app" }
|
|
15
|
+
},
|
|
8
16
|
{
|
|
9
17
|
"type": "sectionView",
|
|
10
18
|
"alias": "Crombolo.SectionView.Bundles.Marketplace",
|
|
@@ -180,6 +188,24 @@
|
|
|
180
188
|
"element": "/crombolo-plugin/read-only-banner.js",
|
|
181
189
|
"weight": 1000
|
|
182
190
|
},
|
|
191
|
+
{
|
|
192
|
+
"type": "headerApp",
|
|
193
|
+
"kind": "button",
|
|
194
|
+
"alias": "Crombolo.HeaderApp.Settings",
|
|
195
|
+
"name": "Crombolo Settings Header App",
|
|
196
|
+
"weight": 300,
|
|
197
|
+
"meta": {
|
|
198
|
+
"label": "Settings",
|
|
199
|
+
"icon": "icon-settings",
|
|
200
|
+
"href": "section/settings"
|
|
201
|
+
},
|
|
202
|
+
"conditions": [
|
|
203
|
+
{
|
|
204
|
+
"alias": "Umb.Condition.SectionUserPermission",
|
|
205
|
+
"match": "Umb.Section.Settings"
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
},
|
|
183
209
|
{
|
|
184
210
|
"type": "workspaceFooterApp",
|
|
185
211
|
"alias": "Crombolo.WorkspaceFooterApp.ElementTypeHint",
|
|
@@ -193,26 +219,6 @@
|
|
|
193
219
|
}
|
|
194
220
|
]
|
|
195
221
|
},
|
|
196
|
-
{
|
|
197
|
-
"type": "localization",
|
|
198
|
-
"alias": "Crombolo.Localization.EN",
|
|
199
|
-
"name": "Crombolo English Overrides",
|
|
200
|
-
"weight": 0,
|
|
201
|
-
"meta": {
|
|
202
|
-
"culture": "en"
|
|
203
|
-
},
|
|
204
|
-
"js": "/crombolo-plugin/branding/localization-en.js"
|
|
205
|
-
},
|
|
206
|
-
{
|
|
207
|
-
"type": "localization",
|
|
208
|
-
"alias": "Crombolo.Localization.EN_US",
|
|
209
|
-
"name": "Crombolo English (United States) Overrides",
|
|
210
|
-
"weight": 0,
|
|
211
|
-
"meta": {
|
|
212
|
-
"culture": "en-us"
|
|
213
|
-
},
|
|
214
|
-
"js": "/crombolo-plugin/branding/localization-en.js"
|
|
215
|
-
},
|
|
216
222
|
{
|
|
217
223
|
"type": "backofficeEntryPoint",
|
|
218
224
|
"alias": "Crombolo.EntryPoint.Branding",
|
|
@@ -225,12 +231,6 @@
|
|
|
225
231
|
"name": "Crombolo Help Menu",
|
|
226
232
|
"js": "/crombolo-plugin/branding/help-menu.js"
|
|
227
233
|
},
|
|
228
|
-
{
|
|
229
|
-
"type": "backofficeEntryPoint",
|
|
230
|
-
"alias": "Crombolo.EntryPoint.Sysinfo",
|
|
231
|
-
"name": "Crombolo System Information Branding",
|
|
232
|
-
"js": "/crombolo-plugin/branding/sysinfo.js"
|
|
233
|
-
},
|
|
234
234
|
{
|
|
235
235
|
"type": "section",
|
|
236
236
|
"alias": "Crombolo.Section.Forms",
|
|
@@ -13,8 +13,7 @@ const STEPS = [
|
|
|
13
13
|
{
|
|
14
14
|
title: 'Model the content',
|
|
15
15
|
body: html`Open <strong>Settings → Content Types</strong> and describe a page: its
|
|
16
|
-
properties, its tabs, and which types may sit beneath it
|
|
17
|
-
editor Umbraco ships, so anything you know about Umbraco document types applies.`,
|
|
16
|
+
properties, its tabs, and which types may sit beneath it.`,
|
|
18
17
|
},
|
|
19
18
|
{
|
|
20
19
|
title: 'Write a template',
|
|
@@ -44,9 +43,7 @@ export default class CromboloWelcomeDashboardElement extends UmbLitElement {
|
|
|
44
43
|
<uui-box headline="Welcome to Crombolo">
|
|
45
44
|
<p class="lede">
|
|
46
45
|
Crombolo is a content management system that runs on
|
|
47
|
-
<strong>Bun and TypeScript</strong
|
|
48
|
-
are looking at now and follows Umbraco's API contract, so the editing
|
|
49
|
-
experience is Umbraco's — while the server, the templates and the schema are
|
|
46
|
+
<strong>Bun and TypeScript</strong>: the server, the templates and the schema are
|
|
50
47
|
TypeScript, and the database is SQLite or Postgres.
|
|
51
48
|
</p>
|
|
52
49
|
<p class="lede">
|