fix: comprehensive quality improvements

- Fix 2 backend test failures (projectFolders validation, StressTest timeout)
- Fix frontend test dependency (@testing-library/dom)
- Delete App.tsx.bak from repo
- Add rate limiting on auth endpoints (login: 10/min, register: 3/min)
- Add .env.example with all environment variables
- Update BAUPLAN.md with accurate phase status (16/21 phases implemented)
- Add OpenAPI/Swagger documentation (docs/openapi.yaml)
- Add E2E workflow test (15 steps: register→project→drawing→layer→element→CRUD→delete)
- Add Forgejo CI/CD pipeline (.forgejo/workflows/ci.yml)
- Improve .gitignore (db-wal, .env.*, *.bak, coverage)
- Add security comment for default user in schema.sql

All 628 tests passing (254 backend + 374 frontend)
This commit is contained in:
2026-07-26 22:47:50 +02:00
parent 5d9c3f9495
commit 02308dc54a
15 changed files with 1250 additions and 1163 deletions
+2
View File
@@ -16,6 +16,8 @@ CREATE TABLE IF NOT EXISTS users (
);
-- ─── Default User (seed) ────────────────────────────
-- Default user for foreign key constraints. Cannot login (empty password_hash
-- means bcrypt.compare always fails). Real users must register explicitly.
INSERT OR IGNORE INTO users (id, email, password_hash, name, role) VALUES ('user-default', 'default@webcad.local', '', 'Default User', 'admin');
-- ─── Projects ───────────────────────────────────────
+9
View File
@@ -3,10 +3,15 @@
*/
import type { FastifyInstance } from 'fastify';
import type { AuthService } from '../auth/AuthService.js';
import { checkLoginRateLimit, checkRegisterRateLimit, getClientIp } from '../utils/rateLimiter.js';
export function registerAuthRoutes(fastify: FastifyInstance, authService: AuthService) {
// Register new user
fastify.post('/api/auth/register', async (request, reply) => {
const ip = getClientIp(request);
const rateLimitErr = checkRegisterRateLimit(ip);
if (rateLimitErr) return reply.code(429).send({ error: rateLimitErr });
const { email, password, name, role } = request.body as {
email?: string; password?: string; name?: string; role?: string;
};
@@ -22,6 +27,10 @@ export function registerAuthRoutes(fastify: FastifyInstance, authService: AuthSe
// Login
fastify.post('/api/auth/login', async (request, reply) => {
const ip = getClientIp(request);
const rateLimitErr = checkLoginRateLimit(ip);
if (rateLimitErr) return reply.code(429).send({ error: rateLimitErr });
const { email, password } = request.body as { email?: string; password?: string };
if (!email || !password) {
return reply.code(400).send({ error: 'Email and password are required' });
+2
View File
@@ -68,6 +68,8 @@ export function registerProjectFolderRoutes(fastify: FastifyInstance, db: Databa
const nameErr = validateName(body.name, 'name');
if (nameErr) return reply.code(400).send({ error: nameErr });
updates.name = body.name;
} else {
return reply.code(400).send({ error: 'name is required' });
}
if (body.parent_id !== undefined) updates.parent_id = body.parent_id;
const updated = db.updateProjectFolder(id, updates);
+67
View File
@@ -0,0 +1,67 @@
/**
* Simple in-memory rate limiter for auth endpoints.
* Limits requests per IP address within a time window.
*/
interface RateLimitEntry {
count: number;
resetTime: number;
}
const WINDOW_MS = 60 * 1000; // 1 minute
const MAX_REQUESTS = 10; // max requests per window per IP
const MAX_REGISTER = 3; // stricter for registration
const loginLimiter = new Map<string, RateLimitEntry>();
const registerLimiter = new Map<string, RateLimitEntry>();
/**
* Check rate limit for a given IP and limiter map.
* Returns null if allowed, or an error message if rate limit exceeded.
*/
function checkRateLimit(
ip: string,
limiter: Map<string, RateLimitEntry>,
maxRequests: number,
): string | null {
const now = Date.now();
const entry = limiter.get(ip);
if (!entry || now > entry.resetTime) {
limiter.set(ip, { count: 1, resetTime: now + WINDOW_MS });
return null;
}
if (entry.count >= maxRequests) {
const retryAfter = Math.ceil((entry.resetTime - now) / 1000);
return `Rate limit exceeded. Try again in ${retryAfter} seconds.`;
}
entry.count++;
return null;
}
/**
* Check login rate limit for an IP.
*/
export function checkLoginRateLimit(ip: string): string | null {
return checkRateLimit(ip, loginLimiter, MAX_REQUESTS);
}
/**
* Check register rate limit for an IP.
*/
export function checkRegisterRateLimit(ip: string): string | null {
return checkRateLimit(ip, registerLimiter, MAX_REGISTER);
}
/**
* Extract client IP from Fastify request.
*/
export function getClientIp(request: { ip: string; headers: Record<string, string | string[] | undefined> }): string {
const forwarded = request.headers['x-forwarded-for'];
if (typeof forwarded === 'string') {
return forwarded.split(',')[0].trim();
}
return request.ip;
}