feat: API contenuti taggati

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 17:18:30 +02:00
parent b5c0c107bc
commit eda4d73aac
7 changed files with 182 additions and 11 deletions
+17
View File
@@ -0,0 +1,17 @@
import type { APIRoute } from 'astro';
import { getDb } from '../../../../lib/db';
import { applyContentUpdate } from '../../../../lib/content-update';
import { invalidateContentCache } from '../../../../lib/content';
export const prerender = false;
const json = (status: number, body: object) =>
new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
export const PUT: APIRoute = async ({ params, request, locals }) => {
let data: Record<string, unknown>;
try { data = await request.json(); } catch { return json(400, { error: 'Dati non validi.' }); }
const r = applyContentUpdate(getDb(), params.tag ?? '', data, locals.user!.username);
if (!r.ok) return json(r.status, { error: r.error });
invalidateContentCache();
return json(200, { ok: true });
};
@@ -0,0 +1,47 @@
import type { APIRoute } from 'astro';
import { writeFile, mkdir } from 'node:fs/promises';
import { join } from 'node:path';
import { getDb } from '../../../../../lib/db';
import { getEnv } from '../../../../../lib/env';
import { invalidateContentCache } from '../../../../../lib/content';
import { validateUploadFile } from '../../../../../lib/upload-rules';
export const prerender = false;
const json = (status: number, body: object) =>
new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
function findImageTag(tag: string): { ok: true } | { ok: false; status: number; error: string } {
const row = getDb().prepare('SELECT type FROM content_blocks WHERE tag = ?').get(tag) as { type: string } | undefined;
if (!row) return { ok: false, status: 404, error: 'Tag inesistente.' };
if (row.type !== 'image') return { ok: false, status: 400, error: 'Il tag non è di tipo immagine.' };
return { ok: true };
}
export const POST: APIRoute = async ({ params, request, locals }) => {
const tag = params.tag ?? '';
const found = findImageTag(tag);
if (!found.ok) return json(found.status, { error: found.error });
const form = await request.formData().catch(() => null);
const checked = validateUploadFile(form?.get('file'));
if (!checked.ok) return json(400, { error: checked.error });
const dir = join(getEnv('UPLOADS_DIR', 'uploads'), 'content');
await mkdir(dir, { recursive: true });
const name = `${tag.replaceAll('.', '-')}-${Date.now()}${checked.ext}`;
await writeFile(join(dir, name), Buffer.from(await checked.file.arrayBuffer()));
getDb().prepare(
`UPDATE content_blocks SET value = ?, updated_at = datetime('now'), updated_by = ? WHERE tag = ?`
).run(`content/${name}`, locals.user!.username, tag);
invalidateContentCache();
return json(200, { url: `/uploads/content/${name}` });
};
export const DELETE: APIRoute = ({ params, locals }) => {
const tag = params.tag ?? '';
const found = findImageTag(tag);
if (!found.ok) return json(found.status, { error: found.error });
getDb().prepare(
`UPDATE content_blocks SET value = '', updated_at = datetime('now'), updated_by = ? WHERE tag = ?`
).run(locals.user!.username, tag);
invalidateContentCache();
return json(200, { ok: true });
};
+28
View File
@@ -0,0 +1,28 @@
import type { APIRoute } from 'astro';
import { getDb } from '../../../../lib/db';
import { seedByTag } from '../../../../data/content-seed';
export const prerender = false;
const json = (status: number, body: object) =>
new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
export const GET: APIRoute = () => {
const rows = getDb().prepare(
'SELECT tag, type, value, styles, updated_at, updated_by FROM content_blocks ORDER BY tag'
).all() as { tag: string; type: string; value: string; styles: string; updated_at: string; updated_by: string | null }[];
const items = rows.map((r) => {
let styles: Record<string, string> = {};
try { styles = JSON.parse(r.styles); } catch { /* styles corrotto: si ignora */ }
if (typeof styles !== 'object' || styles === null) styles = {};
return {
tag: r.tag,
type: r.type,
value: r.value,
styles,
styleOptions: seedByTag.get(r.tag)?.styleOptions ?? [],
updatedAt: r.updated_at,
updatedBy: r.updated_by,
};
});
return json(200, { items });
};
+6 -11
View File
@@ -1,26 +1,21 @@
import type { APIRoute } from 'astro';
import { writeFile, mkdir } from 'node:fs/promises';
import { randomBytes } from 'node:crypto';
import { join, extname } from 'node:path';
import { join } from 'node:path';
import { getEnv } from '../../../lib/env';
import { validateUploadFile } from '../../../lib/upload-rules';
export const prerender = false;
const ALLOWED: Record<string, string> = { '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.webp': 'image/webp', '.gif': 'image/gif' };
const MAX_BYTES = 5 * 1024 * 1024;
const json = (status: number, body: object) =>
new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
export const POST: APIRoute = async ({ request }) => {
const form = await request.formData().catch(() => null);
const file = form?.get('file');
if (!(file instanceof File)) return json(400, { error: 'Nessun file.' });
const ext = extname(file.name).toLowerCase();
if (!ALLOWED[ext]) return json(400, { error: 'Formato non consentito (jpg, png, webp, gif).' });
if (file.size > MAX_BYTES) return json(400, { error: 'File troppo grande (max 5 MB).' });
const checked = validateUploadFile(form?.get('file'));
if (!checked.ok) return json(400, { error: checked.error });
const dir = getEnv('UPLOADS_DIR', 'uploads');
await mkdir(dir, { recursive: true });
const name = `${Date.now()}-${randomBytes(4).toString('hex')}${ext}`;
await writeFile(join(dir, name), Buffer.from(await file.arrayBuffer()));
const name = `${Date.now()}-${randomBytes(4).toString('hex')}${checked.ext}`;
await writeFile(join(dir, name), Buffer.from(await checked.file.arrayBuffer()));
return json(200, { url: `/uploads/${name}` });
};