merge: combine all features from both codebases - mobile dashboard + layer panel + notifications + shares + all fixes
This commit is contained in:
@@ -3,26 +3,43 @@
|
||||
*/
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type { DatabaseInterface, DBBlock } from '../database/DatabaseInterface.js';
|
||||
import { requireAuth } from '../auth/authMiddleware.js';
|
||||
import type { AuthService } from '../auth/AuthService.js';
|
||||
import { validateName, validateIdParam } from '../utils/validation.js';
|
||||
|
||||
export function registerBlockRoutes(fastify: FastifyInstance, db: DatabaseInterface) {
|
||||
export function registerBlockRoutes(fastify: FastifyInstance, db: DatabaseInterface, authService: AuthService) {
|
||||
// List all blocks for a drawing
|
||||
fastify.get('/api/drawings/:drawingId/blocks', async (request) => {
|
||||
fastify.get('/api/drawings/:drawingId/blocks', async (request, reply) => {
|
||||
if (!requireAuth(request, reply, authService)) return;
|
||||
const { drawingId } = request.params as { drawingId: string };
|
||||
const idErr = validateIdParam(drawingId, 'drawingId');
|
||||
if (idErr) return reply.code(400).send({ error: idErr });
|
||||
return db.listBlocks(drawingId);
|
||||
});
|
||||
|
||||
// Create a new block
|
||||
fastify.post('/api/drawings/:drawingId/blocks', async (request, reply) => {
|
||||
if (!requireAuth(request, reply, authService)) return;
|
||||
const { drawingId } = request.params as { drawingId: string };
|
||||
const idErr = validateIdParam(drawingId, 'drawingId');
|
||||
if (idErr) return reply.code(400).send({ error: idErr });
|
||||
const body = request.body as Partial<DBBlock>;
|
||||
if (!body.name) return reply.code(400).send({ error: 'Name is required' });
|
||||
const nameErr = validateName(body.name, 'name');
|
||||
if (nameErr) return reply.code(400).send({ error: nameErr });
|
||||
return reply.code(201).send(db.createBlock({ ...body, drawing_id: drawingId }));
|
||||
});
|
||||
|
||||
// Update a block
|
||||
fastify.patch('/api/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 body = request.body as Partial<DBBlock>;
|
||||
if (body.name !== undefined) {
|
||||
const nameErr = validateName(body.name, 'name');
|
||||
if (nameErr) return reply.code(400).send({ error: nameErr });
|
||||
}
|
||||
const updated = db.updateBlock(id, body);
|
||||
if (!updated) return reply.code(404).send({ error: 'Block not found' });
|
||||
return updated;
|
||||
@@ -30,7 +47,10 @@ export function registerBlockRoutes(fastify: FastifyInstance, db: DatabaseInterf
|
||||
|
||||
// Delete a block
|
||||
fastify.delete('/api/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 ok = db.deleteBlock(id);
|
||||
if (!ok) return reply.code(404).send({ error: 'Block not found' });
|
||||
return reply.code(204).send();
|
||||
|
||||
@@ -3,17 +3,26 @@
|
||||
*/
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type { DatabaseInterface, DBDrawing } from '../database/DatabaseInterface.js';
|
||||
import { requireAuth } from '../auth/authMiddleware.js';
|
||||
import type { AuthService } from '../auth/AuthService.js';
|
||||
import { validateName, validateIdParam } from '../utils/validation.js';
|
||||
|
||||
export function registerDrawingRoutes(fastify: FastifyInstance, db: DatabaseInterface) {
|
||||
export function registerDrawingRoutes(fastify: FastifyInstance, db: DatabaseInterface, authService: AuthService) {
|
||||
// List drawings for a project
|
||||
fastify.get('/api/projects/:projectId/drawings', async (request) => {
|
||||
fastify.get('/api/projects/:projectId/drawings', async (request, reply) => {
|
||||
if (!requireAuth(request, reply, authService)) return;
|
||||
const { projectId } = request.params as { projectId: string };
|
||||
const idErr = validateIdParam(projectId, 'projectId');
|
||||
if (idErr) return reply.code(400).send({ error: idErr });
|
||||
return db.listDrawings(projectId);
|
||||
});
|
||||
|
||||
// Get single drawing
|
||||
fastify.get('/api/drawings/: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 drawing = db.getDrawing(id);
|
||||
if (!drawing) return reply.code(404).send({ error: 'Drawing not found' });
|
||||
return drawing;
|
||||
@@ -21,15 +30,29 @@ export function registerDrawingRoutes(fastify: FastifyInstance, db: DatabaseInte
|
||||
|
||||
// Create drawing
|
||||
fastify.post('/api/projects/:projectId/drawings', async (request, reply) => {
|
||||
if (!requireAuth(request, reply, authService)) return;
|
||||
const { projectId } = request.params as { projectId: string };
|
||||
const idErr = validateIdParam(projectId, 'projectId');
|
||||
if (idErr) return reply.code(400).send({ error: idErr });
|
||||
const body = request.body as Partial<DBDrawing>;
|
||||
if (body.name !== undefined) {
|
||||
const nameErr = validateName(body.name);
|
||||
if (nameErr) return reply.code(400).send({ error: nameErr });
|
||||
}
|
||||
return reply.code(201).send(db.createDrawing({ ...body, project_id: projectId }));
|
||||
});
|
||||
|
||||
// Update drawing
|
||||
fastify.patch('/api/drawings/: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 body = request.body as Partial<DBDrawing>;
|
||||
if (body.name !== undefined) {
|
||||
const nameErr = validateName(body.name);
|
||||
if (nameErr) return reply.code(400).send({ error: nameErr });
|
||||
}
|
||||
const updated = db.updateDrawing(id, body);
|
||||
if (!updated) return reply.code(404).send({ error: 'Drawing not found' });
|
||||
return updated;
|
||||
@@ -37,7 +60,10 @@ export function registerDrawingRoutes(fastify: FastifyInstance, db: DatabaseInte
|
||||
|
||||
// Delete drawing
|
||||
fastify.delete('/api/drawings/: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 ok = db.deleteDrawing(id);
|
||||
if (!ok) return reply.code(404).send({ error: 'Drawing not found' });
|
||||
return reply.code(204).send();
|
||||
|
||||
@@ -3,24 +3,36 @@
|
||||
*/
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type { DatabaseInterface, DBElement } from '../database/DatabaseInterface.js';
|
||||
import { requireAuth } from '../auth/authMiddleware.js';
|
||||
import type { AuthService } from '../auth/AuthService.js';
|
||||
import { validateIdParam } from '../utils/validation.js';
|
||||
|
||||
export function registerElementRoutes(fastify: FastifyInstance, db: DatabaseInterface) {
|
||||
export function registerElementRoutes(fastify: FastifyInstance, db: DatabaseInterface, authService: AuthService) {
|
||||
// List elements for a drawing
|
||||
fastify.get('/api/drawings/:drawingId/elements', async (request) => {
|
||||
fastify.get('/api/drawings/:drawingId/elements', async (request, reply) => {
|
||||
if (!requireAuth(request, reply, authService)) return;
|
||||
const { drawingId } = request.params as { drawingId: string };
|
||||
const idErr = validateIdParam(drawingId, 'drawingId');
|
||||
if (idErr) return reply.code(400).send({ error: idErr });
|
||||
return db.listElements(drawingId);
|
||||
});
|
||||
|
||||
// Create element
|
||||
fastify.post('/api/drawings/:drawingId/elements', async (request, reply) => {
|
||||
if (!requireAuth(request, reply, authService)) return;
|
||||
const { drawingId } = request.params as { drawingId: string };
|
||||
const idErr = validateIdParam(drawingId, 'drawingId');
|
||||
if (idErr) return reply.code(400).send({ error: idErr });
|
||||
const body = request.body as Partial<DBElement>;
|
||||
return reply.code(201).send(db.createElement({ ...body, drawing_id: drawingId }));
|
||||
});
|
||||
|
||||
// Update element
|
||||
fastify.patch('/api/elements/: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 body = request.body as Partial<DBElement>;
|
||||
const updated = db.updateElement(id, body);
|
||||
if (!updated) return reply.code(404).send({ error: 'Element not found' });
|
||||
@@ -29,7 +41,10 @@ export function registerElementRoutes(fastify: FastifyInstance, db: DatabaseInte
|
||||
|
||||
// Delete element
|
||||
fastify.delete('/api/elements/: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 ok = db.deleteElement(id);
|
||||
if (!ok) return reply.code(404).send({ error: 'Element not found' });
|
||||
return reply.code(204).send();
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Global Blocks Routes – CRUD for global block folders and blocks
|
||||
*/
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type { DatabaseInterface } from '../database/DatabaseInterface.js';
|
||||
import { requireAuth } from '../auth/authMiddleware.js';
|
||||
import type { AuthService } from '../auth/AuthService.js';
|
||||
import { validateName, validateIdParam } from '../utils/validation.js';
|
||||
|
||||
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 (!requireAuth(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 (!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 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 (!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 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 (!requireAuth(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 (!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 body = request.body as { name?: string; folder_id?: string | null; block_data?: string; svg_data?: string };
|
||||
if (body.name !== undefined) {
|
||||
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' });
|
||||
}
|
||||
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 (!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 ok = db.deleteGlobalBlock(id);
|
||||
if (!ok) return reply.code(404).send({ error: 'Global block not found' });
|
||||
return reply.code(204).send();
|
||||
});
|
||||
}
|
||||
@@ -3,25 +3,45 @@
|
||||
*/
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type { DatabaseInterface, DBLayer } from '../database/DatabaseInterface.js';
|
||||
import { requireAuth } from '../auth/authMiddleware.js';
|
||||
import type { AuthService } from '../auth/AuthService.js';
|
||||
import { validateName, validateIdParam } from '../utils/validation.js';
|
||||
|
||||
export function registerLayerRoutes(fastify: FastifyInstance, db: DatabaseInterface) {
|
||||
export function registerLayerRoutes(fastify: FastifyInstance, db: DatabaseInterface, authService: AuthService) {
|
||||
// List layers for a drawing
|
||||
fastify.get('/api/drawings/:drawingId/layers', async (request) => {
|
||||
fastify.get('/api/drawings/:drawingId/layers', async (request, reply) => {
|
||||
if (!requireAuth(request, reply, authService)) return;
|
||||
const { drawingId } = request.params as { drawingId: string };
|
||||
const idErr = validateIdParam(drawingId, 'drawingId');
|
||||
if (idErr) return reply.code(400).send({ error: idErr });
|
||||
return db.listLayers(drawingId);
|
||||
});
|
||||
|
||||
// Create layer
|
||||
fastify.post('/api/drawings/:drawingId/layers', async (request, reply) => {
|
||||
if (!requireAuth(request, reply, authService)) return;
|
||||
const { drawingId } = request.params as { drawingId: string };
|
||||
const idErr = validateIdParam(drawingId, 'drawingId');
|
||||
if (idErr) return reply.code(400).send({ error: idErr });
|
||||
const body = request.body as Partial<DBLayer>;
|
||||
if (body.name !== undefined) {
|
||||
const nameErr = validateName(body.name);
|
||||
if (nameErr) return reply.code(400).send({ error: nameErr });
|
||||
}
|
||||
return reply.code(201).send(db.createLayer({ ...body, drawing_id: drawingId }));
|
||||
});
|
||||
|
||||
// Update layer
|
||||
fastify.patch('/api/layers/: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 body = request.body as Partial<DBLayer>;
|
||||
if (body.name !== undefined) {
|
||||
const nameErr = validateName(body.name);
|
||||
if (nameErr) return reply.code(400).send({ error: nameErr });
|
||||
}
|
||||
const updated = db.updateLayer(id, body);
|
||||
if (!updated) return reply.code(404).send({ error: 'Layer not found' });
|
||||
return updated;
|
||||
@@ -29,7 +49,10 @@ export function registerLayerRoutes(fastify: FastifyInstance, db: DatabaseInterf
|
||||
|
||||
// Delete layer
|
||||
fastify.delete('/api/layers/: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 ok = db.deleteLayer(id);
|
||||
if (!ok) return reply.code(404).send({ error: 'Layer not found' });
|
||||
return reply.code(204).send();
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type { DatabaseInterface } from '../database/DatabaseInterface.js';
|
||||
import type { AuthService } from '../auth/AuthService.js';
|
||||
import { validateIdParam } from '../utils/validation.js';
|
||||
|
||||
function extractToken(request: any): string | null {
|
||||
const auth = request.headers?.authorization;
|
||||
@@ -46,6 +47,8 @@ export function registerNotificationRoutes(fastify: FastifyInstance, db: Databas
|
||||
const user = authService.getUserFromSession(token);
|
||||
if (!user) return reply.code(401).send({ error: 'Invalid or expired session' });
|
||||
const { id } = request.params as { id: string };
|
||||
const idErr = validateIdParam(id, 'id');
|
||||
if (idErr) return reply.code(400).send({ error: idErr });
|
||||
const notif = db.getNotification(id);
|
||||
if (!notif) return reply.code(404).send({ error: 'Notification not found' });
|
||||
if (notif.user_id !== user.id) return reply.code(403).send({ error: 'Forbidden' });
|
||||
@@ -61,6 +64,8 @@ export function registerNotificationRoutes(fastify: FastifyInstance, db: Databas
|
||||
const user = authService.getUserFromSession(token);
|
||||
if (!user) return reply.code(401).send({ error: 'Invalid or expired session' });
|
||||
const { id } = request.params as { id: string };
|
||||
const idErr = validateIdParam(id, 'id');
|
||||
if (idErr) return reply.code(400).send({ error: idErr });
|
||||
const notif = db.getNotification(id);
|
||||
if (!notif) return reply.code(404).send({ error: 'Notification not found' });
|
||||
if (notif.user_id !== user.id) return reply.code(403).send({ error: 'Forbidden' });
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Project Folders Routes – CRUD for project folders + move project to folder
|
||||
*/
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type { DatabaseInterface } from '../database/DatabaseInterface.js';
|
||||
import { requireAuth } from '../auth/authMiddleware.js';
|
||||
import type { AuthService } from '../auth/AuthService.js';
|
||||
import { validateName, validateIdParam } from '../utils/validation.js';
|
||||
|
||||
export function registerProjectFolderRoutes(fastify: FastifyInstance, db: DatabaseInterface, authService: AuthService) {
|
||||
// ─── Project Folders ──────────────────────────────────
|
||||
|
||||
// List all folders for the authenticated user
|
||||
fastify.get('/api/project-folders', async (request, reply) => {
|
||||
const user = requireAuth(request, reply, authService);
|
||||
if (!user) return;
|
||||
return db.listProjectFolders(user.id);
|
||||
});
|
||||
|
||||
// Get a single folder
|
||||
fastify.get('/api/project-folders/:id', async (request, reply) => {
|
||||
const user = requireAuth(request, reply, authService);
|
||||
if (!user) 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.getProjectFolder(id);
|
||||
if (!folder) return reply.code(404).send({ error: 'Folder not found' });
|
||||
if (folder.owner_id !== user.id) return reply.code(403).send({ error: 'Forbidden' });
|
||||
return folder;
|
||||
});
|
||||
|
||||
// Create a new folder
|
||||
fastify.post('/api/project-folders', async (request, reply) => {
|
||||
const user = requireAuth(request, reply, authService);
|
||||
if (!user) 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 });
|
||||
// Verify parent exists and belongs to user
|
||||
const parent = db.getProjectFolder(body.parent_id);
|
||||
if (!parent) return reply.code(404).send({ error: 'Parent folder not found' });
|
||||
if (parent.owner_id !== user.id) return reply.code(403).send({ error: 'Forbidden' });
|
||||
}
|
||||
return reply.code(201).send(db.createProjectFolder({
|
||||
name: body.name,
|
||||
parent_id: body.parent_id ?? null,
|
||||
owner_id: user.id,
|
||||
}));
|
||||
});
|
||||
|
||||
// Rename a folder
|
||||
fastify.put('/api/project-folders/:id', async (request, reply) => {
|
||||
const user = requireAuth(request, reply, authService);
|
||||
if (!user) 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.getProjectFolder(id);
|
||||
if (!folder) return reply.code(404).send({ error: 'Folder not found' });
|
||||
if (folder.owner_id !== user.id) return reply.code(403).send({ error: 'Forbidden' });
|
||||
const body = request.body as { name?: string };
|
||||
const nameErr = validateName(body.name, 'name');
|
||||
if (nameErr) return reply.code(400).send({ error: nameErr });
|
||||
const updated = db.updateProjectFolder(id, { name: body.name });
|
||||
if (!updated) return reply.code(404).send({ error: 'Folder not found' });
|
||||
return updated;
|
||||
});
|
||||
|
||||
// Delete a folder (projects inside move to root)
|
||||
fastify.delete('/api/project-folders/:id', async (request, reply) => {
|
||||
const user = requireAuth(request, reply, authService);
|
||||
if (!user) 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.getProjectFolder(id);
|
||||
if (!folder) return reply.code(404).send({ error: 'Folder not found' });
|
||||
if (folder.owner_id !== user.id) return reply.code(403).send({ error: 'Forbidden' });
|
||||
const ok = db.deleteProjectFolder(id);
|
||||
if (!ok) return reply.code(404).send({ error: 'Folder not found' });
|
||||
return reply.code(204).send();
|
||||
});
|
||||
|
||||
// Move project to folder
|
||||
fastify.put('/api/projects/:id/folder', async (request, reply) => {
|
||||
const user = requireAuth(request, reply, authService);
|
||||
if (!user) return;
|
||||
const { id } = request.params as { id: string };
|
||||
const idErr = validateIdParam(id, 'id');
|
||||
if (idErr) return reply.code(400).send({ error: idErr });
|
||||
const project = db.getProject(id);
|
||||
if (!project) return reply.code(404).send({ error: 'Project not found' });
|
||||
if (project.owner_id !== user.id) return reply.code(403).send({ error: 'Forbidden' });
|
||||
const body = request.body as { folder_id?: string | null };
|
||||
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 });
|
||||
// Verify folder exists and belongs to user
|
||||
const folder = db.getProjectFolder(body.folder_id);
|
||||
if (!folder) return reply.code(404).send({ error: 'Folder not found' });
|
||||
if (folder.owner_id !== user.id) return reply.code(403).send({ error: 'Forbidden' });
|
||||
}
|
||||
const updated = db.moveProjectToFolder(id, body.folder_id ?? null);
|
||||
if (!updated) return reply.code(404).send({ error: 'Project not found' });
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
@@ -3,16 +3,30 @@
|
||||
*/
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type { DatabaseInterface, DBProject } from '../database/DatabaseInterface.js';
|
||||
import { requireAuth } from '../auth/authMiddleware.js';
|
||||
import type { AuthService } from '../auth/AuthService.js';
|
||||
import { validateName, validateIdParam } from '../utils/validation.js';
|
||||
|
||||
export function registerProjectRoutes(fastify: FastifyInstance, db: DatabaseInterface) {
|
||||
// List all projects
|
||||
fastify.get('/api/projects', async () => {
|
||||
return db.listProjects();
|
||||
export function registerProjectRoutes(fastify: FastifyInstance, db: DatabaseInterface, authService: AuthService) {
|
||||
// List projects owned by the authenticated user, optionally filtered by folder
|
||||
fastify.get('/api/projects', async (request, reply) => {
|
||||
const user = requireAuth(request, reply, authService);
|
||||
if (!user) return;
|
||||
const folderId = (request.query as { folderId?: string | null }).folderId;
|
||||
if (folderId === undefined || folderId === '' || folderId === 'null') {
|
||||
return db.listProjects(user.id, folderId === 'null' ? null : undefined);
|
||||
}
|
||||
const fidErr = validateIdParam(folderId, 'folderId');
|
||||
if (fidErr) return reply.code(400).send({ error: fidErr });
|
||||
return db.listProjects(user.id, folderId);
|
||||
});
|
||||
|
||||
// Get single project
|
||||
fastify.get('/api/projects/: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 project = db.getProject(id);
|
||||
if (!project) return reply.code(404).send({ error: 'Project not found' });
|
||||
return project;
|
||||
@@ -20,15 +34,25 @@ export function registerProjectRoutes(fastify: FastifyInstance, db: DatabaseInte
|
||||
|
||||
// Create project
|
||||
fastify.post('/api/projects', async (request, reply) => {
|
||||
const user = requireAuth(request, reply, authService);
|
||||
if (!user) return;
|
||||
const body = request.body as Partial<DBProject>;
|
||||
if (!body.name) return reply.code(400).send({ error: 'Name is required' });
|
||||
return reply.code(201).send(db.createProject(body));
|
||||
const nameErr = validateName(body.name);
|
||||
if (nameErr) return reply.code(400).send({ error: nameErr });
|
||||
return reply.code(201).send(db.createProject({ ...body, owner_id: user.id }));
|
||||
});
|
||||
|
||||
// Update project
|
||||
fastify.patch('/api/projects/: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 body = request.body as Partial<DBProject>;
|
||||
if (body.name !== undefined) {
|
||||
const nameErr = validateName(body.name);
|
||||
if (nameErr) return reply.code(400).send({ error: nameErr });
|
||||
}
|
||||
const updated = db.updateProject(id, body);
|
||||
if (!updated) return reply.code(404).send({ error: 'Project not found' });
|
||||
return updated;
|
||||
@@ -36,7 +60,10 @@ export function registerProjectRoutes(fastify: FastifyInstance, db: DatabaseInte
|
||||
|
||||
// Delete project
|
||||
fastify.delete('/api/projects/: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 ok = db.deleteProject(id);
|
||||
if (!ok) return reply.code(404).send({ error: 'Project not found' });
|
||||
return reply.code(204).send();
|
||||
|
||||
@@ -3,24 +3,32 @@
|
||||
*/
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type { DatabaseInterface } from '../database/DatabaseInterface.js';
|
||||
import { requireAuth } from '../auth/authMiddleware.js';
|
||||
import type { AuthService } from '../auth/AuthService.js';
|
||||
import { validateSettingsKey, validateSettingsValue } from '../utils/validation.js';
|
||||
|
||||
export function registerSettingsRoutes(fastify: FastifyInstance, db: DatabaseInterface) {
|
||||
export function registerSettingsRoutes(fastify: FastifyInstance, db: DatabaseInterface, authService: AuthService) {
|
||||
// Get a single setting by key
|
||||
fastify.get('/api/settings/:key', async (request, reply) => {
|
||||
if (!requireAuth(request, reply, authService)) return;
|
||||
const { key } = request.params as { key: string };
|
||||
const keyErr = validateSettingsKey(key);
|
||||
if (keyErr) return reply.code(400).send({ error: keyErr });
|
||||
const setting = db.getSetting(key);
|
||||
if (!setting) return reply.code(404).send({ error: 'Setting not found' });
|
||||
return setting;
|
||||
});
|
||||
|
||||
// Create or update a setting (upsert)
|
||||
fastify.put('/api/settings/:key', async (request) => {
|
||||
fastify.put('/api/settings/:key', async (request, reply) => {
|
||||
if (!requireAuth(request, reply, authService)) return;
|
||||
const { key } = request.params as { key: string };
|
||||
const keyErr = validateSettingsKey(key);
|
||||
if (keyErr) return reply.code(400).send({ error: keyErr });
|
||||
const body = request.body as { value?: string };
|
||||
if (body.value === undefined) {
|
||||
return { error: 'Value is required' };
|
||||
}
|
||||
db.setSetting(key, body.value);
|
||||
return { key, value: body.value, updated_at: new Date().toISOString() };
|
||||
const valueErr = validateSettingsValue(body.value);
|
||||
if (valueErr) return reply.code(400).send({ error: valueErr });
|
||||
db.setSetting(key, body.value as string);
|
||||
return { key, value: body.value as string, updated_at: new Date().toISOString() };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type { DatabaseInterface } from '../database/DatabaseInterface.js';
|
||||
import type { AuthService } from '../auth/AuthService.js';
|
||||
import { validateIdParam } from '../utils/validation.js';
|
||||
|
||||
function extractToken(request: any): string | null {
|
||||
const auth = request.headers?.authorization;
|
||||
@@ -21,6 +22,8 @@ export function registerShareRoutes(fastify: FastifyInstance, db: DatabaseInterf
|
||||
const user = authService.getUserFromSession(token);
|
||||
if (!user) return reply.code(401).send({ error: 'Invalid or expired session' });
|
||||
const { projectId } = request.params as { projectId: string };
|
||||
const projIdErr = validateIdParam(projectId, 'projectId');
|
||||
if (projIdErr) return reply.code(400).send({ error: projIdErr });
|
||||
const project = db.getProject(projectId);
|
||||
if (!project) return reply.code(404).send({ error: 'Project not found' });
|
||||
if (project.owner_id !== user.id) return reply.code(403).send({ error: 'Forbidden' });
|
||||
@@ -34,6 +37,8 @@ export function registerShareRoutes(fastify: FastifyInstance, db: DatabaseInterf
|
||||
const user = authService.getUserFromSession(token);
|
||||
if (!user) return reply.code(401).send({ error: 'Invalid or expired session' });
|
||||
const { projectId } = request.params as { projectId: string };
|
||||
const projIdErr = validateIdParam(projectId, 'projectId');
|
||||
if (projIdErr) return reply.code(400).send({ error: projIdErr });
|
||||
const project = db.getProject(projectId);
|
||||
if (!project) return reply.code(404).send({ error: 'Project not found' });
|
||||
if (project.owner_id !== user.id) return reply.code(403).send({ error: 'Forbidden' });
|
||||
@@ -69,6 +74,8 @@ export function registerShareRoutes(fastify: FastifyInstance, db: DatabaseInterf
|
||||
const user = authService.getUserFromSession(token);
|
||||
if (!user) return reply.code(401).send({ error: 'Invalid or expired session' });
|
||||
const { id } = request.params as { id: string };
|
||||
const idErr = validateIdParam(id, 'id');
|
||||
if (idErr) return reply.code(400).send({ error: idErr });
|
||||
const share = db.getProjectShare(id);
|
||||
if (!share) return reply.code(404).send({ error: 'Share not found' });
|
||||
const project = db.getProject(share.project_id);
|
||||
|
||||
@@ -1,29 +1,63 @@
|
||||
/**
|
||||
* Users Routes – Admin user management
|
||||
*/
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||
import type { DatabaseInterface, DBUser } from '../database/DatabaseInterface.js';
|
||||
import type { AuthService } from '../auth/AuthService.js';
|
||||
import { validateIdParam } from '../utils/validation.js';
|
||||
|
||||
function extractToken(request: FastifyRequest): string | null {
|
||||
const auth = request.headers?.authorization;
|
||||
if (auth && auth.startsWith('Bearer ')) {
|
||||
return auth.slice(7);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function requireAdmin(request: FastifyRequest, reply: FastifyReply, authService: AuthService): DBUser | null {
|
||||
const token = extractToken(request);
|
||||
if (!token) {
|
||||
reply.code(401).send({ error: 'Authentication required' });
|
||||
return null;
|
||||
}
|
||||
const user = authService.getUserFromSession(token);
|
||||
if (!user) {
|
||||
reply.code(401).send({ error: 'Invalid or expired session' });
|
||||
return null;
|
||||
}
|
||||
if (user.role !== 'admin') {
|
||||
reply.code(403).send({ error: 'Admin access required' });
|
||||
return null;
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
export function registerUserRoutes(fastify: FastifyInstance, db: DatabaseInterface, authService: AuthService) {
|
||||
// List all users (admin only — TODO: add role check middleware)
|
||||
fastify.get('/api/users', async () => {
|
||||
// List all users (admin only)
|
||||
fastify.get('/api/users', async (request, reply) => {
|
||||
if (!requireAdmin(request, reply, authService)) return;
|
||||
const users = db.listUsers();
|
||||
return users.map(({ password_hash, ...safe }) => safe);
|
||||
});
|
||||
|
||||
// Get single user
|
||||
// Get single user (admin only)
|
||||
fastify.get('/api/users/:id', async (request, reply) => {
|
||||
if (!requireAdmin(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 user = db.getUser(id);
|
||||
if (!user) return reply.code(404).send({ error: 'User not found' });
|
||||
const { password_hash, ...safe } = user;
|
||||
return safe;
|
||||
});
|
||||
|
||||
// Update user (name, role, email)
|
||||
// Update user (admin only)
|
||||
fastify.patch('/api/users/:id', async (request, reply) => {
|
||||
if (!requireAdmin(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 Partial<DBUser>;
|
||||
// Prevent password update via this endpoint
|
||||
delete body.password_hash;
|
||||
@@ -33,9 +67,12 @@ export function registerUserRoutes(fastify: FastifyInstance, db: DatabaseInterfa
|
||||
return safe;
|
||||
});
|
||||
|
||||
// Delete user
|
||||
// Delete user (admin only)
|
||||
fastify.delete('/api/users/:id', async (request, reply) => {
|
||||
if (!requireAdmin(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.deleteUser(id);
|
||||
if (!ok) return reply.code(404).send({ error: 'User not found' });
|
||||
return reply.code(204).send();
|
||||
|
||||
Reference in New Issue
Block a user