@beechcms/api 0.4.0-preview.8 → 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,110 +1,128 @@
|
|
|
1
|
-
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
-
import type { Context } from 'hono'
|
|
3
|
-
import bcrypt from 'bcryptjs'
|
|
4
|
-
import type { Env, Variables } from '../../types'
|
|
5
|
-
import { sendPasswordChangedEmail, resolveEmailLocale } from '../email'
|
|
6
|
-
|
|
7
|
-
const MIN_PASSWORD_LENGTH = 8
|
|
8
|
-
const MAX_PASSWORD_LENGTH = 128
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
1
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
+
import type { Context } from 'hono'
|
|
3
|
+
import bcrypt from 'bcryptjs'
|
|
4
|
+
import type { Env, Variables } from '../../types'
|
|
5
|
+
import { sendPasswordChangedEmail, resolveEmailLocale } from '../email'
|
|
6
|
+
|
|
7
|
+
const MIN_PASSWORD_LENGTH = 8
|
|
8
|
+
const MAX_PASSWORD_LENGTH = 128
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Computes the SHA-256 hash of a string and returns it as a hex string.
|
|
12
|
+
*/
|
|
13
|
+
async function computeSha256Hash(text: string): Promise<string> {
|
|
14
|
+
const encoder = new TextEncoder()
|
|
15
|
+
const data = encoder.encode(text)
|
|
16
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
|
|
17
|
+
|
|
18
|
+
return Array.from(new Uint8Array(hashBuffer))
|
|
19
|
+
.map(byte => byte.toString(16).padStart(2, '0'))
|
|
20
|
+
.join('')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Handles the actual password reset process using a valid token.
|
|
25
|
+
*/
|
|
26
|
+
export async function resetPassword(
|
|
27
|
+
context: Context<{ Bindings: Env; Variables: Variables }>
|
|
28
|
+
): Promise<Response> {
|
|
29
|
+
const { env, req, executionCtx } = context
|
|
30
|
+
|
|
31
|
+
if (!env.RESEND_API_KEY) {
|
|
32
|
+
return context.json({ error: 'Service not available' }, 503)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Rate limiting: e.g., 5 attempts per IP per 60 seconds
|
|
36
|
+
if (env.RESET_PASSWORD_RATE_LIMITER) {
|
|
37
|
+
const clientIpAddress = req.raw.headers.get('cf-connecting-ip') ?? 'unknown'
|
|
38
|
+
const { success: isRateLimitAllowed } = await env.RESET_PASSWORD_RATE_LIMITER.limit({ key: clientIpAddress })
|
|
39
|
+
|
|
40
|
+
if (!isRateLimitAllowed) {
|
|
41
|
+
return context.json({ error: 'Too many requests' }, 429)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let payload: Record<string, unknown>
|
|
46
|
+
try {
|
|
47
|
+
payload = await req.json()
|
|
48
|
+
} catch {
|
|
49
|
+
return context.json({ error: 'Invalid request body' }, 400)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const resetToken = payload.token
|
|
53
|
+
if (typeof resetToken !== 'string' || !resetToken) {
|
|
54
|
+
return context.json({ error: 'Invalid or expired token' }, 400)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const newPassword = payload.password
|
|
58
|
+
if (
|
|
59
|
+
typeof newPassword !== 'string' ||
|
|
60
|
+
newPassword.length < MIN_PASSWORD_LENGTH ||
|
|
61
|
+
newPassword.length > MAX_PASSWORD_LENGTH
|
|
62
|
+
) {
|
|
63
|
+
return context.json({
|
|
64
|
+
error: `Password must be between ${MIN_PASSWORD_LENGTH} and ${MAX_PASSWORD_LENGTH} characters`,
|
|
65
|
+
}, 400)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const emailLocale = resolveEmailLocale(payload.locale)
|
|
69
|
+
const hashedResetToken = await computeSha256Hash(resetToken)
|
|
70
|
+
const currentTimestamp = Math.floor(Date.now() / 1000)
|
|
71
|
+
|
|
72
|
+
// JOIN with users table to retrieve the email in a single query - needed for notification.
|
|
73
|
+
const resetTokenRecord = await env.DB
|
|
74
|
+
.prepare(
|
|
75
|
+
`SELECT prt.id, prt.user_id, u.email
|
|
76
|
+
FROM password_reset_tokens prt
|
|
77
|
+
JOIN users u ON u.id = prt.user_id
|
|
78
|
+
WHERE prt.token_hash = ? AND prt.expires_at > ? AND prt.used_at IS NULL`,
|
|
79
|
+
)
|
|
80
|
+
.bind(hashedResetToken, currentTimestamp)
|
|
81
|
+
.first<{ id: string; user_id: string; email: string }>()
|
|
82
|
+
|
|
83
|
+
if (!resetTokenRecord) {
|
|
84
|
+
return context.json({ error: 'Invalid or expired token' }, 400)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const hashedNewPassword = await bcrypt.hash(newPassword, 10)
|
|
88
|
+
|
|
89
|
+
// Mark token as used, update user password, and revoke all active sessions - atomically.
|
|
90
|
+
await env.DB.batch([
|
|
91
|
+
env.DB
|
|
92
|
+
.prepare('UPDATE password_reset_tokens SET used_at = unixepoch() WHERE id = ?')
|
|
93
|
+
.bind(resetTokenRecord.id),
|
|
94
|
+
env.DB
|
|
95
|
+
.prepare('UPDATE users SET password_hash = ? WHERE id = ?')
|
|
96
|
+
.bind(hashedNewPassword, resetTokenRecord.user_id),
|
|
97
|
+
env.DB
|
|
98
|
+
.prepare('UPDATE refresh_tokens SET revoked_at = unixepoch() WHERE user_id = ? AND revoked_at IS NULL')
|
|
99
|
+
.bind(resetTokenRecord.user_id),
|
|
100
|
+
])
|
|
101
|
+
|
|
102
|
+
// Notify user that password was changed - fire-and-forget via waitUntil, doesn't block response.
|
|
103
|
+
const sendNotification = async () => {
|
|
104
|
+
try {
|
|
105
|
+
await sendPasswordChangedEmail({
|
|
106
|
+
to: resetTokenRecord.email,
|
|
107
|
+
locale: emailLocale,
|
|
108
|
+
apiKey: env.RESEND_API_KEY!,
|
|
109
|
+
from: env.EMAIL_FROM,
|
|
110
|
+
isDev: env.ENV !== 'production',
|
|
111
|
+
})
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (env.ENV !== 'production') {
|
|
114
|
+
console.error('[password-reset] Failed to send password change notification:', error)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
executionCtx.waitUntil(sendNotification())
|
|
121
|
+
} catch {
|
|
122
|
+
// executionCtx might not be available in some testing environments
|
|
123
|
+
void sendNotification()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return context.json({ success: true })
|
|
127
|
+
}
|
|
128
|
+
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { rotateFieldApp } from './rotate-field.handler'
|
|
1
|
+
export { rotateFieldApp } from './rotate-field.handler'
|
|
@@ -1,82 +1,132 @@
|
|
|
1
|
-
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
-
import { Hono } from 'hono'
|
|
3
|
-
import { resolvePolicies, verifyHashField, sha256hex, validateAndSanitizeSeedPayload, serializeForDb } from '@beechcms/core'
|
|
4
|
-
import { publicProblem } from '../../public/problem-details'
|
|
5
|
-
import {
|
|
6
|
-
import type { Env, Variables } from '../../types'
|
|
7
|
-
|
|
8
|
-
const rotateFieldApp = new Hono<{ Bindings: Env; Variables: Variables }>()
|
|
9
|
-
|
|
10
|
-
rotateFieldApp.post('/:slug/:id/rotate-field', async (
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
const seed =
|
|
15
|
-
if (!seed) {
|
|
16
|
-
return publicProblem(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return publicProblem(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (!
|
|
50
|
-
return publicProblem(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
1
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
+
import { Hono } from 'hono'
|
|
3
|
+
import { resolvePolicies, verifyHashField, sha256hex, validateAndSanitizeSeedPayload, serializeForDb } from '@beechcms/core'
|
|
4
|
+
import { publicProblem } from '../../public/problem-details'
|
|
5
|
+
import { rotateFieldRequestSchema } from './rotate-field.schema'
|
|
6
|
+
import type { Env, Variables } from '../../types'
|
|
7
|
+
|
|
8
|
+
const rotateFieldApp = new Hono<{ Bindings: Env; Variables: Variables }>()
|
|
9
|
+
|
|
10
|
+
rotateFieldApp.post('/:slug/:id/rotate-field', async (context) => {
|
|
11
|
+
const seedSlug = context.req.param('slug')
|
|
12
|
+
const entryId = context.req.param('id')
|
|
13
|
+
|
|
14
|
+
const seed = context.get('getSeed')(seedSlug)
|
|
15
|
+
if (!seed) {
|
|
16
|
+
return publicProblem(context, {
|
|
17
|
+
type: 'content-seed-not-found',
|
|
18
|
+
title: 'Not Found',
|
|
19
|
+
status: 404,
|
|
20
|
+
detail: `Seed '${seedSlug}' not found`
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let requestBody: unknown
|
|
25
|
+
try {
|
|
26
|
+
requestBody = await context.req.json()
|
|
27
|
+
} catch {
|
|
28
|
+
return publicProblem(context, {
|
|
29
|
+
type: 'rotate-field-invalid-json',
|
|
30
|
+
title: 'Bad Request',
|
|
31
|
+
status: 400,
|
|
32
|
+
detail: 'Invalid JSON body'
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const parsedRequestBody = rotateFieldRequestSchema.safeParse(requestBody)
|
|
37
|
+
if (!parsedRequestBody.success) {
|
|
38
|
+
return publicProblem(context, {
|
|
39
|
+
type: 'rotate-field-invalid-body',
|
|
40
|
+
title: 'Bad Request',
|
|
41
|
+
status: 400,
|
|
42
|
+
detail: parsedRequestBody.error.issues[0]?.message ?? 'Invalid body'
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { fieldAlias, currentValue, nextValue } = parsedRequestBody.data
|
|
47
|
+
|
|
48
|
+
const targetFieldBranch = seed.branches.find((branch) => branch.alias === fieldAlias)
|
|
49
|
+
if (!targetFieldBranch) {
|
|
50
|
+
return publicProblem(context, {
|
|
51
|
+
type: 'rotate-field-unknown-field',
|
|
52
|
+
title: 'Bad Request',
|
|
53
|
+
status: 400,
|
|
54
|
+
detail: `Field '${fieldAlias}' does not exist in seed '${seedSlug}'`
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const { privacy } = resolvePolicies(targetFieldBranch)
|
|
59
|
+
if (privacy !== 'hash') {
|
|
60
|
+
return publicProblem(context, {
|
|
61
|
+
type: 'rotate-field-not-hashable',
|
|
62
|
+
title: 'Unprocessable Entity',
|
|
63
|
+
status: 422,
|
|
64
|
+
detail: `Field '${fieldAlias}' does not use hash privacy and cannot be rotated with this endpoint`
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const database = context.env.DB
|
|
69
|
+
|
|
70
|
+
// In v0.4.0, the hash value is stored in a dedicated column: contentRecord[targetFieldBranch.alias]
|
|
71
|
+
const contentRecord = await database.prepare(`SELECT ${targetFieldBranch.alias} FROM content_${seedSlug} WHERE id = ? LIMIT 1`)
|
|
72
|
+
.bind(entryId)
|
|
73
|
+
.first<Record<string, unknown>>()
|
|
74
|
+
|
|
75
|
+
if (!contentRecord) {
|
|
76
|
+
return publicProblem(context, {
|
|
77
|
+
type: 'content-not-found',
|
|
78
|
+
title: 'Not Found',
|
|
79
|
+
status: 404,
|
|
80
|
+
detail: `Entry '${entryId}' not found`
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const storedFieldValueHash = contentRecord[targetFieldBranch.alias]
|
|
85
|
+
|
|
86
|
+
if (typeof storedFieldValueHash !== 'string' || storedFieldValueHash.length === 0) {
|
|
87
|
+
return publicProblem(context, {
|
|
88
|
+
type: 'rotate-field-not-set',
|
|
89
|
+
title: 'Unprocessable Entity',
|
|
90
|
+
status: 422,
|
|
91
|
+
detail: `Field '${fieldAlias}' has no stored value to rotate`
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const isCurrentValueValid = await verifyHashField(storedFieldValueHash, currentValue)
|
|
96
|
+
if (!isCurrentValueValid) {
|
|
97
|
+
return publicProblem(context, {
|
|
98
|
+
type: 'rotate-field-current-mismatch',
|
|
99
|
+
title: 'Forbidden',
|
|
100
|
+
status: 403,
|
|
101
|
+
detail: 'Current value does not match stored value'
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const fieldValidationResult = validateAndSanitizeSeedPayload(
|
|
106
|
+
seed,
|
|
107
|
+
{ [fieldAlias]: nextValue },
|
|
108
|
+
{ operation: 'update', allowNull: false, requireAtLeastOneValidField: true, enforceRequiredFields: false }
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if (fieldValidationResult.details.length > 0) {
|
|
112
|
+
return publicProblem(context, {
|
|
113
|
+
type: 'rotate-field-invalid-next',
|
|
114
|
+
title: 'Bad Request',
|
|
115
|
+
status: 400,
|
|
116
|
+
detail: `Invalid value for field '${fieldAlias}': ${fieldValidationResult.details[0]?.message ?? 'validation failed'}`
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const newFieldValueHash = await sha256hex(nextValue)
|
|
121
|
+
const currentTimestamp = Math.floor(Date.now() / 1000)
|
|
122
|
+
|
|
123
|
+
await database.prepare(`UPDATE content_${seedSlug} SET ${targetFieldBranch.alias} = ?, updated_at = ? WHERE id = ?`)
|
|
124
|
+
.bind(serializeForDb(targetFieldBranch, newFieldValueHash), currentTimestamp, entryId)
|
|
125
|
+
.run()
|
|
126
|
+
|
|
127
|
+
return context.json({ success: true })
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
export { rotateFieldApp }
|
|
132
|
+
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Schema for rotating a hashed field value.
|
|
5
|
+
* Used to update fields that have 'hash' privacy (e.g., passwords, PINs).
|
|
6
|
+
*/
|
|
7
|
+
export const rotateFieldRequestSchema = z.object({
|
|
8
|
+
fieldAlias: z.string().min(1, "The field alias is required"),
|
|
9
|
+
currentValue: z.string().min(1, "The current value is required"),
|
|
10
|
+
nextValue: z.string().min(1, "The new value is required"),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export type RotateFieldRequest = z.infer<typeof rotateFieldRequestSchema>
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
-
import { Hono } from 'hono'
|
|
3
|
-
import type { Env, Variables } from '../../types'
|
|
4
|
-
|
|
5
|
-
const schemaApp = new Hono<{ Bindings: Env; Variables: Variables }>()
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Ritorna l'intero schema del CMS (la lista dei Seed configurati).
|
|
9
|
-
* Usato dalla Dashboard per generare dinamicamente il menu e le form.
|
|
10
|
-
*/
|
|
11
|
-
schemaApp.get('/', async (
|
|
12
|
-
const registry =
|
|
13
|
-
return
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
export { schemaApp }
|
|
1
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
+
import { Hono } from 'hono'
|
|
3
|
+
import type { Env, Variables } from '../../types'
|
|
4
|
+
|
|
5
|
+
const schemaApp = new Hono<{ Bindings: Env; Variables: Variables }>()
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Ritorna l'intero schema del CMS (la lista dei Seed configurati).
|
|
9
|
+
* Usato dalla Dashboard per generare dinamicamente il menu e le form.
|
|
10
|
+
*/
|
|
11
|
+
schemaApp.get('/', async (context) => {
|
|
12
|
+
const registry = context.get('seedRegistry')
|
|
13
|
+
return context.json(Object.values(registry))
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export { schemaApp }
|