Compare commits
2 Commits
dc2b2d534d
...
e0faa93b78
Author | SHA1 | Date | |
---|---|---|---|
e0faa93b78 | |||
4dfdb17b1e |
2
.env
2
.env
@ -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
29
db.js
@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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
136
package-lock.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
55
server.js
55
server.js
@ -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);
|
||||
}
|
||||
|
Reference in New Issue
Block a user