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 + e1b96310 improvements):
- LayerPanel.tsx: kept HEAD display:flex + e1b96310 aria-label
- PropertiesPanel.tsx: kept HEAD onDelete handler + e1b96310 formatPos helpers
- PropertiesPanel.tsx: kept HEAD delete button (feature) + full inline styles
- Topbar.tsx: kept HEAD onClick + e1b96310 camelCase SVG attrs

Refs: CODE_ANALYSIS.md (Issue #1 partially improved), e1b96310
This commit is contained in:
2026-06-30 11:32:33 +02:00
parent 4b9140a99b
commit 707214447b
35 changed files with 631 additions and 165 deletions
+35
View File
@@ -11,6 +11,7 @@ describe('Drawings API', () => {
let db: SqliteAdapter;
let projectId: string;
let createdDrawingId: string;
let authToken: string;
beforeAll(async () => {
db = new SqliteAdapter(':memory:');
@@ -18,10 +19,18 @@ describe('Drawings API', () => {
app = await createServer({ db, port: 0 });
await app.ready();
const regRes = await app.inject({
method: 'POST',
url: '/api/auth/register',
payload: { email: 'admin-test@example.com', password: 'Password123!', name: 'Admin Test', role: 'admin' },
});
authToken = JSON.parse(regRes.body).session.token;
// Create a project first (drawings belong to projects)
const projectRes = await app.inject({
method: 'POST',
url: '/api/projects',
headers: { authorization: `Bearer ${authToken}` },
payload: { name: 'Drawings Test Project' },
});
projectId = JSON.parse(projectRes.body).id;
@@ -32,6 +41,18 @@ describe('Drawings API', () => {
db.close();
});
// ─── Auth Check ──────────────────────────────────────
describe('Authentication', () => {
it('should return 401 without authorization header', async () => {
const response = await app.inject({
method: 'GET',
url: `/api/projects/${projectId}/drawings`,
});
expect(response.statusCode).toBe(401);
});
});
// ─── Create ─────────────────────────────────────────
describe('POST /api/projects/:projectId/drawings', () => {
@@ -39,6 +60,7 @@ describe('Drawings API', () => {
const response = await app.inject({
method: 'POST',
url: `/api/projects/${projectId}/drawings`,
headers: { authorization: `Bearer ${authToken}` },
payload: { name: 'Ground Floor' },
});
expect(response.statusCode).toBe(201);
@@ -53,6 +75,7 @@ describe('Drawings API', () => {
const response = await app.inject({
method: 'POST',
url: `/api/projects/${projectId}/drawings`,
headers: { authorization: `Bearer ${authToken}` },
payload: {},
});
expect(response.statusCode).toBe(201);
@@ -69,6 +92,7 @@ describe('Drawings API', () => {
const response = await app.inject({
method: 'GET',
url: `/api/projects/${projectId}/drawings`,
headers: { authorization: `Bearer ${authToken}` },
});
expect(response.statusCode).toBe(200);
const body = JSON.parse(response.body);
@@ -81,6 +105,7 @@ describe('Drawings API', () => {
const projRes = await app.inject({
method: 'POST',
url: '/api/projects',
headers: { authorization: `Bearer ${authToken}` },
payload: { name: 'Empty Project' },
});
const emptyProjId = JSON.parse(projRes.body).id;
@@ -88,6 +113,7 @@ describe('Drawings API', () => {
const response = await app.inject({
method: 'GET',
url: `/api/projects/${emptyProjId}/drawings`,
headers: { authorization: `Bearer ${authToken}` },
});
expect(response.statusCode).toBe(200);
const body = JSON.parse(response.body);
@@ -103,6 +129,7 @@ describe('Drawings API', () => {
const response = await app.inject({
method: 'GET',
url: `/api/drawings/${createdDrawingId}`,
headers: { authorization: `Bearer ${authToken}` },
});
expect(response.statusCode).toBe(200);
const body = JSON.parse(response.body);
@@ -114,6 +141,7 @@ describe('Drawings API', () => {
const response = await app.inject({
method: 'GET',
url: '/api/drawings/non-existent-id',
headers: { authorization: `Bearer ${authToken}` },
});
expect(response.statusCode).toBe(404);
});
@@ -126,6 +154,7 @@ describe('Drawings API', () => {
const response = await app.inject({
method: 'PATCH',
url: `/api/drawings/${createdDrawingId}`,
headers: { authorization: `Bearer ${authToken}` },
payload: { name: 'First Floor' },
});
expect(response.statusCode).toBe(200);
@@ -137,6 +166,7 @@ describe('Drawings API', () => {
const response = await app.inject({
method: 'PATCH',
url: `/api/drawings/${createdDrawingId}`,
headers: { authorization: `Bearer ${authToken}` },
payload: { data_json: '{"layers":[]}' },
});
expect(response.statusCode).toBe(200);
@@ -148,6 +178,7 @@ describe('Drawings API', () => {
const response = await app.inject({
method: 'PATCH',
url: '/api/drawings/non-existent-id',
headers: { authorization: `Bearer ${authToken}` },
payload: { name: 'New Name' },
});
expect(response.statusCode).toBe(404);
@@ -162,6 +193,7 @@ describe('Drawings API', () => {
const createRes = await app.inject({
method: 'POST',
url: `/api/projects/${projectId}/drawings`,
headers: { authorization: `Bearer ${authToken}` },
payload: { name: 'To Be Deleted' },
});
const drawingId = JSON.parse(createRes.body).id;
@@ -169,6 +201,7 @@ describe('Drawings API', () => {
const response = await app.inject({
method: 'DELETE',
url: `/api/drawings/${drawingId}`,
headers: { authorization: `Bearer ${authToken}` },
});
expect(response.statusCode).toBe(204);
@@ -176,6 +209,7 @@ describe('Drawings API', () => {
const getRes = await app.inject({
method: 'GET',
url: `/api/drawings/${drawingId}`,
headers: { authorization: `Bearer ${authToken}` },
});
expect(getRes.statusCode).toBe(404);
});
@@ -184,6 +218,7 @@ describe('Drawings API', () => {
const response = await app.inject({
method: 'DELETE',
url: '/api/drawings/non-existent-id',
headers: { authorization: `Bearer ${authToken}` },
});
expect(response.statusCode).toBe(404);
});