feat: API contenuti taggati
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import type Database from 'better-sqlite3';
|
||||
import { validateStyles } from './style-presets';
|
||||
import { sanitizeInline } from './sanitize';
|
||||
|
||||
interface Body { value?: unknown; styles?: unknown }
|
||||
|
||||
export function applyContentUpdate(db: Database.Database, tag: string, body: Body, username: string):
|
||||
{ ok: true } | { ok: false; status: number; error: string } {
|
||||
const row = db.prepare('SELECT type FROM content_blocks WHERE tag = ?').get(tag) as { type: string } | undefined;
|
||||
if (!row) return { ok: false, status: 404, error: 'Tag inesistente.' };
|
||||
|
||||
const sets: string[] = [];
|
||||
const args: unknown[] = [];
|
||||
if (body.value !== undefined) {
|
||||
if (typeof body.value !== 'string') return { ok: false, status: 400, error: 'Valore non valido.' };
|
||||
if (row.type === 'image') return { ok: false, status: 400, error: 'Le immagini si aggiornano via upload.' };
|
||||
sets.push('value = ?');
|
||||
args.push(row.type === 'html' ? sanitizeInline(body.value) : body.value);
|
||||
}
|
||||
if (body.styles !== undefined) {
|
||||
const v = validateStyles(body.styles);
|
||||
if (!v.ok) return { ok: false, status: 400, error: v.error };
|
||||
sets.push('styles = ?');
|
||||
args.push(JSON.stringify(v.styles));
|
||||
}
|
||||
if (!sets.length) return { ok: false, status: 400, error: 'Nessuna modifica.' };
|
||||
sets.push(`updated_at = datetime('now')`, 'updated_by = ?');
|
||||
args.push(username, tag);
|
||||
db.prepare(`UPDATE content_blocks SET ${sets.join(', ')} WHERE tag = ?`).run(...args);
|
||||
return { ok: true };
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { extname } from 'node:path';
|
||||
|
||||
export const ALLOWED: Record<string, string> = {
|
||||
'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.webp': 'image/webp', '.gif': 'image/gif',
|
||||
};
|
||||
export const MAX_BYTES = 5 * 1024 * 1024;
|
||||
|
||||
export function validateUploadFile(file: unknown):
|
||||
| { ok: true; file: File; ext: string }
|
||||
| { ok: false; error: string } {
|
||||
if (!(file instanceof File)) return { ok: false, error: 'Nessun file.' };
|
||||
const ext = extname(file.name).toLowerCase();
|
||||
if (!ALLOWED[ext]) return { ok: false, error: 'Formato non consentito (jpg, png, webp, gif).' };
|
||||
if (file.size > MAX_BYTES) return { ok: false, error: 'File troppo grande (max 5 MB).' };
|
||||
return { ok: true, file, ext };
|
||||
}
|
||||
@@ -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 });
|
||||
};
|
||||
@@ -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 });
|
||||
};
|
||||
@@ -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}` });
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user