@alstar/studio 0.0.0-beta.10 → 0.0.0-beta.11
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/api/api-key.ts +40 -44
- package/api/auth.ts +66 -0
- package/api/backup.ts +30 -28
- package/api/block.ts +109 -110
- package/api/index.ts +17 -10
- package/api/mcp.ts +39 -42
- package/components/AdminPanel.ts +17 -4
- package/components/Backup.ts +5 -2
- package/components/BlockFieldRenderer.ts +6 -2
- package/components/Entries.ts +2 -2
- package/components/FieldRenderer.ts +1 -1
- package/components/Settings.ts +21 -18
- package/components/SiteLayout.ts +8 -5
- package/components/Users.ts +46 -0
- package/components/fields/Markdown.ts +1 -1
- package/components/fields/Slug.ts +113 -0
- package/components/fields/Text.ts +1 -1
- package/components/fields/index.ts +4 -1
- package/components/icons.ts +36 -0
- package/index.ts +40 -12
- package/package.json +6 -3
- package/pages/entry/[id].ts +1 -3
- package/pages/error.ts +14 -0
- package/pages/index.ts +1 -1
- package/pages/login.ts +21 -0
- package/pages/register.ts +33 -0
- package/pages/settings.ts +1 -3
- package/public/studio/css/settings.css +4 -0
- package/public/studio/js/markdown-editor.js +1 -1
- package/public/studio/js/sortable-list.js +1 -1
- package/public/studio/main.css +5 -0
- package/public/studio/main.js +12 -0
- package/queries/block-2.ts +339 -0
- package/queries/getBlockTrees-2.ts +71 -0
- package/queries/index.ts +1 -1
- package/readme.md +2 -2
- package/schema.sql +18 -0
- package/schemas.ts +11 -1
- package/types.ts +11 -0
- package/utils/auth.ts +54 -0
- package/utils/create-hash.ts +9 -0
- package/utils/define.ts +1 -3
- package/utils/startup-log.ts +15 -6
package/components/AdminPanel.ts
CHANGED
|
@@ -11,7 +11,7 @@ export default () => {
|
|
|
11
11
|
return html`
|
|
12
12
|
<div class="admin-panel" id="admin_panel">
|
|
13
13
|
<h1>
|
|
14
|
-
<a href="/
|
|
14
|
+
<a href="/studio" aria-label="Go to dashboard"> ${logo} </a>
|
|
15
15
|
</h1>
|
|
16
16
|
|
|
17
17
|
<aside style="width: 100%;">
|
|
@@ -24,7 +24,7 @@ export default () => {
|
|
|
24
24
|
<ul>
|
|
25
25
|
<li>
|
|
26
26
|
<a
|
|
27
|
-
href="/
|
|
27
|
+
href="/studio/entry/${data.id}"
|
|
28
28
|
id="block_link_${data.id}"
|
|
29
29
|
>
|
|
30
30
|
${block.label}
|
|
@@ -37,7 +37,7 @@ export default () => {
|
|
|
37
37
|
|
|
38
38
|
return html`
|
|
39
39
|
<form
|
|
40
|
-
data-on-submit="@post('/
|
|
40
|
+
data-on-submit="@post('/studio/api/block', { contentType: 'form' })"
|
|
41
41
|
style="display: flex; align-items: center; gap: 1rem;"
|
|
42
42
|
>
|
|
43
43
|
<input type="hidden" name="name" value="${name}" />
|
|
@@ -60,14 +60,27 @@ export default () => {
|
|
|
60
60
|
<footer>
|
|
61
61
|
<a
|
|
62
62
|
role="button"
|
|
63
|
-
href="/
|
|
63
|
+
href="/studio/settings"
|
|
64
64
|
class="ghost"
|
|
65
65
|
style="padding: 10px; margin: 0 -13px; display: flex;"
|
|
66
66
|
data-tooltip="Settings"
|
|
67
67
|
data-placement="right"
|
|
68
|
+
aria-label="Settings"
|
|
68
69
|
>
|
|
69
70
|
${icons.cog}
|
|
70
71
|
</a>
|
|
72
|
+
|
|
73
|
+
<a
|
|
74
|
+
role="button"
|
|
75
|
+
href="/"
|
|
76
|
+
class="ghost"
|
|
77
|
+
style="padding: 10px; margin: 0 -13px; display: flex;"
|
|
78
|
+
data-tooltip="Leave Studio"
|
|
79
|
+
data-placement="right"
|
|
80
|
+
aria-label="Leave"
|
|
81
|
+
>
|
|
82
|
+
${icons.leave}
|
|
83
|
+
</a>
|
|
71
84
|
</footer>
|
|
72
85
|
</div>
|
|
73
86
|
`
|
package/components/Backup.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { html } from 'hono/html'
|
|
2
2
|
|
|
3
3
|
export default () => {
|
|
4
|
-
return html`<article>
|
|
4
|
+
return html`<article data-signals="{ status: null, message: '' }">
|
|
5
5
|
<header>Backup</header>
|
|
6
|
-
<form
|
|
6
|
+
<form
|
|
7
|
+
data-on-submit="@post('/studio/api/backup', { contentType: 'form' })"
|
|
8
|
+
>
|
|
7
9
|
<button type="submit">Backup database</button>
|
|
10
|
+
<p data-style-color="$status === 200 ? 'green' : 'red'" data-text="$message || ' '"></p>
|
|
8
11
|
</form>
|
|
9
12
|
</article>`
|
|
10
13
|
}
|
|
@@ -42,7 +42,7 @@ export default (props: {
|
|
|
42
42
|
return html`
|
|
43
43
|
<li>
|
|
44
44
|
<form
|
|
45
|
-
data-on-submit="@post('/
|
|
45
|
+
data-on-submit="@post('/studio/api/new-block', { contentType: 'form' })"
|
|
46
46
|
>
|
|
47
47
|
<button type="submit" class="ghost">${block.label}</button>
|
|
48
48
|
<input type="hidden" name="type" value="${block.type}" />
|
|
@@ -78,6 +78,10 @@ export default (props: {
|
|
|
78
78
|
${struct.label}
|
|
79
79
|
|
|
80
80
|
<aside>
|
|
81
|
+
<!-- <label style="margin: 0; border-bottom: none" data-tooltip="Disable" data-placement="top">
|
|
82
|
+
<input name="enable" type="checkbox" role="switch" checked />
|
|
83
|
+
</label> -->
|
|
84
|
+
|
|
81
85
|
<button
|
|
82
86
|
data-handle-for="${data.id}"
|
|
83
87
|
class="ghost handle text-secondary"
|
|
@@ -87,7 +91,7 @@ export default (props: {
|
|
|
87
91
|
</button>
|
|
88
92
|
|
|
89
93
|
<form
|
|
90
|
-
data-on-submit="@delete('/
|
|
94
|
+
data-on-submit="@delete('/studio/api/block', { contentType: 'form' })"
|
|
91
95
|
>
|
|
92
96
|
<button
|
|
93
97
|
type="submit"
|
package/components/Entries.ts
CHANGED
|
@@ -17,12 +17,12 @@ export default ({ name }: { name: string }) => {
|
|
|
17
17
|
|
|
18
18
|
return html`
|
|
19
19
|
<li>
|
|
20
|
-
<a href="/
|
|
20
|
+
<a href="/studio/entry/${block.id}" id="block_link_${block.id}">
|
|
21
21
|
${title?.value || 'Untitled'}
|
|
22
22
|
</a>
|
|
23
23
|
|
|
24
24
|
<form
|
|
25
|
-
data-on-submit="@delete('/
|
|
25
|
+
data-on-submit="@delete('/studio/api/block', { contentType: 'form' })"
|
|
26
26
|
>
|
|
27
27
|
<input type="hidden" name="id" value="${block.id}" />
|
|
28
28
|
<button
|
package/components/Settings.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { html } from 'hono/html'
|
|
|
3
3
|
import { sql } from '../utils/sql.ts'
|
|
4
4
|
import * as icons from './icons.ts'
|
|
5
5
|
import Backup from './Backup.ts'
|
|
6
|
+
import Users from './Users.ts'
|
|
6
7
|
|
|
7
8
|
export default () => {
|
|
8
9
|
const apiKeys = db.database
|
|
@@ -23,30 +24,30 @@ export default () => {
|
|
|
23
24
|
<ul>
|
|
24
25
|
${apiKeys.map((apiKey) => {
|
|
25
26
|
return html`<li>
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
<p>${apiKey.name}</p>
|
|
28
|
+
<input type="text" disabled value="${apiKey.hint}" />
|
|
29
|
+
<form
|
|
30
|
+
data-on-submit="@delete('/studio/api/api-key', { contentType: 'form' })"
|
|
31
|
+
>
|
|
32
|
+
<button
|
|
33
|
+
data-tooltip="Delete API key"
|
|
34
|
+
data-placement="left"
|
|
35
|
+
type="submit"
|
|
36
|
+
class="ghost"
|
|
30
37
|
>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
data-placement="left"
|
|
34
|
-
type="submit"
|
|
35
|
-
class="ghost"
|
|
36
|
-
>
|
|
37
|
-
${icons.trash}
|
|
38
|
-
</button>
|
|
38
|
+
${icons.trash}
|
|
39
|
+
</button>
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
<input type="hidden" name="value" value="${apiKey.value}" />
|
|
42
|
+
</form>
|
|
43
|
+
</li>`
|
|
43
44
|
})}
|
|
44
45
|
</ul>
|
|
45
46
|
|
|
46
47
|
<form
|
|
47
|
-
data-on-submit="@post('/
|
|
48
|
+
data-on-submit="@post('/studio/api/api-key', { contentType: 'form' })"
|
|
48
49
|
>
|
|
49
|
-
<label for="api_key_name">Generate API Key</label>
|
|
50
|
+
<label for="api_key_name"><small>Generate API Key</small></label>
|
|
50
51
|
|
|
51
52
|
<input
|
|
52
53
|
data-bind="name"
|
|
@@ -95,7 +96,9 @@ export default () => {
|
|
|
95
96
|
</dialog>
|
|
96
97
|
</article>
|
|
97
98
|
|
|
98
|
-
|
|
99
|
+
${Backup()}
|
|
100
|
+
|
|
101
|
+
${Users()}
|
|
99
102
|
</div>
|
|
100
103
|
`
|
|
101
104
|
}
|
package/components/SiteLayout.ts
CHANGED
|
@@ -3,13 +3,14 @@ import { html } from 'hono/html'
|
|
|
3
3
|
import { type HtmlEscapedString } from 'hono/utils/html'
|
|
4
4
|
import { studioConfig } from '../index.ts'
|
|
5
5
|
|
|
6
|
-
export default (
|
|
6
|
+
export default (
|
|
7
7
|
content:
|
|
8
8
|
| string
|
|
9
9
|
| Promise<string>
|
|
10
10
|
| HtmlEscapedString
|
|
11
|
-
| Promise<HtmlEscapedString
|
|
12
|
-
|
|
11
|
+
| Promise<HtmlEscapedString>,
|
|
12
|
+
includeAdminPanel = true,
|
|
13
|
+
) => {
|
|
13
14
|
return html`
|
|
14
15
|
<!DOCTYPE html>
|
|
15
16
|
<html lang="en">
|
|
@@ -45,11 +46,13 @@ export default (props: {
|
|
|
45
46
|
</head>
|
|
46
47
|
|
|
47
48
|
<body data-barba="wrapper">
|
|
48
|
-
|
|
49
|
+
${includeAdminPanel
|
|
50
|
+
? html`<section style="margin-bottom: 0;">${adminPanel()}</section>`
|
|
51
|
+
: html`<div></div>`}
|
|
49
52
|
|
|
50
53
|
<main>
|
|
51
54
|
<section data-barba="container" data-barba-namespace="default">
|
|
52
|
-
${
|
|
55
|
+
${content}
|
|
53
56
|
</section>
|
|
54
57
|
</main>
|
|
55
58
|
</body>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { db } from '@alstar/db'
|
|
2
|
+
import { html } from 'hono/html'
|
|
3
|
+
import { sql } from '../utils/sql.ts'
|
|
4
|
+
|
|
5
|
+
export default () => {
|
|
6
|
+
const users = db.database
|
|
7
|
+
.prepare(sql`
|
|
8
|
+
select
|
|
9
|
+
email
|
|
10
|
+
from
|
|
11
|
+
users
|
|
12
|
+
`)
|
|
13
|
+
.all()
|
|
14
|
+
|
|
15
|
+
return html`
|
|
16
|
+
<article>
|
|
17
|
+
<header>Users</header>
|
|
18
|
+
<ul>
|
|
19
|
+
${users.map(user => html`<li>${user.email}</li>`)}
|
|
20
|
+
</ul>
|
|
21
|
+
<article>
|
|
22
|
+
<header>Register user</header>
|
|
23
|
+
<form
|
|
24
|
+
data-on-submit="@post('/studio/api/auth/register', { contentType: 'form' })"
|
|
25
|
+
>
|
|
26
|
+
<label for="register_email"><small>Email</small></label>
|
|
27
|
+
<input
|
|
28
|
+
id="register_email"
|
|
29
|
+
name="email"
|
|
30
|
+
type="email"
|
|
31
|
+
placeholder="Email"
|
|
32
|
+
/>
|
|
33
|
+
<label for="register_password"><small>Password</small></label>
|
|
34
|
+
<input
|
|
35
|
+
id="register_password"
|
|
36
|
+
name="password"
|
|
37
|
+
type="password"
|
|
38
|
+
placeholder="Password"
|
|
39
|
+
/>
|
|
40
|
+
<br />
|
|
41
|
+
<button type="submit" class="ghost">Create</button>
|
|
42
|
+
</form>
|
|
43
|
+
</article>
|
|
44
|
+
</article>
|
|
45
|
+
`
|
|
46
|
+
}
|
|
@@ -17,7 +17,7 @@ export default (props: {
|
|
|
17
17
|
|
|
18
18
|
return html`
|
|
19
19
|
<form
|
|
20
|
-
data-on-input="@patch('/
|
|
20
|
+
data-on-input="@patch('/studio/api/block', { contentType: 'form' })"
|
|
21
21
|
>
|
|
22
22
|
<hgroup>
|
|
23
23
|
<label for="block-${data.id}">${structure.label}</label>
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { getOrCreateRow } from '../../utils/get-or-create-row.ts'
|
|
2
|
+
import { html } from '../../utils/html.ts'
|
|
3
|
+
import type { FieldDefStructure } from '../../types.ts'
|
|
4
|
+
import * as icons from '../icons.ts'
|
|
5
|
+
import { Hono } from 'hono'
|
|
6
|
+
import { type HttpBindings } from '@hono/node-server'
|
|
7
|
+
import { streamSSE } from 'hono/streaming'
|
|
8
|
+
import { query } from '../../queries/index.ts'
|
|
9
|
+
import { slugify } from '../../utils/slugify.ts'
|
|
10
|
+
|
|
11
|
+
const app = new Hono<{ Bindings: HttpBindings }>()
|
|
12
|
+
|
|
13
|
+
app.get('/slug', async (c) => {
|
|
14
|
+
try {
|
|
15
|
+
const params = await c.req.query()
|
|
16
|
+
|
|
17
|
+
if (!params.entryId) {
|
|
18
|
+
return c.json({
|
|
19
|
+
status: 404,
|
|
20
|
+
message: 'Needs an entryId to generate slug',
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const entry = query.root({ id: params.entryId })
|
|
25
|
+
const title = entry?.fields?.title?.value
|
|
26
|
+
|
|
27
|
+
if (!title) {
|
|
28
|
+
return c.json({ status: 404, message: 'No title to generate slug from' })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log('slug', slugify(title))
|
|
32
|
+
|
|
33
|
+
return streamSSE(c, async (stream) => {
|
|
34
|
+
await stream.writeSSE({
|
|
35
|
+
event: 'datastar-patch-signals',
|
|
36
|
+
data: `signals { slug: '${slugify(title)}' }`,
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.log(error)
|
|
41
|
+
return c.text('Error generating slug')
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
export const routes = app
|
|
46
|
+
|
|
47
|
+
export default (props: {
|
|
48
|
+
entryId: number
|
|
49
|
+
parentId: number
|
|
50
|
+
name: string
|
|
51
|
+
id?: number
|
|
52
|
+
structure: FieldDefStructure
|
|
53
|
+
sortOrder?: number
|
|
54
|
+
}) => {
|
|
55
|
+
const { entryId, parentId, name, structure, sortOrder = 0, id } = props
|
|
56
|
+
|
|
57
|
+
const data = getOrCreateRow({
|
|
58
|
+
parentId,
|
|
59
|
+
name,
|
|
60
|
+
field: structure,
|
|
61
|
+
sortOrder,
|
|
62
|
+
id,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
if (!data) return html`<p>No block</p>`
|
|
66
|
+
|
|
67
|
+
return html`
|
|
68
|
+
<div
|
|
69
|
+
style="display: flex; align-items: center"
|
|
70
|
+
data-signals="{ slug: '${data.value}' }"
|
|
71
|
+
>
|
|
72
|
+
<form
|
|
73
|
+
data-ref="form"
|
|
74
|
+
data-on-input="@patch('/studio/api/block', { contentType: 'form' })"
|
|
75
|
+
data-on-submit="@patch('/studio/api/block', { contentType: 'form' })"
|
|
76
|
+
data-on-signal-patch="$form.requestSubmit()"
|
|
77
|
+
>
|
|
78
|
+
<hgroup>
|
|
79
|
+
<label for="block-${data.id}">${structure.label}</label>
|
|
80
|
+
<p><small>${structure.description}</small></p>
|
|
81
|
+
</hgroup>
|
|
82
|
+
|
|
83
|
+
<input
|
|
84
|
+
id="block-${data.id}"
|
|
85
|
+
name="value"
|
|
86
|
+
type="text"
|
|
87
|
+
data-bind="slug"
|
|
88
|
+
/>
|
|
89
|
+
|
|
90
|
+
<input type="hidden" name="type" value="${structure.type}" />
|
|
91
|
+
<input type="hidden" name="id" value="${data.id}" />
|
|
92
|
+
<input type="hidden" name="entryId" value="${entryId}" />
|
|
93
|
+
<input type="hidden" name="parentId" value="${parentId}" />
|
|
94
|
+
<input type="hidden" name="name" value="${name}" />
|
|
95
|
+
</form>
|
|
96
|
+
|
|
97
|
+
<form
|
|
98
|
+
style="margin-top: 21px"
|
|
99
|
+
data-on-submit="@get('/studio/api/field/slug', { contentType: 'form' })"
|
|
100
|
+
>
|
|
101
|
+
<input type="hidden" name="entryId" value="${entryId}" />
|
|
102
|
+
<button
|
|
103
|
+
class="ghost"
|
|
104
|
+
aria-label="Generate slug"
|
|
105
|
+
data-tooltip="Generate slug"
|
|
106
|
+
data-placement="top"
|
|
107
|
+
>
|
|
108
|
+
${icons.arrowsRound}
|
|
109
|
+
</button>
|
|
110
|
+
</form>
|
|
111
|
+
</div>
|
|
112
|
+
`
|
|
113
|
+
}
|
|
@@ -18,7 +18,7 @@ export default (props: {
|
|
|
18
18
|
|
|
19
19
|
return html`
|
|
20
20
|
<form
|
|
21
|
-
data-on-input="@patch('/
|
|
21
|
+
data-on-input="@patch('/studio/api/block', { contentType: 'form' })"
|
|
22
22
|
>
|
|
23
23
|
<hgroup>
|
|
24
24
|
<label for="block-${data.id}">${structure.label}</label>
|
package/components/icons.ts
CHANGED
|
@@ -194,3 +194,39 @@ export const clipboard = html`
|
|
|
194
194
|
/>
|
|
195
195
|
</svg>
|
|
196
196
|
`
|
|
197
|
+
|
|
198
|
+
export const leave = html`
|
|
199
|
+
<svg
|
|
200
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
201
|
+
width="16"
|
|
202
|
+
height="16"
|
|
203
|
+
viewBox="0 0 20 20"
|
|
204
|
+
>
|
|
205
|
+
<!-- Icon from HeroIcons by Refactoring UI Inc - https://github.com/tailwindlabs/heroicons/blob/master/LICENSE -->
|
|
206
|
+
<g fill="currentColor" fill-rule="evenodd" clip-rule="evenodd">
|
|
207
|
+
<path
|
|
208
|
+
d="M3 4.25A2.25 2.25 0 0 1 5.25 2h5.5A2.25 2.25 0 0 1 13 4.25v2a.75.75 0 0 1-1.5 0v-2a.75.75 0 0 0-.75-.75h-5.5a.75.75 0 0 0-.75.75v11.5c0 .414.336.75.75.75h5.5a.75.75 0 0 0 .75-.75v-2a.75.75 0 0 1 1.5 0v2A2.25 2.25 0 0 1 10.75 18h-5.5A2.25 2.25 0 0 1 3 15.75z"
|
|
209
|
+
/>
|
|
210
|
+
<path
|
|
211
|
+
d="M19 10a.75.75 0 0 0-.75-.75H8.704l1.048-.943a.75.75 0 1 0-1.004-1.114l-2.5 2.25a.75.75 0 0 0 0 1.114l2.5 2.25a.75.75 0 1 0 1.004-1.114l-1.048-.943h9.546A.75.75 0 0 0 19 10"
|
|
212
|
+
/>
|
|
213
|
+
</g>
|
|
214
|
+
</svg>
|
|
215
|
+
`
|
|
216
|
+
|
|
217
|
+
export const arrowsRound = html`
|
|
218
|
+
<svg
|
|
219
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
220
|
+
width="16"
|
|
221
|
+
height="16"
|
|
222
|
+
viewBox="0 0 20 20"
|
|
223
|
+
>
|
|
224
|
+
<!-- Icon from HeroIcons by Refactoring UI Inc - https://github.com/tailwindlabs/heroicons/blob/master/LICENSE -->
|
|
225
|
+
<path
|
|
226
|
+
fill="currentColor"
|
|
227
|
+
fill-rule="evenodd"
|
|
228
|
+
d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.433a.75.75 0 0 0 0-1.5H3.989a.75.75 0 0 0-.75.75v4.242a.75.75 0 0 0 1.5 0v-2.43l.31.31a7 7 0 0 0 11.712-3.138a.75.75 0 0 0-1.449-.39m1.23-3.723a.75.75 0 0 0 .219-.53V2.929a.75.75 0 0 0-1.5 0V5.36l-.31-.31A7 7 0 0 0 3.239 8.188a.75.75 0 1 0 1.448.389A5.5 5.5 0 0 1 13.89 6.11l.311.31h-2.432a.75.75 0 0 0 0 1.5h4.243a.75.75 0 0 0 .53-.219"
|
|
229
|
+
clip-rule="evenodd"
|
|
230
|
+
/>
|
|
231
|
+
</svg>
|
|
232
|
+
`
|
package/index.ts
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
1
3
|
import { Hono } from 'hono'
|
|
2
|
-
import { loadDb } from '@alstar/db'
|
|
3
4
|
import { serve } from '@hono/node-server'
|
|
4
5
|
import { serveStatic } from '@hono/node-server/serve-static'
|
|
6
|
+
import { HTTPException } from 'hono/http-exception'
|
|
7
|
+
|
|
8
|
+
import { loadDb } from '@alstar/db'
|
|
5
9
|
import { createRefresher } from '@alstar/refresher'
|
|
6
10
|
|
|
7
|
-
import * as types from './types.ts'
|
|
8
11
|
import { createStudioTables } from './utils/create-studio-tables.ts'
|
|
9
12
|
import { fileBasedRouter } from './utils/file-based-router.ts'
|
|
10
13
|
import { getConfig } from './utils/get-config.ts'
|
|
11
14
|
import startupLog from './utils/startup-log.ts'
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
+
import { apiRoutes } from './api/index.ts'
|
|
16
|
+
import { mcpRoutes } from './api/mcp.ts'
|
|
17
|
+
|
|
18
|
+
import auth from './utils/auth.ts'
|
|
19
|
+
import ErrorPage from './pages/error.ts'
|
|
20
|
+
|
|
21
|
+
import * as types from './types.ts'
|
|
15
22
|
|
|
16
23
|
export let rootdir = './node_modules/@alstar/studio'
|
|
17
24
|
|
|
@@ -23,7 +30,8 @@ export let studioConfig: types.StudioConfig = {
|
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
const createStudio = async (config: types.StudioConfig) => {
|
|
26
|
-
const refresher = await createRefresher({ rootdir: '.' })
|
|
33
|
+
// const refresher = await createRefresher({ rootdir: ['.', import.meta.dirname] })
|
|
34
|
+
const refresher = await createRefresher({ rootdir: ['.'] })
|
|
27
35
|
|
|
28
36
|
loadDb('./studio.db')
|
|
29
37
|
createStudioTables()
|
|
@@ -44,26 +52,46 @@ const createStudio = async (config: types.StudioConfig) => {
|
|
|
44
52
|
app.use('*', serveStatic({ root: path.join(rootdir, 'public') }))
|
|
45
53
|
app.use('*', serveStatic({ root: './public' }))
|
|
46
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Require authentication to access Studio
|
|
57
|
+
*/
|
|
58
|
+
app.use('/studio/*', auth)
|
|
59
|
+
|
|
47
60
|
/**
|
|
48
61
|
* Studio API routes
|
|
49
62
|
*/
|
|
50
|
-
app.route('/
|
|
51
|
-
app.route('/
|
|
63
|
+
app.route('/studio/api', apiRoutes)
|
|
64
|
+
app.route('/studio/mcp', mcpRoutes)
|
|
52
65
|
|
|
53
66
|
/**
|
|
54
67
|
* Studio pages
|
|
55
68
|
*/
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
if (adminPages) app.route('/admin', adminPages)
|
|
69
|
+
const studioPages = await fileBasedRouter(path.join(rootdir, 'pages'))
|
|
70
|
+
if (studioPages) app.route('/studio', studioPages)
|
|
59
71
|
|
|
60
72
|
/**
|
|
61
73
|
* User pages
|
|
62
74
|
*/
|
|
63
75
|
const pages = await fileBasedRouter('./pages')
|
|
64
|
-
|
|
65
76
|
if (pages) app.route('/', pages)
|
|
66
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Error pages
|
|
80
|
+
*/
|
|
81
|
+
app.notFound((c) => c.html(ErrorPage()))
|
|
82
|
+
app.onError((err, c) => {
|
|
83
|
+
if (err instanceof HTTPException) {
|
|
84
|
+
// Get the custom response
|
|
85
|
+
const error = err.getResponse()
|
|
86
|
+
return c.html(ErrorPage(err))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return c.notFound()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Run server
|
|
94
|
+
*/
|
|
67
95
|
const server = serve({
|
|
68
96
|
fetch: app.fetch,
|
|
69
97
|
port: studioConfig.port,
|
package/package.json
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alstar/studio",
|
|
3
|
-
"version": "0.0.0-beta.
|
|
3
|
+
"version": "0.0.0-beta.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=23.8"
|
|
8
|
+
},
|
|
6
9
|
"dependencies": {
|
|
7
10
|
"@hono/node-server": "^1.18.1",
|
|
8
11
|
"@starfederation/datastar-sdk": "1.0.0-RC.1",
|
|
9
12
|
"hono": "^4.8.12",
|
|
10
|
-
"@alstar/
|
|
13
|
+
"@alstar/db": "0.0.0-beta.1",
|
|
11
14
|
"@alstar/ui": "0.0.0-beta.1",
|
|
12
|
-
"@alstar/
|
|
15
|
+
"@alstar/refresher": "0.0.0-beta.3"
|
|
13
16
|
},
|
|
14
17
|
"devDependencies": {
|
|
15
18
|
"@types/node": "^24.1.0",
|
package/pages/entry/[id].ts
CHANGED
package/pages/error.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { html } from "hono/html";
|
|
2
|
+
import SiteLayout from "../components/SiteLayout.ts";
|
|
3
|
+
import type { HTTPResponseError } from "hono/types";
|
|
4
|
+
|
|
5
|
+
export default ((err?: Error | HTTPResponseError) => {
|
|
6
|
+
|
|
7
|
+
return SiteLayout(html`
|
|
8
|
+
<article>
|
|
9
|
+
<header>Something went wrong</header>
|
|
10
|
+
<p>Try again</p>
|
|
11
|
+
<p>${err?.message}</p>
|
|
12
|
+
</article>
|
|
13
|
+
`, false)
|
|
14
|
+
})
|
package/pages/index.ts
CHANGED
package/pages/login.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { html } from "hono/html";
|
|
2
|
+
import { defineEntry } from "../utils/define.ts";
|
|
3
|
+
import SiteLayout from "../components/SiteLayout.ts";
|
|
4
|
+
|
|
5
|
+
export default defineEntry(c => {
|
|
6
|
+
return SiteLayout(html`
|
|
7
|
+
<div class="login-form">
|
|
8
|
+
<article>
|
|
9
|
+
<header>Login</header>
|
|
10
|
+
<form data-on-submit="@post('/studio/api/auth/login', { contentType: 'form' })">
|
|
11
|
+
<label for="email">Email</label>
|
|
12
|
+
<input id="email" name="email" type="text" placeholder="Email">
|
|
13
|
+
<label for="password">Password</label>
|
|
14
|
+
<input id="password" name="password" type="password" placeholder="Password">
|
|
15
|
+
<br>
|
|
16
|
+
<button style="width: 100%;">Login</button>
|
|
17
|
+
</form>
|
|
18
|
+
</article>
|
|
19
|
+
</div>
|
|
20
|
+
`, false)
|
|
21
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { html } from 'hono/html'
|
|
2
|
+
import { defineEntry } from '../utils/define.ts'
|
|
3
|
+
import SiteLayout from '../components/SiteLayout.ts'
|
|
4
|
+
|
|
5
|
+
export default defineEntry((c) => {
|
|
6
|
+
return SiteLayout(
|
|
7
|
+
html`
|
|
8
|
+
<div class="register-form" style="width: 300px">
|
|
9
|
+
<article>
|
|
10
|
+
<header>Register user</header>
|
|
11
|
+
<form
|
|
12
|
+
data-signals="{ status: 0 }"
|
|
13
|
+
data-on-submit="@post('/studio/api/auth/register', { contentType: 'form' })"
|
|
14
|
+
data-on-signal-patch="patch.status === 200 && window.location.reload()"
|
|
15
|
+
>
|
|
16
|
+
<label for="email">Email</label>
|
|
17
|
+
<input id="email" name="email" type="text" placeholder="Email" />
|
|
18
|
+
<label for="password">Password</label>
|
|
19
|
+
<input
|
|
20
|
+
id="password"
|
|
21
|
+
name="password"
|
|
22
|
+
type="password"
|
|
23
|
+
placeholder="Password"
|
|
24
|
+
/>
|
|
25
|
+
<br />
|
|
26
|
+
<button style="width: 100%;">Register</button>
|
|
27
|
+
</form>
|
|
28
|
+
</article>
|
|
29
|
+
</div>
|
|
30
|
+
`,
|
|
31
|
+
false,
|
|
32
|
+
)
|
|
33
|
+
})
|