Compare commits

...

2 Commits

7 changed files with 288 additions and 26 deletions

2
.env
View File

@ -13,3 +13,5 @@ JWT_EXPIRES_IN=1h # How long the login token is valid
# Server Configuration # Server Configuration
PORT=3000 # Port the Node.js server will run on PORT=3000 # Port the Node.js server will run on
NODE_ENV=development # Set to 'production' in production environment

29
db.js
View File

@ -12,12 +12,22 @@ const pool = new Pool({
database: process.env.DB_DATABASE, database: process.env.DB_DATABASE,
password: process.env.DB_PASSWORD, password: process.env.DB_PASSWORD,
port: process.env.DB_PORT, port: process.env.DB_PORT,
// Add connection pool settings for better performance
max: 20, // Maximum number of clients
idleTimeoutMillis: 30000, // How long a client is allowed to remain idle before being closed
connectionTimeoutMillis: 2000, // How long to wait for a connection
// Optional: Add SSL configuration if required by your database provider // Optional: Add SSL configuration if required by your database provider
// ssl: { // ssl: {
// rejectUnauthorized: false // Adjust as needed // rejectUnauthorized: false // Adjust as needed
// } // }
}); });
// Listen for errors on the pool
pool.on('error', (err) => {
console.error('Unexpected error on idle client', err);
process.exit(-1);
});
// Test the database connection on startup // Test the database connection on startup
pool.connect((err, client, release) => { pool.connect((err, client, release) => {
if (err) { if (err) {
@ -31,9 +41,22 @@ pool.connect((err, client, release) => {
} }
}); });
// Export a query function to interact with the database // Export functions to interact with the database
module.exports = { module.exports = {
query: (text, params) => pool.query(text, params), query: (text, params) => pool.query(text, params),
// You can add more specific database functions here if needed // Add transaction helper
// Example: getClient: () => pool.connect() withTransaction: async (callback) => {
const client = await pool.connect();
try {
await client.query('BEGIN');
const result = await callback(client);
await client.query('COMMIT');
return result;
} catch (e) {
await client.query('ROLLBACK');
throw e;
} finally {
client.release();
}
}
}; };

View File

@ -6,6 +6,12 @@ require('dotenv').config();
const JWT_SECRET = process.env.JWT_SECRET; const JWT_SECRET = process.env.JWT_SECRET;
// Security measure - add token expiration check
const tokenIsExpired = (exp) => {
const currentTime = Math.floor(Date.now() / 1000);
return exp < currentTime;
};
const authenticateToken = (req, res, next) => { const authenticateToken = (req, res, next) => {
// Get token from the 'token' cookie // Get token from the 'token' cookie
const token = req.cookies.token; const token = req.cookies.token;
@ -41,17 +47,31 @@ const authenticateToken = (req, res, next) => {
jwt.verify(token, JWT_SECRET, (err, user) => { jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) { if (err) {
console.error('JWT Verification Error:', err.message, err.name); console.error('JWT Verification Error:', err.message, err.name);
// If token is invalid or expired
if (isApiRequest) {
// Clear the invalid cookie and return 403 Forbidden for API requests
res.clearCookie('token'); res.clearCookie('token');
if (err.name === 'TokenExpiredError') {
if (isApiRequest) {
return res.status(401).json({ message: 'Sitzung abgelaufen. Bitte melden Sie sich erneut an.' });
}
return res.redirect('/login?expired=true');
}
if (isApiRequest) {
return res.status(403).json({ message: 'Token ungültig oder abgelaufen.' }); return res.status(403).json({ message: 'Token ungültig oder abgelaufen.' });
} }
// Clear the invalid cookie and redirect to login for page requests
res.clearCookie('token');
return res.redirect('/login'); return res.redirect('/login');
} }
// Additional check for token expiration as a security measure
if (user.exp && tokenIsExpired(user.exp)) {
console.warn('Token expired but not caught by jwt.verify');
res.clearCookie('token');
if (isApiRequest) {
return res.status(401).json({ message: 'Sitzung abgelaufen. Bitte melden Sie sich erneut an.' });
}
return res.redirect('/login?expired=true');
}
// If token is valid, attach the decoded user information (payload) to the request object // If token is valid, attach the decoded user information (payload) to the request object
req.user = user; req.user = user;
// Add debug logging for successful auth // Add debug logging for successful auth

136
package-lock.json generated
View File

@ -10,10 +10,13 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"compression": "^1.8.0",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"express": "^5.1.0", "express": "^5.1.0",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0",
"pg": "^8.14.1" "pg": "^8.14.1"
}, },
"devDependencies": { "devDependencies": {
@ -120,6 +123,24 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/basic-auth": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.1.2"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/basic-auth/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/bcrypt": { "node_modules/bcrypt": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
@ -277,6 +298,60 @@
"color-support": "bin.js" "color-support": "bin.js"
} }
}, },
"node_modules/compressible": {
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
"integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
"license": "MIT",
"dependencies": {
"mime-db": ">= 1.43.0 < 2"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/compression": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz",
"integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"compressible": "~2.0.18",
"debug": "2.6.9",
"negotiator": "~0.6.4",
"on-headers": "~1.0.2",
"safe-buffer": "5.2.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/compression/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/compression/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/compression/node_modules/negotiator": {
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
"integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -777,6 +852,15 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/helmet": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
"integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/http-errors": { "node_modules/http-errors": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@ -1130,6 +1214,49 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/morgan": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
"integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
"license": "MIT",
"dependencies": {
"basic-auth": "~2.0.1",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-finished": "~2.3.0",
"on-headers": "~1.0.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/morgan/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/morgan/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/morgan/node_modules/on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -1271,6 +1398,15 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/on-headers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/once": { "node_modules/once": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",

View File

@ -10,7 +10,6 @@
"create-db-container": "docker compose -f \"docker-compose.yml\" up -d --build", "create-db-container": "docker compose -f \"docker-compose.yml\" up -d --build",
"db:create": "docker exec -i postgresql psql -U postgres -c \"CREATE DATABASE todo_app_db;\"", "db:create": "docker exec -i postgresql psql -U postgres -c \"CREATE DATABASE todo_app_db;\"",
"db:init": "docker cp db/init_postgres.sql postgresql:/tmp/init.sql && docker exec -i postgresql psql -U postgres -d todo_app_db -f /tmp/init.sql" "db:init": "docker cp db/init_postgres.sql postgresql:/tmp/init.sql && docker exec -i postgresql psql -U postgres -d todo_app_db -f /tmp/init.sql"
}, },
"keywords": [ "keywords": [
"todo", "todo",
@ -20,14 +19,17 @@
"authentication", "authentication",
"jwt" "jwt"
], ],
"author": "Your Name", "author": "CodeDevMLH",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"compression": "^1.8.0",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"express": "^5.1.0", "express": "^5.1.0",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0",
"pg": "^8.14.1" "pg": "^8.14.1"
}, },
"devDependencies": { "devDependencies": {

View File

@ -12,15 +12,38 @@ const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '1h'; const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '1h';
const SALT_ROUNDS = 10; // Cost factor for bcrypt hashing const SALT_ROUNDS = 10; // Cost factor for bcrypt hashing
// Simple rate limiting for login attempts
const loginAttempts = {};
const MAX_ATTEMPTS = 5;
const LOCKOUT_TIME = 15 * 60 * 1000; // 15 minutes
// Password validation function
const isPasswordValid = (password) => {
// At least 8 characters, containing a number and a letter
return password.length >= 8 &&
/\d/.test(password) &&
/[a-zA-Z]/.test(password);
};
// POST /api/auth/register - User Registration // POST /api/auth/register - User Registration
router.post('/register', async (req, res) => { router.post('/register', async (req, res) => {
const { username, password } = req.body; const { username, password } = req.body;
// Basic validation // Enhanced validation
if (!username || !password) { if (!username || !password) {
return res.status(400).json({ message: 'Benutzername und Passwort sind erforderlich.' }); return res.status(400).json({ message: 'Benutzername und Passwort sind erforderlich.' });
} }
if (username.length < 3 || username.length > 30) {
return res.status(400).json({ message: 'Benutzername muss zwischen 3 und 30 Zeichen lang sein.' });
}
if (!isPasswordValid(password)) {
return res.status(400).json({
message: 'Passwort muss mindestens 8 Zeichen lang sein und mindestens eine Zahl und einen Buchstaben enthalten.'
});
}
try { try {
// Check if user already exists // Check if user already exists
const userCheck = await db.query('SELECT * FROM users WHERE username = $1', [username]); const userCheck = await db.query('SELECT * FROM users WHERE username = $1', [username]);
@ -51,6 +74,21 @@ router.post('/register', async (req, res) => {
// POST /api/auth/login - User Login // POST /api/auth/login - User Login
router.post('/login', async (req, res) => { router.post('/login', async (req, res) => {
const { username, password } = req.body; const { username, password } = req.body;
const ip = req.ip;
// Check for login attempts rate limiting
if (loginAttempts[ip] && loginAttempts[ip].count >= MAX_ATTEMPTS) {
const timeElapsed = Date.now() - loginAttempts[ip].timestamp;
if (timeElapsed < LOCKOUT_TIME) {
const minutesLeft = Math.ceil((LOCKOUT_TIME - timeElapsed) / 60000);
return res.status(429).json({
message: `Zu viele Anmeldeversuche. Bitte versuchen Sie es in ${minutesLeft} Minuten erneut.`
});
} else {
// Reset attempts after lockout period
delete loginAttempts[ip];
}
}
if (!username || !password) { if (!username || !password) {
return res.status(400).json({ message: 'Benutzername und Passwort sind erforderlich.' }); return res.status(400).json({ message: 'Benutzername und Passwort sind erforderlich.' });
@ -63,9 +101,19 @@ router.post('/login', async (req, res) => {
// Check if user exists and password is correct // Check if user exists and password is correct
if (!user || !(await bcrypt.compare(password, user.password_hash))) { if (!user || !(await bcrypt.compare(password, user.password_hash))) {
// Track failed login attempts
if (!loginAttempts[ip]) {
loginAttempts[ip] = { count: 0, timestamp: Date.now() };
}
loginAttempts[ip].count++;
loginAttempts[ip].timestamp = Date.now();
return res.status(401).json({ message: 'Ungültiger Benutzername oder Passwort.' }); // 401 Unauthorized return res.status(401).json({ message: 'Ungültiger Benutzername oder Passwort.' }); // 401 Unauthorized
} }
// Reset login attempts on successful login
delete loginAttempts[ip];
// User authenticated successfully, create JWT payload // User authenticated successfully, create JWT payload
const payload = { const payload = {
id: user.id, id: user.id,

View File

@ -5,6 +5,9 @@ require('dotenv').config(); // Load environment variables first
const express = require('express'); const express = require('express');
const path = require('path'); const path = require('path');
const cookieParser = require('cookie-parser'); const cookieParser = require('cookie-parser');
const helmet = require('helmet'); // Add security headers
const compression = require('compression'); // Add compression
const morgan = require('morgan'); // Add request logging
// Import route handlers // Import route handlers
const authRoutes = require('./routes/authRoutes'); const authRoutes = require('./routes/authRoutes');
@ -17,6 +20,23 @@ const PORT = process.env.PORT || 3000;
// --- Middleware --- // --- Middleware ---
// Add security headers
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
},
}
}));
// Add compression to improve performance
app.use(compression());
// Add request logging
app.use(morgan('dev'));
// Parse JSON request bodies // Parse JSON request bodies
app.use(express.json()); app.use(express.json());
// Parse URL-encoded request bodies // Parse URL-encoded request bodies
@ -28,7 +48,6 @@ app.use(cookieParser());
// Files in 'public' will be accessible directly, e.g., /style.css, /script.js // Files in 'public' will be accessible directly, e.g., /style.css, /script.js
app.use(express.static(path.join(__dirname, 'public'))); app.use(express.static(path.join(__dirname, 'public')));
// --- Routes --- // --- Routes ---
// API routes // API routes
@ -40,28 +59,40 @@ app.use('/api/todos', todoRoutes); // Todo CRUD routes (protected by auth middle
// to avoid conflicts with static files or API routes. // to avoid conflicts with static files or API routes.
app.use('/', viewRoutes); app.use('/', viewRoutes);
// --- Global Error Handler (Basic Example) --- // --- Global Error Handler (Basic Example) ---
// Catches errors passed via next(error) or uncaught errors in route handlers
// ***** GEÄNDERT: Sendet jetzt JSON zurück *****
app.use((err, req, res, next) => { app.use((err, req, res, next) => {
console.error("Global Error Handler:", err.stack || err); // Log the full error stack console.error("Global Error Handler:", err.stack || err);
// Check if the response headers have already been sent
if (res.headersSent) { if (res.headersSent) {
return next(err); // Delegate to default Express error handler if headers are sent return next(err);
} }
// Send a generic JSON error response // Send a generic JSON error response
res.status(500).json({ res.status(500).json({
message: 'Ein unerwarteter Serverfehler ist aufgetreten.', message: 'Ein unerwarteter Serverfehler ist aufgetreten.',
// Optional: Nur im Entwicklungsmodus detailliertere Fehler senden error: process.env.NODE_ENV === 'development' ? err.message : undefined
// error: process.env.NODE_ENV === 'development' ? err.message : undefined
}); });
}); });
// --- Start Server --- // --- Start Server ---
app.listen(PORT, () => { const server = app.listen(PORT, () => {
console.log(`Server läuft auf http://localhost:${PORT}`); console.log(`Server läuft auf http://localhost:${PORT}`);
// Database connection message is handled in db.js
}); });
// Handle graceful shutdown
process.on('SIGTERM', gracefulShutdown);
process.on('SIGINT', gracefulShutdown);
function gracefulShutdown() {
console.log('Gracefully shutting down...');
server.close(() => {
console.log('Server closed');
process.exit(0);
});
// Force close after 10s if server hasn't closed gracefully
setTimeout(() => {
console.error('Server close timeout, forcing exit');
process.exit(1);
}, 10000);
}