@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.
Files changed (41) hide show
  1. package/assets/login.js +89 -2
  2. package/package.json +2 -2
  3. package/plugin/assistant/umbraco-package.json +1 -1
  4. package/plugin/branding/branding.js +9 -14
  5. package/plugin/branding/localization-en.js +8 -0
  6. package/plugin/branding/theme.css +2 -0
  7. package/plugin/localizations/ar.js +3 -39
  8. package/plugin/localizations/bs.js +3 -29
  9. package/plugin/localizations/cs.js +3 -24
  10. package/plugin/localizations/cy.js +3 -32
  11. package/plugin/localizations/da.js +3 -41
  12. package/plugin/localizations/de.js +3 -27
  13. package/plugin/localizations/es.js +3 -19
  14. package/plugin/localizations/fr.js +3 -26
  15. package/plugin/localizations/hr.js +3 -29
  16. package/plugin/localizations/it.js +3 -29
  17. package/plugin/localizations/ja.js +3 -11
  18. package/plugin/localizations/ko.js +3 -11
  19. package/plugin/localizations/nb.js +3 -18
  20. package/plugin/localizations/nl.js +3 -29
  21. package/plugin/localizations/pl.js +3 -11
  22. package/plugin/localizations/pt-br.js +0 -10
  23. package/plugin/localizations/pt.js +3 -39
  24. package/plugin/localizations/ru.js +3 -9
  25. package/plugin/localizations/sv.js +3 -20
  26. package/plugin/localizations/tr.js +3 -26
  27. package/plugin/localizations/uk.js +3 -9
  28. package/plugin/localizations/umbraco-package.json +22 -2
  29. package/plugin/localizations/vi.js +3 -42
  30. package/plugin/localizations/zh-tw.js +0 -6
  31. package/plugin/localizations/zh.js +0 -6
  32. package/plugin/seo/umbraco-package.json +1 -1
  33. package/plugin/sign-in/entry.js +20 -0
  34. package/plugin/sign-in/sign-in-dialog.js +127 -0
  35. package/plugin/sign-in/umbraco-package.json +15 -0
  36. package/plugin/two-factor-setup.js +147 -0
  37. package/plugin/umbraco-package.json +27 -27
  38. package/plugin/welcome-dashboard.js +2 -5
  39. package/plugin/workflow-designer.js +245 -170
  40. package/src/shell.ts +4 -1
  41. 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.13.2",
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. This is the same
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>. It serves the real Umbraco backoffice you
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">