@beechcms/api 0.4.3 → 0.6.0-preview.1
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/Searching.svg +1 -0
- package/assets/dashboard/assets/index-BSjk0Sx9.js +634 -0
- package/assets/dashboard/assets/index-kRCYVN2B.css +1 -0
- package/assets/dashboard/index.html +2 -2
- package/assets/dashboard/noResult.svg +43 -0
- package/assets/dashboard/working.svg +1 -0
- package/migrations/0000_v040_base.sql +27 -0
- package/migrations/0030_test_seeds.sql +125 -0
- package/package.json +14 -9
- package/src/auth/{in-memory-hash-provider.ts → __fixtures__/in-memory-hash-provider.ts} +4 -0
- package/src/auth/{static-token-service.ts → __fixtures__/static-token-service.ts} +4 -0
- package/src/auth/bcrypt-hash-provider.ts +6 -2
- package/src/auth/constants.ts +4 -0
- package/src/auth/generate-refresh-token.test.ts +8 -4
- package/src/auth/hash-provider.test.ts +14 -5
- package/src/auth/jose-token-service.ts +7 -0
- package/src/auth/jwt-claims-passthrough.test.ts +87 -0
- package/src/auth/login.test.ts +9 -1
- package/src/auth/login.ts +5 -1
- package/src/auth/refresh.ts +7 -1
- package/src/auth/token-service.test.ts +9 -1
- package/src/factory.ts +37 -16
- package/src/features/automations/__tests__/action-executors.test.ts +59 -72
- package/src/features/automations/__tests__/automation-runner.test.ts +4 -0
- package/src/features/automations/__tests__/automation-runner.utils.test.ts +4 -0
- package/src/features/automations/__tests__/automations.handler.test.ts +7 -3
- package/src/features/automations/__tests__/automations.repository.test.ts +4 -0
- package/src/features/automations/__tests__/automations.schema.test.ts +92 -2
- package/src/features/automations/__tests__/context-resolver.test.ts +4 -0
- package/src/features/automations/__tests__/cron-runner.test.ts +11 -7
- package/src/features/automations/__tests__/cron-runner.utils.test.ts +4 -0
- package/src/features/automations/__tests__/set-variable.executor.test.ts +77 -0
- package/src/features/automations/__tests__/template-grammar.test.ts +4 -0
- package/src/features/automations/__tests__/webhook.executor.test.ts +142 -0
- package/src/features/automations/__tests__/when-evaluator.test.ts +4 -0
- package/src/features/automations/__tests__/when-pushdown.test.ts +5 -1
- package/src/features/automations/action-executors/create-entry.executor.ts +4 -0
- package/src/features/automations/action-executors/edit-field.executor.ts +4 -0
- package/src/features/automations/action-executors/index.ts +5 -1
- package/src/features/automations/action-executors/send-mail.executor.ts +13 -2
- package/src/features/automations/action-executors/set-variable.executor.ts +15 -2
- package/src/features/automations/action-executors/webhook.executor.ts +55 -13
- package/src/features/automations/automation-runner.ts +4 -0
- package/src/features/automations/automation-runner.utils.ts +4 -0
- package/src/features/automations/automations.handler.ts +4 -0
- package/src/features/automations/automations.schema.ts +19 -2
- package/src/features/automations/context-resolver.ts +4 -0
- package/src/features/automations/cron-runner.ts +4 -0
- package/src/features/automations/cron-runner.utils.ts +4 -0
- package/src/features/automations/filter-translation.ts +4 -0
- package/src/features/automations/index.ts +4 -0
- package/src/features/automations/template-grammar.ts +4 -0
- package/src/features/automations/var-access-resolver.ts +4 -0
- package/src/features/automations/when-evaluator.ts +4 -0
- package/src/features/automations/when-pushdown.ts +4 -0
- package/src/features/backrefs/__tests__/backrefs.handler.test.ts +359 -0
- package/src/features/backrefs/backrefs.handler.ts +168 -0
- package/src/features/backrefs/d1-backref.repository.ts +84 -0
- package/src/features/backrefs/index.ts +5 -0
- package/src/features/content/constants.ts +6 -0
- package/src/features/content/handlers/bulk.handler.ts +185 -0
- package/src/features/content/handlers/create.ts +19 -55
- package/src/features/content/handlers/delete.ts +12 -37
- package/src/features/content/handlers/facets.ts +4 -0
- package/src/features/content/handlers/get.ts +4 -0
- package/src/features/content/handlers/helpers.test.ts +180 -0
- package/src/features/content/handlers/helpers.ts +93 -0
- package/src/features/content/handlers/list.ts +105 -19
- package/src/features/content/handlers/update.ts +19 -64
- package/src/features/content/index.ts +6 -0
- package/src/features/draft/draft.handler.ts +33 -8
- package/src/features/draft/draft.middleware.ts +4 -0
- package/src/features/draft/index.ts +4 -0
- package/src/features/email/email.provider.ts +4 -0
- package/src/features/email/email.service.ts +50 -45
- package/src/features/email/email.types.ts +12 -1
- package/src/features/email/index.ts +4 -0
- package/src/features/email/providers/resend.ts +4 -0
- package/src/features/email/providers/smtp.ts +55 -0
- package/src/features/email/templates/automation-mail.ts +4 -0
- package/src/features/email/templates/password-changed.ts +4 -0
- package/src/features/email/templates/password-reset.ts +4 -0
- package/src/features/email/templates/shell.ts +4 -0
- package/src/features/notifications/index.ts +4 -0
- package/src/features/notifications/notifications.handler.ts +4 -0
- package/src/features/password-reset/index.ts +5 -1
- package/src/features/password-reset/request.ts +12 -2
- package/src/features/password-reset/reset.ts +12 -2
- package/src/features/rotate-field/index.ts +4 -0
- package/src/features/rotate-field/rotate-field.handler.ts +4 -0
- package/src/features/rotate-field/rotate-field.schema.ts +4 -0
- package/src/features/schema/schema.handler.ts +118 -3
- package/src/features/seeds/index.ts +5 -0
- package/src/features/seeds/seeds.handler.test.ts +632 -0
- package/src/features/seeds/seeds.handler.ts +693 -0
- package/src/features/settings/__tests__/settings.handler.test.ts +555 -0
- package/src/features/settings/settings.handler.ts +98 -8
- package/src/features/setup/index.ts +124 -11
- package/src/features/stats/index.ts +4 -0
- package/src/features/stats/stats.handler.ts +11 -21
- package/src/features/webhooks/index.ts +63 -0
- package/src/index.ts +28 -19
- package/src/media-utils.ts +4 -0
- package/src/middleware/auth-providers.middleware.ts +13 -0
- package/src/middleware/observability.middleware.ts +21 -3
- package/src/middleware/rate-limit.middleware.ts +4 -0
- package/src/middleware/repository.middleware.ts +25 -2
- package/src/middleware/seed-registry.middleware.test.ts +97 -0
- package/src/middleware/seed-registry.middleware.ts +21 -0
- package/src/middleware/storage.middleware.ts +4 -0
- package/src/middleware.ts +5 -7
- package/src/public/access-policy.ts +4 -0
- package/src/public/api-key-middleware.ts +4 -0
- package/src/public/cache-utils.ts +38 -34
- package/src/public/entry-projection.ts +46 -42
- package/src/public/idempotency.ts +23 -19
- package/src/public/index.ts +4 -0
- package/src/public/problem-details.ts +71 -0
- package/src/public/public-add.ts +4 -0
- package/src/public/public-edit.ts +4 -0
- package/src/public/public-errors.ts +4 -0
- package/src/public/public-read.ts +4 -0
- package/src/public/public-routes.ts +4 -0
- package/src/public/query-builder.test.ts +4 -0
- package/src/public/query-builder.ts +4 -0
- package/src/public/rate-limit-middleware.ts +4 -0
- package/src/public/read-list.ts +54 -50
- package/src/public/read-single.ts +48 -44
- package/src/public/response-builder.ts +4 -0
- package/src/public/sanitize.ts +4 -0
- package/src/public/slug-utils.ts +4 -0
- package/src/rate-limit/cloudflare-rate-limiter.test.ts +4 -0
- package/src/rate-limit/cloudflare-rate-limiter.ts +4 -0
- package/src/rate-limit/in-memory-rate-limiter.test.ts +4 -0
- package/src/rate-limit/in-memory-rate-limiter.ts +4 -0
- package/src/rate-limit/no-op-rate-limiter.test.ts +4 -0
- package/src/rate-limit/no-op-rate-limiter.ts +4 -0
- package/src/search-utils.test.ts +4 -0
- package/src/search-utils.ts +5 -1
- package/src/search.ts +5 -1
- package/src/shared/apply-policies.test.ts +4 -0
- package/src/shared/apply-policies.ts +4 -0
- package/src/shared/automations.repository.d1.ts +4 -0
- package/src/shared/background-notification-service.test.ts +4 -0
- package/src/shared/background-notification-service.ts +4 -0
- package/src/shared/base.repository.d1.ts +4 -0
- package/src/shared/content-utils.test.ts +4 -0
- package/src/shared/content-utils.ts +4 -0
- package/src/shared/content.repository.d1.test.ts +147 -1
- package/src/shared/content.repository.d1.ts +535 -66
- package/src/shared/d1-activity-log.repository.test.ts +4 -0
- package/src/shared/d1-activity-log.repository.ts +4 -0
- package/src/shared/d1-activity-logger.test.ts +4 -0
- package/src/shared/d1-activity-logger.ts +4 -0
- package/src/shared/d1-analytics.repository.test.ts +4 -0
- package/src/shared/d1-analytics.repository.ts +4 -0
- package/src/shared/d1-content-scan.repository.test.ts +9 -5
- package/src/shared/d1-content-scan.repository.ts +15 -7
- package/src/shared/d1-notification.repository.test.ts +4 -0
- package/src/shared/d1-notification.repository.ts +4 -0
- package/src/shared/d1-password-reset-token.repository.test.ts +4 -0
- package/src/shared/d1-password-reset-token.repository.ts +4 -0
- package/src/shared/d1-search.repository.test.ts +4 -0
- package/src/shared/d1-search.repository.ts +4 -0
- package/src/shared/d1-session.repository.test.ts +4 -0
- package/src/shared/d1-session.repository.ts +4 -0
- package/src/shared/d1-setup-checklist.repository.ts +36 -0
- package/src/shared/d1-user.repository.test.ts +8 -4
- package/src/shared/d1-user.repository.ts +15 -5
- package/src/shared/d1-widget.repository.test.ts +4 -0
- package/src/shared/d1-widget.repository.ts +4 -0
- package/src/shared/demo-data-sql.ts +54 -0
- package/src/shared/demo-data.repository.d1.ts +20 -0
- package/src/shared/execution-context-scheduler.ts +4 -0
- package/src/shared/fixed-clock.ts +4 -0
- package/src/shared/fts-sync.ts +4 -0
- package/src/shared/idempotency.repository.d1.test.ts +4 -0
- package/src/shared/idempotency.repository.d1.ts +4 -0
- package/src/shared/in-memory-activity-logger.ts +4 -0
- package/src/shared/in-memory-notification-service.ts +4 -0
- package/src/shared/in-memory-seed.repository.ts +51 -0
- package/src/shared/media.repository.d1.test.ts +4 -0
- package/src/shared/media.repository.d1.ts +4 -0
- package/src/shared/qstash-notification-service.test.ts +67 -0
- package/src/shared/qstash-notification-service.ts +55 -0
- package/src/shared/query-utils.ts +4 -0
- package/src/shared/request-utils.ts +4 -0
- package/src/shared/schema-mutator.d1.ts +64 -0
- package/src/shared/seed-layout.repository.d1.test.ts +114 -0
- package/src/shared/seed-layout.repository.d1.ts +62 -0
- package/src/shared/seed-registry-cache.test.ts +68 -0
- package/src/shared/seed-registry-cache.ts +49 -0
- package/src/shared/seed.repository.d1.test.ts +143 -0
- package/src/shared/seed.repository.d1.ts +98 -0
- package/src/shared/sequential-id-generator.ts +11 -0
- package/src/shared/site-settings.repository.d1.ts +53 -0
- package/src/shared/storage/factory.ts +16 -15
- package/src/shared/storage/s3-bucket.ts +34 -2
- package/src/shared/storage-utils.ts +4 -0
- package/src/shared/system-stats.repository.d1.test.ts +4 -0
- package/src/shared/system-stats.repository.d1.ts +5 -1
- package/src/types.ts +23 -3
- package/src/upload.ts +134 -123
- package/src/widget.ts +6 -2
- package/assets/dashboard/assets/index-BKWnlvnV.css +0 -1
- package/assets/dashboard/assets/index-BMkd1Irh.js +0 -629
- package/src/shared/storage/r2-binding-bucket.ts +0 -81
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
|
|
3
|
+
// See LICENSE in the repository root for license terms.
|
|
4
|
+
|
|
1
5
|
/// <reference types="@cloudflare/workers-types" />
|
|
2
6
|
import { Hono } from 'hono'
|
|
7
|
+
import { sha256hex, type SiteSettings } from '@beechcms/core'
|
|
3
8
|
import type { Env, Variables } from '../../types'
|
|
4
9
|
|
|
5
10
|
const settingsApp = new Hono<{ Bindings: Env; Variables: Variables }>()
|
|
@@ -12,24 +17,96 @@ const ACTIVITY_LOG_LIMIT = 30
|
|
|
12
17
|
|
|
13
18
|
/**
|
|
14
19
|
* GET /api/settings
|
|
15
|
-
* Retrieves the general site configuration.
|
|
20
|
+
* Retrieves the general site configuration from the database.
|
|
16
21
|
*/
|
|
17
22
|
settingsApp.get('/', async (context) => {
|
|
23
|
+
const s = await context.get('siteSettingsRepository').getAll()
|
|
18
24
|
return context.json({
|
|
19
|
-
siteTitle:
|
|
25
|
+
siteTitle: s.siteTitle,
|
|
20
26
|
siteLogo: '/beechLogoDark.svg',
|
|
21
|
-
defaultLanguage:
|
|
27
|
+
defaultLanguage: s.defaultLanguage,
|
|
28
|
+
timezone: s.timezone,
|
|
29
|
+
currency: s.currency,
|
|
30
|
+
company: {
|
|
31
|
+
name: s.companyName,
|
|
32
|
+
website: s.companyWebsite,
|
|
33
|
+
abbreviation: s.companyAbbreviation,
|
|
34
|
+
},
|
|
22
35
|
dateFormat: context.env.DATE_FORMAT || 'DD-MM-YYYY',
|
|
23
36
|
features: {
|
|
24
37
|
drafts: true,
|
|
25
38
|
media: true,
|
|
26
39
|
search: true,
|
|
27
40
|
activityLog: true,
|
|
28
|
-
email: !!(context.env.EMAIL_API_KEY || context.env.RESEND_API_KEY),
|
|
29
|
-
}
|
|
41
|
+
email: context.env.EMAIL_PROVIDER === 'smtp' || !!(context.env.EMAIL_API_KEY || context.env.RESEND_API_KEY),
|
|
42
|
+
},
|
|
30
43
|
})
|
|
31
44
|
})
|
|
32
45
|
|
|
46
|
+
/**
|
|
47
|
+
* PUT /api/settings
|
|
48
|
+
* Updates the general site configuration in the database.
|
|
49
|
+
*/
|
|
50
|
+
settingsApp.put('/', async (context) => {
|
|
51
|
+
let payload: Record<string, unknown>
|
|
52
|
+
try {
|
|
53
|
+
payload = await context.req.json()
|
|
54
|
+
} catch {
|
|
55
|
+
return context.json({ error: 'Invalid JSON body' }, 400)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const siteTitle = typeof payload.siteTitle === 'string' ? payload.siteTitle.trim() : undefined
|
|
59
|
+
const defaultLanguage = typeof payload.defaultLanguage === 'string' ? payload.defaultLanguage.trim() : undefined
|
|
60
|
+
const timezone = typeof payload.timezone === 'string' ? payload.timezone.trim() : undefined
|
|
61
|
+
const currency = typeof payload.currency === 'string' ? payload.currency.trim() : undefined
|
|
62
|
+
|
|
63
|
+
let companyName: string | undefined | null = undefined
|
|
64
|
+
let companyWebsite: string | undefined | null = undefined
|
|
65
|
+
let companyAbbreviation: string | undefined | null = undefined
|
|
66
|
+
|
|
67
|
+
if (payload.company !== undefined) {
|
|
68
|
+
if (payload.company === null) {
|
|
69
|
+
companyName = null
|
|
70
|
+
companyWebsite = null
|
|
71
|
+
companyAbbreviation = null
|
|
72
|
+
} else if (typeof payload.company === 'object') {
|
|
73
|
+
const company = payload.company as Record<string, unknown>
|
|
74
|
+
companyName = typeof company.name === 'string' ? company.name.trim() : (company.name === null ? null : undefined)
|
|
75
|
+
companyWebsite = typeof company.website === 'string' ? (company.website.trim() || null) : (company.website === null ? null : undefined)
|
|
76
|
+
companyAbbreviation = typeof company.abbreviation === 'string' ? (company.abbreviation.trim() || null) : (company.abbreviation === null ? null : undefined)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (defaultLanguage !== undefined && !['it', 'en'].includes(defaultLanguage)) {
|
|
81
|
+
return context.json({ type: 'bad-request', title: 'Bad Request', status: 400, detail: 'Invalid default language (must be it or en)' }, 400)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (companyWebsite && companyWebsite !== '') {
|
|
85
|
+
try {
|
|
86
|
+
new URL(companyWebsite)
|
|
87
|
+
} catch {
|
|
88
|
+
return context.json({ type: 'bad-request', title: 'Bad Request', status: 400, detail: 'Invalid company website URL' }, 400)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const fieldsToUpdate: Partial<SiteSettings> = {}
|
|
93
|
+
if (siteTitle !== undefined) fieldsToUpdate.siteTitle = siteTitle
|
|
94
|
+
if (defaultLanguage !== undefined) fieldsToUpdate.defaultLanguage = defaultLanguage
|
|
95
|
+
if (timezone !== undefined) fieldsToUpdate.timezone = timezone
|
|
96
|
+
if (currency !== undefined) fieldsToUpdate.currency = currency
|
|
97
|
+
if (companyName !== undefined) fieldsToUpdate.companyName = companyName
|
|
98
|
+
if (companyWebsite !== undefined) fieldsToUpdate.companyWebsite = companyWebsite
|
|
99
|
+
if (companyAbbreviation !== undefined) fieldsToUpdate.companyAbbreviation = companyAbbreviation
|
|
100
|
+
|
|
101
|
+
// If companyName is updated and siteTitle isn't specified, sync siteTitle
|
|
102
|
+
if (companyName && siteTitle === undefined) {
|
|
103
|
+
fieldsToUpdate.siteTitle = companyName
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
await context.get('siteSettingsRepository').setMany(fieldsToUpdate)
|
|
107
|
+
return context.json({ success: true })
|
|
108
|
+
})
|
|
109
|
+
|
|
33
110
|
/**
|
|
34
111
|
* GET /api/settings/me
|
|
35
112
|
* Retrieves the currently authenticated user's profile and preferences.
|
|
@@ -49,11 +126,18 @@ settingsApp.get('/me', async (context) => {
|
|
|
49
126
|
notificationPreferences = {}
|
|
50
127
|
}
|
|
51
128
|
|
|
129
|
+
let avatarUrl = currentUser.avatarUrl
|
|
130
|
+
if (!avatarUrl && currentUser.email) {
|
|
131
|
+
const emailHash = await sha256hex(currentUser.email.trim().toLowerCase())
|
|
132
|
+
avatarUrl = `https://gravatar.com/avatar/${emailHash}?d=mp`
|
|
133
|
+
}
|
|
134
|
+
|
|
52
135
|
return context.json({
|
|
53
136
|
id: currentUser.id,
|
|
54
137
|
email: currentUser.email,
|
|
55
138
|
name: currentUser.name,
|
|
56
|
-
|
|
139
|
+
surname: currentUser.surname,
|
|
140
|
+
avatarUrl,
|
|
57
141
|
notificationPrefs: {
|
|
58
142
|
contentCreate: notificationPreferences.contentCreate ?? true,
|
|
59
143
|
contentUpdate: notificationPreferences.contentUpdate ?? true,
|
|
@@ -78,6 +162,7 @@ settingsApp.put('/profile', async (context) => {
|
|
|
78
162
|
}
|
|
79
163
|
|
|
80
164
|
const nameInput = typeof payload.name === 'string' ? payload.name.trim() : null
|
|
165
|
+
const surnameInput = typeof payload.surname === 'string' ? payload.surname.trim() : null
|
|
81
166
|
const emailInput = typeof payload.email === 'string' ? payload.email.trim().toLowerCase() : null
|
|
82
167
|
|
|
83
168
|
if (emailInput !== null && !EMAIL_VALIDATION_REGEX.test(emailInput)) {
|
|
@@ -88,7 +173,11 @@ settingsApp.put('/profile', async (context) => {
|
|
|
88
173
|
return context.json({ type: 'bad-request', title: 'Bad Request', status: 400, detail: 'Name is too long (maximum 100 characters)' }, 400)
|
|
89
174
|
}
|
|
90
175
|
|
|
91
|
-
|
|
176
|
+
if (surnameInput !== null && surnameInput.length > 100) {
|
|
177
|
+
return context.json({ type: 'bad-request', title: 'Bad Request', status: 400, detail: 'Surname is too long (maximum 100 characters)' }, 400)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const hasNoFields = nameInput === null && surnameInput === null && emailInput === null
|
|
92
181
|
if (hasNoFields) {
|
|
93
182
|
return context.json({ error: 'No fields to update' }, 400)
|
|
94
183
|
}
|
|
@@ -100,8 +189,9 @@ settingsApp.put('/profile', async (context) => {
|
|
|
100
189
|
}
|
|
101
190
|
}
|
|
102
191
|
|
|
103
|
-
const fieldsToUpdate: { name?: string; email?: string } = {}
|
|
192
|
+
const fieldsToUpdate: { name?: string; surname?: string; email?: string } = {}
|
|
104
193
|
if (nameInput !== null) fieldsToUpdate.name = nameInput
|
|
194
|
+
if (surnameInput !== null) fieldsToUpdate.surname = surnameInput
|
|
105
195
|
if (emailInput !== null) fieldsToUpdate.email = emailInput
|
|
106
196
|
|
|
107
197
|
await context.get('userRepository').updateProfile(userId, fieldsToUpdate)
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
|
|
3
|
+
// See LICENSE in the repository root for license terms.
|
|
4
|
+
|
|
1
5
|
/// <reference types="@cloudflare/workers-types" />
|
|
2
6
|
import { Hono } from 'hono'
|
|
3
7
|
import type { Env, Variables } from '../../types'
|
|
@@ -7,16 +11,25 @@ const setupApp = new Hono<{ Bindings: Env; Variables: Variables }>()
|
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* GET /auth/setup
|
|
10
|
-
*
|
|
14
|
+
* Returns setup status + environment flags for the wizard.
|
|
11
15
|
*/
|
|
12
16
|
setupApp.get('/auth/setup', async (context) => {
|
|
13
17
|
const userCount = await context.get('userRepository').countAll()
|
|
14
|
-
|
|
18
|
+
const isDeveloper = context.env.ENV === 'development'
|
|
19
|
+
const mail =
|
|
20
|
+
context.env.EMAIL_PROVIDER === 'smtp' ||
|
|
21
|
+
!!(context.env.EMAIL_API_KEY || context.env.RESEND_API_KEY)
|
|
22
|
+
const qstash = !!context.env.QSTASH_TOKEN
|
|
23
|
+
|
|
24
|
+
return context.json({
|
|
25
|
+
needsSetup: userCount === 0,
|
|
26
|
+
environment: { isDeveloper, services: { mail, qstash } },
|
|
27
|
+
})
|
|
15
28
|
})
|
|
16
29
|
|
|
17
30
|
/**
|
|
18
31
|
* POST /auth/setup
|
|
19
|
-
* Creates the first administrator account
|
|
32
|
+
* Creates the first administrator account and persists site defaults.
|
|
20
33
|
*/
|
|
21
34
|
setupApp.post('/auth/setup', async (context) => {
|
|
22
35
|
const userCount = await context.get('userRepository').countAll()
|
|
@@ -26,7 +39,7 @@ setupApp.post('/auth/setup', async (context) => {
|
|
|
26
39
|
type: 'setup-already-done',
|
|
27
40
|
title: 'Setup already completed',
|
|
28
41
|
status: 403,
|
|
29
|
-
detail: 'An administrator account already exists. Initial setup can only be performed once.'
|
|
42
|
+
detail: 'An administrator account already exists. Initial setup can only be performed once.',
|
|
30
43
|
})
|
|
31
44
|
}
|
|
32
45
|
|
|
@@ -38,7 +51,7 @@ setupApp.post('/auth/setup', async (context) => {
|
|
|
38
51
|
type: 'bad-request',
|
|
39
52
|
title: 'Invalid JSON body',
|
|
40
53
|
status: 400,
|
|
41
|
-
detail: 'The request body could not be parsed as valid JSON.'
|
|
54
|
+
detail: 'The request body could not be parsed as valid JSON.',
|
|
42
55
|
})
|
|
43
56
|
}
|
|
44
57
|
|
|
@@ -47,18 +60,19 @@ setupApp.post('/auth/setup', async (context) => {
|
|
|
47
60
|
type: 'bad-request',
|
|
48
61
|
title: 'Invalid request',
|
|
49
62
|
status: 400,
|
|
50
|
-
detail: 'The request payload is missing or invalid.'
|
|
63
|
+
detail: 'The request payload is missing or invalid.',
|
|
51
64
|
})
|
|
52
65
|
}
|
|
53
66
|
|
|
54
|
-
const
|
|
67
|
+
const p = payload as Record<string, unknown>
|
|
68
|
+
const { email, password, name, surname, settings, track, company, loadDemoData } = p
|
|
55
69
|
|
|
56
70
|
if (typeof email !== 'string' || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) {
|
|
57
71
|
return publicProblem(context, {
|
|
58
72
|
type: 'validation-error',
|
|
59
73
|
title: 'Valid email required',
|
|
60
74
|
status: 422,
|
|
61
|
-
detail: 'A valid email address is required for the administrator account.'
|
|
75
|
+
detail: 'A valid email address is required for the administrator account.',
|
|
62
76
|
})
|
|
63
77
|
}
|
|
64
78
|
|
|
@@ -67,13 +81,97 @@ setupApp.post('/auth/setup', async (context) => {
|
|
|
67
81
|
type: 'validation-error',
|
|
68
82
|
title: 'Invalid password',
|
|
69
83
|
status: 422,
|
|
70
|
-
detail: 'Password must be between 8 and 128 characters long.'
|
|
84
|
+
detail: 'Password must be between 8 and 128 characters long.',
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!settings || typeof settings !== 'object') {
|
|
89
|
+
return publicProblem(context, {
|
|
90
|
+
type: 'validation-error',
|
|
91
|
+
title: 'Settings required',
|
|
92
|
+
status: 422,
|
|
93
|
+
detail: 'settings.language, settings.timezone, and settings.currency are required.',
|
|
71
94
|
})
|
|
72
95
|
}
|
|
73
96
|
|
|
97
|
+
const s = settings as Record<string, unknown>
|
|
98
|
+
const language = typeof s.language === 'string' ? s.language : ''
|
|
99
|
+
const timezone = typeof s.timezone === 'string' ? s.timezone.trim() : ''
|
|
100
|
+
const currency = typeof s.currency === 'string' ? s.currency.trim() : ''
|
|
101
|
+
|
|
102
|
+
if (!['it', 'en'].includes(language)) {
|
|
103
|
+
return publicProblem(context, {
|
|
104
|
+
type: 'validation-error',
|
|
105
|
+
title: 'Invalid language',
|
|
106
|
+
status: 422,
|
|
107
|
+
detail: 'settings.language must be "it" or "en".',
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!timezone) {
|
|
112
|
+
return publicProblem(context, {
|
|
113
|
+
type: 'validation-error',
|
|
114
|
+
title: 'Timezone required',
|
|
115
|
+
status: 422,
|
|
116
|
+
detail: 'settings.timezone must be a non-empty IANA timezone string.',
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!currency) {
|
|
121
|
+
return publicProblem(context, {
|
|
122
|
+
type: 'validation-error',
|
|
123
|
+
title: 'Currency required',
|
|
124
|
+
status: 422,
|
|
125
|
+
detail: 'settings.currency must be a non-empty ISO 4217 currency code.',
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (track === 'normal') {
|
|
130
|
+
if (!company || typeof company !== 'object') {
|
|
131
|
+
return publicProblem(context, {
|
|
132
|
+
type: 'validation-error',
|
|
133
|
+
title: 'Company info required',
|
|
134
|
+
status: 422,
|
|
135
|
+
detail: 'company.name and company.website are required for the normal track.',
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
const c = company as Record<string, unknown>
|
|
139
|
+
if (typeof c.name !== 'string' || !c.name.trim()) {
|
|
140
|
+
return publicProblem(context, {
|
|
141
|
+
type: 'validation-error',
|
|
142
|
+
title: 'Company name required',
|
|
143
|
+
status: 422,
|
|
144
|
+
detail: 'company.name must be a non-empty string.',
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
if (typeof c.website !== 'string' || !c.website.trim()) {
|
|
148
|
+
return publicProblem(context, {
|
|
149
|
+
type: 'validation-error',
|
|
150
|
+
title: 'Company website required',
|
|
151
|
+
status: 422,
|
|
152
|
+
detail: 'company.website must be a valid URL.',
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
new URL(c.website.trim())
|
|
157
|
+
} catch {
|
|
158
|
+
return publicProblem(context, {
|
|
159
|
+
type: 'validation-error',
|
|
160
|
+
title: 'Invalid company website',
|
|
161
|
+
status: 422,
|
|
162
|
+
detail: 'company.website must be a valid URL.',
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (track === 'developer' && loadDemoData === true) {
|
|
168
|
+
await context.get('demoDataRepository').loadDemoData()
|
|
169
|
+
}
|
|
170
|
+
|
|
74
171
|
const passwordHash = await context.get('hashProvider').hash(password)
|
|
75
172
|
const normalizedEmail = email.trim().toLowerCase()
|
|
76
173
|
const normalizedName = typeof name === 'string' ? name.trim() : null
|
|
174
|
+
const normalizedSurname = typeof surname === 'string' ? surname.trim() : null
|
|
77
175
|
|
|
78
176
|
await context.get('userRepository').create({
|
|
79
177
|
id: context.get('idGenerator').uuid(),
|
|
@@ -81,11 +179,26 @@ setupApp.post('/auth/setup', async (context) => {
|
|
|
81
179
|
passwordHash,
|
|
82
180
|
role: 'admin',
|
|
83
181
|
name: normalizedName,
|
|
182
|
+
surname: normalizedSurname,
|
|
84
183
|
})
|
|
85
184
|
|
|
185
|
+
if (track === 'normal' && company && typeof company === 'object') {
|
|
186
|
+
const c = company as Record<string, unknown>
|
|
187
|
+
const companyName = (c.name as string).trim()
|
|
188
|
+
await context.get('siteSettingsRepository').setMany({
|
|
189
|
+
defaultLanguage: language,
|
|
190
|
+
timezone,
|
|
191
|
+
currency,
|
|
192
|
+
companyName,
|
|
193
|
+
companyWebsite: (c.website as string).trim(),
|
|
194
|
+
companyAbbreviation: typeof c.abbreviation === 'string' ? c.abbreviation.trim() || null : null,
|
|
195
|
+
siteTitle: companyName,
|
|
196
|
+
})
|
|
197
|
+
} else {
|
|
198
|
+
await context.get('siteSettingsRepository').setMany({ defaultLanguage: language, timezone, currency })
|
|
199
|
+
}
|
|
200
|
+
|
|
86
201
|
return context.json({ success: true }, 201)
|
|
87
202
|
})
|
|
88
203
|
|
|
89
204
|
export { setupApp }
|
|
90
|
-
|
|
91
|
-
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
|
|
3
|
+
// See LICENSE in the repository root for license terms.
|
|
4
|
+
|
|
1
5
|
/// <reference types="@cloudflare/workers-types" />
|
|
2
6
|
import { Hono } from 'hono'
|
|
3
7
|
import { SystemClock } from '@beechcms/core'
|
|
@@ -118,46 +122,32 @@ statsApp.get('/stats/unused-media', async (context) => {
|
|
|
118
122
|
*/
|
|
119
123
|
statsApp.get('/stats/setup-checklist', async (context) => {
|
|
120
124
|
try {
|
|
121
|
-
const
|
|
125
|
+
const setupChecklistRepository = context.get('setupChecklistRepository')
|
|
122
126
|
const seeds = context.get('seedRegistry').all()
|
|
123
127
|
|
|
124
|
-
|
|
128
|
+
const existingTables = await setupChecklistRepository.getExistingTableNames()
|
|
129
|
+
|
|
125
130
|
const systemTableNames = [
|
|
126
131
|
'users', 'refresh_tokens', 'media_objects',
|
|
127
132
|
'analytics', 'system_stats', 'activity_logs',
|
|
128
133
|
]
|
|
129
|
-
const
|
|
130
|
-
`SELECT name FROM sqlite_master WHERE type='table'`
|
|
131
|
-
).all<{ name: string }>()
|
|
132
|
-
const existingTables = new Set((tablesResult.results ?? []).map(row => row.name))
|
|
133
|
-
const systemTablesOk = systemTableNames.every(tableName => existingTables.has(tableName))
|
|
134
|
+
const systemTablesOk = systemTableNames.every(name => existingTables.has(name))
|
|
134
135
|
|
|
135
|
-
// 2. Seeds defined
|
|
136
136
|
const seedsCount = seeds.length
|
|
137
|
-
|
|
138
|
-
// 3. Content tables created (seed:load was run)
|
|
139
137
|
const contentTablesOk = seedsCount > 0 && seeds.every(seed => existingTables.has(`content_${seed.slug}`))
|
|
140
138
|
|
|
141
|
-
// 4. Admin account exists
|
|
142
139
|
let adminExists = false
|
|
143
140
|
try {
|
|
144
|
-
|
|
145
|
-
`SELECT COUNT(*) as count FROM users WHERE role = 'admin'`
|
|
146
|
-
).first<{ count: number }>()
|
|
147
|
-
adminExists = (adminCountResult?.count ?? 0) > 0
|
|
141
|
+
adminExists = (await setupChecklistRepository.countAdmins()) > 0
|
|
148
142
|
} catch {
|
|
149
|
-
// table may not exist yet
|
|
143
|
+
// users table may not exist yet
|
|
150
144
|
}
|
|
151
145
|
|
|
152
|
-
// 5. At least one content entry in the first seed's table
|
|
153
146
|
let hasContent = false
|
|
154
147
|
const firstSeedSlug = seeds[0]?.slug ?? null
|
|
155
148
|
if (firstSeedSlug && existingTables.has(`content_${firstSeedSlug}`)) {
|
|
156
149
|
try {
|
|
157
|
-
|
|
158
|
-
`SELECT COUNT(*) as count FROM content_${firstSeedSlug}`
|
|
159
|
-
).first<{ count: number }>()
|
|
160
|
-
hasContent = (contentCountResult?.count ?? 0) > 0
|
|
150
|
+
hasContent = (await setupChecklistRepository.countEntriesInSeed(firstSeedSlug)) > 0
|
|
161
151
|
} catch {
|
|
162
152
|
// ignore
|
|
163
153
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
|
|
3
|
+
// See LICENSE in the repository root for license terms.
|
|
4
|
+
|
|
5
|
+
import { Hono } from 'hono'
|
|
6
|
+
import { Receiver } from '@upstash/qstash'
|
|
7
|
+
import type { AppEnv } from '../../types'
|
|
8
|
+
import type { CreateNotificationInput } from '@beechcms/core'
|
|
9
|
+
|
|
10
|
+
export const webhooksApp = new Hono<AppEnv>()
|
|
11
|
+
|
|
12
|
+
webhooksApp.post('/qstash', async (context) => {
|
|
13
|
+
const currentSigningKey = context.env.QSTASH_CURRENT_SIGNING_KEY
|
|
14
|
+
const nextSigningKey = context.env.QSTASH_NEXT_SIGNING_KEY
|
|
15
|
+
|
|
16
|
+
if (!currentSigningKey || !nextSigningKey) {
|
|
17
|
+
console.error('QStash webhook called but signing keys are not configured')
|
|
18
|
+
return context.text('Configuration error', 500)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const receiver = new Receiver({
|
|
22
|
+
currentSigningKey,
|
|
23
|
+
nextSigningKey,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const body = await context.req.text()
|
|
27
|
+
const signature = context.req.header('Upstash-Signature')
|
|
28
|
+
|
|
29
|
+
if (!signature) {
|
|
30
|
+
return context.text('Missing signature', 401)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const isValid = await receiver.verify({
|
|
35
|
+
signature,
|
|
36
|
+
body,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
if (!isValid) {
|
|
40
|
+
return context.text('Invalid signature', 401)
|
|
41
|
+
}
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error('QStash signature verification failed', err)
|
|
44
|
+
return context.text('Invalid signature', 401)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const input = JSON.parse(body) as CreateNotificationInput
|
|
49
|
+
|
|
50
|
+
const notificationRepository = context.get('notificationRepository')
|
|
51
|
+
|
|
52
|
+
await notificationRepository.create({
|
|
53
|
+
title: input.title,
|
|
54
|
+
message: input.message,
|
|
55
|
+
type: input.type ?? 'info',
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
return context.text('OK', 200)
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Failed to process QStash webhook payload', error)
|
|
61
|
+
return context.text('Internal server error', 500)
|
|
62
|
+
}
|
|
63
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -1,31 +1,38 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
|
|
3
|
+
// See LICENSE in the repository root for license terms.
|
|
4
|
+
|
|
1
5
|
import { createBeechApp } from './factory'
|
|
2
6
|
import { SeedRegistry, SystemIdGenerator } from '@beechcms/core'
|
|
3
7
|
import { runCronAutomations } from './features/automations'
|
|
4
8
|
import { D1AutomationRepository } from './shared/automations.repository.d1'
|
|
5
9
|
import { D1ContentRepository } from './shared/content.repository.d1'
|
|
10
|
+
import { D1SeedRepository } from './shared/seed.repository.d1'
|
|
6
11
|
import type { Env } from './types'
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
try {
|
|
11
|
-
// @ts-ignore
|
|
12
|
-
const mod = await import('../seed.ts')
|
|
13
|
-
const registry = mod.default || mod.SEED_REGISTRY || mod
|
|
14
|
-
seeds = (typeof registry === 'object' && !Array.isArray(registry))
|
|
15
|
-
? Object.values(registry)
|
|
16
|
-
: registry
|
|
17
|
-
} catch (e) {
|
|
18
|
-
// Fallback se seed.ts non esiste
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const app = createBeechApp({ seeds })
|
|
13
|
+
const app = createBeechApp({ seeds: [] })
|
|
22
14
|
|
|
23
|
-
app.get('/', (c) => c.text('Beech API is running
|
|
24
|
-
|
|
25
|
-
const validSeeds = seeds.filter((s: any) => s && typeof s === 'object' && 'slug' in s)
|
|
15
|
+
app.get('/', (c) => c.text('Beech API is running'))
|
|
26
16
|
|
|
27
17
|
export default {
|
|
28
|
-
fetch:
|
|
18
|
+
fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
|
19
|
+
if (env.ENV === 'development') {
|
|
20
|
+
const checks: Array<{ name: string; url: string }> = [
|
|
21
|
+
{ name: 'MinIO', url: (env.R2_ENDPOINT ?? 'http://localhost:9000') + '/minio/health/live' },
|
|
22
|
+
{ name: 'Mailpit', url: `http://${env.SMTP_HOST ?? 'localhost'}:${env.SMTP_PORT ?? '8025'}/livez` },
|
|
23
|
+
]
|
|
24
|
+
for (const c of checks) {
|
|
25
|
+
fetch(c.url).catch(() => {
|
|
26
|
+
console.warn(
|
|
27
|
+
`\n⚠️ ${c.name} non raggiungibile su ${c.url}\n` +
|
|
28
|
+
` Beech in dev richiede lo stack Docker completo.\n` +
|
|
29
|
+
` Avvialo con: npm run dev:full\n`
|
|
30
|
+
)
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return app.fetch(request, env, ctx)
|
|
35
|
+
},
|
|
29
36
|
|
|
30
37
|
async scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext) {
|
|
31
38
|
const scheduledTime = controller?.scheduledTime ?? Date.now()
|
|
@@ -37,7 +44,9 @@ export default {
|
|
|
37
44
|
|
|
38
45
|
const automationRepository = new D1AutomationRepository(env.DB)
|
|
39
46
|
const contentRepository = new D1ContentRepository(env.DB)
|
|
40
|
-
const
|
|
47
|
+
const seedRepository = new D1SeedRepository(env.DB)
|
|
48
|
+
const seeds = await seedRepository.listActive()
|
|
49
|
+
const registry = new SeedRegistry(seeds)
|
|
41
50
|
const getSeed = (slug: string) => registry.get(slug) ?? null
|
|
42
51
|
|
|
43
52
|
ctx.waitUntil(
|
package/src/media-utils.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
|
|
3
|
+
// See LICENSE in the repository root for license terms.
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
6
|
* Media utils: estrazione chiavi R2 dal data di un'entry.
|
|
3
7
|
* Usato alla cancellazione entry per eliminare i file associati da R2.
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
|
|
3
|
+
// See LICENSE in the repository root for license terms.
|
|
4
|
+
|
|
1
5
|
import { createMiddleware } from 'hono/factory'
|
|
2
6
|
import type { IHashProvider, ITokenService, IClock } from '@beechcms/core'
|
|
3
7
|
import { SystemClock } from '@beechcms/core'
|
|
@@ -11,8 +15,17 @@ export interface AuthProviderOverrides {
|
|
|
11
15
|
clock?: IClock
|
|
12
16
|
}
|
|
13
17
|
|
|
18
|
+
// Valore di default presente in .dev.vars.example: mai accettabile in produzione.
|
|
19
|
+
const DEV_JWT_SECRET = 'sviluppo-secret-cambiami-almeno-32-byte-per-sicurezza-hono'
|
|
20
|
+
|
|
14
21
|
export const authProvidersMiddleware = (overrides?: AuthProviderOverrides) => {
|
|
15
22
|
return createMiddleware<AppEnv>(async (context, next) => {
|
|
23
|
+
if (context.env.ENV === 'production' && context.env.JWT_SECRET === DEV_JWT_SECRET) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
'JWT_SECRET non configurato: in produzione impostare un segreto univoco con `wrangler secret put JWT_SECRET`',
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
16
29
|
const resolvedClock = overrides?.clock ?? SystemClock
|
|
17
30
|
const hashProvider = overrides?.hashProvider ?? new BcryptHashProvider()
|
|
18
31
|
const tokenService = overrides?.tokenService ?? new JoseTokenService(
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
|
|
3
|
+
// See LICENSE in the repository root for license terms.
|
|
4
|
+
|
|
1
5
|
import { createMiddleware } from 'hono/factory'
|
|
2
6
|
import type { IActivityLogger, INotificationService, IClock, IIdGenerator } from '@beechcms/core'
|
|
3
7
|
import { SystemClock, SystemIdGenerator } from '@beechcms/core'
|
|
4
8
|
import type { AppEnv } from '../types'
|
|
5
9
|
import { D1ActivityLogger } from '../shared/d1-activity-logger'
|
|
6
10
|
import { BackgroundNotificationService } from '../shared/background-notification-service'
|
|
11
|
+
import { QStashNotificationService } from '../shared/qstash-notification-service'
|
|
7
12
|
|
|
8
13
|
export interface ObservabilityOverrides {
|
|
9
14
|
activityLogger?: IActivityLogger
|
|
@@ -40,9 +45,22 @@ export const observabilityMiddleware = (overrides?: ObservabilityOverrides) => {
|
|
|
40
45
|
overrides?.activityLogger ??
|
|
41
46
|
new D1ActivityLogger(context.env.DB, resolvedClock, resolvedIdGenerator, scheduleBackgroundTask)
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
let notificationService: INotificationService
|
|
49
|
+
if (overrides?.notificationService) {
|
|
50
|
+
notificationService = overrides.notificationService
|
|
51
|
+
} else if (context.env.QSTASH_TOKEN && context.env.APP_URL) {
|
|
52
|
+
notificationService = new QStashNotificationService(
|
|
53
|
+
context.env.QSTASH_TOKEN,
|
|
54
|
+
context.env.APP_URL,
|
|
55
|
+
scheduleBackgroundTask,
|
|
56
|
+
context.env.QSTASH_URL
|
|
57
|
+
)
|
|
58
|
+
} else {
|
|
59
|
+
notificationService = new BackgroundNotificationService(
|
|
60
|
+
context.get('notificationRepository'),
|
|
61
|
+
scheduleBackgroundTask
|
|
62
|
+
)
|
|
63
|
+
}
|
|
46
64
|
|
|
47
65
|
context.set('activityLogger', activityLogger)
|
|
48
66
|
context.set('notificationService', notificationService)
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
|
|
3
|
+
// See LICENSE in the repository root for license terms.
|
|
4
|
+
|
|
1
5
|
/// <reference types="@cloudflare/workers-types" />
|
|
2
6
|
import { createMiddleware } from 'hono/factory'
|
|
3
7
|
import type { IRateLimiter } from '@beechcms/core'
|