@bakery-framework/plugin-dashboard 1.0.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/LICENSE +19 -0
- package/README.md +44 -0
- package/package.json +42 -0
- package/src/authorize.ts +76 -0
- package/src/client/dashboard.ts +165 -0
- package/src/client/parts/database.ts +1176 -0
- package/src/client/parts/effects.ts +327 -0
- package/src/client/parts/logs.ts +132 -0
- package/src/client/parts/sessions.ts +382 -0
- package/src/client/parts/stats.ts +957 -0
- package/src/client/parts/utils.ts +237 -0
- package/src/components/DBBrowser.tsx +365 -0
- package/src/components/LogsPanel.tsx +35 -0
- package/src/components/SessionsPanel.tsx +90 -0
- package/src/components/StatsPanel.tsx +265 -0
- package/src/components/TopPagesPanel.tsx +57 -0
- package/src/endpoints/database.ts +211 -0
- package/src/endpoints/sessions.ts +59 -0
- package/src/index.ts +42 -0
- package/src/paths.ts +14 -0
- package/src/setup.ts +268 -0
- package/src/shell.tsx +137 -0
- package/src/test-fixtures.ts +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2026 Kyle Cyrus Santos Obille
|
|
2
|
+
|
|
3
|
+
The Software is provided subject to the standard MIT License, as detailed below, with the addition of the Commons Clause v1.0.
|
|
4
|
+
|
|
5
|
+
The Commons Clause v1.0
|
|
6
|
+
|
|
7
|
+
The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition.
|
|
8
|
+
|
|
9
|
+
Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, the right to Sell the Software.
|
|
10
|
+
|
|
11
|
+
For purposes of the foregoing, “Sell” means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any license notice or attribution required by the License must also include this Commons Clause License Condition notice.
|
|
12
|
+
|
|
13
|
+
Standard MIT License
|
|
14
|
+
|
|
15
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software (subject to the Commons Clause condition above), and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# @bakery-framework/plugin-dashboard
|
|
2
|
+
|
|
3
|
+
A built-in admin console for
|
|
4
|
+
[Bakery](https://github.com/obillekyle/bakery): database browsing, logs and
|
|
5
|
+
runtime state.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @bakery-framework/plugin-dashboard
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// server.config.ts
|
|
15
|
+
import { defineConfig } from '@bakery-framework/core'
|
|
16
|
+
import dashboardPlugin from '@bakery-framework/plugin-dashboard'
|
|
17
|
+
|
|
18
|
+
export default defineConfig({
|
|
19
|
+
root: 'src',
|
|
20
|
+
plugins: [
|
|
21
|
+
dashboardPlugin({
|
|
22
|
+
authorize: req => Boolean(req.session?.get('isAdmin')),
|
|
23
|
+
}),
|
|
24
|
+
],
|
|
25
|
+
})
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**The dashboard does not authenticate anyone itself.** Your application already
|
|
29
|
+
knows who its users are, so it decides: `authorize` returns true to allow a
|
|
30
|
+
request through. Ship it without one and you are exposing your database browser.
|
|
31
|
+
|
|
32
|
+
`enabled: false` keeps it out of a build entirely — the documented way to
|
|
33
|
+
disable it in production.
|
|
34
|
+
|
|
35
|
+
## License
|
|
36
|
+
|
|
37
|
+
MIT with the Commons Clause v1.0 — see [LICENSE](./LICENSE).
|
|
38
|
+
|
|
39
|
+
**Not an OSI-approved licence.** The Commons Clause removes the right to *sell*
|
|
40
|
+
the software — meaning to charge for a product or service whose value derives
|
|
41
|
+
substantially from it, hosting and support included. Everything else the MIT
|
|
42
|
+
licence grants is unchanged: use it, modify it, ship it inside your own product.
|
|
43
|
+
If your organisation only permits OSI-approved dependencies, this will not pass
|
|
44
|
+
that check.
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bakery-framework/plugin-dashboard",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Bakery dashboard plugin.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"bakery",
|
|
7
|
+
"bun",
|
|
8
|
+
"dashboard",
|
|
9
|
+
"admin",
|
|
10
|
+
"plugin"
|
|
11
|
+
],
|
|
12
|
+
"author": "obillekyle",
|
|
13
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/obillekyle/bakery.git",
|
|
17
|
+
"directory": "packages/plugins/dashboard"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/obillekyle/bakery#readme",
|
|
20
|
+
"bugs": "https://github.com/obillekyle/bakery/issues",
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "./src/index.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": "./src/index.ts",
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"src",
|
|
32
|
+
"!src/**/*.test.ts",
|
|
33
|
+
"!src/tests"
|
|
34
|
+
],
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@bakery-framework/core": "^1.0.0",
|
|
37
|
+
"@bakery-framework/orm": "^1.0.0"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"bun": ">=1.3.14"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/authorize.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { getClientIp } from '@bakery-framework/core/utils/http'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Decides whether a request may use the console.
|
|
5
|
+
*
|
|
6
|
+
* The dashboard used to run its own identity system: a shared `DASHPASS`
|
|
7
|
+
* secret, a login form, a session flag, a constant-time compare and a
|
|
8
|
+
* failed-attempt backoff map. That is a lot of security-sensitive surface for
|
|
9
|
+
* a framework to own, and it composed with nothing — an app with real users
|
|
10
|
+
* and roles still had to hand out a second, shared password.
|
|
11
|
+
*
|
|
12
|
+
* So the dashboard no longer authenticates anyone. The host application, which
|
|
13
|
+
* already knows who its users are, supplies a predicate.
|
|
14
|
+
*/
|
|
15
|
+
export type AuthorizeFn = (req: Request) => boolean | Promise<boolean>
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Addresses only. `'localhost'` used to be a member because the request's
|
|
19
|
+
* *hostname* was compared against this set as well — see below for why that is
|
|
20
|
+
* gone. A peer address is never the string `localhost`, and accepting it would
|
|
21
|
+
* mean an `X-Forwarded-For: localhost` counted as loopback under `trustProxy`.
|
|
22
|
+
*/
|
|
23
|
+
const LOOPBACK = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1'])
|
|
24
|
+
|
|
25
|
+
/** True when the request came from this machine. */
|
|
26
|
+
export function isLoopback(req: Request): boolean {
|
|
27
|
+
// The peer address is the only evidence here the client does not choose.
|
|
28
|
+
// This used to fall back to `new URL(req.url).hostname`, which Bun builds
|
|
29
|
+
// from the client's own `Host` header — and `DEFAULT_HOST` is 0.0.0.0, so a
|
|
30
|
+
// dev server listens on every interface. Any peer on the LAN could send
|
|
31
|
+
// `Host: localhost` and be handed the database browser.
|
|
32
|
+
//
|
|
33
|
+
// getClientIp reads config and the live server, either of which may be
|
|
34
|
+
// absent (tests, early boot). An address that cannot be determined is
|
|
35
|
+
// indeterminate, and an indeterminate answer is a denial — not a reason to
|
|
36
|
+
// consult something the requester controls.
|
|
37
|
+
let ip = ''
|
|
38
|
+
try {
|
|
39
|
+
ip = getClientIp(req)
|
|
40
|
+
} catch {
|
|
41
|
+
// See above: no server and no config means no evidence, which is a denial.
|
|
42
|
+
ip = ''
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return LOOPBACK.has(ip)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Fail closed. With no predicate configured the console is reachable only from
|
|
50
|
+
* loopback in development, and from nowhere in production — so forgetting to
|
|
51
|
+
* configure it cannot expose a database browser to the internet.
|
|
52
|
+
*/
|
|
53
|
+
export function defaultAuthorize(req: Request): boolean {
|
|
54
|
+
if (!import.meta.env.DEV) return false
|
|
55
|
+
return isLoopback(req)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function resolveAuthorize(authorize?: AuthorizeFn): AuthorizeFn {
|
|
59
|
+
return authorize ?? defaultAuthorize
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Run a predicate without letting a throwing one grant access.
|
|
64
|
+
*/
|
|
65
|
+
export async function isAuthorized(
|
|
66
|
+
authorize: AuthorizeFn,
|
|
67
|
+
req: Request,
|
|
68
|
+
): Promise<boolean> {
|
|
69
|
+
try {
|
|
70
|
+
return Boolean(await authorize(req))
|
|
71
|
+
} catch {
|
|
72
|
+
// An authorization check that errors is indeterminate, and an
|
|
73
|
+
// indeterminate answer is a denial.
|
|
74
|
+
return false
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addActiveFilter,
|
|
3
|
+
changePageSize,
|
|
4
|
+
clearActiveFilters,
|
|
5
|
+
closeEditModal,
|
|
6
|
+
closeExportMenuIfOutside,
|
|
7
|
+
closeImportModal,
|
|
8
|
+
closeInsertModal,
|
|
9
|
+
deleteTableRow,
|
|
10
|
+
exportToCSV,
|
|
11
|
+
exportToJSON,
|
|
12
|
+
fetchTableData,
|
|
13
|
+
filterTablesList,
|
|
14
|
+
handleCsvFileSelect,
|
|
15
|
+
inspectTable,
|
|
16
|
+
loadSchema,
|
|
17
|
+
nextPage,
|
|
18
|
+
openEditModal,
|
|
19
|
+
openImportModal,
|
|
20
|
+
openInsertModal,
|
|
21
|
+
prevPage,
|
|
22
|
+
removeActiveFilter,
|
|
23
|
+
runQuery,
|
|
24
|
+
selectDatabaseTable,
|
|
25
|
+
startInlineEdit,
|
|
26
|
+
submitEditRow,
|
|
27
|
+
submitImportCsv,
|
|
28
|
+
submitInsertRow,
|
|
29
|
+
toggleExportMenu,
|
|
30
|
+
toggleGridSort,
|
|
31
|
+
truncateCurrentTable,
|
|
32
|
+
} from './parts/database'
|
|
33
|
+
import { refreshShimmerCache } from './parts/effects'
|
|
34
|
+
import { clearLogs, initLogsWebSocket, toggleLogsPlay } from './parts/logs'
|
|
35
|
+
import {
|
|
36
|
+
changeSessionPageSize,
|
|
37
|
+
loadSessions,
|
|
38
|
+
nextSessionPage,
|
|
39
|
+
openSessionKeyEditor,
|
|
40
|
+
prevSessionPage,
|
|
41
|
+
queueSessionSearch,
|
|
42
|
+
revokeSession,
|
|
43
|
+
sessionKeyAction,
|
|
44
|
+
} from './parts/sessions'
|
|
45
|
+
import {
|
|
46
|
+
bindSparklineTooltips,
|
|
47
|
+
changePagesFilter,
|
|
48
|
+
changeTimescale,
|
|
49
|
+
initAnalyticsWebSocket,
|
|
50
|
+
loadStats,
|
|
51
|
+
resetAnalytics,
|
|
52
|
+
} from './parts/stats'
|
|
53
|
+
import { SegmentedProgress } from './parts/utils'
|
|
54
|
+
|
|
55
|
+
declare const match: any
|
|
56
|
+
|
|
57
|
+
function toggleProfileDropdown(event: Event) {
|
|
58
|
+
if (event) event.stopPropagation()
|
|
59
|
+
const menu = document.getElementById('profile-menu')
|
|
60
|
+
if (menu) {
|
|
61
|
+
const isVisible = menu.style.display === 'flex'
|
|
62
|
+
menu.style.display = isVisible ? 'none' : 'flex'
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function switchTab(tabId: string) {
|
|
67
|
+
const tabBtns = document.querySelectorAll('.tab-btn')
|
|
68
|
+
const panels = document.querySelectorAll('.panel')
|
|
69
|
+
|
|
70
|
+
for (const btn of tabBtns) {
|
|
71
|
+
btn.classList.remove('active')
|
|
72
|
+
|
|
73
|
+
if (btn.getAttribute('onclick')?.includes(tabId)) {
|
|
74
|
+
btn.classList.add('active')
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const panel of panels) {
|
|
79
|
+
panel.classList.toggle('active', panel.id === `panel-${tabId}`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const crumb = document.getElementById('crumb-current')
|
|
83
|
+
const label = document.querySelector(`.tab-btn.active span:last-child`)
|
|
84
|
+
if (crumb && label) crumb.textContent = label.textContent
|
|
85
|
+
|
|
86
|
+
match(tabId, {
|
|
87
|
+
sessions: loadSessions,
|
|
88
|
+
database: loadSchema,
|
|
89
|
+
logs: initLogsWebSocket,
|
|
90
|
+
'top-pages': () => loadStats(true),
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
refreshShimmerCache()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
window.addEventListener('click', closeExportMenuIfOutside)
|
|
97
|
+
|
|
98
|
+
window.addEventListener('click', e => {
|
|
99
|
+
const menu = document.getElementById('profile-menu')
|
|
100
|
+
const trigger = document.querySelector('.profile-trigger-btn')
|
|
101
|
+
if (
|
|
102
|
+
menu &&
|
|
103
|
+
trigger &&
|
|
104
|
+
!trigger.contains(e.target as Node) &&
|
|
105
|
+
!menu.contains(e.target as Node)
|
|
106
|
+
) {
|
|
107
|
+
menu.style.display = 'none'
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const w = window as any
|
|
112
|
+
w.SegmentedProgress = SegmentedProgress
|
|
113
|
+
w.switchTab = switchTab
|
|
114
|
+
w.resetAnalytics = resetAnalytics
|
|
115
|
+
w.changePagesFilter = changePagesFilter
|
|
116
|
+
w.toggleProfileDropdown = toggleProfileDropdown
|
|
117
|
+
w.changeTimescale = changeTimescale
|
|
118
|
+
|
|
119
|
+
w.loadSessions = loadSessions
|
|
120
|
+
w.revokeSession = revokeSession
|
|
121
|
+
w.queueSessionSearch = queueSessionSearch
|
|
122
|
+
w.prevSessionPage = prevSessionPage
|
|
123
|
+
w.nextSessionPage = nextSessionPage
|
|
124
|
+
w.changeSessionPageSize = changeSessionPageSize
|
|
125
|
+
w.sessionKeyAction = sessionKeyAction
|
|
126
|
+
w.openSessionKeyEditor = openSessionKeyEditor
|
|
127
|
+
|
|
128
|
+
w.loadSchema = loadSchema
|
|
129
|
+
w.filterTablesList = filterTablesList
|
|
130
|
+
w.selectDatabaseTable = selectDatabaseTable
|
|
131
|
+
w.fetchTableData = fetchTableData
|
|
132
|
+
w.toggleGridSort = toggleGridSort
|
|
133
|
+
w.prevPage = prevPage
|
|
134
|
+
w.nextPage = nextPage
|
|
135
|
+
w.changePageSize = changePageSize
|
|
136
|
+
w.startInlineEdit = startInlineEdit
|
|
137
|
+
w.addActiveFilter = addActiveFilter
|
|
138
|
+
w.removeActiveFilter = removeActiveFilter
|
|
139
|
+
w.clearActiveFilters = clearActiveFilters
|
|
140
|
+
w.openInsertModal = openInsertModal
|
|
141
|
+
w.closeInsertModal = closeInsertModal
|
|
142
|
+
w.submitInsertRow = submitInsertRow
|
|
143
|
+
w.openEditModal = openEditModal
|
|
144
|
+
w.closeEditModal = closeEditModal
|
|
145
|
+
w.submitEditRow = submitEditRow
|
|
146
|
+
w.openImportModal = openImportModal
|
|
147
|
+
w.closeImportModal = closeImportModal
|
|
148
|
+
w.handleCsvFileSelect = handleCsvFileSelect
|
|
149
|
+
w.submitImportCsv = submitImportCsv
|
|
150
|
+
w.toggleExportMenu = toggleExportMenu
|
|
151
|
+
w.exportToCSV = exportToCSV
|
|
152
|
+
w.exportToJSON = exportToJSON
|
|
153
|
+
w.truncateCurrentTable = truncateCurrentTable
|
|
154
|
+
w.deleteTableRow = deleteTableRow
|
|
155
|
+
w.inspectTable = inspectTable
|
|
156
|
+
w.runQuery = runQuery
|
|
157
|
+
|
|
158
|
+
w.initLogsWebSocket = initLogsWebSocket
|
|
159
|
+
w.toggleLogsPlay = toggleLogsPlay
|
|
160
|
+
w.clearLogs = clearLogs
|
|
161
|
+
|
|
162
|
+
if (document.getElementById('panel-stats')) {
|
|
163
|
+
bindSparklineTooltips()
|
|
164
|
+
initAnalyticsWebSocket()
|
|
165
|
+
}
|