fix: HIGH issues #12,#15,#16,#17,#21 — backend persistence for import/layers/blocks + owner filtering

This commit is contained in:
A0 Orchestrator
2026-06-30 12:16:49 +02:00
parent fb5ec8f4ec
commit 5a21fc0bfa
4 changed files with 150 additions and 28 deletions
+1 -1
View File
@@ -112,7 +112,7 @@ export interface DatabaseInterface {
listUsers(): DBUser[]; listUsers(): DBUser[];
// Projects // Projects
listProjects(): DBProject[]; listProjects(ownerId?: string): DBProject[];
getProject(id: string): DBProject | null; getProject(id: string): DBProject | null;
createProject(data: Partial<DBProject>): DBProject; createProject(data: Partial<DBProject>): DBProject;
updateProject(id: string, data: Partial<DBProject>): DBProject | null; updateProject(id: string, data: Partial<DBProject>): DBProject | null;
+4 -1
View File
@@ -73,7 +73,10 @@ export class SqliteAdapter implements DatabaseInterface {
} }
// ─── Projects ────────────────────────────────────── // ─── Projects ──────────────────────────────────────
listProjects(): DBProject[] { listProjects(ownerId?: string): DBProject[] {
if (ownerId) {
return this.db.prepare('SELECT * FROM projects WHERE owner_id = ? ORDER BY updated_at DESC').all(ownerId) as DBProject[];
}
return this.db.prepare('SELECT * FROM projects ORDER BY updated_at DESC').all() as DBProject[]; return this.db.prepare('SELECT * FROM projects ORDER BY updated_at DESC').all() as DBProject[];
} }
+7 -5
View File
@@ -7,10 +7,11 @@ import { requireAuth } from '../auth/authMiddleware.js';
import type { AuthService } from '../auth/AuthService.js'; import type { AuthService } from '../auth/AuthService.js';
export function registerProjectRoutes(fastify: FastifyInstance, db: DatabaseInterface, authService: AuthService) { export function registerProjectRoutes(fastify: FastifyInstance, db: DatabaseInterface, authService: AuthService) {
// List all projects // List projects owned by the authenticated user
fastify.get('/api/projects', async (request, reply) => { fastify.get('/api/projects', async (request, reply) => {
if (!requireAuth(request, reply, authService)) return; const user = requireAuth(request, reply, authService);
return db.listProjects(); if (!user) return;
return db.listProjects(user.id);
}); });
// Get single project // Get single project
@@ -24,10 +25,11 @@ export function registerProjectRoutes(fastify: FastifyInstance, db: DatabaseInte
// Create project // Create project
fastify.post('/api/projects', async (request, reply) => { fastify.post('/api/projects', async (request, reply) => {
if (!requireAuth(request, reply, authService)) return; const user = requireAuth(request, reply, authService);
if (!user) return;
const body = request.body as Partial<DBProject>; const body = request.body as Partial<DBProject>;
if (!body.name) return reply.code(400).send({ error: 'Name is required' }); if (!body.name) return reply.code(400).send({ error: 'Name is required' });
return reply.code(201).send(db.createProject(body)); return reply.code(201).send(db.createProject({ ...body, owner_id: user.id }));
}); });
// Update project // Update project
+138 -21
View File
@@ -534,7 +534,13 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
const handleRenameBlock = useCallback((id: string, name: string) => { const handleRenameBlock = useCallback((id: string, name: string) => {
setBlocks((prev) => prev.map((b) => b.id === id ? { ...b, name } : b)); setBlocks((prev) => prev.map((b) => b.id === id ? { ...b, name } : b));
setCommandHistory((prev) => [...prev, { prefix: '·', text: `Block umbenannt: ${name}`, type: 'info' }]); setCommandHistory((prev) => [...prev, { prefix: '·', text: `Block umbenannt: ${name}`, type: 'info' }]);
}, []); // Persist rename to backend
if (token) {
updateBlock(token, id, { name }).catch((err) => {
console.error('Failed to save block rename:', err);
});
}
}, [token]);
const handleDuplicateBlock = useCallback((id: string) => { const handleDuplicateBlock = useCallback((id: string) => {
setBlocks((prev) => { setBlocks((prev) => {
@@ -547,10 +553,16 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
name: `${block.name} (Kopie)`, name: `${block.name} (Kopie)`,
elements: block.elements.map((el) => ({ ...el, id: `${el.id}_copy_${Date.now()}` })), elements: block.elements.map((el) => ({ ...el, id: `${el.id}_copy_${Date.now()}` })),
}; };
// Persist duplicate to backend
if (drawingId && token) {
createBlockTyped(token, drawingId, copy).catch((err) => {
console.error('Failed to save duplicated block:', err);
});
}
return [...prev, copy]; return [...prev, copy];
}); });
setCommandHistory((prev) => [...prev, { prefix: '·', text: 'Block dupliziert', type: 'info' }]); setCommandHistory((prev) => [...prev, { prefix: '·', text: 'Block dupliziert', type: 'info' }]);
}, []); }, [drawingId, token]);
const handleDeleteBlock = useCallback((id: string) => { const handleDeleteBlock = useCallback((id: string) => {
setBlocks((prev) => prev.filter((b) => b.id !== id)); setBlocks((prev) => prev.filter((b) => b.id !== id));
@@ -643,15 +655,42 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
const result = await importFile(file); const result = await importFile(file);
if (result.success) { if (result.success) {
setElements((prev) => [...prev, ...result.elements]); setElements((prev) => [...prev, ...result.elements]);
// Push imported elements to Yjs and backend
for (const el of result.elements) {
collab.setElement(el);
}
if (drawingId && token) {
setSavedStatus('Speichert…');
Promise.all(result.elements.map(el => createElementTyped(token, drawingId, el)))
.then(() => setSavedStatus('gespeichert'))
.catch((err) => {
console.error('Failed to save imported elements:', err);
setSavedStatus('Fehler beim Speichern');
});
}
if (result.layers && result.layers.length > 0) { if (result.layers && result.layers.length > 0) {
setLayers((prev) => { const existingIds = new Set(layers.map(l => l.id));
const existingIds = new Set(prev.map(l => l.id)); const newLayers = result.layers.filter(l => !existingIds.has(l.id));
const newLayers = result.layers!.filter(l => !existingIds.has(l.id)); setLayers((prev) => [...prev, ...newLayers]);
return [...prev, ...newLayers]; // Push imported layers to Yjs and backend
}); for (const layer of newLayers) {
collab.setLayer(layer);
}
if (drawingId && token) {
Promise.all(newLayers.map(l => createLayerTyped(token, drawingId, l)))
.catch((err) => console.error('Failed to save imported layers:', err));
}
} }
if (result.blocks && result.blocks.length > 0) { if (result.blocks && result.blocks.length > 0) {
setBlocks((prev) => [...prev, ...result.blocks!]); setBlocks((prev) => [...prev, ...result.blocks!]);
// Push imported blocks to Yjs and backend
for (const block of result.blocks) {
collab.setBlock(block);
}
if (drawingId && token) {
Promise.all(result.blocks.map(b => createBlockTyped(token, drawingId, b)))
.catch((err) => console.error('Failed to save imported blocks:', err));
}
} }
setCommandHistory((prev) => [...prev, { prefix: '·', text: `Importiert: ${result.elements.length} Elemente aus ${file.name}`, type: 'info' }]); setCommandHistory((prev) => [...prev, { prefix: '·', text: `Importiert: ${result.elements.length} Elemente aus ${file.name}`, type: 'info' }]);
if (result.warnings.length > 0) { if (result.warnings.length > 0) {
@@ -660,7 +699,7 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
} else { } else {
setCommandHistory((prev) => [...prev, { prefix: '·', text: `Import fehlgeschlagen: ${result.error || 'Unbekannter Fehler'}`, type: 'info' }]); setCommandHistory((prev) => [...prev, { prefix: '·', text: `Import fehlgeschlagen: ${result.error || 'Unbekannter Fehler'}`, type: 'info' }]);
} }
}, []); }, [drawingId, token, collab, layers]);
const handleExport = useCallback(async (format: ExportFormat) => { const handleExport = useCallback(async (format: ExportFormat) => {
const projectData: ProjectData = { const projectData: ProjectData = {
@@ -866,8 +905,20 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
}); });
}, [drawingId, token]); }, [drawingId, token]);
const handleToggleLayer = useCallback((id: string) => { const handleToggleLayer = useCallback((id: string) => {
setLayers((prev) => prev.map((l) => l.id === id ? { ...l, visible: !l.visible } : l)); setLayers((prev) => {
}, []); const updated = prev.map((l) => l.id === id ? { ...l, visible: !l.visible } : l);
const changed = updated.find((l) => l.id === id);
if (changed) {
collab.setLayer(changed);
if (token) {
updateLayer(token, id, { visible: changed.visible }).catch((err) => {
console.error('Failed to save layer visibility:', err);
});
}
}
return updated;
});
}, [token, collab]);
const handleDeleteLayer = useCallback((id: string) => { const handleDeleteLayer = useCallback((id: string) => {
setLayers((prev) => prev.filter((l) => l.id !== id)); setLayers((prev) => prev.filter((l) => l.id !== id));
// Delete from backend // Delete from backend
@@ -878,29 +929,95 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
} }
}, [token]); }, [token]);
const handleRenameLayer = useCallback((id: string, name: string) => { const handleRenameLayer = useCallback((id: string, name: string) => {
setLayers((prev) => prev.map((l) => l.id === id ? { ...l, name } : l)); setLayers((prev) => {
}, []); const updated = prev.map((l) => l.id === id ? { ...l, name } : l);
const changed = updated.find((l) => l.id === id);
if (changed) {
collab.setLayer(changed);
if (token) {
updateLayer(token, id, { name }).catch((err) => {
console.error('Failed to save layer rename:', err);
});
}
}
return updated;
});
}, [token, collab]);
const handleDuplicateLayer = useCallback((id: string) => { const handleDuplicateLayer = useCallback((id: string) => {
setLayers((prev) => { setLayers((prev) => {
const layer = prev.find((l) => l.id === id); const layer = prev.find((l) => l.id === id);
if (!layer) return prev; if (!layer) return prev;
const newId = `layer-${Date.now()}`; const newId = `layer-${Date.now()}`;
const copy: CADLayer = { ...layer, id: newId, name: `${layer.name} (Kopie)`, sortOrder: prev.length }; const copy: CADLayer = { ...layer, id: newId, name: `${layer.name} (Kopie)`, sortOrder: prev.length };
collab.setLayer(copy);
if (drawingId && token) {
createLayerTyped(token, drawingId, copy).catch((err) => {
console.error('Failed to save duplicated layer:', err);
});
}
return [...prev, copy]; return [...prev, copy];
}); });
}, []); }, [drawingId, token, collab]);
const handleToggleLock = useCallback((id: string) => { const handleToggleLock = useCallback((id: string) => {
setLayers((prev) => prev.map((l) => l.id === id ? { ...l, locked: !l.locked } : l)); setLayers((prev) => {
}, []); const updated = prev.map((l) => l.id === id ? { ...l, locked: !l.locked } : l);
const changed = updated.find((l) => l.id === id);
if (changed) {
collab.setLayer(changed);
if (token) {
updateLayer(token, id, { locked: changed.locked }).catch((err) => {
console.error('Failed to save layer lock state:', err);
});
}
}
return updated;
});
}, [token, collab]);
const handleUpdateLayerColor = useCallback((id: string, color: string) => { const handleUpdateLayerColor = useCallback((id: string, color: string) => {
setLayers((prev) => prev.map((l) => l.id === id ? { ...l, color } : l)); setLayers((prev) => {
}, []); const updated = prev.map((l) => l.id === id ? { ...l, color } : l);
const changed = updated.find((l) => l.id === id);
if (changed) {
collab.setLayer(changed);
if (token) {
updateLayer(token, id, { color }).catch((err) => {
console.error('Failed to save layer color:', err);
});
}
}
return updated;
});
}, [token, collab]);
const handleUpdateLayerLineType = useCallback((id: string, lineType: 'solid' | 'dashed' | 'dotted') => { const handleUpdateLayerLineType = useCallback((id: string, lineType: 'solid' | 'dashed' | 'dotted') => {
setLayers((prev) => prev.map((l) => l.id === id ? { ...l, lineType } : l)); setLayers((prev) => {
}, []); const updated = prev.map((l) => l.id === id ? { ...l, lineType } : l);
const changed = updated.find((l) => l.id === id);
if (changed) {
collab.setLayer(changed);
if (token) {
updateLayer(token, id, { lineType }).catch((err) => {
console.error('Failed to save layer line type:', err);
});
}
}
return updated;
});
}, [token, collab]);
const handleUpdateLayerTransparency = useCallback((id: string, transparency: number) => { const handleUpdateLayerTransparency = useCallback((id: string, transparency: number) => {
setLayers((prev) => prev.map((l) => l.id === id ? { ...l, transparency } : l)); setLayers((prev) => {
}, []); const updated = prev.map((l) => l.id === id ? { ...l, transparency } : l);
const changed = updated.find((l) => l.id === id);
if (changed) {
collab.setLayer(changed);
if (token) {
updateLayer(token, id, { transparency }).catch((err) => {
console.error('Failed to save layer transparency:', err);
});
}
}
return updated;
});
}, [token, collab]);
const handleZoomIn = useCallback(() => { const handleZoomIn = useCallback(() => {
setCommandHistory((prev) => [...prev, { prefix: '·', text: 'Zoom: vergrößert', type: 'info' }]); setCommandHistory((prev) => [...prev, { prefix: '·', text: 'Zoom: vergrößert', type: 'info' }]);