@cardinali-studio/adminkit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +195 -0
- package/dist/api.js +118 -0
- package/dist/app/App.d.ts +3 -0
- package/dist/app/App.js +320 -0
- package/dist/app/app.css +2068 -0
- package/dist/app/index.d.ts +18 -0
- package/dist/app/index.js +33 -0
- package/dist/app/lib/bookings.d.ts +132 -0
- package/dist/app/lib/bookings.js +79 -0
- package/dist/app/lib/deliveries.d.ts +95 -0
- package/dist/app/lib/deliveries.js +104 -0
- package/dist/app/lib/delivery-drop.d.ts +1 -0
- package/dist/app/lib/delivery-drop.js +72 -0
- package/dist/app/lib/delivery-upload.d.ts +78 -0
- package/dist/app/lib/delivery-upload.js +585 -0
- package/dist/app/lib/groups.d.ts +9 -0
- package/dist/app/lib/groups.js +32 -0
- package/dist/app/lib/home.d.ts +11 -0
- package/dist/app/lib/home.js +67 -0
- package/dist/app/lib/overview.d.ts +82 -0
- package/dist/app/lib/overview.js +68 -0
- package/dist/app/lib/pages.d.ts +24 -0
- package/dist/app/lib/pages.js +156 -0
- package/dist/app/lib/publish.d.ts +18 -0
- package/dist/app/lib/publish.js +63 -0
- package/dist/app/lib/reorder.d.ts +3 -0
- package/dist/app/lib/reorder.js +31 -0
- package/dist/app/lib/requests.d.ts +10 -0
- package/dist/app/lib/requests.js +67 -0
- package/dist/app/lib/router.d.ts +1 -0
- package/dist/app/lib/router.js +14 -0
- package/dist/app/lib/settings.d.ts +29 -0
- package/dist/app/lib/settings.js +82 -0
- package/dist/app/lib/slug.d.ts +3 -0
- package/dist/app/lib/slug.js +23 -0
- package/dist/app/lib/text.d.ts +1 -0
- package/dist/app/lib/text.js +13 -0
- package/dist/app/lib/theme.d.ts +7 -0
- package/dist/app/lib/theme.js +43 -0
- package/dist/app/lib/upload-queue.d.ts +35 -0
- package/dist/app/lib/upload-queue.js +167 -0
- package/dist/app/routes/AboutEditor.d.ts +15 -0
- package/dist/app/routes/AboutEditor.js +122 -0
- package/dist/app/routes/Bookings.d.ts +21 -0
- package/dist/app/routes/Bookings.js +134 -0
- package/dist/app/routes/Deliveries.d.ts +37 -0
- package/dist/app/routes/Deliveries.js +563 -0
- package/dist/app/routes/GallerySectionsEditor.d.ts +12 -0
- package/dist/app/routes/GallerySectionsEditor.js +107 -0
- package/dist/app/routes/HomeEditor.d.ts +10 -0
- package/dist/app/routes/HomeEditor.js +188 -0
- package/dist/app/routes/Overview.d.ts +17 -0
- package/dist/app/routes/Overview.js +318 -0
- package/dist/app/routes/PageActions.d.ts +12 -0
- package/dist/app/routes/PageActions.js +93 -0
- package/dist/app/routes/PageEditor.d.ts +23 -0
- package/dist/app/routes/PageEditor.js +233 -0
- package/dist/app/routes/PageSeoEditor.d.ts +13 -0
- package/dist/app/routes/PageSeoEditor.js +102 -0
- package/dist/app/routes/PagesList.d.ts +10 -0
- package/dist/app/routes/PagesList.js +76 -0
- package/dist/app/routes/PortfolioWorksEditor.d.ts +12 -0
- package/dist/app/routes/PortfolioWorksEditor.js +139 -0
- package/dist/app/routes/Requests.d.ts +13 -0
- package/dist/app/routes/Requests.js +30 -0
- package/dist/app/routes/SiteSettingsEditor.d.ts +21 -0
- package/dist/app/routes/SiteSettingsEditor.js +66 -0
- package/dist/app/routes/UploadDock.d.ts +1 -0
- package/dist/app/routes/UploadDock.js +66 -0
- package/dist/app/routes/WorkshopEditionsEditor.d.ts +12 -0
- package/dist/app/routes/WorkshopEditionsEditor.js +101 -0
- package/dist/app/site.d.ts +37 -0
- package/dist/app/site.js +18 -0
- package/dist/app/ui/MediaSlot.d.ts +10 -0
- package/dist/app/ui/MediaSlot.js +20 -0
- package/dist/app/ui/OpenCard.d.ts +18 -0
- package/dist/app/ui/OpenCard.js +20 -0
- package/dist/app/ui/ParagraphEditor.d.ts +8 -0
- package/dist/app/ui/ParagraphEditor.js +37 -0
- package/dist/app/ui/dnd.d.ts +1 -0
- package/dist/app/ui/dnd.js +11 -0
- package/dist/app/ui/icons.d.ts +26 -0
- package/dist/app/ui/icons.js +45 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +26 -0
- package/dist/form/MediaBrowser.d.ts +13 -0
- package/dist/form/MediaBrowser.js +24 -0
- package/dist/form/MediaPicker.d.ts +10 -0
- package/dist/form/MediaPicker.js +20 -0
- package/dist/form/SchemaForm.d.ts +21 -0
- package/dist/form/SchemaForm.js +67 -0
- package/dist/form/Trash.d.ts +1 -0
- package/dist/form/Trash.js +42 -0
- package/dist/form/fields/index.d.ts +1 -0
- package/dist/form/fields/index.js +110 -0
- package/dist/form/fields/objectList.d.ts +1 -0
- package/dist/form/fields/objectList.js +99 -0
- package/dist/form/index.d.ts +8 -0
- package/dist/form/index.js +8 -0
- package/dist/form/media/Lightbox.d.ts +4 -0
- package/dist/form/media/Lightbox.js +70 -0
- package/dist/form/media/MediaFolders.d.ts +20 -0
- package/dist/form/media/MediaFolders.js +116 -0
- package/dist/form/media/MediaGrid.d.ts +8 -0
- package/dist/form/media/MediaGrid.js +139 -0
- package/dist/form/media/UploadDialog.d.ts +4 -0
- package/dist/form/media/UploadDialog.js +37 -0
- package/dist/form/media/useMediaLibrary.d.ts +55 -0
- package/dist/form/media/useMediaLibrary.js +495 -0
- package/dist/form/prose/ProseBar.d.ts +21 -0
- package/dist/form/prose/ProseBar.js +111 -0
- package/dist/form/prose/ProseField.d.ts +11 -0
- package/dist/form/prose/ProseField.js +152 -0
- package/dist/form/prose/pages.d.ts +6 -0
- package/dist/form/prose/pages.js +13 -0
- package/dist/form/registry.d.ts +20 -0
- package/dist/form/registry.js +15 -0
- package/dist/frontmatter.d.ts +20 -0
- package/dist/frontmatter.js +202 -0
- package/dist/functions/_middleware.js +101 -0
- package/dist/functions/_shared/analytics-ga4.js +248 -0
- package/dist/functions/_shared/analytics-umami.js +119 -0
- package/dist/functions/_shared/assets.js +90 -0
- package/dist/functions/_shared/b2.js +300 -0
- package/dist/functions/_shared/delivery-crypto.js +99 -0
- package/dist/functions/_shared/delivery-purge.js +222 -0
- package/dist/functions/_shared/edge-cache.js +37 -0
- package/dist/functions/_shared/exif.js +53 -0
- package/dist/functions/_shared/github.js +138 -0
- package/dist/functions/_shared/imagesize.js +101 -0
- package/dist/functions/_shared/media-content.js +200 -0
- package/dist/functions/_shared/pending-media.js +98 -0
- package/dist/functions/_shared/refs.js +155 -0
- package/dist/functions/api/analytics.js +106 -0
- package/dist/functions/api/assets/delete.js +241 -0
- package/dist/functions/api/assets/folder.js +43 -0
- package/dist/functions/api/assets/folders.js +32 -0
- package/dist/functions/api/assets/move.js +253 -0
- package/dist/functions/api/assets/preview.js +17 -0
- package/dist/functions/api/assets/trash/delete.js +13 -0
- package/dist/functions/api/assets/trash/empty.js +16 -0
- package/dist/functions/api/assets/trash/restore.js +43 -0
- package/dist/functions/api/assets/trash.js +28 -0
- package/dist/functions/api/assets/upload.js +54 -0
- package/dist/functions/api/assets/usage.js +41 -0
- package/dist/functions/api/assets.js +94 -0
- package/dist/functions/api/deliveries.js +629 -0
- package/dist/functions/api/delivery/hero.js +21 -0
- package/dist/functions/api/delivery/image.js +35 -0
- package/dist/functions/api/delivery/multipart.js +261 -0
- package/dist/functions/api/pages/get.js +26 -0
- package/dist/functions/api/pages.js +354 -0
- package/dist/functions/api/publish.js +134 -0
- package/dist/functions/api/requests.js +57 -0
- package/dist/functions/api/settings.js +236 -0
- package/dist/functions/api/singleton.js +105 -0
- package/dist/functions/api/storage.js +125 -0
- package/dist/functions/index.js +124 -0
- package/dist/functions/site.js +38 -0
- package/dist/gallery.d.ts +28 -0
- package/dist/gallery.js +18 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/model.d.ts +2 -0
- package/dist/model.js +47 -0
- package/dist/normalize.d.ts +50 -0
- package/dist/normalize.js +120 -0
- package/dist/preview.d.ts +3 -0
- package/dist/preview.js +82 -0
- package/dist/prose.d.ts +17 -0
- package/dist/prose.js +205 -0
- package/dist/registries.d.ts +43 -0
- package/dist/registries.js +29 -0
- package/dist/serialize.d.ts +26 -0
- package/dist/serialize.js +136 -0
- package/dist/testing.d.ts +26 -0
- package/dist/testing.js +66 -0
- package/dist/theme.css +636 -0
- package/dist/yaml.d.ts +8 -0
- package/dist/yaml.js +90 -0
- package/package.json +97 -0
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
/* CRUD immediato delle consegne.
|
|
2
|
+
*
|
|
3
|
+
* Sta sotto /admin/: Access + verifica JWT sono già applicati dal middleware.
|
|
4
|
+
* Nessun salvataggio tocca Git. Le operazioni sui byte sono versionate e i delete
|
|
5
|
+
* cancellano subito, nella stessa richiesta: non c'è un cestino dei transfer, e
|
|
6
|
+
* rimandare non serviva a nessuno. La tombstone D1 sopravvive solo il tempo del
|
|
7
|
+
* lavoro su B2, come punto di ripresa se una chiamata fallisce.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { b2ObjectKey, b2PutSmall } from "../_shared/b2.js";
|
|
11
|
+
import { randomToken, seal, sha256, unseal } from "../_shared/delivery-crypto.js";
|
|
12
|
+
/* Il lavoro di cancellazione sta in _shared perché lo condivide con il cron:
|
|
13
|
+
* workers/delivery-housekeeping. Qui si usa nelle due direzioni — subito dopo
|
|
14
|
+
* una delete, e in coda a una GET per finire ciò che si fosse fermato. */
|
|
15
|
+
import { purgeDeliveryNow, purgeDue, purgePackageNow } from "../_shared/delivery-purge.js";
|
|
16
|
+
|
|
17
|
+
const DAY = 86_400_000;
|
|
18
|
+
|
|
19
|
+
export async function onRequestGet({ request, env, waitUntil }) {
|
|
20
|
+
if (!env.DB) return json({ ok: false, error: "Il servizio dei transfer non è configurato" }, 500);
|
|
21
|
+
if (typeof waitUntil === "function") {
|
|
22
|
+
waitUntil(purgeDue(env).catch((error) => console.error("delivery purge:", error)));
|
|
23
|
+
}
|
|
24
|
+
const id = new URL(request.url).searchParams.get("id");
|
|
25
|
+
try {
|
|
26
|
+
if (id) {
|
|
27
|
+
const delivery = await deliveryDetail(env, id, new URL(request.url).origin);
|
|
28
|
+
return delivery ? json({ ok: true, delivery }) : json({ ok: false, error: "Transfer non trovato" }, 404);
|
|
29
|
+
}
|
|
30
|
+
const { results } = await env.DB.prepare(`
|
|
31
|
+
SELECT d.*,
|
|
32
|
+
(SELECT COUNT(*) FROM delivery_packages p WHERE p.delivery_id=d.id AND p.state='ready') package_count,
|
|
33
|
+
(SELECT COALESCE(SUM(p.stored_bytes),0) FROM delivery_packages p WHERE p.delivery_id=d.id AND p.state='ready') stored_bytes,
|
|
34
|
+
(SELECT COUNT(*) FROM delivery_links l WHERE l.delivery_id=d.id AND l.revoked_at IS NULL) link_count,
|
|
35
|
+
(SELECT COALESCE(SUM(l.download_count),0) FROM delivery_links l WHERE l.delivery_id=d.id) download_count
|
|
36
|
+
FROM deliveries d
|
|
37
|
+
WHERE d.status != 'deleted'
|
|
38
|
+
ORDER BY d.updated_at DESC
|
|
39
|
+
`).all();
|
|
40
|
+
const deliveries = (results || []).map(normalizeDelivery);
|
|
41
|
+
const inSevenDays = new Date(Date.now() + 7 * DAY).toISOString();
|
|
42
|
+
const expiring = deliveries.filter((d) =>
|
|
43
|
+
d.status === "active" && d.filesExpiresAt && d.filesExpiresAt <= inSevenDays &&
|
|
44
|
+
d.filesExpiresAt > new Date().toISOString()).length;
|
|
45
|
+
return json({ ok: true, deliveries, expiring, settings: await deliverySettings(env) });
|
|
46
|
+
} catch (e) {
|
|
47
|
+
console.error("deliveries get:", e);
|
|
48
|
+
return json({ ok: false, error: message(e) }, 500);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function onRequestPost({ request, env }) {
|
|
53
|
+
if (!env.DB) return json({ ok: false, error: "Il servizio dei transfer non è configurato" }, 500);
|
|
54
|
+
let body;
|
|
55
|
+
try { body = await request.json(); }
|
|
56
|
+
catch { return json({ ok: false, error: "Corpo JSON non valido" }, 400); }
|
|
57
|
+
const action = String(body?.action || "");
|
|
58
|
+
try {
|
|
59
|
+
switch (action) {
|
|
60
|
+
case "create": return json({ ok: true, delivery: await createDelivery(env, body, new URL(request.url).origin) });
|
|
61
|
+
case "quickCreate": return json({ ok: true, delivery: await createQuickDelivery(env, body, new URL(request.url).origin) });
|
|
62
|
+
case "update": return json({ ok: true, delivery: await updateDelivery(env, body, new URL(request.url).origin) });
|
|
63
|
+
case "activate": return json({ ok: true, delivery: await activateDelivery(env, body, new URL(request.url).origin) });
|
|
64
|
+
case "archive": return json({ ok: true, delivery: await archiveDelivery(env, body, new URL(request.url).origin) });
|
|
65
|
+
case "extend": return json({ ok: true, delivery: await extendDelivery(env, body, new URL(request.url).origin) });
|
|
66
|
+
case "duplicate": return json({ ok: true, delivery: await duplicateDelivery(env, body, new URL(request.url).origin) });
|
|
67
|
+
case "delete": return json({ ok: true, ...(await deleteDelivery(env, body)) });
|
|
68
|
+
case "createPackage": return json({ ok: true, package: await createPackage(env, body) });
|
|
69
|
+
case "finalizePackage": return json({ ok: true, package: await finalizePackage(env, body) });
|
|
70
|
+
case "deletePackage": return json({ ok: true, ...(await deletePackage(env, body)) });
|
|
71
|
+
case "commitHeroes": return json({ ok: true, delivery: await commitHeroes(env, body, new URL(request.url).origin) });
|
|
72
|
+
case "saveSettings": return json({ ok: true, settings: await saveSettings(env, body) });
|
|
73
|
+
case "createLink": return json({ ok: true, link: await createLink(env, body, new URL(request.url).origin) });
|
|
74
|
+
case "updateLink": return json({ ok: true, link: await updateLink(env, body, new URL(request.url).origin) });
|
|
75
|
+
case "revokeLink": return json({ ok: true, ...(await revokeLink(env, body)) });
|
|
76
|
+
default: return json({ ok: false, error: "Azione non valida" }, 400);
|
|
77
|
+
}
|
|
78
|
+
} catch (e) {
|
|
79
|
+
console.error(`deliveries ${action}:`, e);
|
|
80
|
+
const status = e instanceof InputError ? 400 : e instanceof ConflictError ? 409 : 500;
|
|
81
|
+
return json({ ok: false, error: message(e), code: status === 409 ? "conflict" : "invalid" }, status);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function createQuickDelivery(env, body, origin) {
|
|
86
|
+
const title = required(body.title, "Il nome del transfer è obbligatorio");
|
|
87
|
+
const id = crypto.randomUUID();
|
|
88
|
+
const now = new Date().toISOString();
|
|
89
|
+
await env.DB.prepare(`
|
|
90
|
+
INSERT INTO deliveries
|
|
91
|
+
(id,client_name,title,files_expires_at,status,created_at,updated_at)
|
|
92
|
+
VALUES (?,'',?,?,'uploading',?,?)
|
|
93
|
+
`).bind(id, title, nullableDate(body.filesExpiresAt), now, now).run();
|
|
94
|
+
return deliveryDetail(env, id, origin);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function createDelivery(env, body, origin) {
|
|
98
|
+
const client = required(body.clientName, "Il cliente è obbligatorio");
|
|
99
|
+
const title = required(body.title, "Il transfer ha bisogno di un nome");
|
|
100
|
+
const id = crypto.randomUUID();
|
|
101
|
+
const now = new Date().toISOString();
|
|
102
|
+
await env.DB.prepare(`
|
|
103
|
+
INSERT INTO deliveries
|
|
104
|
+
(id,client_name,title,description,license_notes,cta_enabled,cta_label,cta_url,
|
|
105
|
+
expires_at,files_expires_at,status,created_at,updated_at)
|
|
106
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,'draft',?,?)
|
|
107
|
+
`).bind(
|
|
108
|
+
id, client, title, text(body.description), text(body.licenseNotes),
|
|
109
|
+
bool(body.ctaEnabled), text(body.ctaLabel) || "Lascia una recensione",
|
|
110
|
+
optionalHttpUrl(body.ctaUrl), nullableDate(body.expiresAt),
|
|
111
|
+
nullableDate(body.filesExpiresAt), now, now,
|
|
112
|
+
).run();
|
|
113
|
+
return deliveryDetail(env, id, origin);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function updateDelivery(env, body, origin) {
|
|
117
|
+
const id = required(body.id, "id del transfer mancante");
|
|
118
|
+
const current = await env.DB.prepare("SELECT version,status FROM deliveries WHERE id=? AND status!='deleted'")
|
|
119
|
+
.bind(id).first();
|
|
120
|
+
if (!current) throw new InputError("Transfer non trovato");
|
|
121
|
+
if (body.version && Number(body.version) !== Number(current.version)) {
|
|
122
|
+
throw new ConflictError("Questo transfer è stato modificato in un’altra scheda. Ricarica la pagina.");
|
|
123
|
+
}
|
|
124
|
+
const client = text(body.clientName);
|
|
125
|
+
const title = required(body.title, "Il transfer ha bisogno di un nome");
|
|
126
|
+
await env.DB.prepare(`
|
|
127
|
+
UPDATE deliveries SET
|
|
128
|
+
client_name=?, title=?, description=?, license_notes=?,
|
|
129
|
+
cta_enabled=?, cta_label=?, cta_url=?, expires_at=?, files_expires_at=?,
|
|
130
|
+
version=version+1, updated_at=?
|
|
131
|
+
WHERE id=? AND status!='deleted'
|
|
132
|
+
`).bind(
|
|
133
|
+
client, title, text(body.description), text(body.licenseNotes),
|
|
134
|
+
bool(body.ctaEnabled), text(body.ctaLabel) || "Lascia una recensione",
|
|
135
|
+
optionalHttpUrl(body.ctaUrl), nullableDate(body.expiresAt), nullableDate(body.filesExpiresAt),
|
|
136
|
+
new Date().toISOString(), id,
|
|
137
|
+
).run();
|
|
138
|
+
if (body.syncPackageLabel) {
|
|
139
|
+
await env.DB.prepare(`
|
|
140
|
+
UPDATE delivery_packages SET label=?,updated_at=?
|
|
141
|
+
WHERE delivery_id=? AND position=(
|
|
142
|
+
SELECT MIN(position) FROM delivery_packages WHERE delivery_id=?
|
|
143
|
+
)
|
|
144
|
+
`).bind(title, new Date().toISOString(), id, id).run();
|
|
145
|
+
}
|
|
146
|
+
return deliveryDetail(env, id, origin);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function activateDelivery(env, body, origin) {
|
|
150
|
+
const id = required(body.id, "id del transfer mancante");
|
|
151
|
+
const ready = await env.DB.prepare(
|
|
152
|
+
"SELECT COUNT(*) n FROM delivery_packages WHERE delivery_id=? AND state='ready' AND originals_key IS NOT NULL"
|
|
153
|
+
).bind(id).first();
|
|
154
|
+
if (!ready?.n) throw new InputError("Prima carica almeno un file");
|
|
155
|
+
const now = new Date().toISOString();
|
|
156
|
+
await env.DB.batch([
|
|
157
|
+
env.DB.prepare(`
|
|
158
|
+
UPDATE deliveries SET status='active', activated_at=COALESCE(activated_at,?),
|
|
159
|
+
updated_at=?, version=version+1 WHERE id=? AND status!='deleted'
|
|
160
|
+
`).bind(now, now, id),
|
|
161
|
+
env.DB.prepare(`
|
|
162
|
+
UPDATE delivery_packages SET originals_purge_after=NULL
|
|
163
|
+
WHERE delivery_id=? AND originals_key IS NOT NULL
|
|
164
|
+
`).bind(id),
|
|
165
|
+
]);
|
|
166
|
+
const link = await env.DB.prepare(
|
|
167
|
+
"SELECT id FROM delivery_links WHERE delivery_id=? AND revoked_at IS NULL LIMIT 1"
|
|
168
|
+
).bind(id).first();
|
|
169
|
+
if (!link) await createLink(env, { id, label: "Link principale" }, origin);
|
|
170
|
+
return deliveryDetail(env, id, origin);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function archiveDelivery(env, body, origin) {
|
|
174
|
+
const id = required(body.id, "id del transfer mancante");
|
|
175
|
+
const current = await env.DB.prepare("SELECT status FROM deliveries WHERE id=?").bind(id).first();
|
|
176
|
+
if (current?.status !== "active") throw new InputError("Gli originali si possono togliere solo da un transfer già pronto");
|
|
177
|
+
const now = new Date();
|
|
178
|
+
const purge = new Date(now.getTime() + 7 * DAY).toISOString();
|
|
179
|
+
await env.DB.batch([
|
|
180
|
+
env.DB.prepare(`
|
|
181
|
+
UPDATE deliveries SET status='archived',updated_at=?,version=version+1
|
|
182
|
+
WHERE id=? AND status='active'
|
|
183
|
+
`).bind(now.toISOString(), id),
|
|
184
|
+
env.DB.prepare(`
|
|
185
|
+
UPDATE delivery_packages SET originals_purge_after=?
|
|
186
|
+
WHERE delivery_id=? AND state='ready' AND originals_key IS NOT NULL
|
|
187
|
+
`).bind(purge, id),
|
|
188
|
+
]);
|
|
189
|
+
return deliveryDetail(env, id, origin);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function extendDelivery(env, body, origin) {
|
|
193
|
+
const id = required(body.id, "id del transfer mancante");
|
|
194
|
+
const row = await env.DB.prepare(
|
|
195
|
+
"SELECT files_expires_at FROM deliveries WHERE id=? AND status!='deleted'"
|
|
196
|
+
).bind(id).first();
|
|
197
|
+
if (!row) throw new InputError("Transfer non trovato");
|
|
198
|
+
const requestedDays = Number(body.days || 30);
|
|
199
|
+
if (!Number.isInteger(requestedDays)) throw new InputError("Durata non valida");
|
|
200
|
+
const days = Math.min(365, Math.max(1, requestedDays));
|
|
201
|
+
const base = row.files_expires_at && new Date(row.files_expires_at).getTime() > Date.now()
|
|
202
|
+
? new Date(row.files_expires_at).getTime() : Date.now();
|
|
203
|
+
const expires = new Date(base + days * DAY).toISOString();
|
|
204
|
+
await env.DB.prepare(
|
|
205
|
+
"UPDATE deliveries SET files_expires_at=?,updated_at=?,version=version+1 WHERE id=?"
|
|
206
|
+
).bind(expires, new Date().toISOString(), id).run();
|
|
207
|
+
return deliveryDetail(env, id, origin);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function duplicateDelivery(env, body, origin) {
|
|
211
|
+
const source = await env.DB.prepare(
|
|
212
|
+
"SELECT * FROM deliveries WHERE id=? AND status!='deleted'"
|
|
213
|
+
).bind(required(body.id, "id del transfer mancante")).first();
|
|
214
|
+
if (!source) throw new InputError("Transfer non trovato");
|
|
215
|
+
return createDelivery(env, {
|
|
216
|
+
clientName: source.client_name,
|
|
217
|
+
title: `${source.title} — copia`,
|
|
218
|
+
description: source.description,
|
|
219
|
+
licenseNotes: source.license_notes,
|
|
220
|
+
ctaEnabled: !!source.cta_enabled,
|
|
221
|
+
ctaLabel: source.cta_label,
|
|
222
|
+
ctaUrl: source.cta_url,
|
|
223
|
+
expiresAt: source.expires_at,
|
|
224
|
+
filesExpiresAt: source.files_expires_at,
|
|
225
|
+
}, origin);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* Elimina davvero, subito: niente finestra di grazia. Non esiste un cestino dei
|
|
229
|
+
* transfer, quindi una tombstone di sette giorni non era recuperabile da
|
|
230
|
+
* nessuna parte — prometteva soltanto un ripensamento impossibile e lasciava i
|
|
231
|
+
* byte su B2 finché qualcuno non riapriva il pannello. La tombstone resta come
|
|
232
|
+
* rete: se una chiamata a B2 fallisce, `purge_after` è già scaduto e
|
|
233
|
+
* l'housekeeping riprende il lavoro dov'era. */
|
|
234
|
+
async function deleteDelivery(env, body) {
|
|
235
|
+
const id = required(body.id, "id del transfer mancante");
|
|
236
|
+
const now = new Date().toISOString();
|
|
237
|
+
await env.DB.batch([
|
|
238
|
+
env.DB.prepare("UPDATE deliveries SET status='deleted',deleted_at=?,purge_after=?,updated_at=? WHERE id=?")
|
|
239
|
+
.bind(now, now, now, id),
|
|
240
|
+
env.DB.prepare("UPDATE delivery_links SET revoked_at=COALESCE(revoked_at,?) WHERE delivery_id=?")
|
|
241
|
+
.bind(now, id),
|
|
242
|
+
env.DB.prepare("UPDATE delivery_packages SET state='deleted',deleted_at=?,purge_after=? WHERE delivery_id=? AND state!='deleted'")
|
|
243
|
+
.bind(now, now, id),
|
|
244
|
+
env.DB.prepare("UPDATE delivery_hero_objects SET state='deleted',purge_after=?,updated_at=? WHERE delivery_id=? AND state!='deleted'")
|
|
245
|
+
.bind(now, now, id),
|
|
246
|
+
]);
|
|
247
|
+
return { purged: await purgeDeliveryNow(env, id) };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function createPackage(env, body) {
|
|
251
|
+
const deliveryId = required(body.id, "id del transfer mancante");
|
|
252
|
+
const delivery = await env.DB.prepare("SELECT id,status FROM deliveries WHERE id=? AND status!='deleted'")
|
|
253
|
+
.bind(deliveryId).first();
|
|
254
|
+
if (!delivery) throw new InputError("Transfer non trovato");
|
|
255
|
+
const id = crypto.randomUUID();
|
|
256
|
+
const prefix = `deliveries/${deliveryId}/${id}/v1`;
|
|
257
|
+
const now = new Date().toISOString();
|
|
258
|
+
const pos = await env.DB.prepare(
|
|
259
|
+
"SELECT COALESCE(MAX(position),-1)+1 n FROM delivery_packages WHERE delivery_id=?"
|
|
260
|
+
).bind(deliveryId).first();
|
|
261
|
+
await env.DB.batch([
|
|
262
|
+
env.DB.prepare(`
|
|
263
|
+
INSERT INTO delivery_packages
|
|
264
|
+
(id,delivery_id,label,position,gallery_enabled,web_enabled,state,storage_prefix,
|
|
265
|
+
source_fingerprint,file_count,source_bytes,created_at,updated_at)
|
|
266
|
+
VALUES (?,?,?,?,?,?,'uploading',?,?,?,?,?,?)
|
|
267
|
+
`).bind(
|
|
268
|
+
id, deliveryId, text(body.label) || "Fotografie", Number(pos?.n || 0),
|
|
269
|
+
bool(body.galleryEnabled), bool(body.webEnabled), prefix,
|
|
270
|
+
required(body.sourceFingerprint, "impronta dei file mancante"),
|
|
271
|
+
Number(body.fileCount || 0), Number(body.sourceBytes || 0), now, now,
|
|
272
|
+
),
|
|
273
|
+
env.DB.prepare("UPDATE deliveries SET status=CASE WHEN status='draft' THEN 'uploading' ELSE status END,updated_at=? WHERE id=?")
|
|
274
|
+
.bind(now, deliveryId),
|
|
275
|
+
]);
|
|
276
|
+
return normalizePackage(await env.DB.prepare("SELECT * FROM delivery_packages WHERE id=?").bind(id).first());
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function finalizePackage(env, body) {
|
|
280
|
+
const deliveryId = required(body.id, "id del transfer mancante");
|
|
281
|
+
const packageId = required(body.packageId, "id dei file mancante");
|
|
282
|
+
const pkg = await env.DB.prepare(
|
|
283
|
+
"SELECT * FROM delivery_packages WHERE id=? AND delivery_id=? AND state='uploading'"
|
|
284
|
+
).bind(packageId, deliveryId).first();
|
|
285
|
+
if (!pkg) throw new InputError("Non trovo il caricamento da completare");
|
|
286
|
+
const uploads = await env.DB.prepare(
|
|
287
|
+
"SELECT variant,object_key,state FROM delivery_uploads WHERE package_id=?"
|
|
288
|
+
).bind(packageId).all();
|
|
289
|
+
const byVariant = Object.fromEntries((uploads.results || []).map((u) => [u.variant, u]));
|
|
290
|
+
if (byVariant.originals?.state !== "completed") throw new InputError("Il caricamento delle foto non è arrivato in fondo");
|
|
291
|
+
if ((pkg.web_enabled || pkg.gallery_enabled) && byVariant.web?.state !== "completed") {
|
|
292
|
+
throw new InputError("La versione leggera non è arrivata in fondo");
|
|
293
|
+
}
|
|
294
|
+
if (pkg.gallery_enabled && byVariant.thumbs?.state !== "completed") throw new InputError("Le anteprime non sono arrivate in fondo");
|
|
295
|
+
|
|
296
|
+
const manifest = body.manifest && typeof body.manifest === "object" ? body.manifest : null;
|
|
297
|
+
if (!manifest || !Array.isArray(manifest.entries)) throw new InputError("L’elenco delle foto è arrivato incompleto");
|
|
298
|
+
const manifestJson = JSON.stringify(manifest);
|
|
299
|
+
if (manifestJson.length > 1_800_000) throw new InputError("Troppe foto in un transfer solo: dividile in due invii");
|
|
300
|
+
const manifestKey = b2ObjectKey(pkg.storage_prefix, "manifest.json");
|
|
301
|
+
await b2PutSmall(env, manifestKey, manifestJson, "application/json");
|
|
302
|
+
|
|
303
|
+
const now = new Date().toISOString();
|
|
304
|
+
await env.DB.prepare(`
|
|
305
|
+
UPDATE delivery_packages SET state='ready', originals_key=?, web_key=?, thumbs_key=?,
|
|
306
|
+
manifest_key=?, manifest_json=?, file_count=?, source_bytes=?, stored_bytes=?, updated_at=?
|
|
307
|
+
WHERE id=?
|
|
308
|
+
`).bind(
|
|
309
|
+
byVariant.originals.object_key,
|
|
310
|
+
byVariant.web?.object_key || null,
|
|
311
|
+
byVariant.thumbs?.object_key || null,
|
|
312
|
+
manifestKey, manifestJson, Number(body.fileCount || manifest.entries.length),
|
|
313
|
+
Number(body.sourceBytes || 0), Number(body.storedBytes || 0), now, packageId,
|
|
314
|
+
).run();
|
|
315
|
+
return normalizePackage(await env.DB.prepare("SELECT * FROM delivery_packages WHERE id=?").bind(packageId).first());
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function deletePackage(env, body) {
|
|
319
|
+
const packageId = required(body.packageId, "id dei file mancante");
|
|
320
|
+
const now = new Date().toISOString();
|
|
321
|
+
await env.DB.batch([
|
|
322
|
+
env.DB.prepare(`
|
|
323
|
+
UPDATE delivery_packages SET state='deleted',deleted_at=?,purge_after=?,updated_at=? WHERE id=?
|
|
324
|
+
`).bind(now, now, now, packageId),
|
|
325
|
+
env.DB.prepare(`
|
|
326
|
+
UPDATE delivery_hero_objects SET state='deleted',purge_after=?,updated_at=?
|
|
327
|
+
WHERE package_id=? AND state!='deleted'
|
|
328
|
+
`).bind(now, now, packageId),
|
|
329
|
+
]);
|
|
330
|
+
return { purged: await purgePackageNow(env, packageId) };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function commitHeroes(env, body, origin) {
|
|
334
|
+
const deliveryId = required(body.id, "id del transfer mancante");
|
|
335
|
+
const requested = Array.isArray(body.heroes) ? body.heroes.slice(0, 8) : [];
|
|
336
|
+
const delivery = await env.DB.prepare(
|
|
337
|
+
"SELECT hero_json FROM deliveries WHERE id=? AND status!='deleted'"
|
|
338
|
+
).bind(deliveryId).first();
|
|
339
|
+
if (!delivery) throw new InputError("Transfer non trovato");
|
|
340
|
+
const sanitized = [];
|
|
341
|
+
const objectUpdates = [];
|
|
342
|
+
for (const item of requested) {
|
|
343
|
+
if (item?.type === "package") {
|
|
344
|
+
const pkg = await env.DB.prepare(`
|
|
345
|
+
SELECT manifest_json FROM delivery_packages
|
|
346
|
+
WHERE id=? AND delivery_id=? AND state='ready' AND web_key IS NOT NULL
|
|
347
|
+
`).bind(text(item.packageId), deliveryId).first();
|
|
348
|
+
const index = Number(item.index);
|
|
349
|
+
const entry = parseManifest(pkg?.manifest_json)?.entries?.[index];
|
|
350
|
+
if (!pkg || !Number.isInteger(index) || index < 0 || !entry?.web) {
|
|
351
|
+
throw new InputError("Una delle foto di copertina non è più disponibile");
|
|
352
|
+
}
|
|
353
|
+
sanitized.push({ type: "package", packageId: text(item.packageId), index });
|
|
354
|
+
} else if (item?.type === "object") {
|
|
355
|
+
const id = text(item.id);
|
|
356
|
+
const row = await env.DB.prepare(`
|
|
357
|
+
SELECT id FROM delivery_hero_objects
|
|
358
|
+
WHERE id=? AND delivery_id=? AND state IN ('pending','ready')
|
|
359
|
+
`).bind(id, deliveryId).first();
|
|
360
|
+
if (!row) throw new InputError("Una delle immagini di copertina caricate non è disponibile");
|
|
361
|
+
sanitized.push({ type: "object", id });
|
|
362
|
+
objectUpdates.push({
|
|
363
|
+
id, sourceName: text(item.sourceName), width: positiveInt(item.width),
|
|
364
|
+
height: positiveInt(item.height), bytes: positiveInt(item.bytes),
|
|
365
|
+
});
|
|
366
|
+
} else {
|
|
367
|
+
throw new InputError("Copertina non valida");
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const oldObjects = parseHeroRefs(delivery.hero_json)
|
|
371
|
+
.filter((item) => item.type === "object").map((item) => item.id);
|
|
372
|
+
const selectedObjects = new Set(objectUpdates.map((item) => item.id));
|
|
373
|
+
const now = new Date();
|
|
374
|
+
const purge = new Date(now.getTime() + 7 * DAY).toISOString();
|
|
375
|
+
const statements = [
|
|
376
|
+
env.DB.prepare(`
|
|
377
|
+
UPDATE deliveries SET hero_json=?,version=version+1,updated_at=? WHERE id=?
|
|
378
|
+
`).bind(JSON.stringify(sanitized), now.toISOString(), deliveryId),
|
|
379
|
+
...objectUpdates.map((item) => env.DB.prepare(`
|
|
380
|
+
UPDATE delivery_hero_objects SET state='ready',source_name=?,width=?,height=?,bytes=?,
|
|
381
|
+
updated_at=?,purge_after=NULL WHERE id=? AND delivery_id=?
|
|
382
|
+
`).bind(
|
|
383
|
+
item.sourceName, item.width, item.height, item.bytes,
|
|
384
|
+
now.toISOString(), item.id, deliveryId,
|
|
385
|
+
)),
|
|
386
|
+
...oldObjects.filter((id) => !selectedObjects.has(id)).map((id) => env.DB.prepare(`
|
|
387
|
+
UPDATE delivery_hero_objects SET state='deleted',purge_after=?,updated_at=?
|
|
388
|
+
WHERE id=? AND delivery_id=?
|
|
389
|
+
`).bind(purge, now.toISOString(), id, deliveryId)),
|
|
390
|
+
];
|
|
391
|
+
await env.DB.batch(statements);
|
|
392
|
+
return deliveryDetail(env, deliveryId, origin);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function saveSettings(env, body) {
|
|
396
|
+
const values = Array.isArray(body.defaultHeroes) ? body.defaultHeroes.slice(0, 8) : [];
|
|
397
|
+
const heroes = values.map((value) => {
|
|
398
|
+
const url = text(value);
|
|
399
|
+
if (!/^\/media\/[^?#]+$/.test(url)) throw new InputError("La copertina predefinita si sceglie dalla libreria Media");
|
|
400
|
+
return url;
|
|
401
|
+
});
|
|
402
|
+
const now = new Date().toISOString();
|
|
403
|
+
await env.DB.prepare(`
|
|
404
|
+
INSERT INTO delivery_settings(key,value_json,updated_at) VALUES ('default_heroes',?,?)
|
|
405
|
+
ON CONFLICT(key) DO UPDATE SET value_json=excluded.value_json,updated_at=excluded.updated_at
|
|
406
|
+
`).bind(JSON.stringify(heroes), now).run();
|
|
407
|
+
return { defaultHeroes: heroes };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function deliverySettings(env) {
|
|
411
|
+
const row = await env.DB.prepare(
|
|
412
|
+
"SELECT value_json FROM delivery_settings WHERE key='default_heroes'"
|
|
413
|
+
).first();
|
|
414
|
+
return { defaultHeroes: parseStringArray(row?.value_json) };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function createLink(env, body, origin) {
|
|
418
|
+
const deliveryId = required(body.id, "id del transfer mancante");
|
|
419
|
+
const exists = await env.DB.prepare("SELECT id FROM deliveries WHERE id=? AND status!='deleted'")
|
|
420
|
+
.bind(deliveryId).first();
|
|
421
|
+
if (!exists) throw new InputError("Transfer non trovato");
|
|
422
|
+
const token = randomToken(24);
|
|
423
|
+
const id = crypto.randomUUID();
|
|
424
|
+
const now = new Date().toISOString();
|
|
425
|
+
const pin = text(body.pin);
|
|
426
|
+
await env.DB.prepare(`
|
|
427
|
+
INSERT INTO delivery_links
|
|
428
|
+
(id,delivery_id,label,token_hash,token_ciphertext,pin_ciphertext,expires_at,created_at)
|
|
429
|
+
VALUES (?,?,?,?,?,?,?,?)
|
|
430
|
+
`).bind(
|
|
431
|
+
id, deliveryId, text(body.label) || "Link principale", await sha256(token),
|
|
432
|
+
await seal(env.DELIVERY_SECRET, token),
|
|
433
|
+
pin ? await seal(env.DELIVERY_SECRET, pin) : null,
|
|
434
|
+
nullableDate(body.expiresAt), now,
|
|
435
|
+
).run();
|
|
436
|
+
return {
|
|
437
|
+
id, label: text(body.label) || "Link principale", token,
|
|
438
|
+
url: `${origin}/c/${token}`, pin: pin || null, expiresAt: nullableDate(body.expiresAt),
|
|
439
|
+
revokedAt: null, openedCount: 0, downloadCount: 0, createdAt: now,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function updateLink(env, body, origin) {
|
|
444
|
+
const deliveryId = required(body.id, "id del transfer mancante");
|
|
445
|
+
const linkId = required(body.linkId, "id link mancante");
|
|
446
|
+
const link = await env.DB.prepare(`
|
|
447
|
+
SELECT * FROM delivery_links WHERE id=? AND delivery_id=? AND revoked_at IS NULL
|
|
448
|
+
`).bind(linkId, deliveryId).first();
|
|
449
|
+
if (!link) throw new InputError("Link non trovato");
|
|
450
|
+
const pin = text(body.pin);
|
|
451
|
+
const label = text(body.label) || link.label || "Link principale";
|
|
452
|
+
const expiresAt = nullableDate(body.expiresAt);
|
|
453
|
+
await env.DB.prepare(`
|
|
454
|
+
UPDATE delivery_links SET label=?,pin_ciphertext=?,expires_at=? WHERE id=? AND delivery_id=?
|
|
455
|
+
`).bind(
|
|
456
|
+
label, pin ? await seal(env.DELIVERY_SECRET, pin) : null,
|
|
457
|
+
expiresAt, linkId, deliveryId,
|
|
458
|
+
).run();
|
|
459
|
+
const token = await unseal(env.DELIVERY_SECRET, link.token_ciphertext);
|
|
460
|
+
return {
|
|
461
|
+
id: linkId, label, token, url: `${origin}/c/${token}`, pin: pin || null,
|
|
462
|
+
expiresAt, revokedAt: null, createdAt: link.created_at,
|
|
463
|
+
openedCount: Number(link.opened_count || 0), lastOpenedAt: link.last_opened_at,
|
|
464
|
+
downloadCount: Number(link.download_count || 0), lastDownloadAt: link.last_download_at,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function revokeLink(env, body) {
|
|
469
|
+
const linkId = required(body.linkId, "id link mancante");
|
|
470
|
+
const at = new Date().toISOString();
|
|
471
|
+
await env.DB.prepare("UPDATE delivery_links SET revoked_at=COALESCE(revoked_at,?) WHERE id=?")
|
|
472
|
+
.bind(at, linkId).run();
|
|
473
|
+
return { revokedAt: at };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function deliveryDetail(env, id, origin) {
|
|
477
|
+
const row = await env.DB.prepare("SELECT * FROM deliveries WHERE id=? AND status!='deleted'").bind(id).first();
|
|
478
|
+
if (!row) return null;
|
|
479
|
+
const [packagesResult, linksResult] = await Promise.all([
|
|
480
|
+
env.DB.prepare("SELECT * FROM delivery_packages WHERE delivery_id=? AND state!='deleted' ORDER BY position").bind(id).all(),
|
|
481
|
+
env.DB.prepare("SELECT * FROM delivery_links WHERE delivery_id=? ORDER BY created_at").bind(id).all(),
|
|
482
|
+
]);
|
|
483
|
+
const links = [];
|
|
484
|
+
for (const link of (linksResult.results || [])) {
|
|
485
|
+
let token = "";
|
|
486
|
+
let pin = null;
|
|
487
|
+
try {
|
|
488
|
+
token = await unseal(env.DELIVERY_SECRET, link.token_ciphertext);
|
|
489
|
+
pin = link.pin_ciphertext ? await unseal(env.DELIVERY_SECRET, link.pin_ciphertext) : null;
|
|
490
|
+
} catch (e) {
|
|
491
|
+
console.error("delivery link decrypt:", e);
|
|
492
|
+
}
|
|
493
|
+
links.push({
|
|
494
|
+
id: link.id, label: link.label, token, pin,
|
|
495
|
+
url: token ? `${origin}/c/${token}` : "",
|
|
496
|
+
expiresAt: link.expires_at, revokedAt: link.revoked_at, createdAt: link.created_at,
|
|
497
|
+
openedCount: Number(link.opened_count || 0), lastOpenedAt: link.last_opened_at,
|
|
498
|
+
downloadCount: Number(link.download_count || 0), lastDownloadAt: link.last_download_at,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
return {
|
|
502
|
+
...normalizeDelivery(row),
|
|
503
|
+
packages: (packagesResult.results || []).map(packageDetail),
|
|
504
|
+
heroes: await normalizeHeroes(env, row.hero_json),
|
|
505
|
+
links,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function normalizeDelivery(row) {
|
|
510
|
+
return {
|
|
511
|
+
id: row.id, clientName: row.client_name, title: row.title,
|
|
512
|
+
description: row.description || "", licenseNotes: row.license_notes || "",
|
|
513
|
+
ctaEnabled: !!row.cta_enabled, ctaLabel: row.cta_label || "",
|
|
514
|
+
ctaUrl: row.cta_url || "", expiresAt: row.expires_at,
|
|
515
|
+
filesExpiresAt: row.files_expires_at || null,
|
|
516
|
+
heroes: [],
|
|
517
|
+
status: row.status, version: Number(row.version || 1),
|
|
518
|
+
createdAt: row.created_at, updatedAt: row.updated_at, activatedAt: row.activated_at,
|
|
519
|
+
packageCount: Number(row.package_count || 0), linkCount: Number(row.link_count || 0),
|
|
520
|
+
storedBytes: Number(row.stored_bytes || 0), downloadCount: Number(row.download_count || 0),
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function normalizePackage(row) {
|
|
525
|
+
return {
|
|
526
|
+
id: row.id, deliveryId: row.delivery_id, label: row.label, position: Number(row.position || 0),
|
|
527
|
+
galleryEnabled: !!row.gallery_enabled, webEnabled: !!row.web_enabled, state: row.state,
|
|
528
|
+
sourceFingerprint: row.source_fingerprint || "",
|
|
529
|
+
fileCount: Number(row.file_count || 0), sourceBytes: Number(row.source_bytes || 0),
|
|
530
|
+
storedBytes: Number(row.stored_bytes || 0), createdAt: row.created_at, updatedAt: row.updated_at,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function packageDetail(row) {
|
|
535
|
+
const pkg = normalizePackage(row);
|
|
536
|
+
const manifest = parseManifest(row.manifest_json);
|
|
537
|
+
return {
|
|
538
|
+
...pkg,
|
|
539
|
+
images: (manifest?.entries || []).map((entry, index) => entry?.web ? {
|
|
540
|
+
index, name: entry.name || `Immagine ${index + 1}`,
|
|
541
|
+
previewUrl: `/admin/api/delivery/image?packageId=${encodeURIComponent(row.id)}` +
|
|
542
|
+
`&variant=${entry.thumb ? "thumb" : "web"}&index=${index}`,
|
|
543
|
+
} : null).filter(Boolean),
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async function normalizeHeroes(env, value) {
|
|
548
|
+
const out = [];
|
|
549
|
+
for (const ref of parseHeroRefs(value)) {
|
|
550
|
+
if (ref.type === "package") {
|
|
551
|
+
const pkg = await env.DB.prepare(`
|
|
552
|
+
SELECT manifest_json FROM delivery_packages
|
|
553
|
+
WHERE id=? AND state='ready' AND web_key IS NOT NULL
|
|
554
|
+
`).bind(text(ref.packageId)).first();
|
|
555
|
+
const index = Number(ref.index);
|
|
556
|
+
if (pkg && Number.isInteger(index) && parseManifest(pkg.manifest_json)?.entries?.[index]?.web) {
|
|
557
|
+
out.push({
|
|
558
|
+
type: "package", packageId: text(ref.packageId), index,
|
|
559
|
+
previewUrl: `/admin/api/delivery/image?packageId=${encodeURIComponent(ref.packageId)}` +
|
|
560
|
+
`&variant=web&index=${index}`,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
} else {
|
|
564
|
+
const row = await env.DB.prepare(`
|
|
565
|
+
SELECT source_name,width,height FROM delivery_hero_objects
|
|
566
|
+
WHERE id=? AND state='ready'
|
|
567
|
+
`).bind(ref.id).first();
|
|
568
|
+
if (row) out.push({
|
|
569
|
+
...ref, sourceName: row.source_name, width: Number(row.width || 0),
|
|
570
|
+
height: Number(row.height || 0),
|
|
571
|
+
previewUrl: `/admin/api/delivery/hero?id=${encodeURIComponent(ref.id)}`,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return out;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function parseManifest(value) {
|
|
579
|
+
try { return JSON.parse(value || "{}"); } catch { return { entries: [] }; }
|
|
580
|
+
}
|
|
581
|
+
function parseHeroRefs(value) {
|
|
582
|
+
try {
|
|
583
|
+
const parsed = JSON.parse(value || "[]");
|
|
584
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
585
|
+
} catch { return []; }
|
|
586
|
+
}
|
|
587
|
+
function parseStringArray(value) {
|
|
588
|
+
try {
|
|
589
|
+
const parsed = JSON.parse(value || "[]");
|
|
590
|
+
return Array.isArray(parsed) ? parsed.map(String) : [];
|
|
591
|
+
} catch { return []; }
|
|
592
|
+
}
|
|
593
|
+
function positiveInt(value) {
|
|
594
|
+
const n = Number(value || 0);
|
|
595
|
+
return Number.isFinite(n) && n > 0 ? Math.round(n) : 0;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function text(value) { return String(value ?? "").trim(); }
|
|
599
|
+
function bool(value) { return value ? 1 : 0; }
|
|
600
|
+
function required(value, error) {
|
|
601
|
+
const out = text(value);
|
|
602
|
+
if (!out) throw new InputError(error);
|
|
603
|
+
return out;
|
|
604
|
+
}
|
|
605
|
+
function nullableDate(value) {
|
|
606
|
+
if (!value) return null;
|
|
607
|
+
const d = new Date(value);
|
|
608
|
+
if (Number.isNaN(d.getTime())) throw new InputError("Data non valida");
|
|
609
|
+
return d.toISOString();
|
|
610
|
+
}
|
|
611
|
+
function optionalHttpUrl(value) {
|
|
612
|
+
const raw = text(value);
|
|
613
|
+
if (!raw) return "";
|
|
614
|
+
let url;
|
|
615
|
+
try { url = new URL(raw); }
|
|
616
|
+
catch { throw new InputError("Indirizzo del pulsante non valido"); }
|
|
617
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
618
|
+
throw new InputError("L’indirizzo del pulsante deve iniziare con https:// o http://");
|
|
619
|
+
}
|
|
620
|
+
return url.toString();
|
|
621
|
+
}
|
|
622
|
+
function message(e) { return e instanceof Error ? e.message : String(e); }
|
|
623
|
+
class InputError extends Error {}
|
|
624
|
+
class ConflictError extends Error {}
|
|
625
|
+
function json(data, status = 200) {
|
|
626
|
+
return new Response(JSON.stringify(data), {
|
|
627
|
+
status, headers: { "Content-Type": "application/json", "Cache-Control": "no-store" },
|
|
628
|
+
});
|
|
629
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/* Derivata hero dedicata: oggetto piccolo B2, mai l'originale fotografico. */
|
|
2
|
+
import { b2Get } from "../../_shared/b2.js";
|
|
3
|
+
|
|
4
|
+
export async function onRequestGet({ request, env }) {
|
|
5
|
+
if (!env.DB) return new Response("D1 non configurato", { status: 500 });
|
|
6
|
+
const id = new URL(request.url).searchParams.get("id") || "";
|
|
7
|
+
const hero = await env.DB.prepare(`
|
|
8
|
+
SELECT object_key,bytes FROM delivery_hero_objects WHERE id=? AND state='ready'
|
|
9
|
+
`).bind(id).first();
|
|
10
|
+
if (!hero) return new Response("Not found", { status: 404 });
|
|
11
|
+
const upstream = await b2Get(env, hero.object_key);
|
|
12
|
+
if (!upstream.ok) return new Response("Not found", { status: upstream.status });
|
|
13
|
+
const headers = new Headers({
|
|
14
|
+
"Content-Type": "image/webp",
|
|
15
|
+
"Cache-Control": "private, max-age=3600",
|
|
16
|
+
"X-Content-Type-Options": "nosniff",
|
|
17
|
+
});
|
|
18
|
+
const length = upstream.headers.get("content-length") || (hero.bytes ? String(hero.bytes) : "");
|
|
19
|
+
if (length) headers.set("Content-Length", length);
|
|
20
|
+
return new Response(upstream.body, { headers });
|
|
21
|
+
}
|