@crombolo/backoffice-host 0.0.0-stage → 0.9.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/LICENSE +21 -0
- package/NOTICE +69 -0
- package/assets/login.js +352 -0
- package/package.json +35 -4
- package/plugin/assistant/client.js +53 -0
- package/plugin/assistant/drawer.js +865 -0
- package/plugin/assistant/umbraco-package.json +16 -0
- package/plugin/branding/backdrop.svg +1 -0
- package/plugin/branding/branding.js +100 -0
- package/plugin/branding/bundle-builder-labels.js +63 -0
- package/plugin/branding/client-credentials.js +49 -0
- package/plugin/branding/components-tree.js +40 -0
- package/plugin/branding/content-type-labels.js +27 -0
- package/plugin/branding/favicon.svg +1 -0
- package/plugin/branding/help-menu.js +46 -0
- package/plugin/branding/icon-mark.js +15 -0
- package/plugin/branding/icon-registry.js +40 -0
- package/plugin/branding/localization-en.js +115 -0
- package/plugin/branding/log-viewer-menu.js +70 -0
- package/plugin/branding/logo-header.svg +1 -0
- package/plugin/branding/logo-surface.svg +1 -0
- package/plugin/branding/query-builder.js +28 -0
- package/plugin/branding/sign-in.js +80 -0
- package/plugin/branding/sysinfo.js +89 -0
- package/plugin/branding/theme.css +102 -0
- package/plugin/bundles-client.js +34 -0
- package/plugin/bundles-created-overview.js +150 -0
- package/plugin/bundles-created.js +70 -0
- package/plugin/bundles-installed.js +147 -0
- package/plugin/bundles-marketplace.js +199 -0
- package/plugin/changes-dashboard.js +132 -0
- package/plugin/element-type-hint.js +58 -0
- package/plugin/form-picker.js +116 -0
- package/plugin/forms-client.js +52 -0
- package/plugin/forms-entries.js +244 -0
- package/plugin/forms-overview.js +380 -0
- package/plugin/localizations/ar.js +97 -0
- package/plugin/localizations/bs.js +86 -0
- package/plugin/localizations/cs.js +73 -0
- package/plugin/localizations/cy.js +92 -0
- package/plugin/localizations/da.js +101 -0
- package/plugin/localizations/de.js +79 -0
- package/plugin/localizations/en.js +71 -0
- package/plugin/localizations/es.js +52 -0
- package/plugin/localizations/fr.js +78 -0
- package/plugin/localizations/he.js +24 -0
- package/plugin/localizations/hr.js +89 -0
- package/plugin/localizations/it.js +86 -0
- package/plugin/localizations/ja.js +43 -0
- package/plugin/localizations/ko.js +24 -0
- package/plugin/localizations/nb.js +44 -0
- package/plugin/localizations/nl.js +85 -0
- package/plugin/localizations/pl.js +44 -0
- package/plugin/localizations/pt-br.js +25 -0
- package/plugin/localizations/pt.js +100 -0
- package/plugin/localizations/ro.js +9 -0
- package/plugin/localizations/ru.js +46 -0
- package/plugin/localizations/sv.js +46 -0
- package/plugin/localizations/tr.js +80 -0
- package/plugin/localizations/uk.js +46 -0
- package/plugin/localizations/umbraco-package.json +279 -0
- package/plugin/localizations/vi.js +104 -0
- package/plugin/localizations/zh-tw.js +38 -0
- package/plugin/localizations/zh.js +38 -0
- package/plugin/read-only-banner.js +48 -0
- package/plugin/report-client.js +13 -0
- package/plugin/seo/client.js +35 -0
- package/plugin/seo/dashboard.js +242 -0
- package/plugin/seo/document-view.js +161 -0
- package/plugin/seo/type-hint.js +45 -0
- package/plugin/seo/umbraco-package.json +61 -0
- package/plugin/settings-dashboard.js +123 -0
- package/plugin/tsx-editors.js +337 -0
- package/plugin/tsx-snippets.js +63 -0
- package/plugin/umbraco-package.json +298 -0
- package/plugin/welcome-dashboard.js +106 -0
- package/src/branding.ts +59 -0
- package/src/extensions.ts +228 -0
- package/src/graphics.ts +87 -0
- package/src/index.ts +7 -0
- package/src/login-assets.ts +18 -0
- package/src/manifests.ts +154 -0
- package/src/paths.ts +117 -0
- package/src/shell.ts +139 -0
- package/src/static.ts +107 -0
- package/src/text-imports.d.ts +23 -0
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kineco
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/NOTICE
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
crombolo
|
|
2
|
+
Copyright (c) 2026 Kineco
|
|
3
|
+
|
|
4
|
+
This product includes third-party software. crombolo is an independent project.
|
|
5
|
+
It is not affiliated with, endorsed by, or sponsored by Umbraco A/S, which owns
|
|
6
|
+
the Umbraco trademark.
|
|
7
|
+
|
|
8
|
+
There are three places third-party material is redistributed, rather than merely
|
|
9
|
+
depended on:
|
|
10
|
+
|
|
11
|
+
@crombolo/backoffice-dist the built Umbraco backoffice and the browser
|
|
12
|
+
packages bundled into it, produced by
|
|
13
|
+
`bun run vendor:backoffice`
|
|
14
|
+
@crombolo/backoffice-dist three CSS files copied unmodified from the Umbraco
|
|
15
|
+
client source tree
|
|
16
|
+
@crombolo/contracts the Umbraco Management API OpenAPI document,
|
|
17
|
+
copied unmodified
|
|
18
|
+
|
|
19
|
+
No Umbraco trademark is redistributed. The Umbraco wordmark, the "U" roundel
|
|
20
|
+
favicon and the installer illustration are deliberately not carried in this
|
|
21
|
+
repository: MIT grants copyright permission and says nothing about trademarks, so
|
|
22
|
+
crombolo's own marks are served at those paths instead. See `BRANDED_ASSETS` in
|
|
23
|
+
`@crombolo/backoffice-host`.
|
|
24
|
+
|
|
25
|
+
The Umbraco backoffice
|
|
26
|
+
----------------------
|
|
27
|
+
@umbraco-cms/backoffice 18.2.0 — MIT — https://github.com/umbraco/Umbraco-CMS
|
|
28
|
+
@umbraco-ui/uui 2.0.2 — MIT — https://github.com/umbraco/Umbraco.UI
|
|
29
|
+
|
|
30
|
+
Static files from the Umbraco client source tree
|
|
31
|
+
------------------------------------------------
|
|
32
|
+
css/umb-css.css, css/rte-content.css, css/umbraco-blockgridlayout.css
|
|
33
|
+
— copied unmodified from Umbraco-CMS release-18.2.0 — MIT
|
|
34
|
+
— https://github.com/umbraco/Umbraco-CMS
|
|
35
|
+
Shipped in @crombolo/backoffice-dist under `upstream-static/`.
|
|
36
|
+
|
|
37
|
+
`rte-content.css` itself carries Tiptap's default editor styles, copied from
|
|
38
|
+
tiptap v2.11.7 (MIT); the file records this in its own header.
|
|
39
|
+
|
|
40
|
+
The Umbraco Management API contract
|
|
41
|
+
-----------------------------------
|
|
42
|
+
OpenApi.json — copied unmodified from Umbraco-CMS release-18.2.0
|
|
43
|
+
(src/Umbraco.Cms.Api.Management/OpenApi.json) — MIT
|
|
44
|
+
— https://github.com/umbraco/Umbraco-CMS
|
|
45
|
+
Shipped in @crombolo/contracts, which also ships the TypeScript types generated
|
|
46
|
+
from it.
|
|
47
|
+
|
|
48
|
+
Browser runtime dependencies bundled into the backoffice build
|
|
49
|
+
--------------------------------------------------------------
|
|
50
|
+
@heximal/expressions 0.1.5 — BSD-3-Clause
|
|
51
|
+
@microsoft/signalr 10.0.11 — MIT
|
|
52
|
+
@tiptap/* 3.31.3 — MIT
|
|
53
|
+
diff 9.0.0 — BSD-3-Clause
|
|
54
|
+
dompurify 3.4.16 — Apache-2.0, elected from "MPL-2.0 OR Apache-2.0"
|
|
55
|
+
element-internals-polyfill 3.0.2 — MIT
|
|
56
|
+
lit 3.3.3 — BSD-3-Clause
|
|
57
|
+
luxon 3.7.2 — MIT
|
|
58
|
+
marked 18.0.14 — MIT
|
|
59
|
+
monaco-editor 0.57.0 — MIT
|
|
60
|
+
rxjs 7.8.2 — Apache-2.0
|
|
61
|
+
uuid 14.0.2 — MIT
|
|
62
|
+
|
|
63
|
+
dompurify is offered under either licence; Apache-2.0 is elected here, so no
|
|
64
|
+
MPL-2.0 obligation attaches to the copy bundled into the backoffice build. Of the
|
|
65
|
+
Apache-2.0 packages above, none ships a NOTICE file of its own, so Apache-2.0
|
|
66
|
+
section 4(d) adds nothing to this file.
|
|
67
|
+
|
|
68
|
+
Each package's own licence text ships inside its npm distribution and applies
|
|
69
|
+
unchanged to the copies bundled here.
|
package/assets/login.js
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crombolo login page.
|
|
3
|
+
*
|
|
4
|
+
* Umbraco's own login screen ships as a separate, unpublished npm package, so
|
|
5
|
+
* this is our equivalent, built from the component library that IS published
|
|
6
|
+
* (@umbraco-cms/backoffice/external/uui) and resolved through the import map the
|
|
7
|
+
* page embeds. It posts to the same endpoint Umbraco's does and then returns to
|
|
8
|
+
* the authorize URL that sent us here. It also serves the two links a user may
|
|
9
|
+
* be sent, as Umbraco's does: `?flow=invite-user` to choose a first password and
|
|
10
|
+
* `?flow=reset-password` to choose a new one, plus the request for a reset link.
|
|
11
|
+
*/
|
|
12
|
+
import { css, html, LitElement, nothing } from '@umbraco-cms/backoffice/external/lit'
|
|
13
|
+
import '@umbraco-cms/backoffice/external/uui'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Where to go once signed in, if that is somewhere on this site.
|
|
17
|
+
*
|
|
18
|
+
* The server reduces `returnUrl` to a local path before it reaches the attribute,
|
|
19
|
+
* so this is the second lock rather than the only one — worth having because this
|
|
20
|
+
* navigation happens the instant a session exists, which is the most valuable
|
|
21
|
+
* moment to send someone elsewhere. Resolving against this origin is what catches
|
|
22
|
+
* `//host` and `/\host`; a browser reads both as leaving the site while a text
|
|
23
|
+
* check reads them as local.
|
|
24
|
+
*/
|
|
25
|
+
function localTarget(value, fallback) {
|
|
26
|
+
if (!value) return fallback
|
|
27
|
+
try {
|
|
28
|
+
const resolved = new URL(value, window.location.origin)
|
|
29
|
+
if (resolved.origin !== window.location.origin) return fallback
|
|
30
|
+
return `${resolved.pathname}${resolved.search}${resolved.hash}`
|
|
31
|
+
} catch {
|
|
32
|
+
return fallback
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class CromboloLoginElement extends LitElement {
|
|
37
|
+
static properties = {
|
|
38
|
+
returnUrl: { type: String, attribute: 'return-url' },
|
|
39
|
+
logoImage: { type: String, attribute: 'logo-image-alternative' },
|
|
40
|
+
usernameIsEmail: { type: Boolean, attribute: 'username-is-email' },
|
|
41
|
+
allowPasswordReset: { type: Boolean, attribute: 'allow-password-reset' },
|
|
42
|
+
_busy: { state: true },
|
|
43
|
+
_error: { state: true },
|
|
44
|
+
_view: { state: true },
|
|
45
|
+
_notice: { state: true },
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
constructor() {
|
|
49
|
+
super()
|
|
50
|
+
this.returnUrl = ''
|
|
51
|
+
this.logoImage = ''
|
|
52
|
+
this.usernameIsEmail = true
|
|
53
|
+
this.allowPasswordReset = false
|
|
54
|
+
this._busy = false
|
|
55
|
+
this._error = ''
|
|
56
|
+
this._notice = ''
|
|
57
|
+
const params = new URLSearchParams(window.location.search)
|
|
58
|
+
const flow = params.get('flow')
|
|
59
|
+
this._link =
|
|
60
|
+
flow === 'invite-user'
|
|
61
|
+
? { kind: 'invite', user: params.get('userId'), code: params.get('inviteCode') }
|
|
62
|
+
: flow === 'reset-password'
|
|
63
|
+
? { kind: 'reset', user: params.get('userId'), code: params.get('resetCode') }
|
|
64
|
+
: undefined
|
|
65
|
+
this._view = this._link ? 'checking' : 'login'
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
connectedCallback() {
|
|
69
|
+
super.connectedCallback()
|
|
70
|
+
if (this._link) this.#checkLink()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async #post(path, body) {
|
|
74
|
+
return fetch(`/umbraco/management/api/v1${path}`, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
credentials: 'include',
|
|
77
|
+
headers: { 'content-type': 'application/json' },
|
|
78
|
+
body: JSON.stringify(body),
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async #checkLink() {
|
|
83
|
+
const { kind, user, code } = this._link
|
|
84
|
+
const response =
|
|
85
|
+
kind === 'invite'
|
|
86
|
+
? await this.#post('/user/invite/verify', { user: { id: user }, token: code })
|
|
87
|
+
: await this.#post('/security/forgot-password/verify', {
|
|
88
|
+
user: { id: user },
|
|
89
|
+
resetCode: code,
|
|
90
|
+
})
|
|
91
|
+
if (response.ok) {
|
|
92
|
+
this._view = 'new-password'
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
this._view = 'login'
|
|
96
|
+
this._error =
|
|
97
|
+
kind === 'invite'
|
|
98
|
+
? 'This invitation is no longer valid. Ask for a new one.'
|
|
99
|
+
: 'This reset link is no longer valid. Ask for a new one.'
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async #setPassword(event) {
|
|
103
|
+
event.preventDefault()
|
|
104
|
+
if (this._busy) return
|
|
105
|
+
const form = event.target
|
|
106
|
+
const password = form.elements.password.value
|
|
107
|
+
if (password !== form.elements.confirm.value) {
|
|
108
|
+
this._error = 'The passwords do not match.'
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
const { kind, user, code } = this._link
|
|
112
|
+
this._busy = true
|
|
113
|
+
this._error = ''
|
|
114
|
+
try {
|
|
115
|
+
const response =
|
|
116
|
+
kind === 'invite'
|
|
117
|
+
? await this.#post('/user/invite/create-password', {
|
|
118
|
+
user: { id: user },
|
|
119
|
+
token: code,
|
|
120
|
+
password,
|
|
121
|
+
})
|
|
122
|
+
: await this.#post('/security/forgot-password/reset', {
|
|
123
|
+
user: { id: user },
|
|
124
|
+
resetCode: code,
|
|
125
|
+
password,
|
|
126
|
+
})
|
|
127
|
+
if (response.ok) {
|
|
128
|
+
this._view = 'login'
|
|
129
|
+
this._notice = 'Your password is set. Sign in with it now.'
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
const problem = await response.json().catch(() => ({}))
|
|
133
|
+
this._error = problem.detail ?? 'The password could not be set.'
|
|
134
|
+
} catch {
|
|
135
|
+
this._error = 'Could not reach the server.'
|
|
136
|
+
} finally {
|
|
137
|
+
this._busy = false
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async #requestReset(event) {
|
|
142
|
+
event.preventDefault()
|
|
143
|
+
if (this._busy) return
|
|
144
|
+
const email = event.target.elements.email.value.trim()
|
|
145
|
+
if (!email) return
|
|
146
|
+
this._busy = true
|
|
147
|
+
try {
|
|
148
|
+
await this.#post('/security/forgot-password', { email })
|
|
149
|
+
this._view = 'login'
|
|
150
|
+
this._notice = 'If that address belongs to an account, a reset link is on its way.'
|
|
151
|
+
} catch {
|
|
152
|
+
this._error = 'Could not reach the server.'
|
|
153
|
+
} finally {
|
|
154
|
+
this._busy = false
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async #submit(event) {
|
|
159
|
+
event.preventDefault()
|
|
160
|
+
if (this._busy) return
|
|
161
|
+
const form = event.target
|
|
162
|
+
const username = form.elements.username.value.trim()
|
|
163
|
+
const password = form.elements.password.value
|
|
164
|
+
if (!username || !password) {
|
|
165
|
+
this._error = 'Enter your credentials to sign in.'
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this._busy = true
|
|
170
|
+
this._error = ''
|
|
171
|
+
try {
|
|
172
|
+
const response = await fetch('/umbraco/management/api/v1/security/back-office/login', {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
credentials: 'include',
|
|
175
|
+
headers: { 'content-type': 'application/json' },
|
|
176
|
+
body: JSON.stringify({ username, password }),
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
if (response.ok) {
|
|
180
|
+
// Back to the authorize endpoint, which now sees a session cookie.
|
|
181
|
+
window.location.href = localTarget(this.returnUrl, '/umbraco')
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
if (response.status === 402) {
|
|
185
|
+
this._error = 'This account requires two-factor authentication, which is not yet supported.'
|
|
186
|
+
} else if (response.status === 403) {
|
|
187
|
+
this._error = 'This account is locked or not approved.'
|
|
188
|
+
} else if (response.status === 401) {
|
|
189
|
+
this._error = 'Those credentials were not recognised.'
|
|
190
|
+
} else {
|
|
191
|
+
this._error = 'Sign-in failed. Please try again.'
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
this._error = 'Could not reach the server.'
|
|
195
|
+
} finally {
|
|
196
|
+
this._busy = false
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
#messages() {
|
|
201
|
+
return html`${this._notice ? html`<p id="notice" role="status">${this._notice}</p>` : nothing}
|
|
202
|
+
${this._error ? html`<p id="error" role="alert">${this._error}</p>` : nothing}`
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
#newPasswordForm() {
|
|
206
|
+
return html`
|
|
207
|
+
<h1 class="uui-h3">${this._link?.kind === 'invite' ? 'Choose your password' : 'Choose a new password'}</h1>
|
|
208
|
+
<form @submit=${this.#setPassword}>
|
|
209
|
+
<uui-form-layout-item>
|
|
210
|
+
<uui-label for="password" slot="label" required>Password</uui-label>
|
|
211
|
+
<uui-input id="password" name="password" type="password" autocomplete="new-password" required></uui-input>
|
|
212
|
+
</uui-form-layout-item>
|
|
213
|
+
<uui-form-layout-item>
|
|
214
|
+
<uui-label for="confirm" slot="label" required>Confirm password</uui-label>
|
|
215
|
+
<uui-input id="confirm" name="confirm" type="password" autocomplete="new-password" required></uui-input>
|
|
216
|
+
</uui-form-layout-item>
|
|
217
|
+
${this.#messages()}
|
|
218
|
+
<uui-button type="submit" look="primary" color="positive" .state=${this._busy ? 'waiting' : undefined} label="Set password"></uui-button>
|
|
219
|
+
</form>
|
|
220
|
+
`
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
#resetRequestForm() {
|
|
224
|
+
return html`
|
|
225
|
+
<h1 class="uui-h3">Forgotten password</h1>
|
|
226
|
+
<form @submit=${this.#requestReset}>
|
|
227
|
+
<uui-form-layout-item>
|
|
228
|
+
<uui-label for="email" slot="label" required>Email</uui-label>
|
|
229
|
+
<uui-input id="email" name="email" type="email" autocomplete="email" required></uui-input>
|
|
230
|
+
</uui-form-layout-item>
|
|
231
|
+
${this.#messages()}
|
|
232
|
+
<uui-button type="submit" look="primary" .state=${this._busy ? 'waiting' : undefined} label="Send reset link"></uui-button>
|
|
233
|
+
<uui-button look="link" label="Back to sign in" @click=${() => {
|
|
234
|
+
this._view = 'login'
|
|
235
|
+
}}></uui-button>
|
|
236
|
+
</form>
|
|
237
|
+
`
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
render() {
|
|
241
|
+
const body =
|
|
242
|
+
this._view === 'checking'
|
|
243
|
+
? html`<uui-loader></uui-loader>`
|
|
244
|
+
: this._view === 'new-password'
|
|
245
|
+
? this.#newPasswordForm()
|
|
246
|
+
: this._view === 'forgot'
|
|
247
|
+
? this.#resetRequestForm()
|
|
248
|
+
: this.#loginForm()
|
|
249
|
+
return html`
|
|
250
|
+
<div id="layout">
|
|
251
|
+
<uui-box id="card">
|
|
252
|
+
${this.logoImage ? html`<img id="logo" src=${this.logoImage} alt="crombolo" />` : nothing}
|
|
253
|
+
${body}
|
|
254
|
+
</uui-box>
|
|
255
|
+
</div>
|
|
256
|
+
`
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
#loginForm() {
|
|
260
|
+
return html`
|
|
261
|
+
<h1 class="uui-h3">Sign in</h1>
|
|
262
|
+
<form @submit=${this.#submit}>
|
|
263
|
+
<uui-form-layout-item>
|
|
264
|
+
<uui-label for="username" slot="label" required>
|
|
265
|
+
${this.usernameIsEmail ? 'Email' : 'Username'}
|
|
266
|
+
</uui-label>
|
|
267
|
+
<uui-input
|
|
268
|
+
id="username"
|
|
269
|
+
name="username"
|
|
270
|
+
type=${this.usernameIsEmail ? 'email' : 'text'}
|
|
271
|
+
autocomplete="username"
|
|
272
|
+
required
|
|
273
|
+
?disabled=${this._busy}></uui-input>
|
|
274
|
+
</uui-form-layout-item>
|
|
275
|
+
|
|
276
|
+
<uui-form-layout-item>
|
|
277
|
+
<uui-label for="password" slot="label" required>Password</uui-label>
|
|
278
|
+
<uui-input
|
|
279
|
+
id="password"
|
|
280
|
+
name="password"
|
|
281
|
+
type="password"
|
|
282
|
+
autocomplete="current-password"
|
|
283
|
+
required
|
|
284
|
+
?disabled=${this._busy}></uui-input>
|
|
285
|
+
</uui-form-layout-item>
|
|
286
|
+
|
|
287
|
+
${this.#messages()}
|
|
288
|
+
|
|
289
|
+
<uui-button
|
|
290
|
+
type="submit"
|
|
291
|
+
look="primary"
|
|
292
|
+
color="positive"
|
|
293
|
+
.state=${this._busy ? 'waiting' : undefined}
|
|
294
|
+
label="Sign in"></uui-button>
|
|
295
|
+
${
|
|
296
|
+
this.allowPasswordReset
|
|
297
|
+
? html`<uui-button look="link" label="Forgotten password?" @click=${() => {
|
|
298
|
+
this._error = ''
|
|
299
|
+
this._view = 'forgot'
|
|
300
|
+
}}></uui-button>`
|
|
301
|
+
: nothing
|
|
302
|
+
}
|
|
303
|
+
</form>
|
|
304
|
+
`
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
static styles = css`
|
|
308
|
+
:host {
|
|
309
|
+
display: block;
|
|
310
|
+
min-height: 100vh;
|
|
311
|
+
background: var(--uui-color-surface-alt, #f4f4f4);
|
|
312
|
+
}
|
|
313
|
+
#layout {
|
|
314
|
+
display: flex;
|
|
315
|
+
align-items: center;
|
|
316
|
+
justify-content: center;
|
|
317
|
+
min-height: 100vh;
|
|
318
|
+
padding: 16px;
|
|
319
|
+
box-sizing: border-box;
|
|
320
|
+
}
|
|
321
|
+
#card {
|
|
322
|
+
width: 100%;
|
|
323
|
+
max-width: 400px;
|
|
324
|
+
}
|
|
325
|
+
#logo {
|
|
326
|
+
display: block;
|
|
327
|
+
height: 48px;
|
|
328
|
+
margin-bottom: var(--uui-size-space-4, 12px);
|
|
329
|
+
}
|
|
330
|
+
h1 {
|
|
331
|
+
margin: 0 0 var(--uui-size-space-5, 18px);
|
|
332
|
+
}
|
|
333
|
+
form {
|
|
334
|
+
display: flex;
|
|
335
|
+
flex-direction: column;
|
|
336
|
+
gap: var(--uui-size-space-4, 12px);
|
|
337
|
+
}
|
|
338
|
+
uui-input {
|
|
339
|
+
width: 100%;
|
|
340
|
+
}
|
|
341
|
+
#error {
|
|
342
|
+
margin: 0;
|
|
343
|
+
color: var(--uui-color-danger, #d42054);
|
|
344
|
+
}
|
|
345
|
+
#notice {
|
|
346
|
+
margin: 0;
|
|
347
|
+
color: var(--uui-color-positive, #0b8152);
|
|
348
|
+
}
|
|
349
|
+
`
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
customElements.define('crombolo-login', CromboloLoginElement)
|
package/package.json
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crombolo/backoffice-host",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.9.1",
|
|
4
|
+
"description": "Serves the Umbraco backoffice SPA: shell HTML, import map and package manifests.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Crombolo",
|
|
7
|
+
"homepage": "https://github.com/kineco-au/crombolo-cms#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/kineco-au/crombolo-cms.git",
|
|
11
|
+
"directory": "packages/backoffice-host"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/kineco-au/crombolo-cms/issues"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"bun": ">=1.3.0"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": "./src/index.ts",
|
|
22
|
+
"./branding": "./src/branding.ts"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"assets",
|
|
26
|
+
"plugin",
|
|
27
|
+
"src",
|
|
28
|
+
"NOTICE"
|
|
29
|
+
],
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@crombolo/core": "0.9.1",
|
|
35
|
+
"@crombolo/backoffice-dist": "18.2.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Talks to the assistant endpoints under <backoffice>/crombolo/api/assistant.
|
|
3
|
+
* The session cookie carries the identity, so there is no token handling here —
|
|
4
|
+
* and the server re-issues every CMS call as that same user.
|
|
5
|
+
*/
|
|
6
|
+
function base() {
|
|
7
|
+
const href = document.querySelector('base')?.getAttribute('href') ?? '/umbraco/'
|
|
8
|
+
return `${href.replace(/\/$/, '')}/crombolo/api/assistant`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function send(path, init = {}) {
|
|
12
|
+
const response = await fetch(`${base()}${path}`, {
|
|
13
|
+
credentials: 'include',
|
|
14
|
+
headers: { accept: 'application/json', ...(init.headers ?? {}) },
|
|
15
|
+
...init,
|
|
16
|
+
})
|
|
17
|
+
const text = await response.text()
|
|
18
|
+
const body = text === '' ? undefined : JSON.parse(text)
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
const error = new Error(body?.error ?? `The assistant responded ${response.status}`)
|
|
21
|
+
error.body = body
|
|
22
|
+
throw error
|
|
23
|
+
}
|
|
24
|
+
return body
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const json = (method, body) => ({
|
|
28
|
+
method,
|
|
29
|
+
headers: { 'content-type': 'application/json' },
|
|
30
|
+
body: JSON.stringify(body),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export const ask = (message, { changesetKey, history, viewing } = {}) =>
|
|
34
|
+
send('/chat', json('POST', { message, changesetKey, history, viewing }))
|
|
35
|
+
|
|
36
|
+
export const changesets = () => send('/changesets')
|
|
37
|
+
|
|
38
|
+
/** Whether the model is usable. `refresh` asks the provider again, model included. */
|
|
39
|
+
export const status = (refresh = false) => send(`/status${refresh ? '?refresh=1' : ''}`)
|
|
40
|
+
|
|
41
|
+
/** One proposal in full, body included — what the editing pane works on. */
|
|
42
|
+
export const change = (key) => send(`/changes/${key}`)
|
|
43
|
+
|
|
44
|
+
/** Replaces what approving would send. The kind and its parameters do not move. */
|
|
45
|
+
export const edit = (key, body) => send(`/changes/${key}`, json('PUT', { body }))
|
|
46
|
+
|
|
47
|
+
/** The proposal against what is there now, read fresh so a stale one says so. */
|
|
48
|
+
export const diff = (key) => send(`/changes/${key}/diff`)
|
|
49
|
+
|
|
50
|
+
/** Approving is the only thing here that changes the CMS, and only a person calls it. */
|
|
51
|
+
export const approve = (key) => send(`/changes/${key}/approve`, { method: 'POST' })
|
|
52
|
+
|
|
53
|
+
export const discard = (key) => send(`/changes/${key}/discard`, { method: 'POST' })
|