feat(syncServer): enhance user authentication with async bcrypt operations, add security headers, and implement health check endpoint

This commit is contained in:
Johannes Millan 2025-11-27 20:42:11 +01:00
parent 585020aa3e
commit da1e4dcada
6 changed files with 32 additions and 6 deletions

View file

@ -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",

View file

@ -120,7 +120,8 @@ export const apiRoutes = async (fastify: FastifyInstance): Promise<void> => {
}
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)}`);

View file

@ -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');

View file

@ -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';

View file

@ -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(

View file

@ -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;