@beechcms/api 0.4.0-preview.12 → 0.4.0-preview.13
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-CTSuGxlX.js → index-FQ6JhvRH.js} +99 -99
- 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 -325
- package/src/features/content/constants.ts +10 -10
- package/src/features/content/handlers/create.ts +153 -163
- package/src/features/content/handlers/delete.ts +76 -85
- package/src/features/content/handlers/facets.ts +45 -45
- package/src/features/content/handlers/get.ts +116 -116
- package/src/features/content/handlers/list.ts +88 -88
- package/src/features/content/handlers/update.ts +207 -216
- package/src/features/content/index.ts +20 -20
- package/src/features/draft/draft.handler.ts +272 -272
- 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 -267
- 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 -24
- package/src/media-utils.ts +78 -78
- package/src/middleware/repository.middleware.ts +24 -18
- 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 -166
- package/src/public/public-edit.ts +158 -158
- package/src/public/public-errors.ts +15 -15
- package/src/public/public-read.ts +216 -216
- package/src/public/public-routes.ts +84 -31
- package/src/public/query-builder.ts +152 -152
- 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 -28
- package/src/shared/content-utils.ts +82 -108
- package/src/shared/content.repository.d1.ts +382 -382
- package/src/shared/fts-sync.ts +4 -4
- package/src/shared/idempotency.repository.d1.ts +56 -45
- 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 -41
- package/src/upload.ts +179 -331
- package/src/widget.ts +349 -349
- package/assets/dashboard/assets/index-ye3325L9.css +0 -1
package/src/upload.ts
CHANGED
|
@@ -1,331 +1,179 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Media Engine: upload e servizio file
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
type
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const fileEntry = formData.get('file')
|
|
181
|
-
// In Cloudflare/Workers il value può essere `string` o un oggetto (File/Blob-like).
|
|
182
|
-
// Usiamo un guard sulle proprietà richieste.
|
|
183
|
-
if (!isFileLike(fileEntry)) {
|
|
184
|
-
return c.json({ error: 'No file provided. Use field name "file"' }, 400)
|
|
185
|
-
}
|
|
186
|
-
const file = fileEntry
|
|
187
|
-
|
|
188
|
-
const mimeOk = ALLOWED_MIME_PREFIXES.some((prefix) => file.type.startsWith(prefix))
|
|
189
|
-
if (!mimeOk) {
|
|
190
|
-
return c.json(
|
|
191
|
-
{ error: 'File type not allowed. Allowed: images and PDF' },
|
|
192
|
-
400
|
|
193
|
-
)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (file.size > MAX_FILE_SIZE_BYTES) {
|
|
197
|
-
return c.json({ error: 'File too large. Max 5MB' }, 400)
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const objectKey = generateObjectKey(file.name)
|
|
201
|
-
const client = createR2Client(c.env)
|
|
202
|
-
|
|
203
|
-
const body = await file.arrayBuffer()
|
|
204
|
-
await client.send(
|
|
205
|
-
new PutObjectCommand({
|
|
206
|
-
Bucket: R2_BUCKET_NAME,
|
|
207
|
-
Key: objectKey,
|
|
208
|
-
Body: new Uint8Array(body),
|
|
209
|
-
ContentType: file.type,
|
|
210
|
-
})
|
|
211
|
-
)
|
|
212
|
-
|
|
213
|
-
// Aggiorna contatore storage in D1
|
|
214
|
-
let executionCtx: { waitUntil: (p: Promise<any>) => void } | undefined
|
|
215
|
-
try {
|
|
216
|
-
executionCtx = c.executionCtx
|
|
217
|
-
} catch {
|
|
218
|
-
// In ambiente di test Hono lancia se non presente
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const uploadedBy = c.var.jwtPayload?.sub ?? ''
|
|
222
|
-
if (executionCtx) {
|
|
223
|
-
executionCtx.waitUntil((async () => {
|
|
224
|
-
try {
|
|
225
|
-
await c.env.DB.prepare(
|
|
226
|
-
"UPDATE system_stats SET value = CAST(value AS INTEGER) + ? WHERE id = 'total_storage_bytes'"
|
|
227
|
-
).bind(file.size).run()
|
|
228
|
-
} catch (err) {
|
|
229
|
-
console.error('Failed to update storage stats on upload:', err)
|
|
230
|
-
}
|
|
231
|
-
try {
|
|
232
|
-
await c.env.DB.prepare(
|
|
233
|
-
'INSERT INTO media_objects (key, filename, mime_type, size_bytes, uploaded_by) VALUES (?, ?, ?, ?, ?)'
|
|
234
|
-
).bind(objectKey, file.name, file.type, file.size, uploadedBy).run()
|
|
235
|
-
} catch (err) {
|
|
236
|
-
console.error('Failed to track media_objects on upload:', err)
|
|
237
|
-
}
|
|
238
|
-
})())
|
|
239
|
-
} else {
|
|
240
|
-
// Fallback sync
|
|
241
|
-
c.env.DB.prepare(
|
|
242
|
-
"UPDATE system_stats SET value = CAST(value AS INTEGER) + ? WHERE id = 'total_storage_bytes'"
|
|
243
|
-
).bind(file.size).run().catch(err => console.error('Failed to update storage stats on upload (sync fallback):', err))
|
|
244
|
-
c.env.DB.prepare(
|
|
245
|
-
'INSERT INTO media_objects (key, filename, mime_type, size_bytes, uploaded_by) VALUES (?, ?, ?, ?, ?)'
|
|
246
|
-
).bind(objectKey, file.name, file.type, file.size, uploadedBy).run().catch(err => console.error('Failed to track media_objects (sync fallback):', err))
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const baseUrl = getMediaBaseUrl(c)
|
|
250
|
-
const publicUrl = `${baseUrl}/api/media/${encodeURIComponent(objectKey)}`
|
|
251
|
-
|
|
252
|
-
logActivity(c, {
|
|
253
|
-
action: 'upload',
|
|
254
|
-
entityType: 'media',
|
|
255
|
-
entityId: objectKey,
|
|
256
|
-
details: { name: file.name, size: file.size, type: file.type }
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
return c.json({ url: publicUrl }, 200)
|
|
260
|
-
} catch (err) {
|
|
261
|
-
if (c.env.ENV !== 'production') {
|
|
262
|
-
console.error('Upload error:', err)
|
|
263
|
-
}
|
|
264
|
-
return c.json({ error: 'Upload failed' }, 500)
|
|
265
|
-
}
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
/** DELETE /api/upload/:key - Elimina un file da R2 */
|
|
269
|
-
uploadRoutes.delete('/upload/:key', async (c) => {
|
|
270
|
-
const key = c.req.param('key')
|
|
271
|
-
if (!key) return c.json({ error: 'Missing key' }, 400)
|
|
272
|
-
|
|
273
|
-
await deleteR2Objects(c.env, [decodeURIComponent(key)])
|
|
274
|
-
return c.json({ success: true }, 200)
|
|
275
|
-
})
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Serve un file da R2. Route pubblica (senza auth) per permettere
|
|
279
|
-
* il caricamento delle immagini nei tag <img>.
|
|
280
|
-
*/
|
|
281
|
-
export async function serveMediaHandler(
|
|
282
|
-
c: { env: UploadBindings; req: { param: (key: string) => string } }
|
|
283
|
-
): Promise<Response> {
|
|
284
|
-
const key = c.req.param('key')
|
|
285
|
-
if (!key) {
|
|
286
|
-
return new Response(JSON.stringify({ error: 'Missing key' }), {
|
|
287
|
-
status: 400,
|
|
288
|
-
headers: { 'Content-Type': 'application/json' },
|
|
289
|
-
})
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const { R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_ENDPOINT, R2_BUCKET_NAME } = c.env
|
|
293
|
-
if (!R2_ACCESS_KEY_ID || !R2_SECRET_ACCESS_KEY || !R2_ENDPOINT || !R2_BUCKET_NAME) {
|
|
294
|
-
return new Response(JSON.stringify({ error: 'R2 not configured' }), {
|
|
295
|
-
status: 500,
|
|
296
|
-
headers: { 'Content-Type': 'application/json' },
|
|
297
|
-
})
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
try {
|
|
301
|
-
const client = createR2Client(c.env)
|
|
302
|
-
const response = await client.send(
|
|
303
|
-
new GetObjectCommand({
|
|
304
|
-
Bucket: R2_BUCKET_NAME,
|
|
305
|
-
Key: decodeURIComponent(key),
|
|
306
|
-
})
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
if (!response.Body) {
|
|
310
|
-
return new Response(JSON.stringify({ error: 'Not found' }), {
|
|
311
|
-
status: 404,
|
|
312
|
-
headers: { 'Content-Type': 'application/json' },
|
|
313
|
-
})
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const headers = new Headers()
|
|
317
|
-
const ct = response.ContentType ?? 'application/octet-stream'
|
|
318
|
-
headers.set('Content-Type', ct)
|
|
319
|
-
headers.set('Cache-Control', 'public, max-age=31536000, immutable')
|
|
320
|
-
|
|
321
|
-
return new Response(response.Body as ReadableStream, {
|
|
322
|
-
status: 200,
|
|
323
|
-
headers,
|
|
324
|
-
})
|
|
325
|
-
} catch {
|
|
326
|
-
return new Response(JSON.stringify({ error: 'Not found' }), {
|
|
327
|
-
status: 404,
|
|
328
|
-
headers: { 'Content-Type': 'application/json' },
|
|
329
|
-
})
|
|
330
|
-
}
|
|
331
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Media Engine: upload e servizio file gestiti tramite BeechBucket e Repository.
|
|
3
|
+
*
|
|
4
|
+
* Astrae lo storage (R2/S3) e il database (D1) per garantire scalabilità
|
|
5
|
+
* e facilità di sviluppo locale.
|
|
6
|
+
*/
|
|
7
|
+
import { Hono } from 'hono'
|
|
8
|
+
import { logActivity } from './shared/activity-logger'
|
|
9
|
+
import { AppEnv } from './types'
|
|
10
|
+
|
|
11
|
+
/** Prefissi MIME consentiti (immagini e PDF) */
|
|
12
|
+
const ALLOWED_MIME_PREFIXES = ['image/', 'application/pdf']
|
|
13
|
+
|
|
14
|
+
/** Dimensione massima file: 5 MB */
|
|
15
|
+
const MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024
|
|
16
|
+
|
|
17
|
+
type FileLike = {
|
|
18
|
+
name: string
|
|
19
|
+
type: string
|
|
20
|
+
size: number
|
|
21
|
+
arrayBuffer: () => Promise<ArrayBuffer>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isFileLike(value: unknown): value is FileLike {
|
|
25
|
+
if (!value || typeof value === 'string') return false
|
|
26
|
+
const v = value as Record<string, unknown>
|
|
27
|
+
return (
|
|
28
|
+
typeof v.name === 'string' &&
|
|
29
|
+
typeof v.type === 'string' &&
|
|
30
|
+
typeof v.size === 'number' &&
|
|
31
|
+
typeof v.arrayBuffer === 'function'
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Sanitizza il nome file */
|
|
36
|
+
function sanitizeFilename(name: string): string {
|
|
37
|
+
const base = name.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 100)
|
|
38
|
+
return base || 'file'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Genera chiave univoca (timestamp-sanitized-name) */
|
|
42
|
+
function generateObjectKey(originalName: string): string {
|
|
43
|
+
const timestamp = Math.floor(Date.now() / 1000)
|
|
44
|
+
const sanitized = sanitizeFilename(originalName)
|
|
45
|
+
return `${timestamp}-${sanitized}`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Elimina oggetti dallo storage e tracciamento dal DB.
|
|
50
|
+
*/
|
|
51
|
+
export async function deleteR2Objects(
|
|
52
|
+
c: { var: { bucket: any, mediaRepository: any, systemStatsRepository: any } },
|
|
53
|
+
objectKeys: string[]
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
const { bucket, mediaRepository, systemStatsRepository } = c.var
|
|
56
|
+
|
|
57
|
+
for (const key of objectKeys) {
|
|
58
|
+
try {
|
|
59
|
+
const media = await mediaRepository.getByKey(key)
|
|
60
|
+
const size = media?.size_bytes ?? 0
|
|
61
|
+
|
|
62
|
+
await bucket.delete(key)
|
|
63
|
+
await mediaRepository.untrack(key)
|
|
64
|
+
|
|
65
|
+
if (size > 0) {
|
|
66
|
+
await systemStatsRepository.decrementStorage(size)
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.warn(`Failed to delete media object: ${key}`, err)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const uploadRoutes = new Hono<AppEnv>()
|
|
75
|
+
|
|
76
|
+
/** POST /upload - Carica file su BeechBucket */
|
|
77
|
+
uploadRoutes.post('/upload', async (c) => {
|
|
78
|
+
try {
|
|
79
|
+
const bucket = c.var.bucket
|
|
80
|
+
const mediaRepo = c.var.mediaRepository
|
|
81
|
+
const statsRepo = c.var.systemStatsRepository
|
|
82
|
+
|
|
83
|
+
const contentType = c.req.header('Content-Type') ?? ''
|
|
84
|
+
if (!contentType.includes('multipart/form-data')) {
|
|
85
|
+
return c.json({ error: 'Content-Type must be multipart/form-data' }, 400)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const formData = await c.req.formData()
|
|
89
|
+
const fileEntry = formData.get('file')
|
|
90
|
+
|
|
91
|
+
if (!isFileLike(fileEntry)) {
|
|
92
|
+
return c.json({ error: 'No file provided. Use field name "file"' }, 400)
|
|
93
|
+
}
|
|
94
|
+
const file = fileEntry
|
|
95
|
+
|
|
96
|
+
const mimeOk = ALLOWED_MIME_PREFIXES.some((prefix) => file.type.startsWith(prefix))
|
|
97
|
+
if (!mimeOk) {
|
|
98
|
+
return c.json({ error: 'File type not allowed. Allowed: images and PDF' }, 400)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (file.size > MAX_FILE_SIZE_BYTES) {
|
|
102
|
+
return c.json({ error: 'File too large. Max 5MB' }, 400)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const objectKey = generateObjectKey(file.name)
|
|
106
|
+
const body = await file.arrayBuffer()
|
|
107
|
+
|
|
108
|
+
// 1. Upload allo storage
|
|
109
|
+
await bucket.put(objectKey, body, { contentType: file.type })
|
|
110
|
+
|
|
111
|
+
// 2. Aggiorna DB e Stats (in background se possibile)
|
|
112
|
+
const uploadedBy = c.var.jwtPayload?.sub ?? ''
|
|
113
|
+
const trackOperation = (async () => {
|
|
114
|
+
try {
|
|
115
|
+
await statsRepo.incrementStorage(file.size)
|
|
116
|
+
await mediaRepo.trackUpload({
|
|
117
|
+
key: objectKey,
|
|
118
|
+
filename: file.name,
|
|
119
|
+
mime_type: file.type,
|
|
120
|
+
size_bytes: file.size,
|
|
121
|
+
uploaded_by: uploadedBy
|
|
122
|
+
})
|
|
123
|
+
} catch (err) {
|
|
124
|
+
console.error('Failed to update DB tracking for upload:', err)
|
|
125
|
+
}
|
|
126
|
+
})()
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
c.executionCtx.waitUntil(trackOperation)
|
|
130
|
+
} catch {
|
|
131
|
+
await trackOperation
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const publicUrl = bucket.getUrl(objectKey)
|
|
135
|
+
|
|
136
|
+
logActivity(c, {
|
|
137
|
+
action: 'upload',
|
|
138
|
+
entityType: 'media',
|
|
139
|
+
entityId: objectKey,
|
|
140
|
+
details: { name: file.name, size: file.size, type: file.type }
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
return c.json({ url: publicUrl }, 200)
|
|
144
|
+
} catch (err) {
|
|
145
|
+
console.error('Upload error:', err)
|
|
146
|
+
return c.json({ error: 'Upload failed' }, 500)
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
/** DELETE /upload/:key - Elimina un file */
|
|
151
|
+
uploadRoutes.delete('/upload/:key', async (c) => {
|
|
152
|
+
const key = c.req.param('key')
|
|
153
|
+
if (!key) return c.json({ error: 'Missing key' }, 400)
|
|
154
|
+
|
|
155
|
+
await deleteR2Objects(c, [decodeURIComponent(key)])
|
|
156
|
+
return c.json({ success: true }, 200)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Serve un file dallo storage.
|
|
161
|
+
*/
|
|
162
|
+
export async function serveMediaHandler(c: any): Promise<Response> {
|
|
163
|
+
const key = c.req.param('key')
|
|
164
|
+
if (!key) return new Response('Missing key', { status: 400 })
|
|
165
|
+
|
|
166
|
+
const bucket = c.var.bucket
|
|
167
|
+
try {
|
|
168
|
+
const object = await bucket.get(decodeURIComponent(key))
|
|
169
|
+
if (!object) return new Response('Not found', { status: 404 })
|
|
170
|
+
|
|
171
|
+
const headers = new Headers()
|
|
172
|
+
headers.set('Content-Type', object.contentType ?? 'application/octet-stream')
|
|
173
|
+
headers.set('Cache-Control', 'public, max-age=31536000, immutable')
|
|
174
|
+
|
|
175
|
+
return new Response(object.body, { status: 200, headers })
|
|
176
|
+
} catch (err) {
|
|
177
|
+
return new Response('Internal error', { status: 500 })
|
|
178
|
+
}
|
|
179
|
+
}
|