diff --git a/backend/app.js b/backend/app.js index f10d65e..57b44eb 100644 --- a/backend/app.js +++ b/backend/app.js @@ -10,6 +10,7 @@ import contactRouter from './routers/contactRouter.js'; import cors from 'cors'; import socialnetworkRouter from './routers/socialnetworkRouter.js'; import forumRouter from './routers/forumRouter.js'; +import './jobs/sessionCleanup.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js index 43576c5..56e25d5 100644 --- a/backend/controllers/authController.js +++ b/backend/controllers/authController.js @@ -6,6 +6,7 @@ class AuthController { this.login = this.login.bind(this); this.forgotPassword = this.forgotPassword.bind(this); this.activateAccount = this.activateAccount.bind(this); + this.logout = this.logout.bind(this); } async register(req, res) { @@ -33,6 +34,11 @@ class AuthController { } } + async logout(req, res) { + const { userid: hashedUserId } = req.headers; + await userService.logoutUser(hashedUserId); + } + async forgotPassword(req, res) { const { email } = req.body; try { diff --git a/backend/jobs/sessionCleanup.js b/backend/jobs/sessionCleanup.js new file mode 100644 index 0000000..9da0664 --- /dev/null +++ b/backend/jobs/sessionCleanup.js @@ -0,0 +1,5 @@ +import { cleanupExpiredSessions } from '../utils/redis.js'; + +setInterval(async () => { + await cleanupExpiredSessions(); +}, 5000); diff --git a/backend/middleware/authMiddleware.js b/backend/middleware/authMiddleware.js index 9e9ba9c..6054345 100644 --- a/backend/middleware/authMiddleware.js +++ b/backend/middleware/authMiddleware.js @@ -1,4 +1,5 @@ import User from '../models/community/user.js'; +import { updateUserTimestamp } from '../utils/redis.js'; export const authenticate = async (req, res, next) => { const userId = req.headers.userid; @@ -6,11 +7,14 @@ export const authenticate = async (req, res, next) => { if (!userId || !authCode) { return res.status(401).json({ error: 'Unauthorized: Missing credentials' }); } - const user = await User.findOne({ where: { hashedId: userId, authCode } }); if (!user) { return res.status(401).json({ error: 'Unauthorized: Invalid credentials' }); } - + try { + await updateUserTimestamp(userId); + } catch (error) { + console.error('Fehler beim Aktualisieren des Zeitstempels:', error); + } next(); }; diff --git a/backend/package-lock.json b/backend/package-lock.json index b3d13c7..073e8c2 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -11,10 +11,12 @@ "dependencies": { "amqplib": "^0.10.4", "bcrypt": "^5.1.1", + "connect-redis": "^7.1.1", "cors": "^2.8.5", "dompurify": "^3.1.7", "dotenv": "^16.4.5", "express": "^4.19.2", + "express-session": "^1.18.1", "i18n": "^0.15.1", "joi": "^17.13.3", "jsdom": "^25.0.1", @@ -23,6 +25,7 @@ "nodemailer": "^6.9.14", "pg": "^8.12.0", "pg-hstore": "^2.3.4", + "redis": "^4.7.0", "sequelize": "^6.37.3", "sharp": "^0.33.5", "socket.io": "^4.7.5", @@ -579,6 +582,59 @@ "node": ">=14" } }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.0.tgz", + "integrity": "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/graph": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", + "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", + "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", + "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -960,6 +1016,14 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -1081,6 +1145,17 @@ "proto-list": "~1.2.1" } }, + "node_modules/connect-redis": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-7.1.1.tgz", + "integrity": "sha512-M+z7alnCJiuzKa8/1qAYdGUXHYfDnLolOGAUjOioB07pP39qxjG+X9ibsud7qUBc4jMV5Mcy3ugGv8eFcgamJQ==", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "express-session": ">=1" + } + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -1611,6 +1686,69 @@ "node": ">= 0.10.0" } }, + "node_modules/express-session": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/express-session/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -1828,6 +1966,14 @@ "is-property": "^1.0.2" } }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "engines": { + "node": ">= 4" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -2806,6 +2952,14 @@ "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==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3060,6 +3214,14 @@ "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -3093,6 +3255,22 @@ "string_decoder": "~0.10.x" } }, + "node_modules/redis": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.0.tgz", + "integrity": "sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==", + "workspaces": [ + "./packages/*" + ], + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.6.0", + "@redis/graph": "1.1.1", + "@redis/json": "1.0.7", + "@redis/search": "1.2.0", + "@redis/time-series": "1.1.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -3715,6 +3893,17 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/umzug": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz", diff --git a/backend/package.json b/backend/package.json index 5189a09..a3311a4 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,10 +13,12 @@ "dependencies": { "amqplib": "^0.10.4", "bcrypt": "^5.1.1", + "connect-redis": "^7.1.1", "cors": "^2.8.5", "dompurify": "^3.1.7", "dotenv": "^16.4.5", "express": "^4.19.2", + "express-session": "^1.18.1", "i18n": "^0.15.1", "joi": "^17.13.3", "jsdom": "^25.0.1", @@ -25,6 +27,7 @@ "nodemailer": "^6.9.14", "pg": "^8.12.0", "pg-hstore": "^2.3.4", + "redis": "^4.7.0", "sequelize": "^6.37.3", "sharp": "^0.33.5", "socket.io": "^4.7.5", diff --git a/backend/routers/authRouter.js b/backend/routers/authRouter.js index c507027..fb81b97 100644 --- a/backend/routers/authRouter.js +++ b/backend/routers/authRouter.js @@ -8,5 +8,6 @@ router.post('/register', authController.register); router.post('/login', authController.login); router.post('/forgot-password', authController.forgotPassword); router.post('/activate', authController.activateAccount); +router.get('/logout', authController.logout); export default router; diff --git a/backend/services/authService.js b/backend/services/authService.js index 7ab61bd..80e5417 100644 --- a/backend/services/authService.js +++ b/backend/services/authService.js @@ -6,6 +6,7 @@ import UserParam from '../models/community/user_param.js'; import UserParamType from '../models/type/user_param.js'; import { sendAccountActivationEmail, sendPasswordResetEmail } from './emailService.js'; import { sequelize } from '../utils/sequelize.js'; +import { setUserSession, deleteUserSession } from '../utils/redis.js'; const saltRounds = 10; @@ -31,22 +32,18 @@ export const registerUser = async ({ email, username, password, language }) => { active: false, registration_date: new Date() }); - const languageType = await UserParamType.findOne({ where: { description: 'language' } }); if (!languageType) { throw new Error('languagenotfound'); } - const languageParam = await UserParam.create({ userId: user.id, paramTypeId: languageType.id, value: language }); - const activationLink = `${process.env.FRONTEND_URL}/activate?token=${resetToken}`; await sendAccountActivationEmail(email, activationLink, username, resetToken, language); const authCode = crypto.randomBytes(20).toString('hex'); - return { id: user.hashedId, username: user.username, active: user.active, param: [languageParam], authCode }; }; @@ -62,6 +59,14 @@ export const loginUser = async ({ username, password }) => { const authCode = crypto.randomBytes(20).toString('hex'); user.authCode = authCode; await user.save(); + const sessionData = { + id: user.hashedId, + username: user.username, + active: user.active, + authCode, + timestamp: Date.now() + }; + await setUserSession(user.id, sessionData); const params = await UserParam.findAll({ where: { userId: user.id @@ -86,6 +91,24 @@ export const loginUser = async ({ username, password }) => { }; }; +export const logoutUser = async (hashedUserId) => { + try { + const user = User.findOne({ + where: { + hashedId: hashedUserId + } + }) + if (!user) { + return; + } + await deleteUserSession(user.id); + console.log('Benutzer erfolgreich aus Redis entfernt:', userId); + } catch (error) { + console.error('Fehler beim Logout:', error); + throw new Error('logoutfailed'); + } +}; + export const handleForgotPassword = async ({ email }) => { const user = await User.findOne({ where: { email } }); if (!user) { diff --git a/backend/utils/redis.js b/backend/utils/redis.js new file mode 100644 index 0000000..84acea0 --- /dev/null +++ b/backend/utils/redis.js @@ -0,0 +1,82 @@ +import dotenv from 'dotenv'; +import { createClient } from 'redis'; +import User from '../models/community/user.js'; + +dotenv.config(); + +const EXPIRATION_TIME = 30 * 60 * 1000; + +const redisClient = createClient({ + url: `redis://${process.env.REDIS_HOST}:${process.env.REDIS_PORT}`, + password: process.env.REDIS_PASSWORD, + legacyMode: true, +}); + +redisClient.connect().catch(console.error); + +const setUserSession = async (userId, sessionData) => { + try { + await redisClient.hSet(`user:${userId}`, sessionData); + } catch (error) { + console.error('Fehler beim Setzen der Benutzersitzung:', error); + } +}; + +const deleteUserSession = async (userId) => { + try { + await redisClient.del(`user:${userId}`); + } catch (error) { + console.error('Fehler beim Löschen der Benutzersitzung:', error); + } +}; + +const getUserSession = async (userId) => { + try { + return await redisClient.hGetAll(`user:${userId}`); + } catch (error) { + console.error('Fehler beim Abrufen der Benutzersitzung:', error); + return null; + } +}; + +const updateUserTimestamp = async (hashedId) => { + try { + const userKey = `user:${hashedId}`; + const userExists = await redisClient.exists(userKey); + if (userExists) { + await redisClient.hSet(userKey, 'timestamp', Date.now()); + console.log(`Zeitstempel für Benutzer ${hashedId} aktualisiert.`); + } else { + console.warn(`Benutzer mit der hashedId ${hashedId} wurde nicht gefunden.`); + } + } catch (error) { + console.error('Fehler beim Aktualisieren des Zeitstempels:', error); + } +}; + +const cleanupExpiredSessions = async () => { + try { + const keys = await redisClient.keys('user:*'); + const now = Date.now(); + for (const key of keys) { + const session = await redisClient.hGetAll(key); + if (session.timestamp && now - parseInt(session.timestamp) > EXPIRATION_TIME) { + const userId = key.split(':')[1]; + await redisClient.del(key); + await User.update({ authCode: '' }, { where: { hashedId: userId } }); + console.log(`Abgelaufene Sitzung für Benutzer ${userId} gelöscht.`); + } + } + } catch (error) { + console.error('Fehler beim Bereinigen abgelaufener Sitzungen:', error); + } +}; + +export { + setUserSession, + deleteUserSession, + getUserSession, + updateUserTimestamp, + cleanupExpiredSessions, + redisClient +};