fix: HIGH issues #12,#15,#16,#17,#21 — backend persistence for import/layers/blocks + owner filtering
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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,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
@@ -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]);
|
||||||
if (result.layers && result.layers.length > 0) {
|
// Push imported elements to Yjs and backend
|
||||||
setLayers((prev) => {
|
for (const el of result.elements) {
|
||||||
const existingIds = new Set(prev.map(l => l.id));
|
collab.setElement(el);
|
||||||
const newLayers = result.layers!.filter(l => !existingIds.has(l.id));
|
}
|
||||||
return [...prev, ...newLayers];
|
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) {
|
||||||
|
const existingIds = new Set(layers.map(l => l.id));
|
||||||
|
const newLayers = result.layers.filter(l => !existingIds.has(l.id));
|
||||||
|
setLayers((prev) => [...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' }]);
|
||||||
|
|||||||
Reference in New Issue
Block a user