@beechcms/api 0.4.0-preview.9 → 0.4.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/assets/dashboard/BeechLogo.svg +18 -18
- package/assets/dashboard/BeechLogoLIght.svg +48 -48
- package/assets/dashboard/assets/index-CewtCjom.css +1 -0
- package/assets/dashboard/assets/index-FQ6JhvRH.js +554 -0
- package/assets/dashboard/beechLogoDark.svg +48 -48
- package/assets/dashboard/index.html +18 -18
- package/assets/dashboard/sol.svg +3 -3
- package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
- package/migrations/0000_v040_base.sql +213 -213
- package/package.json +2 -2
- package/src/auth/constants.ts +10 -10
- package/src/auth/login.ts +91 -91
- package/src/auth/refresh.ts +127 -127
- package/src/factory.ts +347 -303
- package/src/features/content/constants.ts +10 -0
- package/src/features/content/handlers/create.ts +153 -0
- package/src/features/content/handlers/delete.ts +76 -0
- package/src/features/content/handlers/facets.ts +45 -0
- package/src/features/content/handlers/get.ts +116 -0
- package/src/features/content/handlers/list.ts +88 -0
- package/src/features/content/handlers/update.ts +207 -0
- package/src/features/content/index.ts +20 -0
- package/src/features/draft/draft.handler.ts +272 -198
- package/src/features/draft/index.ts +1 -1
- package/src/features/email/email.provider.ts +38 -38
- package/src/features/email/email.service.ts +80 -80
- package/src/features/email/email.types.ts +98 -98
- package/src/features/email/index.ts +28 -28
- package/src/features/email/providers/resend.ts +63 -63
- package/src/features/email/templates/password-changed.ts +59 -59
- package/src/features/email/templates/password-reset.ts +64 -64
- package/src/features/email/templates/shell.ts +92 -93
- package/src/features/notifications/index.ts +1 -1
- package/src/features/notifications/notifications.handler.ts +130 -88
- package/src/features/password-reset/index.ts +15 -15
- package/src/features/password-reset/request.ts +106 -88
- package/src/features/password-reset/reset.ts +128 -110
- package/src/features/rotate-field/index.ts +1 -1
- package/src/features/rotate-field/rotate-field.handler.ts +132 -82
- package/src/features/rotate-field/rotate-field.schema.ts +13 -9
- package/src/features/schema/schema.handler.ts +16 -16
- package/src/features/settings/settings.handler.ts +414 -249
- package/src/features/setup/index.ts +96 -59
- package/src/features/stats/index.ts +1 -1
- package/src/features/stats/stats.handler.ts +458 -395
- package/src/index.ts +24 -11
- package/src/media-utils.ts +78 -78
- package/src/middleware/repository.middleware.ts +24 -0
- package/src/middleware/storage.middleware.ts +21 -0
- package/src/middleware.ts +67 -67
- package/src/public/access-policy.ts +23 -23
- package/src/public/api-key-middleware.ts +53 -53
- package/src/public/index.ts +12 -12
- package/src/public/problem-details.ts +48 -42
- package/src/public/public-add.ts +166 -183
- package/src/public/public-edit.ts +158 -183
- package/src/public/public-errors.ts +15 -15
- package/src/public/public-read.ts +216 -217
- package/src/public/public-routes.ts +84 -31
- package/src/public/query-builder.ts +152 -241
- package/src/public/rate-limit-middleware.ts +42 -42
- package/src/public/response-builder.ts +26 -26
- package/src/public/sanitize.ts +65 -65
- package/src/public/slug-utils.ts +14 -14
- package/src/search-utils.ts +192 -192
- package/src/search.ts +72 -72
- package/src/shared/activity-logger.ts +79 -79
- package/src/shared/apply-policies.ts +63 -63
- package/src/shared/base.repository.d1.ts +28 -0
- package/src/shared/content-utils.ts +82 -108
- package/src/shared/content.repository.d1.ts +382 -0
- package/src/shared/fts-sync.ts +4 -4
- package/src/shared/idempotency.repository.d1.ts +56 -0
- package/src/shared/media.repository.d1.ts +64 -0
- package/src/shared/notification-service.ts +56 -56
- package/src/shared/query-utils.ts +137 -137
- package/src/shared/storage/factory.ts +40 -0
- package/src/shared/storage/r2-binding-bucket.ts +81 -0
- package/src/shared/storage/s3-bucket.ts +163 -0
- package/src/shared/storage-utils.ts +36 -36
- package/src/shared/system-stats.repository.d1.ts +44 -0
- package/src/types.ts +46 -36
- package/src/upload.ts +179 -335
- package/src/widget.ts +349 -349
- package/assets/dashboard/assets/index-CC-jbp6g.js +0 -554
- package/assets/dashboard/assets/index-CQODXprH.css +0 -1
- package/src/content.ts +0 -502
- package/src/features/draft/draft.test.ts +0 -315
- package/src/features/rotate-field/rotate-field.test.ts +0 -297
|
@@ -1,217 +1,216 @@
|
|
|
1
|
-
import { resolvePolicies } from '@beechcms/core'
|
|
2
|
-
import type { Seed } from '@beechcms/core'
|
|
3
|
-
import type { Context } from 'hono'
|
|
4
|
-
import { cleanStr } from '../shared/query-utils'
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
} from '
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
cache
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
1
|
+
import { resolvePolicies, EntryNotFoundError } from '@beechcms/core'
|
|
2
|
+
import type { Seed } from '@beechcms/core'
|
|
3
|
+
import type { Context } from 'hono'
|
|
4
|
+
import { cleanStr } from '../shared/query-utils'
|
|
5
|
+
import { checkPublicOperation } from './access-policy'
|
|
6
|
+
import { publicProblem } from './problem-details'
|
|
7
|
+
import { buildPublicListMeta, buildPublicSingleMeta } from './response-builder'
|
|
8
|
+
import {
|
|
9
|
+
parseLatestCount,
|
|
10
|
+
parsePublicFilter,
|
|
11
|
+
parsePublicPagination,
|
|
12
|
+
toEngineFilters,
|
|
13
|
+
} from './query-builder'
|
|
14
|
+
import { AppEnv } from '../types'
|
|
15
|
+
|
|
16
|
+
function buildSeedNotFoundMessage(seed: string, seedRegistry: Record<string, Seed>): string {
|
|
17
|
+
const available = Object.keys(seedRegistry).join(', ')
|
|
18
|
+
return `The content type '${seed}' does not exist. Available types: ${available}.`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Applies public/visibility policies for the Public API. */
|
|
22
|
+
function applyPublicPolicies(data: Record<string, unknown>, seed: Seed): Record<string, unknown> {
|
|
23
|
+
const result: Record<string, unknown> = {}
|
|
24
|
+
|
|
25
|
+
// System fields are mapped to top-level for public API
|
|
26
|
+
const system = ['id', 'slug', 'status', 'created_at', 'updated_at']
|
|
27
|
+
for (const key of system) {
|
|
28
|
+
if (key in data) result[key] = data[key]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const branch of seed.branches) {
|
|
32
|
+
const value = data[branch.alias]
|
|
33
|
+
const { public: isPublic, visibility } = resolvePolicies(branch)
|
|
34
|
+
if (!isPublic) continue
|
|
35
|
+
if (visibility === 'hidden') continue
|
|
36
|
+
if (visibility === 'masked') {
|
|
37
|
+
result[branch.alias] = typeof value === 'string' && value.length > 0 ? '••••••••' : null
|
|
38
|
+
} else {
|
|
39
|
+
result[branch.alias] = value
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return result
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function toFlatPublicEntry(data: Record<string, unknown>, seed: Seed, fieldsParam?: string): Record<string, unknown> {
|
|
46
|
+
const aliasData = applyPublicPolicies(data, seed)
|
|
47
|
+
const requestedFields = (fieldsParam ?? '').split(',').map((f) => f.trim()).filter(Boolean)
|
|
48
|
+
|
|
49
|
+
if (requestedFields.length === 0) return aliasData
|
|
50
|
+
|
|
51
|
+
const filteredData: Record<string, unknown> = {}
|
|
52
|
+
for (const field of requestedFields) {
|
|
53
|
+
if (field in aliasData) filteredData[field] = aliasData[field]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Always include basic identity fields if they exist in filtered data or if requested
|
|
57
|
+
const identity = ['id', 'slug']
|
|
58
|
+
for (const key of identity) {
|
|
59
|
+
if (key in aliasData && !filteredData[key]) filteredData[key] = aliasData[key]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return filteredData
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function withCache(
|
|
66
|
+
cache: Cache | undefined,
|
|
67
|
+
executionCtx: { waitUntil: (p: Promise<unknown>) => void } | undefined,
|
|
68
|
+
cacheKey: Request,
|
|
69
|
+
response: Response
|
|
70
|
+
): Response {
|
|
71
|
+
if (cache && executionCtx) {
|
|
72
|
+
const cloned = response.clone()
|
|
73
|
+
const headers = new Headers(cloned.headers)
|
|
74
|
+
headers.set('Cache-Control', 'public, max-age=60')
|
|
75
|
+
executionCtx.waitUntil(cache.put(cacheKey, new Response(cloned.body, { status: cloned.status, headers })))
|
|
76
|
+
}
|
|
77
|
+
return response
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildInternalErrorMessage(context: Context<AppEnv>, error: unknown): string {
|
|
81
|
+
if (context.env.ENV !== 'production' && error instanceof Error) return error.message
|
|
82
|
+
return 'An unexpected error occurred.'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function publicReadHandler(context: Context<AppEnv>) {
|
|
86
|
+
const seedSlug = context.req.param('seed') ?? ''
|
|
87
|
+
const seed = context.get('getSeed')(seedSlug)
|
|
88
|
+
if (!seed) {
|
|
89
|
+
return publicProblem(context, {
|
|
90
|
+
type: 'seed-not-found',
|
|
91
|
+
title: 'Seed Not Found',
|
|
92
|
+
status: 404,
|
|
93
|
+
detail: buildSeedNotFoundMessage(seedSlug, context.get('seedRegistry')),
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const access = checkPublicOperation(seed, 'read')
|
|
98
|
+
if (!access.ok) {
|
|
99
|
+
return publicProblem(context, {
|
|
100
|
+
type: 'operation-not-allowed',
|
|
101
|
+
title: access.error.error,
|
|
102
|
+
status: 403,
|
|
103
|
+
detail: access.error.message
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let cache: Cache | undefined
|
|
108
|
+
let executionCtx: { waitUntil: (p: Promise<unknown>) => void } | undefined
|
|
109
|
+
try {
|
|
110
|
+
cache = caches.default
|
|
111
|
+
executionCtx = context.executionCtx as typeof executionCtx
|
|
112
|
+
} catch {}
|
|
113
|
+
|
|
114
|
+
const cacheKey = context.req.raw
|
|
115
|
+
if (cache) {
|
|
116
|
+
const hit = await cache.match(cacheKey)
|
|
117
|
+
if (hit) return hit
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const query = context.req.query()
|
|
121
|
+
const id = cleanStr(query.id)
|
|
122
|
+
const slug = cleanStr(query.slug)
|
|
123
|
+
const publishedOnly = context.env.PUBLIC_PUBLISHED_ONLY !== 'false'
|
|
124
|
+
const repository = context.get('repository')
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
if (id || slug) {
|
|
128
|
+
try {
|
|
129
|
+
const entry = id
|
|
130
|
+
? await repository.findById(seed, id)
|
|
131
|
+
: await repository.findBySlug(seed, slug!)
|
|
132
|
+
|
|
133
|
+
if (publishedOnly && entry.status !== 'published') {
|
|
134
|
+
return publicProblem(context, {
|
|
135
|
+
type: 'entry-not-found',
|
|
136
|
+
title: 'Not Found',
|
|
137
|
+
status: 404,
|
|
138
|
+
detail: `Entry '${id || slug}' not found or not published.`
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return withCache(cache, executionCtx, cacheKey,
|
|
143
|
+
context.json({
|
|
144
|
+
data: toFlatPublicEntry(entry, seed, query.fields),
|
|
145
|
+
meta: buildPublicSingleMeta(seedSlug)
|
|
146
|
+
}, 200)
|
|
147
|
+
)
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (error instanceof EntryNotFoundError) {
|
|
150
|
+
return publicProblem(context, {
|
|
151
|
+
type: 'entry-not-found',
|
|
152
|
+
title: 'Not Found',
|
|
153
|
+
status: 404,
|
|
154
|
+
detail: `Entry '${id || slug}' not found for content type '${seedSlug}'.`
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
throw error
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const parsedFilter = parsePublicFilter(query.filter)
|
|
162
|
+
const allMode = cleanStr(query.all)?.toLowerCase() === 'true'
|
|
163
|
+
const latestMode = cleanStr(query.latest) !== null
|
|
164
|
+
const latestCount = latestMode ? parseLatestCount(query.latest ?? '') : null
|
|
165
|
+
const pagination = allMode ? { page: 1, limit: 100 } : parsePublicPagination(query)
|
|
166
|
+
const offset = (pagination.page - 1) * pagination.limit
|
|
167
|
+
const search = cleanStr(query.search) ?? ''
|
|
168
|
+
|
|
169
|
+
const engineFilters = toEngineFilters(seed, parsedFilter)
|
|
170
|
+
const sortBy = cleanStr(query.orderBy) ?? 'created_at'
|
|
171
|
+
const sortDir = (cleanStr(query.orderDir) ?? 'desc').toLowerCase() === 'asc' ? 'ASC' : 'DESC'
|
|
172
|
+
|
|
173
|
+
const { items, total } = await repository.findMany(seed, {
|
|
174
|
+
filters: engineFilters,
|
|
175
|
+
search: search || undefined,
|
|
176
|
+
status: publishedOnly ? 'published' : null,
|
|
177
|
+
pagination: {
|
|
178
|
+
limit: latestMode ? (latestCount ?? 10) : pagination.limit,
|
|
179
|
+
offset: latestMode ? 0 : offset
|
|
180
|
+
},
|
|
181
|
+
orderBy: latestMode ? { column: 'created_at', dir: 'DESC' } : { column: sortBy, dir: sortDir }
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
const data = items.map((item) => toFlatPublicEntry(item, seed, query.fields))
|
|
185
|
+
|
|
186
|
+
if (latestMode) {
|
|
187
|
+
return withCache(cache, executionCtx, cacheKey,
|
|
188
|
+
context.json({ data, meta: { total, returned: data.length, seed: seedSlug } }, 200)
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return withCache(cache, executionCtx, cacheKey,
|
|
193
|
+
context.json({
|
|
194
|
+
data,
|
|
195
|
+
meta: buildPublicListMeta({
|
|
196
|
+
total,
|
|
197
|
+
page: pagination.page,
|
|
198
|
+
limit: latestMode ? (latestCount ?? 10) : pagination.limit,
|
|
199
|
+
returned: data.length,
|
|
200
|
+
seed: seedSlug
|
|
201
|
+
}),
|
|
202
|
+
}, 200)
|
|
203
|
+
)
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (error instanceof Error && error.message.startsWith('Invalid filter:')) {
|
|
206
|
+
return publicProblem(context, { type: 'invalid-filter', title: 'Bad Request', status: 400, detail: error.message })
|
|
207
|
+
}
|
|
208
|
+
console.error('Public read error:', error)
|
|
209
|
+
return publicProblem(context, {
|
|
210
|
+
type: 'internal-server-error',
|
|
211
|
+
title: 'Internal Server Error',
|
|
212
|
+
status: 500,
|
|
213
|
+
detail: buildInternalErrorMessage(context, error)
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -1,31 +1,84 @@
|
|
|
1
|
-
import { Hono } from 'hono'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
1
|
+
import { Hono } from 'hono'
|
|
2
|
+
import type { AppEnv } from '../types'
|
|
3
|
+
import { publicReadHandler } from './public-read'
|
|
4
|
+
import { publicAddHandler } from './public-add'
|
|
5
|
+
import { publicEditHandler } from './public-edit'
|
|
6
|
+
|
|
7
|
+
const publicApp = new Hono<AppEnv>()
|
|
8
|
+
|
|
9
|
+
publicApp.get('/health', (c) => {
|
|
10
|
+
return c.json({ ok: true, service: 'public-api' }, 200)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
/** Returns the public-facing schema: only seeds with public read or post enabled. */
|
|
14
|
+
publicApp.get('/schema', (c) => {
|
|
15
|
+
const registry = c.get('seedRegistry')
|
|
16
|
+
const publicSeeds = Object.values(registry)
|
|
17
|
+
.filter(seed => seed.allowPublicRead === true || seed.allowPublicPost === true || seed.allowPublicEdit === true)
|
|
18
|
+
.map(seed => ({
|
|
19
|
+
slug: seed.slug,
|
|
20
|
+
label: seed.label,
|
|
21
|
+
labelPlural: seed.labelPlural,
|
|
22
|
+
allowPublicRead: seed.allowPublicRead ?? false,
|
|
23
|
+
allowPublicPost: seed.allowPublicPost ?? false,
|
|
24
|
+
allowPublicEdit: seed.allowPublicEdit ?? false,
|
|
25
|
+
branches: (seed.branches ?? []).map(branch => ({
|
|
26
|
+
alias: branch.alias,
|
|
27
|
+
type: branch.type,
|
|
28
|
+
label: branch.label,
|
|
29
|
+
requiredOnCreate: branch.requiredOnCreate ?? false,
|
|
30
|
+
policies: {
|
|
31
|
+
public: branch.policies?.public ?? true,
|
|
32
|
+
visibility: branch.policies?.visibility ?? 'full',
|
|
33
|
+
},
|
|
34
|
+
})),
|
|
35
|
+
}))
|
|
36
|
+
return c.json({ seeds: publicSeeds })
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/** HTML render of the public schema for quick browser inspection. */
|
|
40
|
+
publicApp.get('/schema.html', (c) => {
|
|
41
|
+
const registry = c.get('seedRegistry')
|
|
42
|
+
const publicSeeds = Object.values(registry)
|
|
43
|
+
.filter(seed => seed.allowPublicRead === true || seed.allowPublicPost === true || seed.allowPublicEdit === true)
|
|
44
|
+
|
|
45
|
+
const seedHtml = publicSeeds.map(seed => {
|
|
46
|
+
const ops = [
|
|
47
|
+
seed.allowPublicRead && 'GET /:seed',
|
|
48
|
+
seed.allowPublicPost && 'POST /:seed/add',
|
|
49
|
+
seed.allowPublicEdit && 'PUT /:seed/edit/:id',
|
|
50
|
+
].filter(Boolean).join(', ')
|
|
51
|
+
|
|
52
|
+
const branchRows = (seed.branches ?? []).map(b =>
|
|
53
|
+
`<tr><td>${b.alias}</td><td>${b.type}</td><td>${b.label}</td><td>${b.requiredOnCreate ? 'yes' : ''}</td><td>${(b.policies?.public ?? true) ? 'readable' : 'hidden'}</td></tr>`
|
|
54
|
+
).join('')
|
|
55
|
+
|
|
56
|
+
return `<section>
|
|
57
|
+
<h2>${seed.label} <code>${seed.slug}</code></h2>
|
|
58
|
+
<p><strong>Allowed operations:</strong> ${ops || 'none'}</p>
|
|
59
|
+
<table><thead><tr><th>alias</th><th>type</th><th>label</th><th>required</th><th>public access</th></tr></thead>
|
|
60
|
+
<tbody>${branchRows}</tbody></table>
|
|
61
|
+
</section>`
|
|
62
|
+
}).join('\n')
|
|
63
|
+
|
|
64
|
+
const html = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">
|
|
65
|
+
<title>BeechCMS Public Schema</title>
|
|
66
|
+
<style>body{font-family:system-ui,sans-serif;max-width:900px;margin:2rem auto;padding:0 1rem;color:#222}
|
|
67
|
+
code{background:#f3f4f6;padding:2px 6px;border-radius:4px}
|
|
68
|
+
table{border-collapse:collapse;width:100%}th,td{text-align:left;padding:6px 10px;border:1px solid #e5e7eb}
|
|
69
|
+
th{background:#f9fafb}h2{margin-top:2rem}section{margin-bottom:2rem}</style>
|
|
70
|
+
</head><body>
|
|
71
|
+
<h1>BeechCMS — Public API Schema</h1>
|
|
72
|
+
<p>Only content types with public access enabled are listed here.</p>
|
|
73
|
+
${seedHtml || '<p><em>No content types with public access configured.</em></p>'}
|
|
74
|
+
</body></html>`
|
|
75
|
+
|
|
76
|
+
return c.html(html)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
publicApp.get('/:seed', publicReadHandler)
|
|
80
|
+
publicApp.post('/:seed/add', publicAddHandler)
|
|
81
|
+
publicApp.put('/:seed/edit/:id', publicEditHandler)
|
|
82
|
+
|
|
83
|
+
export const publicRoutes = publicApp
|
|
84
|
+
|