merge: cherry-pick e1b96310 (security + type-safety + auth middleware) from web-cad
Brings the following 28.06 improvements into web-cad-neu: - backend/src/auth/authMiddleware.ts: shared requireAuth/requireAdmin middleware - backend/src/routes/users.ts: admin role checks (critical security fix) - 6 routes (projects, drawings, elements, layers, blocks, settings): requireAuth - server.ts: per-route auth (defense-in-depth alongside global hook) - 6 test files: auth setup + 401 tests - Frontend: type safety fixes (App.tsx, RenderEngine, SnapEngine, etc.) - 10 components: SVG stroke-width/linecap/linejoin -> camelCase - PropertiesPanel: formatPos/formatDim/parseNum helpers - LayerPanel: aria-labels - WORKLOG.md: historical worklog from 28.06 Conflicts resolved (4 blocks, all kept HEAD +e1b96310improvements): - LayerPanel.tsx: kept HEAD display:flex +e1b96310aria-label - PropertiesPanel.tsx: kept HEAD onDelete handler +e1b96310formatPos helpers - PropertiesPanel.tsx: kept HEAD delete button (feature) + full inline styles - Topbar.tsx: kept HEAD onClick +e1b96310camelCase SVG attrs Refs: CODE_ANALYSIS.md (Issue #1 partially improved),e1b96310
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Shared Auth Middleware – extractToken, requireAuth, requireAdmin
|
||||
*/
|
||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import type { AuthService } from './AuthService.js';
|
||||
import type { DBUser } from '../database/DatabaseInterface.js';
|
||||
|
||||
export function extractToken(request: FastifyRequest): string | null {
|
||||
const auth = request.headers?.authorization;
|
||||
if (auth && auth.startsWith('Bearer ')) {
|
||||
return auth.slice(7);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Require any authenticated user (valid session).
|
||||
* Returns the user object or null (and sends error reply).
|
||||
*/
|
||||
export function requireAuth(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;
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Require admin role (valid session + admin role).
|
||||
* Returns the user object or null (and sends error reply).
|
||||
*/
|
||||
export function requireAdmin(request: FastifyRequest, reply: FastifyReply, authService: AuthService): DBUser | null {
|
||||
const user = requireAuth(request, reply, authService);
|
||||
if (!user) return null;
|
||||
if (user.role !== 'admin') {
|
||||
reply.code(403).send({ error: 'Admin access required' });
|
||||
return null;
|
||||
}
|
||||
return user;
|
||||
}
|
||||
@@ -3,16 +3,20 @@
|
||||
*/
|
||||
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';
|
||||
|
||||
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 };
|
||||
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 body = request.body as Partial<DBBlock>;
|
||||
if (!body.name) return reply.code(400).send({ error: 'Name is required' });
|
||||
@@ -21,6 +25,7 @@ export function registerBlockRoutes(fastify: FastifyInstance, db: DatabaseInterf
|
||||
|
||||
// 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 body = request.body as Partial<DBBlock>;
|
||||
const updated = db.updateBlock(id, body);
|
||||
@@ -30,6 +35,7 @@ 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 ok = db.deleteBlock(id);
|
||||
if (!ok) return reply.code(404).send({ error: 'Block not found' });
|
||||
|
||||
@@ -3,16 +3,20 @@
|
||||
*/
|
||||
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';
|
||||
|
||||
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 };
|
||||
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 drawing = db.getDrawing(id);
|
||||
if (!drawing) return reply.code(404).send({ error: 'Drawing not found' });
|
||||
@@ -21,6 +25,7 @@ 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 body = request.body as Partial<DBDrawing>;
|
||||
return reply.code(201).send(db.createDrawing({ ...body, project_id: projectId }));
|
||||
@@ -28,6 +33,7 @@ export function registerDrawingRoutes(fastify: FastifyInstance, db: DatabaseInte
|
||||
|
||||
// Update drawing
|
||||
fastify.patch('/api/drawings/:id', async (request, reply) => {
|
||||
if (!requireAuth(request, reply, authService)) return;
|
||||
const { id } = request.params as { id: string };
|
||||
const body = request.body as Partial<DBDrawing>;
|
||||
const updated = db.updateDrawing(id, body);
|
||||
@@ -37,6 +43,7 @@ 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 ok = db.deleteDrawing(id);
|
||||
if (!ok) return reply.code(404).send({ error: 'Drawing not found' });
|
||||
|
||||
@@ -3,16 +3,20 @@
|
||||
*/
|
||||
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';
|
||||
|
||||
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 };
|
||||
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 body = request.body as Partial<DBElement>;
|
||||
return reply.code(201).send(db.createElement({ ...body, drawing_id: drawingId }));
|
||||
@@ -20,6 +24,7 @@ export function registerElementRoutes(fastify: FastifyInstance, db: DatabaseInte
|
||||
|
||||
// Update element
|
||||
fastify.patch('/api/elements/:id', async (request, reply) => {
|
||||
if (!requireAuth(request, reply, authService)) return;
|
||||
const { id } = request.params as { id: string };
|
||||
const body = request.body as Partial<DBElement>;
|
||||
const updated = db.updateElement(id, body);
|
||||
@@ -29,6 +34,7 @@ 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 ok = db.deleteElement(id);
|
||||
if (!ok) return reply.code(404).send({ error: 'Element not found' });
|
||||
|
||||
@@ -3,16 +3,20 @@
|
||||
*/
|
||||
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';
|
||||
|
||||
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 };
|
||||
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 body = request.body as Partial<DBLayer>;
|
||||
return reply.code(201).send(db.createLayer({ ...body, drawing_id: drawingId }));
|
||||
@@ -20,6 +24,7 @@ export function registerLayerRoutes(fastify: FastifyInstance, db: DatabaseInterf
|
||||
|
||||
// Update layer
|
||||
fastify.patch('/api/layers/:id', async (request, reply) => {
|
||||
if (!requireAuth(request, reply, authService)) return;
|
||||
const { id } = request.params as { id: string };
|
||||
const body = request.body as Partial<DBLayer>;
|
||||
const updated = db.updateLayer(id, body);
|
||||
@@ -29,6 +34,7 @@ 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 ok = db.deleteLayer(id);
|
||||
if (!ok) return reply.code(404).send({ error: 'Layer not found' });
|
||||
|
||||
@@ -3,15 +3,19 @@
|
||||
*/
|
||||
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';
|
||||
|
||||
export function registerProjectRoutes(fastify: FastifyInstance, db: DatabaseInterface) {
|
||||
export function registerProjectRoutes(fastify: FastifyInstance, db: DatabaseInterface, authService: AuthService) {
|
||||
// List all projects
|
||||
fastify.get('/api/projects', async () => {
|
||||
fastify.get('/api/projects', async (request, reply) => {
|
||||
if (!requireAuth(request, reply, authService)) return;
|
||||
return db.listProjects();
|
||||
});
|
||||
|
||||
// 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 project = db.getProject(id);
|
||||
if (!project) return reply.code(404).send({ error: 'Project not found' });
|
||||
@@ -20,6 +24,7 @@ export function registerProjectRoutes(fastify: FastifyInstance, db: DatabaseInte
|
||||
|
||||
// Create project
|
||||
fastify.post('/api/projects', async (request, reply) => {
|
||||
if (!requireAuth(request, reply, authService)) 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));
|
||||
@@ -27,6 +32,7 @@ export function registerProjectRoutes(fastify: FastifyInstance, db: DatabaseInte
|
||||
|
||||
// Update project
|
||||
fastify.patch('/api/projects/:id', async (request, reply) => {
|
||||
if (!requireAuth(request, reply, authService)) return;
|
||||
const { id } = request.params as { id: string };
|
||||
const body = request.body as Partial<DBProject>;
|
||||
const updated = db.updateProject(id, body);
|
||||
@@ -36,6 +42,7 @@ 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 ok = db.deleteProject(id);
|
||||
if (!ok) return reply.code(404).send({ error: 'Project not found' });
|
||||
|
||||
@@ -3,10 +3,13 @@
|
||||
*/
|
||||
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';
|
||||
|
||||
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 setting = db.getSetting(key);
|
||||
if (!setting) return reply.code(404).send({ error: 'Setting not found' });
|
||||
@@ -14,7 +17,8 @@ export function registerSettingsRoutes(fastify: FastifyInstance, db: DatabaseInt
|
||||
});
|
||||
|
||||
// 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 body = request.body as { value?: string };
|
||||
if (body.value === undefined) {
|
||||
|
||||
@@ -1,19 +1,47 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
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 user = db.getUser(id);
|
||||
if (!user) return reply.code(404).send({ error: 'User not found' });
|
||||
@@ -21,8 +49,9 @@ export function registerUserRoutes(fastify: FastifyInstance, db: DatabaseInterfa
|
||||
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 body = request.body as Partial<DBUser>;
|
||||
// Prevent password update via this endpoint
|
||||
@@ -33,8 +62,9 @@ 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 ok = db.deleteUser(id);
|
||||
if (!ok) return reply.code(404).send({ error: 'User not found' });
|
||||
|
||||
@@ -90,12 +90,12 @@ export async function createServer(opts: ServerOptions) {
|
||||
});
|
||||
|
||||
registerAuthRoutes(fastify, authService);
|
||||
registerProjectRoutes(fastify, opts.db);
|
||||
registerDrawingRoutes(fastify, opts.db);
|
||||
registerLayerRoutes(fastify, opts.db);
|
||||
registerElementRoutes(fastify, opts.db);
|
||||
registerBlockRoutes(fastify, opts.db);
|
||||
registerSettingsRoutes(fastify, opts.db);
|
||||
registerProjectRoutes(fastify, opts.db, authService);
|
||||
registerDrawingRoutes(fastify, opts.db, authService);
|
||||
registerLayerRoutes(fastify, opts.db, authService);
|
||||
registerElementRoutes(fastify, opts.db, authService);
|
||||
registerBlockRoutes(fastify, opts.db, authService);
|
||||
registerSettingsRoutes(fastify, opts.db, authService);
|
||||
registerUserRoutes(fastify, opts.db, authService);
|
||||
registerAIRoutes(fastify, authService);
|
||||
registerNotificationRoutes(fastify, opts.db, authService);
|
||||
|
||||
Reference in New Issue
Block a user