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
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,
password: process.env.DB_PASSWORD,
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
// ssl: {
// 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
pool.connect((err, client, release) => {
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 = {
query: (text, params) => pool.query(text, params),
// You can add more specific database functions here if needed
// Example: getClient: () => pool.connect()
// Add transaction helper
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;
// Security measure - add token expiration check
const tokenIsExpired = (exp) => {
const currentTime = Math.floor(Date.now() / 1000);
return exp < currentTime;
};
const authenticateToken = (req, res, next) => {
// Get token from the 'token' cookie
const token = req.cookies.token;
@ -41,17 +47,31 @@ const authenticateToken = (req, res, next) => {
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) {
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');
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');
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.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
req.user = user;
// Add debug logging for successful auth

136
package-lock.json generated
View File

@ -10,10 +10,13 @@
"license": "ISC",
"dependencies": {
"bcrypt": "^5.1.1",
"compression": "^1.8.0",
"cookie-parser": "^1.4.7",
"dotenv": "^16.4.7",
"express": "^5.1.0",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0",
"pg": "^8.14.1"
},
"devDependencies": {
@ -120,6 +123,24 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"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": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
@ -277,6 +298,60 @@
"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": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -777,6 +852,15 @@
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@ -1130,6 +1214,49 @@
"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": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -1271,6 +1398,15 @@
"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": {
"version": "1.4.0",
"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",
"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"
},
"keywords": [
"todo",
@ -20,17 +19,20 @@
"authentication",
"jwt"
],
"author": "Your Name",
"author": "CodeDevMLH",
"license": "ISC",
"dependencies": {
"bcrypt": "^5.1.1",
"compression": "^1.8.0",
"cookie-parser": "^1.4.7",
"dotenv": "^16.4.7",
"express": "^5.1.0",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0",
"pg": "^8.14.1"
},
"devDependencies": {
"nodemon": "^3.1.9"
}
}
}

View File

@ -12,14 +12,37 @@ const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '1h';
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
router.post('/register', async (req, res) => {
const { username, password } = req.body;
// Basic validation
// Enhanced validation
if (!username || !password) {
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 {
// Check if user already exists
@ -51,6 +74,21 @@ router.post('/register', async (req, res) => {
// POST /api/auth/login - User Login
router.post('/login', async (req, res) => {
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) {
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
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
}
// Reset login attempts on successful login
delete loginAttempts[ip];
// User authenticated successfully, create JWT payload
const payload = {
id: user.id,

View File

@ -5,6 +5,9 @@ require('dotenv').config(); // Load environment variables first
const express = require('express');
const path = require('path');
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
const authRoutes = require('./routes/authRoutes');
@ -17,6 +20,23 @@ const PORT = process.env.PORT || 3000;
// --- 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
app.use(express.json());
// 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
app.use(express.static(path.join(__dirname, 'public')));
// --- 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.
app.use('/', viewRoutes);
// --- 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) => {
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) {
return next(err); // Delegate to default Express error handler if headers are sent
return next(err);
}
// Send a generic JSON error response
res.status(500).json({
message: 'Ein unerwarteter Serverfehler ist aufgetreten.',
// Optional: Nur im Entwicklungsmodus detailliertere Fehler senden
// error: process.env.NODE_ENV === 'development' ? err.message : undefined
message: 'Ein unerwarteter Serverfehler ist aufgetreten.',
error: process.env.NODE_ENV === 'development' ? err.message : undefined
});
});
// --- Start Server ---
app.listen(PORT, () => {
const server = app.listen(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);
}