@beechcms/api 0.6.0-preview.1 → 0.6.0-preview.2
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/chart-BrJFdsGZ.js +39 -0
- package/assets/dashboard/assets/index-CIaoMxuB.css +1 -0
- package/assets/dashboard/assets/index-wTKgygsg.js +636 -0
- package/assets/dashboard/assets/pie-chart-recharts-BEaE1FOE.js +1 -0
- package/assets/dashboard/assets/timeseries-chart-recharts-DmWM6imF.js +1 -0
- package/assets/dashboard/beechLogoDark.svg +48 -48
- package/assets/dashboard/index.html +18 -18
- package/assets/dashboard/noResult.svg +42 -42
- package/assets/dashboard/sol.svg +3 -3
- package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
- package/migrations/0000_v040_base.sql +265 -265
- package/migrations/0029_automations.sql +14 -14
- package/migrations/0030_test_seeds.sql +125 -125
- package/package.json +2 -2
- package/src/auth/__fixtures__/in-memory-hash-provider.ts +17 -17
- package/src/auth/__fixtures__/static-token-service.ts +22 -22
- package/src/auth/bcrypt-hash-provider.ts +24 -24
- package/src/auth/constants.ts +14 -14
- package/src/auth/generate-refresh-token.test.ts +23 -23
- package/src/auth/hash-provider.test.ts +55 -55
- package/src/auth/jose-token-service.ts +62 -62
- package/src/auth/jwt-claims-passthrough.test.ts +87 -87
- package/src/auth/login.test.ts +100 -100
- package/src/auth/login.ts +78 -78
- package/src/auth/refresh.ts +11 -11
- package/src/auth/token-service.test.ts +90 -90
- package/src/factory.ts +370 -368
- package/src/features/automations/__tests__/action-executors.test.ts +255 -255
- package/src/features/automations/__tests__/automation-runner.test.ts +196 -196
- package/src/features/automations/__tests__/automation-runner.utils.test.ts +60 -60
- package/src/features/automations/__tests__/automations.handler.test.ts +264 -264
- package/src/features/automations/__tests__/automations.repository.test.ts +163 -163
- package/src/features/automations/__tests__/automations.schema.test.ts +224 -224
- package/src/features/automations/__tests__/context-resolver.test.ts +126 -126
- package/src/features/automations/__tests__/cron-runner.test.ts +267 -267
- package/src/features/automations/__tests__/cron-runner.utils.test.ts +144 -144
- package/src/features/automations/__tests__/set-variable.executor.test.ts +383 -383
- package/src/features/automations/__tests__/template-grammar.test.ts +308 -308
- package/src/features/automations/__tests__/webhook.executor.test.ts +142 -142
- package/src/features/automations/__tests__/when-evaluator.test.ts +274 -274
- package/src/features/automations/__tests__/when-pushdown.test.ts +281 -281
- package/src/features/automations/action-executors/create-entry.executor.ts +27 -27
- package/src/features/automations/action-executors/edit-field.executor.ts +26 -26
- package/src/features/automations/action-executors/index.ts +37 -37
- package/src/features/automations/action-executors/send-mail.executor.ts +53 -53
- package/src/features/automations/action-executors/set-variable.executor.ts +159 -159
- package/src/features/automations/action-executors/webhook.executor.ts +67 -67
- package/src/features/automations/automation-runner.ts +85 -85
- package/src/features/automations/automation-runner.utils.ts +47 -47
- package/src/features/automations/automations.handler.ts +197 -197
- package/src/features/automations/automations.schema.ts +177 -177
- package/src/features/automations/context-resolver.ts +152 -152
- package/src/features/automations/cron-runner.ts +140 -140
- package/src/features/automations/cron-runner.utils.ts +44 -44
- package/src/features/automations/filter-translation.ts +46 -46
- package/src/features/automations/index.ts +16 -16
- package/src/features/automations/template-grammar.ts +245 -245
- package/src/features/automations/var-access-resolver.ts +140 -140
- package/src/features/automations/when-evaluator.ts +87 -87
- package/src/features/automations/when-pushdown.ts +57 -57
- package/src/features/backrefs/__tests__/backrefs.handler.test.ts +359 -359
- package/src/features/backrefs/backrefs.handler.ts +168 -168
- package/src/features/backrefs/d1-backref.repository.ts +84 -84
- package/src/features/backrefs/index.ts +5 -5
- package/src/features/content/constants.ts +16 -16
- package/src/features/content/handlers/bulk.handler.ts +185 -185
- package/src/features/content/handlers/create.ts +129 -129
- package/src/features/content/handlers/delete.ts +62 -62
- package/src/features/content/handlers/facets.ts +49 -49
- package/src/features/content/handlers/get.ts +120 -120
- package/src/features/content/handlers/helpers.test.ts +180 -180
- package/src/features/content/handlers/helpers.ts +93 -93
- package/src/features/content/handlers/list.ts +174 -174
- package/src/features/content/handlers/update.ts +174 -174
- package/src/features/content/index.ts +26 -26
- package/src/features/dashboard-layout/__tests__/dashboard-layout.handler.test.ts +388 -0
- package/src/features/dashboard-layout/dashboard-layout.handler.ts +206 -0
- package/src/features/dashboard-layout/index.ts +5 -0
- package/src/features/draft/draft.handler.ts +195 -195
- package/src/features/draft/draft.middleware.ts +66 -66
- package/src/features/draft/index.ts +5 -5
- package/src/features/email/email.provider.ts +42 -42
- package/src/features/email/email.service.ts +98 -98
- package/src/features/email/email.types.ts +119 -119
- package/src/features/email/index.ts +33 -33
- package/src/features/email/providers/resend.ts +67 -67
- package/src/features/email/providers/smtp.ts +55 -55
- package/src/features/email/templates/automation-mail.ts +19 -19
- package/src/features/email/templates/password-changed.ts +63 -63
- package/src/features/email/templates/password-reset.ts +68 -68
- package/src/features/email/templates/shell.ts +96 -96
- package/src/features/notifications/index.ts +5 -5
- package/src/features/notifications/notifications.handler.ts +105 -105
- package/src/features/password-reset/index.ts +19 -19
- package/src/features/password-reset/request.ts +92 -92
- package/src/features/password-reset/reset.ts +102 -102
- package/src/features/rotate-field/index.ts +5 -5
- package/src/features/rotate-field/rotate-field.handler.ts +132 -132
- package/src/features/rotate-field/rotate-field.schema.ts +17 -17
- package/src/features/schema/schema.handler.ts +131 -131
- package/src/features/seeds/index.ts +5 -5
- package/src/features/seeds/seeds.handler.test.ts +1199 -632
- package/src/features/seeds/seeds.handler.ts +558 -693
- package/src/features/settings/__tests__/settings.handler.test.ts +555 -555
- package/src/features/settings/settings.handler.ts +392 -392
- package/src/features/setup/index.ts +288 -204
- package/src/features/stats/index.ts +5 -5
- package/src/features/stats/stats.handler.ts +420 -420
- package/src/features/webhooks/index.ts +63 -63
- package/src/index.ts +65 -65
- package/src/media-utils.ts +82 -82
- package/src/middleware/auth-providers.middleware.test.ts +85 -0
- package/src/middleware/auth-providers.middleware.ts +45 -45
- package/src/middleware/observability.middleware.test.ts +93 -0
- package/src/middleware/observability.middleware.ts +70 -70
- package/src/middleware/rate-limit.middleware.ts +45 -45
- package/src/middleware/repository.middleware.ts +117 -114
- package/src/middleware/seed-registry.middleware.test.ts +97 -97
- package/src/middleware/seed-registry.middleware.ts +21 -21
- package/src/middleware/storage.middleware.ts +25 -25
- package/src/middleware.ts +45 -45
- package/src/public/access-policy.ts +27 -27
- package/src/public/api-key-middleware.ts +57 -57
- package/src/public/cache-utils.ts +38 -38
- package/src/public/entry-projection.ts +46 -46
- package/src/public/idempotency.ts +23 -23
- package/src/public/index.ts +16 -16
- package/src/public/problem-details.ts +124 -124
- package/src/public/public-add.ts +114 -114
- package/src/public/public-edit.ts +163 -163
- package/src/public/public-errors.ts +19 -19
- package/src/public/public-read.ts +63 -63
- package/src/public/public-routes.ts +88 -88
- package/src/public/query-builder.test.ts +224 -224
- package/src/public/query-builder.ts +156 -156
- package/src/public/rate-limit-middleware.ts +34 -34
- package/src/public/read-list.ts +54 -54
- package/src/public/read-single.ts +48 -48
- package/src/public/response-builder.ts +30 -30
- package/src/public/sanitize.ts +69 -69
- package/src/public/slug-utils.ts +18 -18
- package/src/rate-limit/cloudflare-rate-limiter.test.ts +30 -30
- package/src/rate-limit/cloudflare-rate-limiter.ts +15 -15
- package/src/rate-limit/in-memory-rate-limiter.test.ts +37 -37
- package/src/rate-limit/in-memory-rate-limiter.ts +17 -17
- package/src/rate-limit/no-op-rate-limiter.test.ts +22 -22
- package/src/rate-limit/no-op-rate-limiter.ts +11 -11
- package/src/search-utils.test.ts +211 -211
- package/src/search-utils.ts +213 -213
- package/src/search.ts +65 -65
- package/src/shared/apply-policies.test.ts +81 -81
- package/src/shared/apply-policies.ts +67 -67
- package/src/shared/automations.repository.d1.ts +150 -150
- package/src/shared/background-notification-service.test.ts +62 -62
- package/src/shared/background-notification-service.ts +52 -52
- package/src/shared/base.repository.d1.ts +32 -32
- package/src/shared/content-utils.test.ts +165 -165
- package/src/shared/content-utils.ts +86 -86
- package/src/shared/content.repository.d1.test.ts +507 -458
- package/src/shared/content.repository.d1.ts +851 -851
- package/src/shared/d1-activity-log.repository.test.ts +140 -140
- package/src/shared/d1-activity-log.repository.ts +105 -105
- package/src/shared/d1-activity-logger.test.ts +86 -86
- package/src/shared/d1-activity-logger.ts +67 -67
- package/src/shared/d1-analytics.repository.test.ts +78 -78
- package/src/shared/d1-analytics.repository.ts +85 -85
- package/src/shared/d1-content-scan.repository.test.ts +80 -80
- package/src/shared/d1-content-scan.repository.ts +37 -37
- package/src/shared/d1-notification.repository.test.ts +128 -128
- package/src/shared/d1-notification.repository.ts +118 -118
- package/src/shared/d1-password-reset-token.repository.test.ts +81 -81
- package/src/shared/d1-password-reset-token.repository.ts +56 -56
- package/src/shared/d1-search.repository.test.ts +87 -87
- package/src/shared/d1-search.repository.ts +88 -88
- package/src/shared/d1-session.repository.test.ts +125 -125
- package/src/shared/d1-session.repository.ts +102 -102
- package/src/shared/d1-setup-checklist.repository.ts +36 -36
- package/src/shared/d1-user.repository.test.ts +151 -151
- package/src/shared/d1-user.repository.ts +119 -119
- package/src/shared/d1-widget.repository.test.ts +267 -221
- package/src/shared/d1-widget.repository.ts +385 -341
- package/src/shared/dashboard-layout.repository.d1.test.ts +135 -0
- package/src/shared/dashboard-layout.repository.d1.ts +58 -0
- package/src/shared/demo-data-sql.ts +216 -54
- package/src/shared/demo-data.repository.d1.ts +20 -20
- package/src/shared/execution-context-scheduler.ts +13 -13
- package/src/shared/fixed-clock.ts +25 -25
- package/src/shared/fts-sync.ts +8 -8
- package/src/shared/idempotency.repository.d1.test.ts +83 -83
- package/src/shared/idempotency.repository.d1.ts +60 -60
- package/src/shared/in-memory-activity-logger.ts +19 -19
- package/src/shared/in-memory-notification-service.ts +19 -19
- package/src/shared/in-memory-seed.repository.ts +58 -51
- package/src/shared/media.repository.d1.test.ts +107 -107
- package/src/shared/media.repository.d1.ts +68 -68
- package/src/shared/qstash-notification-service.test.ts +67 -67
- package/src/shared/qstash-notification-service.ts +55 -55
- package/src/shared/query-utils.ts +141 -141
- package/src/shared/request-utils.ts +26 -26
- package/src/shared/schema-mutator.d1.test.ts +142 -0
- package/src/shared/schema-mutator.d1.ts +64 -64
- package/src/shared/seed-layout.repository.d1.test.ts +114 -114
- package/src/shared/seed-layout.repository.d1.ts +62 -62
- package/src/shared/seed-registry-cache.test.ts +68 -68
- package/src/shared/seed-registry-cache.ts +49 -49
- package/src/shared/seed.repository.d1.test.ts +143 -143
- package/src/shared/seed.repository.d1.ts +98 -98
- package/src/shared/sequential-id-generator.ts +33 -33
- package/src/shared/site-settings.repository.d1.ts +53 -53
- package/src/shared/storage/factory.ts +41 -41
- package/src/shared/storage/s3-bucket.ts +195 -195
- package/src/shared/storage-utils.ts +40 -40
- package/src/shared/system-stats.repository.d1.test.ts +58 -58
- package/src/shared/system-stats.repository.d1.ts +48 -48
- package/src/types.ts +90 -87
- package/src/upload.ts +198 -198
- package/src/widget.test.ts +85 -0
- package/src/widget.ts +251 -212
- package/assets/dashboard/assets/index-BSjk0Sx9.js +0 -634
- package/assets/dashboard/assets/index-kRCYVN2B.css +0 -1
|
@@ -1,105 +1,105 @@
|
|
|
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
|
-
/// <reference types="@cloudflare/workers-types" />
|
|
6
|
-
import { Hono } from 'hono'
|
|
7
|
-
import type { Env, Variables } from '../../types'
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Notifications Feature Handler.
|
|
11
|
-
*
|
|
12
|
-
* Manages retrieval, mark read/unread, and deletion of system notifications.
|
|
13
|
-
* All persistence goes through the {@link INotificationRepository} injected
|
|
14
|
-
* by `repositoryMiddleware`. The handler owns the HTTP concerns only:
|
|
15
|
-
* ETag negotiation, status codes, error mapping.
|
|
16
|
-
*/
|
|
17
|
-
const notificationsApp = new Hono<{ Bindings: Env; Variables: Variables }>()
|
|
18
|
-
|
|
19
|
-
const NOTIFICATION_LIST_LIMIT = 50
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* GET /notifications
|
|
23
|
-
*
|
|
24
|
-
* Returns the most recent notifications. The ETag is built from aggregate
|
|
25
|
-
* stats so unchanged inboxes return 304 without serialising the full list.
|
|
26
|
-
*/
|
|
27
|
-
notificationsApp.get('/notifications', async (context) => {
|
|
28
|
-
try {
|
|
29
|
-
const notificationRepository = context.get('notificationRepository')
|
|
30
|
-
const notificationStats = await notificationRepository.stats()
|
|
31
|
-
const etagValue = `W/"${notificationStats.totalCount}-${notificationStats.latestCreatedAt}-${notificationStats.readCount}"`
|
|
32
|
-
|
|
33
|
-
const ifNoneMatch = context.req.header('If-None-Match')
|
|
34
|
-
if (ifNoneMatch === etagValue) {
|
|
35
|
-
return new Response(null, { status: 304 })
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const notifications = await notificationRepository.list(NOTIFICATION_LIST_LIMIT)
|
|
39
|
-
|
|
40
|
-
context.header('ETag', etagValue)
|
|
41
|
-
context.header('Cache-Control', 'no-cache, must-revalidate')
|
|
42
|
-
|
|
43
|
-
return context.json(notifications)
|
|
44
|
-
} catch (error) {
|
|
45
|
-
console.error('[Notifications] Fetch error:', error)
|
|
46
|
-
return context.json({ error: 'Failed to fetch notifications' }, 500)
|
|
47
|
-
}
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* PATCH /notifications/:id/read — mark a single notification as read.
|
|
52
|
-
*/
|
|
53
|
-
notificationsApp.patch('/notifications/:id/read', async (context) => {
|
|
54
|
-
try {
|
|
55
|
-
const notificationId = context.req.param('id')
|
|
56
|
-
await context.get('notificationRepository').markRead(notificationId)
|
|
57
|
-
return context.json({ success: true })
|
|
58
|
-
} catch (error) {
|
|
59
|
-
console.error('[Notifications] Mark read error:', error)
|
|
60
|
-
return context.json({ error: 'Failed to update notification' }, 500)
|
|
61
|
-
}
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* PATCH /notifications/:id/unread — mark a single notification as unread.
|
|
66
|
-
*/
|
|
67
|
-
notificationsApp.patch('/notifications/:id/unread', async (context) => {
|
|
68
|
-
try {
|
|
69
|
-
const notificationId = context.req.param('id')
|
|
70
|
-
await context.get('notificationRepository').markUnread(notificationId)
|
|
71
|
-
return context.json({ success: true })
|
|
72
|
-
} catch (error) {
|
|
73
|
-
console.error('[Notifications] Mark unread error:', error)
|
|
74
|
-
return context.json({ error: 'Failed to update notification' }, 500)
|
|
75
|
-
}
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* DELETE /notifications/:id — permanently remove a notification.
|
|
80
|
-
*/
|
|
81
|
-
notificationsApp.delete('/notifications/:id', async (context) => {
|
|
82
|
-
try {
|
|
83
|
-
const notificationId = context.req.param('id')
|
|
84
|
-
await context.get('notificationRepository').delete(notificationId)
|
|
85
|
-
return context.json({ success: true })
|
|
86
|
-
} catch (error) {
|
|
87
|
-
console.error('[Notifications] Delete error:', error)
|
|
88
|
-
return context.json({ error: 'Failed to delete notification' }, 500)
|
|
89
|
-
}
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* POST /notifications/mark-all-read — mark every notification as read.
|
|
94
|
-
*/
|
|
95
|
-
notificationsApp.post('/notifications/mark-all-read', async (context) => {
|
|
96
|
-
try {
|
|
97
|
-
await context.get('notificationRepository').markAllRead()
|
|
98
|
-
return context.json({ success: true })
|
|
99
|
-
} catch (error) {
|
|
100
|
-
console.error('[Notifications] Mark all read error:', error)
|
|
101
|
-
return context.json({ error: 'Failed to update notifications' }, 500)
|
|
102
|
-
}
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
export { notificationsApp }
|
|
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
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
6
|
+
import { Hono } from 'hono'
|
|
7
|
+
import type { Env, Variables } from '../../types'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Notifications Feature Handler.
|
|
11
|
+
*
|
|
12
|
+
* Manages retrieval, mark read/unread, and deletion of system notifications.
|
|
13
|
+
* All persistence goes through the {@link INotificationRepository} injected
|
|
14
|
+
* by `repositoryMiddleware`. The handler owns the HTTP concerns only:
|
|
15
|
+
* ETag negotiation, status codes, error mapping.
|
|
16
|
+
*/
|
|
17
|
+
const notificationsApp = new Hono<{ Bindings: Env; Variables: Variables }>()
|
|
18
|
+
|
|
19
|
+
const NOTIFICATION_LIST_LIMIT = 50
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* GET /notifications
|
|
23
|
+
*
|
|
24
|
+
* Returns the most recent notifications. The ETag is built from aggregate
|
|
25
|
+
* stats so unchanged inboxes return 304 without serialising the full list.
|
|
26
|
+
*/
|
|
27
|
+
notificationsApp.get('/notifications', async (context) => {
|
|
28
|
+
try {
|
|
29
|
+
const notificationRepository = context.get('notificationRepository')
|
|
30
|
+
const notificationStats = await notificationRepository.stats()
|
|
31
|
+
const etagValue = `W/"${notificationStats.totalCount}-${notificationStats.latestCreatedAt}-${notificationStats.readCount}"`
|
|
32
|
+
|
|
33
|
+
const ifNoneMatch = context.req.header('If-None-Match')
|
|
34
|
+
if (ifNoneMatch === etagValue) {
|
|
35
|
+
return new Response(null, { status: 304 })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const notifications = await notificationRepository.list(NOTIFICATION_LIST_LIMIT)
|
|
39
|
+
|
|
40
|
+
context.header('ETag', etagValue)
|
|
41
|
+
context.header('Cache-Control', 'no-cache, must-revalidate')
|
|
42
|
+
|
|
43
|
+
return context.json(notifications)
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error('[Notifications] Fetch error:', error)
|
|
46
|
+
return context.json({ error: 'Failed to fetch notifications' }, 500)
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* PATCH /notifications/:id/read — mark a single notification as read.
|
|
52
|
+
*/
|
|
53
|
+
notificationsApp.patch('/notifications/:id/read', async (context) => {
|
|
54
|
+
try {
|
|
55
|
+
const notificationId = context.req.param('id')
|
|
56
|
+
await context.get('notificationRepository').markRead(notificationId)
|
|
57
|
+
return context.json({ success: true })
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error('[Notifications] Mark read error:', error)
|
|
60
|
+
return context.json({ error: 'Failed to update notification' }, 500)
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* PATCH /notifications/:id/unread — mark a single notification as unread.
|
|
66
|
+
*/
|
|
67
|
+
notificationsApp.patch('/notifications/:id/unread', async (context) => {
|
|
68
|
+
try {
|
|
69
|
+
const notificationId = context.req.param('id')
|
|
70
|
+
await context.get('notificationRepository').markUnread(notificationId)
|
|
71
|
+
return context.json({ success: true })
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error('[Notifications] Mark unread error:', error)
|
|
74
|
+
return context.json({ error: 'Failed to update notification' }, 500)
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* DELETE /notifications/:id — permanently remove a notification.
|
|
80
|
+
*/
|
|
81
|
+
notificationsApp.delete('/notifications/:id', async (context) => {
|
|
82
|
+
try {
|
|
83
|
+
const notificationId = context.req.param('id')
|
|
84
|
+
await context.get('notificationRepository').delete(notificationId)
|
|
85
|
+
return context.json({ success: true })
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error('[Notifications] Delete error:', error)
|
|
88
|
+
return context.json({ error: 'Failed to delete notification' }, 500)
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* POST /notifications/mark-all-read — mark every notification as read.
|
|
94
|
+
*/
|
|
95
|
+
notificationsApp.post('/notifications/mark-all-read', async (context) => {
|
|
96
|
+
try {
|
|
97
|
+
await context.get('notificationRepository').markAllRead()
|
|
98
|
+
return context.json({ success: true })
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error('[Notifications] Mark all read error:', error)
|
|
101
|
+
return context.json({ error: 'Failed to update notifications' }, 500)
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
export { notificationsApp }
|
|
@@ -1,19 +1,19 @@
|
|
|
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
|
-
/// <reference types="@cloudflare/workers-types" />
|
|
6
|
-
import { Hono } from 'hono'
|
|
7
|
-
import type { Env, Variables } from '../../types'
|
|
8
|
-
import { requestPasswordReset } from './request'
|
|
9
|
-
import { resetPassword } from './reset'
|
|
10
|
-
|
|
11
|
-
export const passwordResetApp = new Hono<{ Bindings: Env; Variables: Variables }>()
|
|
12
|
-
|
|
13
|
-
// Feature flag: lets the dashboard know whether to show the forgot-password link
|
|
14
|
-
passwordResetApp.get('/auth/features', (context) => {
|
|
15
|
-
return context.json({ passwordReset: context.env.EMAIL_PROVIDER === 'smtp' || Boolean(context.env.RESEND_API_KEY) })
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
passwordResetApp.post('/auth/forgot-password', requestPasswordReset)
|
|
19
|
-
passwordResetApp.post('/auth/reset-password', resetPassword)
|
|
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
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
6
|
+
import { Hono } from 'hono'
|
|
7
|
+
import type { Env, Variables } from '../../types'
|
|
8
|
+
import { requestPasswordReset } from './request'
|
|
9
|
+
import { resetPassword } from './reset'
|
|
10
|
+
|
|
11
|
+
export const passwordResetApp = new Hono<{ Bindings: Env; Variables: Variables }>()
|
|
12
|
+
|
|
13
|
+
// Feature flag: lets the dashboard know whether to show the forgot-password link
|
|
14
|
+
passwordResetApp.get('/auth/features', (context) => {
|
|
15
|
+
return context.json({ passwordReset: context.env.EMAIL_PROVIDER === 'smtp' || Boolean(context.env.RESEND_API_KEY) })
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
passwordResetApp.post('/auth/forgot-password', requestPasswordReset)
|
|
19
|
+
passwordResetApp.post('/auth/reset-password', resetPassword)
|
|
@@ -1,92 +1,92 @@
|
|
|
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
|
-
/// <reference types="@cloudflare/workers-types" />
|
|
6
|
-
import type { Context } from 'hono'
|
|
7
|
-
import type { Env, Variables } from '../../types'
|
|
8
|
-
import { sendPasswordResetEmail, resolveEmailLocale } from '../email'
|
|
9
|
-
import { sha256hex } from '@beechcms/core'
|
|
10
|
-
import { getClientIp } from '../../shared/request-utils'
|
|
11
|
-
|
|
12
|
-
const PASSWORD_RESET_TOKEN_EXPIRY_SECONDS = 30 * 60
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Handles the password reset request.
|
|
16
|
-
* Generates a reset token, stores its hash in the database, and sends an email to the user.
|
|
17
|
-
*/
|
|
18
|
-
export async function requestPasswordReset(
|
|
19
|
-
context: Context<{ Bindings: Env; Variables: Variables }>
|
|
20
|
-
): Promise<Response> {
|
|
21
|
-
const { env, req } = context
|
|
22
|
-
|
|
23
|
-
const useSmtp = env.EMAIL_PROVIDER === 'smtp'
|
|
24
|
-
if (!useSmtp && !env.RESEND_API_KEY) {
|
|
25
|
-
return context.json({ error: 'Service not available' }, 503)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
let payload: Record<string, unknown>
|
|
29
|
-
try {
|
|
30
|
-
payload = await req.json()
|
|
31
|
-
} catch {
|
|
32
|
-
return context.json({ error: 'Invalid request body' }, 400)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const emailInput = payload.email
|
|
36
|
-
if (typeof emailInput !== 'string' || !emailInput.trim()) {
|
|
37
|
-
return context.json({ error: 'Invalid request' }, 400)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const normalizedEmail = emailInput.trim().toLowerCase()
|
|
41
|
-
const emailLocale = resolveEmailLocale(payload.locale)
|
|
42
|
-
|
|
43
|
-
const clientIpAddress = getClientIp(req)
|
|
44
|
-
const forgotPasswordRateLimit = await context.get('rateLimiters').getLimiter('forgotPassword').checkLimit(clientIpAddress)
|
|
45
|
-
if (!forgotPasswordRateLimit.isAllowed) {
|
|
46
|
-
return context.json({ error: 'Too many requests' }, 429)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Always return 200 even when the user is not found to prevent user enumeration.
|
|
50
|
-
const registeredUser = await context.get('userRepository').findByEmail(normalizedEmail)
|
|
51
|
-
if (!registeredUser) {
|
|
52
|
-
return context.json({ success: true })
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const nowTimestamp = Math.floor(Date.now() / 1000)
|
|
56
|
-
await context.get('passwordResetTokenRepository').invalidatePending(registeredUser.id, nowTimestamp)
|
|
57
|
-
|
|
58
|
-
const resetToken = crypto.randomUUID()
|
|
59
|
-
const tokenHash = await sha256hex(resetToken)
|
|
60
|
-
const expiresAt = nowTimestamp + PASSWORD_RESET_TOKEN_EXPIRY_SECONDS
|
|
61
|
-
|
|
62
|
-
await context.get('passwordResetTokenRepository').create({
|
|
63
|
-
userId: registeredUser.id,
|
|
64
|
-
tokenHash,
|
|
65
|
-
expiresAt,
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
const baseUrl = (env.APP_URL ?? new URL(req.url).origin).replace(/\/$/, '')
|
|
69
|
-
const resetUrl = `${baseUrl}/admin/reset-password?token=${resetToken}`
|
|
70
|
-
|
|
71
|
-
const smtpBaseUrl = env.SMTP_HOST
|
|
72
|
-
? `http://${env.SMTP_HOST}:${env.SMTP_PORT ?? '8025'}`
|
|
73
|
-
: undefined
|
|
74
|
-
try {
|
|
75
|
-
await sendPasswordResetEmail({
|
|
76
|
-
to: normalizedEmail,
|
|
77
|
-
resetUrl,
|
|
78
|
-
locale: emailLocale,
|
|
79
|
-
apiKey: env.RESEND_API_KEY ?? '',
|
|
80
|
-
from: env.EMAIL_FROM,
|
|
81
|
-
isDev: env.ENV !== 'production',
|
|
82
|
-
provider: env.EMAIL_PROVIDER as 'smtp' | 'resend' | undefined,
|
|
83
|
-
smtpBaseUrl,
|
|
84
|
-
})
|
|
85
|
-
} catch (error) {
|
|
86
|
-
if (env.ENV !== 'production') {
|
|
87
|
-
console.error('[password-reset] Failed to send email:', error)
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return context.json({ success: true })
|
|
92
|
-
}
|
|
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
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
6
|
+
import type { Context } from 'hono'
|
|
7
|
+
import type { Env, Variables } from '../../types'
|
|
8
|
+
import { sendPasswordResetEmail, resolveEmailLocale } from '../email'
|
|
9
|
+
import { sha256hex } from '@beechcms/core'
|
|
10
|
+
import { getClientIp } from '../../shared/request-utils'
|
|
11
|
+
|
|
12
|
+
const PASSWORD_RESET_TOKEN_EXPIRY_SECONDS = 30 * 60
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Handles the password reset request.
|
|
16
|
+
* Generates a reset token, stores its hash in the database, and sends an email to the user.
|
|
17
|
+
*/
|
|
18
|
+
export async function requestPasswordReset(
|
|
19
|
+
context: Context<{ Bindings: Env; Variables: Variables }>
|
|
20
|
+
): Promise<Response> {
|
|
21
|
+
const { env, req } = context
|
|
22
|
+
|
|
23
|
+
const useSmtp = env.EMAIL_PROVIDER === 'smtp'
|
|
24
|
+
if (!useSmtp && !env.RESEND_API_KEY) {
|
|
25
|
+
return context.json({ error: 'Service not available' }, 503)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let payload: Record<string, unknown>
|
|
29
|
+
try {
|
|
30
|
+
payload = await req.json()
|
|
31
|
+
} catch {
|
|
32
|
+
return context.json({ error: 'Invalid request body' }, 400)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const emailInput = payload.email
|
|
36
|
+
if (typeof emailInput !== 'string' || !emailInput.trim()) {
|
|
37
|
+
return context.json({ error: 'Invalid request' }, 400)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const normalizedEmail = emailInput.trim().toLowerCase()
|
|
41
|
+
const emailLocale = resolveEmailLocale(payload.locale)
|
|
42
|
+
|
|
43
|
+
const clientIpAddress = getClientIp(req)
|
|
44
|
+
const forgotPasswordRateLimit = await context.get('rateLimiters').getLimiter('forgotPassword').checkLimit(clientIpAddress)
|
|
45
|
+
if (!forgotPasswordRateLimit.isAllowed) {
|
|
46
|
+
return context.json({ error: 'Too many requests' }, 429)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Always return 200 even when the user is not found to prevent user enumeration.
|
|
50
|
+
const registeredUser = await context.get('userRepository').findByEmail(normalizedEmail)
|
|
51
|
+
if (!registeredUser) {
|
|
52
|
+
return context.json({ success: true })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const nowTimestamp = Math.floor(Date.now() / 1000)
|
|
56
|
+
await context.get('passwordResetTokenRepository').invalidatePending(registeredUser.id, nowTimestamp)
|
|
57
|
+
|
|
58
|
+
const resetToken = crypto.randomUUID()
|
|
59
|
+
const tokenHash = await sha256hex(resetToken)
|
|
60
|
+
const expiresAt = nowTimestamp + PASSWORD_RESET_TOKEN_EXPIRY_SECONDS
|
|
61
|
+
|
|
62
|
+
await context.get('passwordResetTokenRepository').create({
|
|
63
|
+
userId: registeredUser.id,
|
|
64
|
+
tokenHash,
|
|
65
|
+
expiresAt,
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const baseUrl = (env.APP_URL ?? new URL(req.url).origin).replace(/\/$/, '')
|
|
69
|
+
const resetUrl = `${baseUrl}/admin/reset-password?token=${resetToken}`
|
|
70
|
+
|
|
71
|
+
const smtpBaseUrl = env.SMTP_HOST
|
|
72
|
+
? `http://${env.SMTP_HOST}:${env.SMTP_PORT ?? '8025'}`
|
|
73
|
+
: undefined
|
|
74
|
+
try {
|
|
75
|
+
await sendPasswordResetEmail({
|
|
76
|
+
to: normalizedEmail,
|
|
77
|
+
resetUrl,
|
|
78
|
+
locale: emailLocale,
|
|
79
|
+
apiKey: env.RESEND_API_KEY ?? '',
|
|
80
|
+
from: env.EMAIL_FROM,
|
|
81
|
+
isDev: env.ENV !== 'production',
|
|
82
|
+
provider: env.EMAIL_PROVIDER as 'smtp' | 'resend' | undefined,
|
|
83
|
+
smtpBaseUrl,
|
|
84
|
+
})
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (env.ENV !== 'production') {
|
|
87
|
+
console.error('[password-reset] Failed to send email:', error)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return context.json({ success: true })
|
|
92
|
+
}
|
|
@@ -1,102 +1,102 @@
|
|
|
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
|
-
/// <reference types="@cloudflare/workers-types" />
|
|
6
|
-
import type { Context } from 'hono'
|
|
7
|
-
import type { Env, Variables } from '../../types'
|
|
8
|
-
import { sendPasswordChangedEmail, resolveEmailLocale } from '../email'
|
|
9
|
-
import { sha256hex } from '@beechcms/core'
|
|
10
|
-
import { getClientIp } from '../../shared/request-utils'
|
|
11
|
-
|
|
12
|
-
const MIN_PASSWORD_LENGTH = 8
|
|
13
|
-
const MAX_PASSWORD_LENGTH = 128
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Handles the actual password reset process using a valid token.
|
|
17
|
-
*/
|
|
18
|
-
export async function resetPassword(
|
|
19
|
-
context: Context<{ Bindings: Env; Variables: Variables }>
|
|
20
|
-
): Promise<Response> {
|
|
21
|
-
const { env, req, executionCtx } = context
|
|
22
|
-
|
|
23
|
-
const useSmtp = env.EMAIL_PROVIDER === 'smtp'
|
|
24
|
-
if (!useSmtp && !env.RESEND_API_KEY) {
|
|
25
|
-
return context.json({ error: 'Service not available' }, 503)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const clientIpAddress = getClientIp(req)
|
|
29
|
-
const resetPasswordRateLimit = await context.get('rateLimiters').getLimiter('resetPassword').checkLimit(clientIpAddress)
|
|
30
|
-
if (!resetPasswordRateLimit.isAllowed) {
|
|
31
|
-
return context.json({ error: 'Too many requests' }, 429)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
let payload: Record<string, unknown>
|
|
35
|
-
try {
|
|
36
|
-
payload = await req.json()
|
|
37
|
-
} catch {
|
|
38
|
-
return context.json({ error: 'Invalid request body' }, 400)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const resetToken = payload.token
|
|
42
|
-
if (typeof resetToken !== 'string' || !resetToken) {
|
|
43
|
-
return context.json({ error: 'Invalid or expired token' }, 400)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const newPassword = payload.password
|
|
47
|
-
const isPasswordInvalid =
|
|
48
|
-
typeof newPassword !== 'string' ||
|
|
49
|
-
newPassword.length < MIN_PASSWORD_LENGTH ||
|
|
50
|
-
newPassword.length > MAX_PASSWORD_LENGTH
|
|
51
|
-
|
|
52
|
-
if (isPasswordInvalid) {
|
|
53
|
-
return context.json({
|
|
54
|
-
error: `Password must be between ${MIN_PASSWORD_LENGTH} and ${MAX_PASSWORD_LENGTH} characters`,
|
|
55
|
-
}, 400)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const emailLocale = resolveEmailLocale(payload.locale)
|
|
59
|
-
const tokenHash = await sha256hex(resetToken as string)
|
|
60
|
-
const nowTimestamp = Math.floor(Date.now() / 1000)
|
|
61
|
-
|
|
62
|
-
const tokenRecord = await context.get('passwordResetTokenRepository').findValidByHashWithEmail(tokenHash, nowTimestamp)
|
|
63
|
-
if (!tokenRecord) {
|
|
64
|
-
return context.json({ error: 'Invalid or expired token' }, 400)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const hashedNewPassword = await context.get('hashProvider').hash(newPassword as string)
|
|
68
|
-
|
|
69
|
-
await context.get('passwordResetTokenRepository').markUsed(tokenRecord.id, nowTimestamp)
|
|
70
|
-
await context.get('userRepository').updatePasswordHash(tokenRecord.userId, hashedNewPassword)
|
|
71
|
-
await context.get('sessionRepository').revokeAllForUser(tokenRecord.userId, nowTimestamp)
|
|
72
|
-
|
|
73
|
-
const smtpBaseUrl = env.SMTP_HOST
|
|
74
|
-
? `http://${env.SMTP_HOST}:${env.SMTP_PORT ?? '8025'}`
|
|
75
|
-
: undefined
|
|
76
|
-
const sendNotification = async () => {
|
|
77
|
-
try {
|
|
78
|
-
await sendPasswordChangedEmail({
|
|
79
|
-
to: tokenRecord.email,
|
|
80
|
-
locale: emailLocale,
|
|
81
|
-
apiKey: env.RESEND_API_KEY ?? '',
|
|
82
|
-
from: env.EMAIL_FROM,
|
|
83
|
-
isDev: env.ENV !== 'production',
|
|
84
|
-
provider: env.EMAIL_PROVIDER as 'smtp' | 'resend' | undefined,
|
|
85
|
-
smtpBaseUrl,
|
|
86
|
-
})
|
|
87
|
-
} catch (error) {
|
|
88
|
-
if (env.ENV !== 'production') {
|
|
89
|
-
console.error('[password-reset] Failed to send password change notification:', error)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
executionCtx.waitUntil(sendNotification())
|
|
96
|
-
} catch {
|
|
97
|
-
// executionCtx might not be available in some testing environments
|
|
98
|
-
void sendNotification()
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return context.json({ success: true })
|
|
102
|
-
}
|
|
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
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
6
|
+
import type { Context } from 'hono'
|
|
7
|
+
import type { Env, Variables } from '../../types'
|
|
8
|
+
import { sendPasswordChangedEmail, resolveEmailLocale } from '../email'
|
|
9
|
+
import { sha256hex } from '@beechcms/core'
|
|
10
|
+
import { getClientIp } from '../../shared/request-utils'
|
|
11
|
+
|
|
12
|
+
const MIN_PASSWORD_LENGTH = 8
|
|
13
|
+
const MAX_PASSWORD_LENGTH = 128
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Handles the actual password reset process using a valid token.
|
|
17
|
+
*/
|
|
18
|
+
export async function resetPassword(
|
|
19
|
+
context: Context<{ Bindings: Env; Variables: Variables }>
|
|
20
|
+
): Promise<Response> {
|
|
21
|
+
const { env, req, executionCtx } = context
|
|
22
|
+
|
|
23
|
+
const useSmtp = env.EMAIL_PROVIDER === 'smtp'
|
|
24
|
+
if (!useSmtp && !env.RESEND_API_KEY) {
|
|
25
|
+
return context.json({ error: 'Service not available' }, 503)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const clientIpAddress = getClientIp(req)
|
|
29
|
+
const resetPasswordRateLimit = await context.get('rateLimiters').getLimiter('resetPassword').checkLimit(clientIpAddress)
|
|
30
|
+
if (!resetPasswordRateLimit.isAllowed) {
|
|
31
|
+
return context.json({ error: 'Too many requests' }, 429)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let payload: Record<string, unknown>
|
|
35
|
+
try {
|
|
36
|
+
payload = await req.json()
|
|
37
|
+
} catch {
|
|
38
|
+
return context.json({ error: 'Invalid request body' }, 400)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const resetToken = payload.token
|
|
42
|
+
if (typeof resetToken !== 'string' || !resetToken) {
|
|
43
|
+
return context.json({ error: 'Invalid or expired token' }, 400)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const newPassword = payload.password
|
|
47
|
+
const isPasswordInvalid =
|
|
48
|
+
typeof newPassword !== 'string' ||
|
|
49
|
+
newPassword.length < MIN_PASSWORD_LENGTH ||
|
|
50
|
+
newPassword.length > MAX_PASSWORD_LENGTH
|
|
51
|
+
|
|
52
|
+
if (isPasswordInvalid) {
|
|
53
|
+
return context.json({
|
|
54
|
+
error: `Password must be between ${MIN_PASSWORD_LENGTH} and ${MAX_PASSWORD_LENGTH} characters`,
|
|
55
|
+
}, 400)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const emailLocale = resolveEmailLocale(payload.locale)
|
|
59
|
+
const tokenHash = await sha256hex(resetToken as string)
|
|
60
|
+
const nowTimestamp = Math.floor(Date.now() / 1000)
|
|
61
|
+
|
|
62
|
+
const tokenRecord = await context.get('passwordResetTokenRepository').findValidByHashWithEmail(tokenHash, nowTimestamp)
|
|
63
|
+
if (!tokenRecord) {
|
|
64
|
+
return context.json({ error: 'Invalid or expired token' }, 400)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const hashedNewPassword = await context.get('hashProvider').hash(newPassword as string)
|
|
68
|
+
|
|
69
|
+
await context.get('passwordResetTokenRepository').markUsed(tokenRecord.id, nowTimestamp)
|
|
70
|
+
await context.get('userRepository').updatePasswordHash(tokenRecord.userId, hashedNewPassword)
|
|
71
|
+
await context.get('sessionRepository').revokeAllForUser(tokenRecord.userId, nowTimestamp)
|
|
72
|
+
|
|
73
|
+
const smtpBaseUrl = env.SMTP_HOST
|
|
74
|
+
? `http://${env.SMTP_HOST}:${env.SMTP_PORT ?? '8025'}`
|
|
75
|
+
: undefined
|
|
76
|
+
const sendNotification = async () => {
|
|
77
|
+
try {
|
|
78
|
+
await sendPasswordChangedEmail({
|
|
79
|
+
to: tokenRecord.email,
|
|
80
|
+
locale: emailLocale,
|
|
81
|
+
apiKey: env.RESEND_API_KEY ?? '',
|
|
82
|
+
from: env.EMAIL_FROM,
|
|
83
|
+
isDev: env.ENV !== 'production',
|
|
84
|
+
provider: env.EMAIL_PROVIDER as 'smtp' | 'resend' | undefined,
|
|
85
|
+
smtpBaseUrl,
|
|
86
|
+
})
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (env.ENV !== 'production') {
|
|
89
|
+
console.error('[password-reset] Failed to send password change notification:', error)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
executionCtx.waitUntil(sendNotification())
|
|
96
|
+
} catch {
|
|
97
|
+
// executionCtx might not be available in some testing environments
|
|
98
|
+
void sendNotification()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return context.json({ success: true })
|
|
102
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
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
|
-
export { rotateFieldApp } from './rotate-field.handler'
|
|
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
|
+
export { rotateFieldApp } from './rotate-field.handler'
|