@crombolo/backoffice-host 0.10.0 → 0.11.0
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crombolo/backoffice-host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
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.11.0",
|
|
35
35
|
"@crombolo/backoffice-dist": "18.2.0"
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Audit log dashboard in Settings: who did what, when and from where,
|
|
3
|
+
* newest first, filtered by person, kind of change, words and dates.
|
|
4
|
+
* docs/audit-log.md.
|
|
5
|
+
*/
|
|
6
|
+
import { css, html, LitElement, nothing } from '@umbraco-cms/backoffice/external/lit'
|
|
7
|
+
|
|
8
|
+
const PAGE_SIZE = 50
|
|
9
|
+
|
|
10
|
+
const CATEGORIES = [
|
|
11
|
+
['', 'Everything'],
|
|
12
|
+
['content', 'Content'],
|
|
13
|
+
['media', 'Media'],
|
|
14
|
+
['member', 'Members'],
|
|
15
|
+
['schema', 'Types and templates'],
|
|
16
|
+
['settings', 'Settings'],
|
|
17
|
+
['user', 'Users'],
|
|
18
|
+
['permission', 'Permissions'],
|
|
19
|
+
['sign-in', 'Sign-ins'],
|
|
20
|
+
['system', 'System'],
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
const ACTIONS = {
|
|
24
|
+
Create: 'Created',
|
|
25
|
+
CreateAndPublish: 'Created and published',
|
|
26
|
+
Save: 'Saved',
|
|
27
|
+
SaveAndPublish: 'Saved and published',
|
|
28
|
+
Publish: 'Published',
|
|
29
|
+
PublishWithDescendants: 'Published with everything below it',
|
|
30
|
+
Unpublish: 'Unpublished',
|
|
31
|
+
Delete: 'Deleted',
|
|
32
|
+
Move: 'Moved',
|
|
33
|
+
Trash: 'Moved to the recycle bin',
|
|
34
|
+
Restore: 'Restored from the recycle bin',
|
|
35
|
+
EmptyRecycleBin: 'Emptied the recycle bin',
|
|
36
|
+
Copy: 'Copied',
|
|
37
|
+
Sort: 'Sorted',
|
|
38
|
+
Rename: 'Renamed',
|
|
39
|
+
Rollback: 'Rolled back',
|
|
40
|
+
Revert: 'Reverted',
|
|
41
|
+
PreventCleanup: 'Kept a version',
|
|
42
|
+
AssignDomains: 'Changed domains',
|
|
43
|
+
SetPublicAccess: 'Restricted access',
|
|
44
|
+
AddPublicAccess: 'Restricted access',
|
|
45
|
+
RemovePublicAccess: 'Removed access restrictions',
|
|
46
|
+
SetNotifications: 'Changed notifications',
|
|
47
|
+
Import: 'Imported',
|
|
48
|
+
CreateTemplate: 'Created a template',
|
|
49
|
+
ChangePassword: 'Changed password',
|
|
50
|
+
ResetPassword: 'Reset password',
|
|
51
|
+
ResetPasswordByLink: 'Reset password from an e-mailed link',
|
|
52
|
+
Invite: 'Invited',
|
|
53
|
+
ResendInvite: 'Resent the invitation',
|
|
54
|
+
AcceptInvite: 'Accepted the invitation',
|
|
55
|
+
Disable: 'Disabled',
|
|
56
|
+
Enable: 'Enabled',
|
|
57
|
+
Unlock: 'Unlocked',
|
|
58
|
+
SetUserGroups: 'Changed groups',
|
|
59
|
+
AddUsers: 'Added people',
|
|
60
|
+
RemoveUsers: 'Removed people',
|
|
61
|
+
SaveProfile: 'Updated their profile',
|
|
62
|
+
SetAvatar: 'Changed the picture',
|
|
63
|
+
RemoveAvatar: 'Removed the picture',
|
|
64
|
+
EnableTwoFactor: 'Turned on two-factor sign-in',
|
|
65
|
+
DisableTwoFactor: 'Turned off two-factor sign-in',
|
|
66
|
+
AddClientCredentials: 'Added an API key',
|
|
67
|
+
RemoveClientCredentials: 'Removed an API key',
|
|
68
|
+
SignIn: 'Signed in',
|
|
69
|
+
SignInFailed: 'Failed to sign in',
|
|
70
|
+
LockedOut: 'Locked out',
|
|
71
|
+
SignOut: 'Signed out',
|
|
72
|
+
Rebuild: 'Rebuilt',
|
|
73
|
+
Reload: 'Reloaded',
|
|
74
|
+
Build: 'Built',
|
|
75
|
+
Install: 'Installed',
|
|
76
|
+
Uninstall: 'Uninstalled',
|
|
77
|
+
Commit: 'Sent to source control',
|
|
78
|
+
Sync: 'Applied the schema files',
|
|
79
|
+
Purge: 'Purged',
|
|
80
|
+
Upgrade: 'Upgraded the site',
|
|
81
|
+
UpgradeFix: 'Prepared an upgrade',
|
|
82
|
+
PauseEditing: 'Paused editing',
|
|
83
|
+
ResumeEditing: 'Resumed editing',
|
|
84
|
+
RunMigration: 'Ran a package update',
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const ITEM_TYPES = {
|
|
88
|
+
Document: 'Page',
|
|
89
|
+
DocumentBlueprint: 'Document blueprint',
|
|
90
|
+
DocumentType: 'Document type',
|
|
91
|
+
DictionaryItem: 'Dictionary item',
|
|
92
|
+
MediaType: 'Media type',
|
|
93
|
+
MemberType: 'Member type',
|
|
94
|
+
MemberGroup: 'Member group',
|
|
95
|
+
DataType: 'Data type',
|
|
96
|
+
PartialView: 'Partial view',
|
|
97
|
+
UserGroup: 'User group',
|
|
98
|
+
SeoSettings: 'Search and sharing settings',
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const WORKSPACE = {
|
|
102
|
+
Document: (key) => `section/content/workspace/document/edit/${key}`,
|
|
103
|
+
Media: (key) => `section/media/workspace/media/edit/${key}`,
|
|
104
|
+
Member: (key) => `section/member-management/workspace/member/edit/${key}`,
|
|
105
|
+
User: (key) => `section/user-management/workspace/user/edit/${key}`,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const words = (name) =>
|
|
109
|
+
name.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/^./, (c) => c.toUpperCase())
|
|
110
|
+
const actionLabel = (action) => ACTIONS[action] ?? words(action)
|
|
111
|
+
const itemType = (type) => ITEM_TYPES[type] ?? words(type.replace(/Folder$/, ' folder'))
|
|
112
|
+
|
|
113
|
+
function apiBase() {
|
|
114
|
+
const base = document.querySelector('base')?.getAttribute('href') ?? '/umbraco/'
|
|
115
|
+
return `${base.replace(/\/$/, '')}/crombolo/api/audit`
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function get(path, params) {
|
|
119
|
+
const query = params ? `?${new URLSearchParams(params)}` : ''
|
|
120
|
+
const response = await fetch(`${apiBase()}${path}${query}`, {
|
|
121
|
+
credentials: 'include',
|
|
122
|
+
headers: { accept: 'application/json' },
|
|
123
|
+
})
|
|
124
|
+
if (!response.ok) throw new Error(`The audit log could not be read (${response.status}).`)
|
|
125
|
+
return response.json()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function fromWhere(entry) {
|
|
129
|
+
if (entry.source === 'cli') return 'Command line'
|
|
130
|
+
if (entry.source === 'job') return 'Scheduled'
|
|
131
|
+
const forwarded = entry.forwardedFor?.split(',')[0]?.trim()
|
|
132
|
+
return forwarded || entry.ipAddress || '—'
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export default class CromboloAuditDashboardElement extends LitElement {
|
|
136
|
+
static properties = {
|
|
137
|
+
_page: { state: true },
|
|
138
|
+
_users: { state: true },
|
|
139
|
+
_error: { state: true },
|
|
140
|
+
_filter: { state: true },
|
|
141
|
+
_skip: { state: true },
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
constructor() {
|
|
145
|
+
super()
|
|
146
|
+
this._page = undefined
|
|
147
|
+
this._users = []
|
|
148
|
+
this._error = undefined
|
|
149
|
+
this._filter = { user: '', category: '', text: '', since: '', until: '' }
|
|
150
|
+
this._skip = 0
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
connectedCallback() {
|
|
154
|
+
super.connectedCallback()
|
|
155
|
+
get('/users')
|
|
156
|
+
.then((users) => {
|
|
157
|
+
this._users = users.items
|
|
158
|
+
})
|
|
159
|
+
.catch(() => {})
|
|
160
|
+
this.#load()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async #load() {
|
|
164
|
+
const params = { skip: String(this._skip), take: String(PAGE_SIZE) }
|
|
165
|
+
const { user, category, text, since, until } = this._filter
|
|
166
|
+
if (user) params.user = user
|
|
167
|
+
if (category) params.category = category
|
|
168
|
+
if (text.trim()) params.text = text.trim()
|
|
169
|
+
// Whole local days: "To" includes the day it names.
|
|
170
|
+
if (since) params.since = new Date(`${since}T00:00:00`).toISOString()
|
|
171
|
+
if (until)
|
|
172
|
+
params.until = new Date(new Date(`${until}T00:00:00`).getTime() + 86_400_000).toISOString()
|
|
173
|
+
try {
|
|
174
|
+
this._page = await get('', params)
|
|
175
|
+
this._error = undefined
|
|
176
|
+
} catch (error) {
|
|
177
|
+
this._error = error.message
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#set(field, value) {
|
|
182
|
+
this._filter = { ...this._filter, [field]: value }
|
|
183
|
+
this._skip = 0
|
|
184
|
+
this.#load()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
#go(skip) {
|
|
188
|
+
this._skip = Math.max(0, skip)
|
|
189
|
+
this.#load()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
render() {
|
|
193
|
+
return html`
|
|
194
|
+
<uui-box headline="Audit log">
|
|
195
|
+
<p class="intro">
|
|
196
|
+
Every change made in the backoffice, every sign-in, and what the command line and
|
|
197
|
+
scheduled publishing changed.
|
|
198
|
+
${
|
|
199
|
+
this._page?.retentionDays
|
|
200
|
+
? html`Entries are kept for ${this._page.retentionDays} days.`
|
|
201
|
+
: this._page
|
|
202
|
+
? html`Entries are kept until they are removed.`
|
|
203
|
+
: nothing
|
|
204
|
+
}
|
|
205
|
+
</p>
|
|
206
|
+
${this.#renderFilters()}
|
|
207
|
+
${this.#renderBody()}
|
|
208
|
+
</uui-box>
|
|
209
|
+
`
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
#renderFilters() {
|
|
213
|
+
const f = this._filter
|
|
214
|
+
return html`
|
|
215
|
+
<div class="filters">
|
|
216
|
+
<label>
|
|
217
|
+
Who
|
|
218
|
+
<select .value=${f.user} @change=${(e) => this.#set('user', e.target.value)}>
|
|
219
|
+
<option value="">Anyone</option>
|
|
220
|
+
${this._users.map((u) => html`<option value=${u.key}>${u.name || 'Unnamed'}</option>`)}
|
|
221
|
+
</select>
|
|
222
|
+
</label>
|
|
223
|
+
<label>
|
|
224
|
+
What
|
|
225
|
+
<select .value=${f.category} @change=${(e) => this.#set('category', e.target.value)}>
|
|
226
|
+
${CATEGORIES.map(([value, label]) => html`<option value=${value}>${label}</option>`)}
|
|
227
|
+
</select>
|
|
228
|
+
</label>
|
|
229
|
+
<label>
|
|
230
|
+
Containing
|
|
231
|
+
<uui-input
|
|
232
|
+
label="Containing"
|
|
233
|
+
placeholder="A name"
|
|
234
|
+
.value=${f.text}
|
|
235
|
+
@change=${(e) => this.#set('text', e.target.value)}
|
|
236
|
+
></uui-input>
|
|
237
|
+
</label>
|
|
238
|
+
<label>
|
|
239
|
+
From
|
|
240
|
+
<input type="date" .value=${f.since} @change=${(e) => this.#set('since', e.target.value)} />
|
|
241
|
+
</label>
|
|
242
|
+
<label>
|
|
243
|
+
To
|
|
244
|
+
<input type="date" .value=${f.until} @change=${(e) => this.#set('until', e.target.value)} />
|
|
245
|
+
</label>
|
|
246
|
+
<uui-button look="secondary" compact label="Refresh" @click=${() => this.#load()}>Refresh</uui-button>
|
|
247
|
+
</div>
|
|
248
|
+
`
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
#renderBody() {
|
|
252
|
+
if (this._error) return html`<p class="error">${this._error}</p>`
|
|
253
|
+
if (!this._page) return html`<uui-loader></uui-loader>`
|
|
254
|
+
const { items, total } = this._page
|
|
255
|
+
if (items.length === 0) return html`<p class="empty">Nothing matches.</p>`
|
|
256
|
+
const last = Math.min(this._skip + items.length, total)
|
|
257
|
+
return html`
|
|
258
|
+
<uui-table>
|
|
259
|
+
<uui-table-head>
|
|
260
|
+
<uui-table-head-cell>When</uui-table-head-cell>
|
|
261
|
+
<uui-table-head-cell>Who</uui-table-head-cell>
|
|
262
|
+
<uui-table-head-cell>What</uui-table-head-cell>
|
|
263
|
+
<uui-table-head-cell>Item</uui-table-head-cell>
|
|
264
|
+
<uui-table-head-cell>From</uui-table-head-cell>
|
|
265
|
+
</uui-table-head>
|
|
266
|
+
${items.map((entry) => this.#renderRow(entry))}
|
|
267
|
+
</uui-table>
|
|
268
|
+
<div class="pager">
|
|
269
|
+
<span>${this._skip + 1}–${last} of ${total}</span>
|
|
270
|
+
<uui-button
|
|
271
|
+
compact
|
|
272
|
+
look="secondary"
|
|
273
|
+
label="Newer"
|
|
274
|
+
?disabled=${this._skip === 0}
|
|
275
|
+
@click=${() => this.#go(this._skip - PAGE_SIZE)}
|
|
276
|
+
>Newer</uui-button>
|
|
277
|
+
<uui-button
|
|
278
|
+
compact
|
|
279
|
+
look="secondary"
|
|
280
|
+
label="Older"
|
|
281
|
+
?disabled=${last >= total}
|
|
282
|
+
@click=${() => this.#go(this._skip + PAGE_SIZE)}
|
|
283
|
+
>Older</uui-button>
|
|
284
|
+
</div>
|
|
285
|
+
`
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
#renderRow(entry) {
|
|
289
|
+
const when = new Date(entry.occurredAt)
|
|
290
|
+
const failed = entry.action === 'SignInFailed' || entry.action === 'LockedOut'
|
|
291
|
+
return html`<uui-table-row>
|
|
292
|
+
<uui-table-cell title=${when.toISOString()}>${when.toLocaleString()}</uui-table-cell>
|
|
293
|
+
<uui-table-cell>${entry.user?.name ?? (entry.source === 'job' ? 'Scheduled publishing' : 'Unknown')}</uui-table-cell>
|
|
294
|
+
<uui-table-cell>
|
|
295
|
+
${failed ? html`<uui-tag color="danger">${actionLabel(entry.action)}</uui-tag>` : actionLabel(entry.action)}
|
|
296
|
+
${entry.detail && entry.source !== 'cli' ? html`<small>${entry.detail}</small>` : nothing}
|
|
297
|
+
</uui-table-cell>
|
|
298
|
+
<uui-table-cell>${this.#renderItem(entry)}</uui-table-cell>
|
|
299
|
+
<uui-table-cell title=${entry.forwardedFor ? `Connected from ${entry.ipAddress ?? 'unknown'}` : ''}>
|
|
300
|
+
${fromWhere(entry)}
|
|
301
|
+
</uui-table-cell>
|
|
302
|
+
</uui-table-row>`
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
#renderItem(entry) {
|
|
306
|
+
const item = entry.item
|
|
307
|
+
if (!item) return nothing
|
|
308
|
+
const label = item.name ?? item.key ?? ''
|
|
309
|
+
const link = item.key && WORKSPACE[item.type]?.(item.key)
|
|
310
|
+
const base = document.querySelector('base')?.getAttribute('href') ?? '/umbraco/'
|
|
311
|
+
return html`
|
|
312
|
+
<small>${itemType(item.type)}</small>
|
|
313
|
+
${link ? html`<a href=${`${base.replace(/\/$/, '')}/${link}`}>${label}</a>` : label}
|
|
314
|
+
`
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
static styles = css`
|
|
318
|
+
:host {
|
|
319
|
+
display: block;
|
|
320
|
+
padding: var(--uui-size-layout-1);
|
|
321
|
+
}
|
|
322
|
+
.intro {
|
|
323
|
+
margin-top: 0;
|
|
324
|
+
color: var(--uui-color-text-alt);
|
|
325
|
+
}
|
|
326
|
+
.filters {
|
|
327
|
+
display: flex;
|
|
328
|
+
flex-wrap: wrap;
|
|
329
|
+
gap: var(--uui-size-space-4);
|
|
330
|
+
align-items: flex-end;
|
|
331
|
+
margin-bottom: var(--uui-size-space-5);
|
|
332
|
+
}
|
|
333
|
+
.filters label {
|
|
334
|
+
display: flex;
|
|
335
|
+
flex-direction: column;
|
|
336
|
+
gap: var(--uui-size-space-1);
|
|
337
|
+
font-weight: 600;
|
|
338
|
+
}
|
|
339
|
+
select,
|
|
340
|
+
input[type='date'] {
|
|
341
|
+
font: inherit;
|
|
342
|
+
padding: var(--uui-size-space-2);
|
|
343
|
+
border: 1px solid var(--uui-color-border);
|
|
344
|
+
border-radius: var(--uui-border-radius);
|
|
345
|
+
background: var(--uui-color-surface);
|
|
346
|
+
color: var(--uui-color-text);
|
|
347
|
+
}
|
|
348
|
+
uui-table-cell small {
|
|
349
|
+
display: block;
|
|
350
|
+
color: var(--uui-color-text-alt);
|
|
351
|
+
}
|
|
352
|
+
.pager {
|
|
353
|
+
display: flex;
|
|
354
|
+
gap: var(--uui-size-space-3);
|
|
355
|
+
align-items: center;
|
|
356
|
+
justify-content: flex-end;
|
|
357
|
+
margin-top: var(--uui-size-space-4);
|
|
358
|
+
}
|
|
359
|
+
.empty,
|
|
360
|
+
.error {
|
|
361
|
+
color: var(--uui-color-text-alt);
|
|
362
|
+
}
|
|
363
|
+
.error {
|
|
364
|
+
color: var(--uui-color-danger);
|
|
365
|
+
}
|
|
366
|
+
`
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
customElements.define('crombolo-audit-dashboard', CromboloAuditDashboardElement)
|
|
@@ -2,7 +2,7 @@
|
|
|
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.11.0",
|
|
6
6
|
"allowPublicAccess": false,
|
|
7
7
|
"extensions": [
|
|
8
8
|
{
|
|
@@ -79,6 +79,23 @@
|
|
|
79
79
|
}
|
|
80
80
|
]
|
|
81
81
|
},
|
|
82
|
+
{
|
|
83
|
+
"type": "dashboard",
|
|
84
|
+
"alias": "Crombolo.Dashboard.AuditLog",
|
|
85
|
+
"name": "Crombolo Audit Log Dashboard",
|
|
86
|
+
"element": "/crombolo-plugin/audit-dashboard.js",
|
|
87
|
+
"weight": 4,
|
|
88
|
+
"meta": {
|
|
89
|
+
"label": "Audit log",
|
|
90
|
+
"pathname": "audit-log"
|
|
91
|
+
},
|
|
92
|
+
"conditions": [
|
|
93
|
+
{
|
|
94
|
+
"alias": "Umb.Condition.SectionAlias",
|
|
95
|
+
"match": "Umb.Section.Settings"
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
},
|
|
82
99
|
{
|
|
83
100
|
"type": "dashboard",
|
|
84
101
|
"alias": "Crombolo.Dashboard.Welcome",
|