Files
web-cad/backend/src/routes/globalBlocks.ts
T

362 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Global Blocks Routes CRUD for global block folders and blocks
*/
import type { FastifyInstance } from 'fastify';
import type { DatabaseInterface } from '../database/DatabaseInterface.js';
import { requireAuth, requireLibraryAdmin } from '../auth/authMiddleware.js';
import type { AuthService } from '../auth/AuthService.js';
import { validateName, validateIdParam } from '../utils/validation.js';
import JSZip from 'jszip';
export function registerGlobalBlockRoutes(fastify: FastifyInstance, db: DatabaseInterface, authService: AuthService) {
// ─── Global Block Folders ──────────────────────────────
// List all folders, optionally filtered by parent
fastify.get('/api/global-folders', async (request, reply) => {
if (!requireAuth(request, reply, authService)) return;
const parentId = (request.query as { parentId?: string | null }).parentId;
if (parentId === undefined || parentId === '' || parentId === 'null') {
return db.listGlobalFolders(parentId === 'null' ? null : undefined);
}
const idErr = validateIdParam(parentId, 'parentId');
if (idErr) return reply.code(400).send({ error: idErr });
return db.listGlobalFolders(parentId);
});
// Get a single folder
fastify.get('/api/global-folders/:id', async (request, reply) => {
if (!requireAuth(request, reply, authService)) return;
const { id } = request.params as { id: string };
const idErr = validateIdParam(id, 'id');
if (idErr) return reply.code(400).send({ error: idErr });
const folder = db.getGlobalFolder(id);
if (!folder) return reply.code(404).send({ error: 'Folder not found' });
return folder;
});
// Create a new folder
fastify.post('/api/global-folders', async (request, reply) => {
if (!requireLibraryAdmin(request, reply, authService)) return;
const body = request.body as { name?: string; parent_id?: string | null };
const nameErr = validateName(body.name, 'name');
if (nameErr) return reply.code(400).send({ error: nameErr });
if (body.parent_id !== undefined && body.parent_id !== null) {
const pidErr = validateIdParam(body.parent_id, 'parent_id');
if (pidErr) return reply.code(400).send({ error: pidErr });
}
return reply.code(201).send(db.createGlobalFolder({ name: body.name, parent_id: body.parent_id ?? null }));
});
// Rename / move a folder
fastify.patch('/api/global-folders/:id', async (request, reply) => {
if (!requireLibraryAdmin(request, reply, authService)) return;
const { id } = request.params as { id: string };
const idErr = validateIdParam(id, 'id');
if (idErr) return reply.code(400).send({ error: idErr });
const body = request.body as { name?: string; parent_id?: string | null };
if (body.name !== undefined) {
const nameErr = validateName(body.name, 'name');
if (nameErr) return reply.code(400).send({ error: nameErr });
}
if (body.parent_id !== undefined && body.parent_id !== null) {
const pidErr = validateIdParam(body.parent_id, 'parent_id');
if (pidErr) return reply.code(400).send({ error: pidErr });
// Prevent setting parent to self
if (body.parent_id === id) return reply.code(400).send({ error: 'Cannot set parent to self' });
}
const updated = db.updateGlobalFolder(id, { name: body.name, parent_id: body.parent_id });
if (!updated) return reply.code(404).send({ error: 'Folder not found' });
return updated;
});
// Delete a folder (cascades to children, blocks get folder_id = NULL)
fastify.delete('/api/global-folders/:id', async (request, reply) => {
if (!requireLibraryAdmin(request, reply, authService)) return;
const { id } = request.params as { id: string };
const idErr = validateIdParam(id, 'id');
if (idErr) return reply.code(400).send({ error: idErr });
const ok = db.deleteGlobalFolder(id);
if (!ok) return reply.code(404).send({ error: 'Folder not found' });
return reply.code(204).send();
});
// ─── Global Blocks ────────────────────────────────────
// List all global blocks, optionally filtered by folder
fastify.get('/api/global-blocks', async (request, reply) => {
if (!requireAuth(request, reply, authService)) return;
const folderId = (request.query as { folderId?: string | null }).folderId;
if (folderId === undefined || folderId === '' || folderId === 'null') {
return db.listGlobalBlocks(folderId === 'null' ? null : undefined);
}
const idErr = validateIdParam(folderId, 'folderId');
if (idErr) return reply.code(400).send({ error: idErr });
return db.listGlobalBlocks(folderId);
});
// Get a single global block
fastify.get('/api/global-blocks/:id', async (request, reply) => {
if (!requireAuth(request, reply, authService)) return;
const { id } = request.params as { id: string };
const idErr = validateIdParam(id, 'id');
if (idErr) return reply.code(400).send({ error: idErr });
const block = db.getGlobalBlock(id);
if (!block) return reply.code(404).send({ error: 'Global block not found' });
return block;
});
// Create a new global block
fastify.post('/api/global-blocks', async (request, reply) => {
if (!requireLibraryAdmin(request, reply, authService)) return;
const body = request.body as { name?: string; folder_id?: string | null; block_data?: string; svg_data?: string };
const nameErr = validateName(body.name, 'name');
if (nameErr) return reply.code(400).send({ error: nameErr });
if (body.folder_id !== undefined && body.folder_id !== null) {
const fidErr = validateIdParam(body.folder_id, 'folder_id');
if (fidErr) return reply.code(400).send({ error: fidErr });
}
if (body.block_data !== undefined && typeof body.block_data !== 'string') {
return reply.code(400).send({ error: 'block_data must be a JSON string' });
}
if (body.svg_data !== undefined && typeof body.svg_data !== 'string') {
return reply.code(400).send({ error: 'svg_data must be a string' });
}
return reply.code(201).send(db.createGlobalBlock({
name: body.name,
folder_id: body.folder_id ?? null,
block_data: body.block_data ?? '{}',
svg_data: body.svg_data ?? null,
}));
});
// Update a global block (rename, move, update data)
fastify.patch('/api/global-blocks/:id', async (request, reply) => {
if (!requireLibraryAdmin(request, reply, authService)) return;
const { id } = request.params as { id: string };
const idErr = validateIdParam(id, 'id');
if (idErr) return reply.code(400).send({ error: idErr });
const body = request.body as { name?: string; folder_id?: string | null; block_data?: string; svg_data?: string; is_favorite?: boolean };
if (body.name !== undefined) {
const nameErr = validateName(body.name, 'name');
if (nameErr) return reply.code(400).send({ error: nameErr });
}
if (body.is_favorite !== undefined && typeof body.is_favorite !== 'boolean') {
return reply.code(400).send({ error: 'is_favorite must be a boolean' });
}
if (body.folder_id !== undefined && body.folder_id !== null) {
const fidErr = validateIdParam(body.folder_id, 'folder_id');
if (fidErr) return reply.code(400).send({ error: fidErr });
}
if (body.block_data !== undefined && typeof body.block_data !== 'string') {
return reply.code(400).send({ error: 'block_data must be a JSON string' });
}
if (body.svg_data !== undefined && typeof body.svg_data !== 'string') {
return reply.code(400).send({ error: 'svg_data must be a string' });
}
const updated = db.updateGlobalBlock(id, body);
if (!updated) return reply.code(404).send({ error: 'Global block not found' });
return updated;
});
// Delete a global block
fastify.delete('/api/global-blocks/:id', async (request, reply) => {
if (!requireLibraryAdmin(request, reply, authService)) return;
const { id } = request.params as { id: string };
const idErr = validateIdParam(id, 'id');
if (idErr) return reply.code(400).send({ error: idErr });
const ok = db.deleteGlobalBlock(id);
if (!ok) return reply.code(404).send({ error: 'Global block not found' });
return reply.code(204).send();
});
// ─── Task F1: Library-Paket Export/Import (.wcadlib) ──────
// Export: komplette Library als JSON-Paket (Ordner + Blöcke)
fastify.get('/api/global-blocks/export', async (request, reply) => {
if (!requireAuth(request, reply, authService)) return;
const folders = db.listGlobalFolders();
const blocks = db.listGlobalBlocks();
return {
format: 'wcadlib',
version: 1,
exported_at: new Date().toISOString(),
folders: folders.map((f) => ({ id: f.id, name: f.name, parent_id: f.parent_id })),
blocks: blocks.map((b) => ({
name: b.name,
folder_id: b.folder_id,
block_data: b.block_data,
svg_data: b.svg_data,
})),
};
});
// Import: .wcadlib-Paket einspielen (IDs neu, Ordner-Hierarchie über Mapping erhalten)
fastify.post('/api/global-blocks/import', async (request, reply) => {
if (!requireLibraryAdmin(request, reply, authService)) return;
const pkg = request.body as {
format?: string;
folders?: Array<{ id: string; name: string; parent_id: string | null }>;
blocks?: Array<{ name: string; folder_id: string | null; block_data: string; svg_data: string | null }>;
};
if (!pkg || pkg.format !== 'wcadlib' || !Array.isArray(pkg.blocks)) {
return reply.code(400).send({ error: 'Invalid wcadlib package (format != wcadlib or blocks missing)' });
}
// Ordner mit ID-Mapping importieren:
const folderIdMap = new Map<string, string>();
const sortedFolders = [...(pkg.folders ?? [])].sort((a, b) => {
if (a.parent_id === b.parent_id) return 0;
if (a.parent_id === null) return -1;
if (b.parent_id === null) return 1;
return 0;
});
for (const f of sortedFolders) {
const newParent = f.parent_id ? folderIdMap.get(f.parent_id) ?? null : null;
const created = db.createGlobalFolder({ name: f.name, parent_id: newParent });
folderIdMap.set(f.id, created.id);
}
// Blöcke importieren:
let imported = 0;
for (const b of pkg.blocks) {
const newFolderId = b.folder_id ? folderIdMap.get(b.folder_id) ?? null : null;
db.createGlobalBlock({
name: b.name,
folder_id: newFolderId,
block_data: b.block_data,
svg_data: b.svg_data,
});
imported++;
}
return reply.code(201).send({ imported_blocks: imported, imported_folders: folderIdMap.size });
});
// ─── Task F7: .wcadlib als echtes ZIP-Paket ──────────────
// ZIP-Export: manifest.json + blocks.json + thumbs/ (SVG je Block)
fastify.get('/api/global-blocks/export-zip', async (request, reply) => {
if (!requireAuth(request, reply, authService)) return;
const folders = db.listGlobalFolders();
const blocks = db.listGlobalBlocks();
const zip = new JSZip();
zip.file('manifest.json', JSON.stringify({
format: 'wcadlib-zip',
version: 1,
exported_at: new Date().toISOString(),
block_count: blocks.length,
folder_count: folders.length,
}));
const blockEntries = [] as Array<{
name: string;
folder_id: string | null;
block_data: string;
thumbnail: string | null;
}>;
for (const b of blocks) {
let thumbnail: string | null = null;
if (b.svg_data) {
thumbnail = `thumbs/${b.id}.svg`;
zip.file(thumbnail, b.svg_data);
}
blockEntries.push({
name: b.name,
folder_id: b.folder_id,
block_data: b.block_data,
thumbnail,
});
}
zip.file('blocks.json', JSON.stringify({
folders: folders.map((f) => ({ id: f.id, name: f.name, parent_id: f.parent_id })),
blocks: blockEntries,
}));
const buffer = await zip.generateAsync({ type: 'nodebuffer' });
reply.header('content-type', 'application/zip');
reply.header('content-disposition', 'attachment; filename="library.wcadlib.zip"');
return reply.send(buffer);
});
// ZIP-Import: .wcadlib-ZIP einspielen (Ordner-Mapping, Blöcke, Thumbnails)
fastify.post('/api/global-blocks/import-zip', async (request, reply) => {
if (!requireLibraryAdmin(request, reply, authService)) return;
const body = request.body;
if (!Buffer.isBuffer(body) || body.length === 0) {
return reply.code(400).send({ error: 'ZIP body fehlt oder leer' });
}
let zip: JSZip;
try {
zip = await JSZip.loadAsync(body);
} catch {
return reply.code(400).send({ error: 'Kein valides ZIP-Format' });
}
const blocksFile = zip.file('blocks.json');
if (!blocksFile) {
return reply.code(400).send({ error: 'blocks.json fehlt im ZIP' });
}
let pkg: { folders?: Array<{ id: string; name: string; parent_id: string | null }>; blocks?: Array<{ name: string; folder_id: string | null; block_data: string; thumbnail?: string | null }> };
try {
pkg = JSON.parse(await blocksFile.async('string'));
} catch {
return reply.code(400).send({ error: 'blocks.json ist kein valides JSON' });
}
if (!Array.isArray(pkg.blocks)) {
return reply.code(400).send({ error: 'blocks.json enthaelt kein blocks-Array' });
}
// Optional: manifest pruefen (falls vorhanden)
const manifestFile = zip.file('manifest.json');
if (manifestFile) {
try {
const manifest = JSON.parse(await manifestFile.async('string'));
if (manifest.format && manifest.format !== 'wcadlib-zip') {
return reply.code(400).send({ error: `Unbekanntes Paket-Format: ${manifest.format}` });
}
} catch {
return reply.code(400).send({ error: 'manifest.json ist kein valides JSON' });
}
}
// Ordner mit ID-Mapping importieren (Eltern zuerst):
const folderIdMap = new Map<string, string>();
const sortedFolders = [...(pkg.folders ?? [])].sort((a, b) => {
if (a.parent_id === null) return -1;
if (b.parent_id === null) return 1;
if (a.parent_id === b.parent_id) return 0;
const aRes = folderIdMap.has(a.parent_id!) ? -1 : 0;
return aRes;
});
for (const f of sortedFolders) {
const newParent = f.parent_id ? folderIdMap.get(f.parent_id) ?? null : null;
const created = db.createGlobalFolder({ name: f.name, parent_id: newParent });
folderIdMap.set(f.id, created.id);
}
// Blöcke importieren; Thumbnail aus thumbs/ lesen wenn referenziert:
let imported = 0;
for (const b of pkg.blocks) {
let svgData: string | null = null;
if (b.thumbnail) {
const thumbFile = zip.file(b.thumbnail);
if (thumbFile) {
try { svgData = await thumbFile.async('string'); } catch { svgData = null; }
}
}
const newFolderId = b.folder_id ? folderIdMap.get(b.folder_id) ?? null : null;
db.createGlobalBlock({
name: b.name,
folder_id: newFolderId,
block_data: b.block_data,
svg_data: svgData,
});
imported++;
}
return reply.code(201).send({ imported_blocks: imported, imported_folders: folderIdMap.size });
});
}