@crombolo/backoffice-host 0.13.1 → 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 (53) 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/bundle-builder-labels.js +30 -1
  6. package/plugin/branding/localization-en.js +8 -0
  7. package/plugin/branding/sign-in.js +31 -0
  8. package/plugin/branding/theme.css +2 -0
  9. package/plugin/bundles-created-overview.js +31 -18
  10. package/plugin/bundles-installed.js +127 -61
  11. package/plugin/bundles-marketplace.js +1 -1
  12. package/plugin/changes-dashboard.js +8 -1
  13. package/plugin/download.js +19 -0
  14. package/plugin/form-picker.js +1 -1
  15. package/plugin/forms-client.js +5 -0
  16. package/plugin/forms-entries.js +30 -16
  17. package/plugin/forms-overview.js +55 -33
  18. package/plugin/localizations/ar.js +3 -39
  19. package/plugin/localizations/bs.js +3 -29
  20. package/plugin/localizations/cs.js +3 -24
  21. package/plugin/localizations/cy.js +3 -32
  22. package/plugin/localizations/da.js +3 -41
  23. package/plugin/localizations/de.js +3 -27
  24. package/plugin/localizations/es.js +3 -19
  25. package/plugin/localizations/fr.js +3 -26
  26. package/plugin/localizations/hr.js +3 -29
  27. package/plugin/localizations/it.js +3 -29
  28. package/plugin/localizations/ja.js +3 -11
  29. package/plugin/localizations/ko.js +3 -11
  30. package/plugin/localizations/nb.js +3 -18
  31. package/plugin/localizations/nl.js +3 -29
  32. package/plugin/localizations/pl.js +3 -11
  33. package/plugin/localizations/pt-br.js +0 -10
  34. package/plugin/localizations/pt.js +3 -39
  35. package/plugin/localizations/ru.js +3 -9
  36. package/plugin/localizations/sv.js +3 -20
  37. package/plugin/localizations/tr.js +3 -26
  38. package/plugin/localizations/uk.js +3 -9
  39. package/plugin/localizations/umbraco-package.json +22 -2
  40. package/plugin/localizations/vi.js +3 -42
  41. package/plugin/localizations/zh-tw.js +0 -6
  42. package/plugin/localizations/zh.js +0 -6
  43. package/plugin/seo/umbraco-package.json +1 -1
  44. package/plugin/sign-in/entry.js +20 -0
  45. package/plugin/sign-in/sign-in-dialog.js +127 -0
  46. package/plugin/sign-in/umbraco-package.json +15 -0
  47. package/plugin/two-factor-setup.js +147 -0
  48. package/plugin/umbraco-package.json +30 -30
  49. package/plugin/welcome-dashboard.js +2 -5
  50. package/plugin/workflow-designer.js +298 -187
  51. package/plugin/workflow-requests.js +19 -6
  52. package/src/shell.ts +4 -1
  53. package/plugin/branding/sysinfo.js +0 -89
@@ -3,12 +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
- "paste": {
7
- "errorMessage": "您所粘貼的文本含有特殊字元或格式,Crombolo將清除以適應網頁。"
8
- },
9
- "dashboard": {
10
- "nothinghappens": "如果Crombolo沒有打開,您可能需要允許彈出式視窗。"
11
- },
12
6
  "actions": {
13
7
  "changeDocType": "改變內容類型",
14
8
  "exportDocumentType": "匯出內容類型",
@@ -3,12 +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
- "paste": {
7
- "errorMessage": "您所粘贴的文本含有特殊字符或格式,Crombolo将清除以适应网页。"
8
- },
9
- "dashboard": {
10
- "nothinghappens": "如果Crombolo没有打开,您可能需要允许弹出式窗口。"
11
- },
12
6
  "actions": {
13
7
  "changeDocType": "改变内容类型",
14
8
  "exportDocumentType": "导出内容类型",
@@ -2,7 +2,7 @@
2
2
  "$schema": "../../../backoffice-dist/dist/umbraco-package-schema.json",
3
3
  "name": "Crombolo Search & sharing",
4
4
  "id": "Crombolo.Seo",
5
- "version": "0.13.1",
5
+ "version": "0.14.1",
6
6
  "allowPublicAccess": false,
7
7
  "extensions": [
8
8
  {
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Puts crombolo's sign-in dialog in place of the client's. The client opens
3
+ * `Umb.Modal.AppAuth` when a session ends while the backoffice is open, and
4
+ * draws it before any private manifest has loaded — so it is replaced from a
5
+ * public app entry point, which the client runs before it routes.
6
+ *
7
+ * Re-registered under the same alias rather than through `overwrites`: the
8
+ * modal is looked up with `byTypeAndAlias`, which does not resolve overwrites.
9
+ */
10
+ export const DIALOG_ALIAS = 'Umb.Modal.AppAuth'
11
+
12
+ export const onInit = (_host, extensionRegistry) => {
13
+ extensionRegistry.unregister(DIALOG_ALIAS)
14
+ extensionRegistry.register({
15
+ type: 'modal',
16
+ alias: DIALOG_ALIAS,
17
+ name: 'Crombolo Sign-in Dialog',
18
+ element: () => import('./sign-in-dialog.js'),
19
+ })
20
+ }
@@ -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.1",
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",
@@ -246,7 +246,7 @@
246
246
  "alias": "Crombolo.SectionView.Forms.Overview",
247
247
  "name": "Crombolo Forms Overview",
248
248
  "element": "/crombolo-plugin/forms-overview.js",
249
- "weight": 200,
249
+ "weight": 100,
250
250
  "meta": {
251
251
  "label": "Forms",
252
252
  "pathname": "forms",
@@ -264,7 +264,7 @@
264
264
  "alias": "Crombolo.SectionView.Forms.Entries",
265
265
  "name": "Crombolo Forms Entries",
266
266
  "element": "/crombolo-plugin/forms-entries.js",
267
- "weight": 100,
267
+ "weight": 200,
268
268
  "meta": {
269
269
  "label": "Entries",
270
270
  "pathname": "entries",
@@ -352,7 +352,7 @@
352
352
  "name": "Crombolo Workflow Section",
353
353
  "weight": 40,
354
354
  "meta": {
355
- "label": "Workflow",
355
+ "label": "Workflows",
356
356
  "pathname": "workflow"
357
357
  }
358
358
  },
@@ -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">