From da1e4dcada6deba8cd6801a55380c9417aef8fc4 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Thu, 27 Nov 2025 20:42:11 +0100 Subject: [PATCH] feat(syncServer): enhance user authentication with async bcrypt operations, add security headers, and implement health check endpoint --- packages/super-sync-server/package.json | 1 + packages/super-sync-server/src/api.ts | 3 ++- packages/super-sync-server/src/auth.ts | 8 ++++---- packages/super-sync-server/src/config.ts | 5 +++++ packages/super-sync-server/src/db.ts | 7 +++++++ packages/super-sync-server/src/server.ts | 14 +++++++++++++- 6 files changed, 32 insertions(+), 6 deletions(-) diff --git a/packages/super-sync-server/package.json b/packages/super-sync-server/package.json index 2bd33e1ec3..95637ba061 100644 --- a/packages/super-sync-server/package.json +++ b/packages/super-sync-server/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@fastify/cors": "^11.1.0", + "@fastify/helmet": "^13.0.2", "@fastify/rate-limit": "^10.3.0", "@fastify/static": "^8.3.0", "bcryptjs": "^3.0.3", diff --git a/packages/super-sync-server/src/api.ts b/packages/super-sync-server/src/api.ts index 6e6d9f8ed7..5e4f20e322 100644 --- a/packages/super-sync-server/src/api.ts +++ b/packages/super-sync-server/src/api.ts @@ -120,7 +120,8 @@ export const apiRoutes = async (fastify: FastifyInstance): Promise => { } const { email, password } = parseResult.data; - const result = loginUser(email, password); + const result = await loginUser(email, password); + return reply.send(result); } catch (err) { Logger.error(`Login error: ${errorMessage(err)}`); diff --git a/packages/super-sync-server/src/auth.ts b/packages/super-sync-server/src/auth.ts index 695d0ac2c7..a0cc03adc0 100644 --- a/packages/super-sync-server/src/auth.ts +++ b/packages/super-sync-server/src/auth.ts @@ -26,7 +26,7 @@ export const registerUser = async ( // Password strength validation is handled by Zod in api.ts const db = getDb(); - const passwordHash = bcrypt.hashSync(password, 12); + const passwordHash = await bcrypt.hash(password, 12); const verificationToken = randomBytes(32).toString('hex'); const TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1000; const expiresAt = Date.now() + TWENTY_FOUR_HOURS_MS; @@ -92,10 +92,10 @@ export const verifyEmail = (token: string): boolean => { return true; }; -export const loginUser = ( +export const loginUser = async ( email: string, password: string, -): { token: string; user: { id: number; email: string } } => { +): Promise<{ token: string; user: { id: number; email: string } }> => { const db = getDb(); const user = db.prepare('SELECT * FROM users WHERE email = ?').get(email) as @@ -108,7 +108,7 @@ export const loginUser = ( const dummyHash = '$2a$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW'; const hashToCompare = user ? user.password_hash : dummyHash; - const isMatch = bcrypt.compareSync(password, hashToCompare); + const isMatch = await bcrypt.compare(password, hashToCompare); if (!user || !isMatch) { throw new Error('Invalid credentials'); diff --git a/packages/super-sync-server/src/config.ts b/packages/super-sync-server/src/config.ts index 0d4bdff0f2..f15692a1ae 100644 --- a/packages/super-sync-server/src/config.ts +++ b/packages/super-sync-server/src/config.ts @@ -80,6 +80,11 @@ export const loadConfigFromEnv = ( config.publicUrl = `http://localhost:${config.port}`; } + // Enforce HTTPS for PUBLIC_URL in production + if (process.env.NODE_ENV === 'production' && !config.publicUrl.startsWith('https://')) { + throw new Error('PUBLIC_URL must use HTTPS in production'); + } + // CORS configuration if (process.env.CORS_ENABLED !== undefined) { config.cors.enabled = process.env.CORS_ENABLED === 'true'; diff --git a/packages/super-sync-server/src/db.ts b/packages/super-sync-server/src/db.ts index 1d300636d7..f3ecf9fc0e 100644 --- a/packages/super-sync-server/src/db.ts +++ b/packages/super-sync-server/src/db.ts @@ -38,6 +38,13 @@ export const initDb = (dataDir: string) => { ) `); + // Create index for verification token lookups + db.exec(` + CREATE INDEX IF NOT EXISTS idx_users_verification_token + ON users(verification_token) + WHERE verification_token IS NOT NULL + `); + // Migration: Check if verification_token_expires_at exists const columns = db.pragma('table_info(users)') as { name: string }[]; const hasExpiresAt = columns.some( diff --git a/packages/super-sync-server/src/server.ts b/packages/super-sync-server/src/server.ts index 9e665ab173..9678e985bd 100644 --- a/packages/super-sync-server/src/server.ts +++ b/packages/super-sync-server/src/server.ts @@ -3,6 +3,7 @@ import * as fs from 'fs'; import Fastify, { FastifyInstance } from 'fastify'; import cors from '@fastify/cors'; import rateLimit from '@fastify/rate-limit'; +import helmet from '@fastify/helmet'; import fastifyStatic from '@fastify/static'; import * as path from 'path'; import { loadConfigFromEnv, ServerConfig } from './config'; @@ -132,6 +133,11 @@ export const createServer = ( logger: false, // We use our own logger }); + // Security Headers + await fastifyServer.register(helmet, { + contentSecurityPolicy: false, // Disable CSP for now to avoid conflicts with WebDAV clients/browsers + }); + // Rate Limiting (prevent brute force) // 100 requests per 15 minutes globally is a safe default // We can tighten this for specific routes if needed @@ -191,6 +197,11 @@ export const createServer = ( prefix: '/', }); + // Health Check + fastifyServer.get('/health', async () => { + return { status: 'ok' }; + }); + // API Routes await fastifyServer.register(apiRoutes, { prefix: '/api' }); @@ -210,7 +221,8 @@ export const createServer = ( const urlPath = req.url.split('?')[0]; if ( (req.method === 'GET' && staticFiles.includes(urlPath)) || - urlPath === '/verify-email' + urlPath === '/verify-email' || + urlPath === '/health' ) { done(); return;