From 4b6ad3aefe20fb2ac15b96cde6e7c494fb5feea5 Mon Sep 17 00:00:00 2001 From: Torsten Schulz Date: Sun, 28 Jul 2024 16:12:48 +0200 Subject: [PATCH] En-/decryption fixed --- backend/app.js | 13 +- backend/controllers/authController.js | 1 + backend/controllers/navigationController.js | 13 +- backend/controllers/settingsController.js | 75 +- backend/middleware/authMiddleware.js | 16 + .../2023xxxxxx-change-email-to-bytea.js | 40 + backend/models/community/user.js | 28 +- backend/models/community/user_param.js | 5 +- backend/routers/navigationRouter.js | 3 + backend/routers/settingsRouter.js | 20 +- backend/services/authService.js | 42 +- backend/utils/crypto.js | 20 +- backend/utils/encryption.js | 12 +- backend/utils/initializeTypes.js | 6 +- backend/utils/userdata.js | 10 + frontend/src/components/AppNavigation.vue | 45 +- frontend/src/components/SettingsWidget.vue | 28 +- .../src/components/form/CheckboxWidget.vue | 1 - .../src/components/form/FloatInputWidget.vue | 2 +- frontend/src/i18n/locales/de/settings.json | 44 +- frontend/src/router/index.js | 14 + frontend/src/utils/axios.js | 15 + frontend/src/utils/menuLoader.js | 4 +- frontend/src/views/settings/AccountView.vue | 73 + frontend/src/views/settings/SexualityView.vue | 17 + package-lock.json | 2840 +++++++++++++++++ package.json | 25 +- 27 files changed, 3315 insertions(+), 97 deletions(-) create mode 100644 backend/middleware/authMiddleware.js create mode 100644 backend/migrations/migrations/2023xxxxxx-change-email-to-bytea.js create mode 100644 backend/utils/userdata.js create mode 100644 frontend/src/views/settings/AccountView.vue create mode 100644 frontend/src/views/settings/SexualityView.vue create mode 100644 package-lock.json diff --git a/backend/app.js b/backend/app.js index 9787a7a..d62d93a 100644 --- a/backend/app.js +++ b/backend/app.js @@ -3,7 +3,7 @@ import path from 'path'; import { fileURLToPath } from 'url'; import chatRouter from './routers/chatRouter.js'; import authRouter from './routers/authRouter.js'; -import navigationRouter from './routers/navigationRouter.js' +import navigationRouter from './routers/navigationRouter.js'; import settingsRouter from './routers/settingsRouter.js'; import cors from 'cors'; @@ -11,8 +11,15 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); - -app.use(cors()); + +const corsOptions = { + origin: '*', + methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', + preflightContinue: false, + optionsSuccessStatus: 204 +}; + +app.use(cors(corsOptions)); app.use(express.json()); // To handle JSON request bodies app.use('/api/chat', chatRouter); diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js index 6a1aae2..17174de 100644 --- a/backend/controllers/authController.js +++ b/backend/controllers/authController.js @@ -7,6 +7,7 @@ export const register = async (req, res) => { const result = await userService.registerUser({ email, username, password, language }); res.status(201).json(result); } catch (error) { + console.log(error); res.status(500).json({ error: error.message }); } }; diff --git a/backend/controllers/navigationController.js b/backend/controllers/navigationController.js index 32a9c6f..5fed27e 100644 --- a/backend/controllers/navigationController.js +++ b/backend/controllers/navigationController.js @@ -8,20 +8,23 @@ const menuStructure = { home: { visible: ["all"], children: {}, - path: "/" + path: "/", + icon: "logo_mono.png" }, friends: { visible: ["all"], children: { manageFriends : { visible: ["all"], - path: "/socialnetwork/friends" + path: "/socialnetwork/friends", + icon: "friends24.png" } }, showLoggedinFriends: 1 }, socialnetwork: { visible: ["all"], + icon: "socialnetwork.png", children: { guestbook: { visible: ["all"], @@ -56,6 +59,7 @@ const menuStructure = { }, chats: { visible: ["over12"], + icon: "chat.png", children: { multiChat: { visible: ["over12"], @@ -69,6 +73,7 @@ const menuStructure = { }, falukant: { visible: ["all"], + icon: "falukant24.png", children: { create: { visible: ["nofalukantaccount"], @@ -130,9 +135,11 @@ const menuStructure = { }, minigames: { visible: ["all"], + icon: "minigames24.png", }, settings: { visible: ["all"], + icon: "settings24.png", children: { homepage: { visible: ["all"], @@ -156,7 +163,7 @@ const menuStructure = { }, sexuality: { visible: ["over14"], - path: "/setting/sexuality" + path: "/settings/sexuality" }, notifications: { visible: ["all"], diff --git a/backend/controllers/settingsController.js b/backend/controllers/settingsController.js index b720922..fc3fdbc 100644 --- a/backend/controllers/settingsController.js +++ b/backend/controllers/settingsController.js @@ -3,10 +3,40 @@ import SettingsType from '../models/type/settings.js'; import UserParam from '../models/community/user_param.js'; import User from '../models/community/user.js'; import UserParamValue from '../models/type/user_param_value.js'; +import { calculateAge } from '../utils/userdata.js'; +import { DataTypes, Op } from 'sequelize'; +import { decrypt } from '../utils/encryption.js'; export const filterSettings = async (req, res) => { const { userid, type } = req.body; try { + const user = await User.findOne({ where: { hashedId: userid } }); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + const userParams = await UserParam.findAll({ + where: { userId: user.id }, + include: [ + { + model: UserParamType, + as: 'paramType' + } + ] + }); + let birthdate = null; + let gender = null; + + for (const param of userParams) { + console.log(param.paramType.description); + if (param.paramType.description === 'birthdate') { + birthdate = param.value; + } + if (param.paramType.description === 'gender') { + const genderResult = await UserParamValue.findOne({ where: { id: param.value } }); + gender = genderResult.dataValues.value; + } + } + const age = birthdate ? calculateAge(birthdate) : null; const fields = await UserParamType.findAll({ include: [ { @@ -26,9 +56,14 @@ export const filterSettings = async (req, res) => { } ] } - ] + ], + where: { + [Op.and]: [ + { minAge: { [Op.or]: [null, { [Op.lte]: age }] } }, + { gender: { [Op.or]: [null, gender] } } + ] + } }); - const responseFields = await Promise.all(fields.map(async (field) => { const options = ['singleselect', 'multiselect'].includes(field.datatype) ? await UserParamValue.findAll({ where: { userParamTypeId: field.id } @@ -44,11 +79,10 @@ export const filterSettings = async (req, res) => { options: options.map(opt => ({ id: opt.id, value: opt.value })) }; })); - res.status(200).json(responseFields); } catch (error) { - console.error('Error fetching settings:', error); - res.status(500).json({ error: 'Internal server error' }); + console.error('Error filtering settings:', error); + res.status(500).json({ error: 'An error occurred while filtering the settings' }); } }; @@ -108,4 +142,33 @@ export const getTypeParamValue = async(req, res) => { return; } res.status(200).json({ paramValueId: userParamValueObject.value }); -}; \ No newline at end of file +}; + +export const getAccountSettings = async (req, res) => { + try { + const user = await User.findOne({ where: { hashedId: req.body.userId } }); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + const email = user.email; + res.status(200).json({ username: user.username, email, showinsearch: user.searchable }); + } catch (error) { + console.error('Error retrieving account settings:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +export const setAccountSettings = async(req, res) => { + const { userid: userId, username, email, searchable, oldpassword, newpassword, newpasswordrepeat } = req.body; + const user = await User.findOne({ where: { hashedId: userId }}); + if (!user) { + res.status(404).json({ error: 'User not found' }); + return; + } + user.searchable = searchable; + if (user.password !== oldpassword) { + res.status(401).json({error: 'Wrong password'}); + return; + } + +} \ No newline at end of file diff --git a/backend/middleware/authMiddleware.js b/backend/middleware/authMiddleware.js new file mode 100644 index 0000000..9e9ba9c --- /dev/null +++ b/backend/middleware/authMiddleware.js @@ -0,0 +1,16 @@ +import User from '../models/community/user.js'; + +export const authenticate = async (req, res, next) => { + const userId = req.headers.userid; + const authCode = req.headers.authcode; + 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' }); + } + + next(); +}; diff --git a/backend/migrations/migrations/2023xxxxxx-change-email-to-bytea.js b/backend/migrations/migrations/2023xxxxxx-change-email-to-bytea.js new file mode 100644 index 0000000..99389be --- /dev/null +++ b/backend/migrations/migrations/2023xxxxxx-change-email-to-bytea.js @@ -0,0 +1,40 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.sequelize.transaction(async (transaction) => { + await queryInterface.addColumn( + 'user', + 'email_temp', + { + type: Sequelize.BLOB, + }, + { transaction } + ); + + await queryInterface.sequelize.query( + 'UPDATE "community"."user" SET "email_temp" = "email"::bytea', + { transaction } + ); + + await queryInterface.removeColumn('user', 'email', { transaction }); + + await queryInterface.renameColumn('user', 'email_temp', 'email', { transaction }); + + await queryInterface.changeColumn( + 'user', + 'email', + { + type: Sequelize.BLOB, + allowNull: false, + unique: true, + }, + { transaction } + ); + }); + }, + + down: async (queryInterface, Sequelize) => { + // Rollback code if needed + }, +}; diff --git a/backend/models/community/user.js b/backend/models/community/user.js index fc4043c..c8e1ceb 100644 --- a/backend/models/community/user.js +++ b/backend/models/community/user.js @@ -1,25 +1,18 @@ import { sequelize } from '../../utils/sequelize.js'; import { DataTypes } from 'sequelize'; -import bcrypt from 'bcrypt'; -import { encrypt, generateIv } from '../../utils/encryption.js'; +import { encrypt, decrypt } from '../../utils/encryption.js'; import crypto from 'crypto'; const User = sequelize.define('user', { email: { - type: DataTypes.STRING, + type: DataTypes.BLOB, // Verwende BLOB, um die E-Mail als bytea zu speichern allowNull: false, unique: true, set(value) { if (value) { - const iv = generateIv(); - this.setDataValue('iv', iv.toString('hex')); - this.setDataValue('email', encrypt(value, iv)); + this.setDataValue('email', Buffer.from(encrypt(value), 'hex')); } - } - }, - iv: { - type: DataTypes.STRING, - allowNull: false + }, }, username: { type: DataTypes.STRING, @@ -46,6 +39,14 @@ const User = sequelize.define('user', { hashedId: { type: DataTypes.STRING, allowNull: true + }, + searchable: { + type: DataTypes.BOOLEAN, + defaultValue: true + }, + authCode: { + type: DataTypes.STRING, + allowNull: true } }, { tableName: 'user', @@ -57,6 +58,11 @@ const User = sequelize.define('user', { user.hashedId = hashedId; await user.save(); } + }, + getterMethods: { + email() { + return decrypt(this.getDataValue('email').toString('hex')); + } } }); diff --git a/backend/models/community/user_param.js b/backend/models/community/user_param.js index f1bc236..a53a869 100644 --- a/backend/models/community/user_param.js +++ b/backend/models/community/user_param.js @@ -63,13 +63,14 @@ const UserParam = sequelize.define('user_param', { UserParam.upsertParam = async function (userId, paramTypeId, value) { try { + const val = value !== null && value !== undefined ? value.toString() : ''; const [userParam, created] = await UserParam.findOrCreate({ where: { userId, paramTypeId }, - defaults: { value } + defaults: { value: val } }); if (!created) { - userParam.value = value; + userParam.value = value !== null && value !== undefined ? value.toString() : ''; await userParam.save(); } } catch (error) { diff --git a/backend/routers/navigationRouter.js b/backend/routers/navigationRouter.js index 7cd50b2..42d8f31 100644 --- a/backend/routers/navigationRouter.js +++ b/backend/routers/navigationRouter.js @@ -1,7 +1,10 @@ import { Router } from 'express'; import { menu } from '../controllers/navigationController.js'; +import { authenticate } from '../middleware/authMiddleware.js'; const router = Router(); +router.use(authenticate); + router.get('/:userid', menu); export default router; \ No newline at end of file diff --git a/backend/routers/settingsRouter.js b/backend/routers/settingsRouter.js index 39b1e5a..7dafa7d 100644 --- a/backend/routers/settingsRouter.js +++ b/backend/routers/settingsRouter.js @@ -1,11 +1,15 @@ import { Router } from 'express'; -import { filterSettings, updateSetting, getTypeParamValueId, getTypeParamValues, getTypeParamValue } from '../controllers/settingsController.js'; +import { filterSettings, updateSetting, getTypeParamValueId, getTypeParamValues, getTypeParamValue, getAccountSettings } from '../controllers/settingsController.js'; +import { authenticate } from '../middleware/authMiddleware.js'; -const settingsRouter = Router(); -settingsRouter.post('/filter', filterSettings); -settingsRouter.post('/update', updateSetting); -settingsRouter.post('/getparamvalueid', getTypeParamValueId); -settingsRouter.post('/getparamvalues', getTypeParamValues); -settingsRouter.post('/getparamvalue/:id', getTypeParamValue); +const router = Router(); -export default settingsRouter; +router.post('/filter', authenticate, filterSettings); +router.post('/update', authenticate, updateSetting); +router.post('/account', authenticate, getAccountSettings); + +router.post('/getparamvalues', getTypeParamValues); +router.post('/getparamvalueid', getTypeParamValueId); +router.post('/getparamvalue/:id', getTypeParamValue); + +export default router; diff --git a/backend/services/authService.js b/backend/services/authService.js index a4ef5c8..7ab61bd 100644 --- a/backend/services/authService.js +++ b/backend/services/authService.js @@ -1,51 +1,53 @@ import bcrypt from 'bcrypt'; +import crypto from 'crypto'; import { v4 as uuidv4 } from 'uuid'; import User from '../models/community/user.js'; 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 { encrypt, generateIv } from '../utils/encryption.js'; const saltRounds = 10; export const registerUser = async ({ email, username, password, language }) => { - const iv = generateIv(); - const encryptedEmail = encrypt(email, iv); + const encryptionKey = process.env.SECRET_KEY; const results = await sequelize.query( - `SELECT * FROM "community"."user" WHERE "email" = :email`, + `SELECT * FROM community.user WHERE pgp_sym_decrypt(email::bytea, :key) = :email`, { - replacements: { key: process.env.SECRET_KEY, email: encryptedEmail }, + replacements: { key: encryptionKey, email }, type: sequelize.QueryTypes.SELECT } ); - if (results.length && results.length > 0) { - throw new Error('Email already in use'); + if (results.length > 0) { + throw new Error('emailinuse'); } const hashedPassword = await bcrypt.hash(password, saltRounds); const resetToken = uuidv4(); const user = await User.create({ - email: encryptedEmail, - iv: iv.toString('hex'), + email: email, username, password: hashedPassword, resetToken: resetToken, active: false, registration_date: new Date() }); + const languageType = await UserParamType.findOne({ where: { description: 'language' } }); if (!languageType) { - throw new Error('Language type not found'); + throw new Error('languagenotfound'); } - await UserParam.create({ + + 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 }; + return { id: user.hashedId, username: user.username, active: user.active, param: [languageParam], authCode }; }; export const loginUser = async ({ username, password }) => { @@ -57,6 +59,9 @@ export const loginUser = async ({ username, password }) => { if (!match) { throw new Error('credentialsinvalid'); } + const authCode = crypto.randomBytes(20).toString('hex'); + user.authCode = authCode; + await user.save(); const params = await UserParam.findAll({ where: { userId: user.id @@ -69,9 +74,16 @@ export const loginUser = async ({ username, password }) => { } } }); - const mappedParams = params.map(param => { - return { 'name': param.paramType.description, 'value': param.value }; }); - return { id: user.hashedId, username: user.username, active: user.active, param: mappedParams }; + const mappedParams = params.map(param => { + return { 'name': param.paramType.description, 'value': param.value }; + }); + return { + id: user.hashedId, + username: user.username, + active: user.active, + param: mappedParams, + authCode + }; }; export const handleForgotPassword = async ({ email }) => { diff --git a/backend/utils/crypto.js b/backend/utils/crypto.js index e6a7e1a..1876d92 100644 --- a/backend/utils/crypto.js +++ b/backend/utils/crypto.js @@ -1,16 +1,18 @@ import crypto from 'crypto'; -const algorithm = 'aes-256-ctr'; -const secretKey = process.env.SECRET_KEY; +const algorithm = 'aes-256-ecb'; // Verwende ECB-Modus, der keinen IV benötigt +const key = crypto.scryptSync(process.env.SECRET_KEY, 'salt', 32); // Der Schlüssel sollte eine Länge von 32 Bytes haben export const encrypt = (text) => { - const cipher = crypto.createCipheriv(algorithm, secretKey, Buffer.alloc(16, 0)); - const encrypted = Buffer.concat([cipher.update(text), cipher.final()]); - return encrypted.toString('hex'); + const cipher = crypto.createCipheriv(algorithm, key, null); // Kein IV verwendet + let encrypted = cipher.update(text, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + return encrypted; }; -export const decrypt = (hash) => { - const decipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.alloc(16, 0)); - const decrpyted = Buffer.concat([decipher.update(Buffer.from(hash, 'hex')), decipher.final()]); - return decrpyted.toString(); +export const decrypt = (text) => { + const decipher = crypto.createDecipheriv(algorithm, key, null); // Kein IV verwendet + let decrypted = decipher.update(text, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + return decrypted; }; diff --git a/backend/utils/encryption.js b/backend/utils/encryption.js index 7011fa6..d79da0e 100644 --- a/backend/utils/encryption.js +++ b/backend/utils/encryption.js @@ -1,21 +1,21 @@ import crypto from 'crypto'; -const algorithm = 'aes-256-cbc'; -const secretKey = process.env.SECRET_KEY; +const algorithm = 'aes-256-ecb'; +const key = crypto.scryptSync(process.env.SECRET_KEY, 'salt', 32); export const generateIv = () => { return crypto.randomBytes(16); }; -export const encrypt = (text, iv) => { - const cipher = crypto.createCipheriv(algorithm, Buffer.from(secretKey, 'utf-8'), iv); +export const encrypt = (text) => { + const cipher = crypto.createCipheriv(algorithm, key, null); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); return encrypted; }; -export const decrypt = (text, iv) => { - const decipher = crypto.createDecipheriv(algorithm, Buffer.from(secretKey, 'utf-8'), iv); +export const decrypt = (text) => { + const decipher = crypto.createDecipheriv(algorithm, key, null); let decrypted = decipher.update(text, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; diff --git a/backend/utils/initializeTypes.js b/backend/utils/initializeTypes.js index 38dcfbc..750eab7 100644 --- a/backend/utils/initializeTypes.js +++ b/backend/utils/initializeTypes.js @@ -35,8 +35,8 @@ const initializeTypes = async () => { sexualpreference: { type: 'singleselect', 'setting': 'sexuality', minAge: 14 }, gender: { type: 'singleselect', setting: 'personal' }, pubichair: { type: 'singleselect', setting: 'sexuality', minAge: 14 }, - penislenght: { type: 'int', setting: 'sexuality', minAge: 14, gender: 'm' }, - brasize: { type: 'string', setting: 'sexuality', minAge: 14, gender: 'f' } + penislenght: { type: 'int', setting: 'sexuality', minAge: 14, gender: 'male' }, + brasize: { type: 'string', setting: 'sexuality', minAge: 14, gender: 'female' } }; Object.keys(userParams).forEach(async(key) => { const item = userParams[key]; @@ -56,6 +56,8 @@ const initializeTypes = async () => { hairlength: ['short', 'medium', 'long', 'bald', 'other'], skincolor: ['light', 'medium', 'dark', 'other'], freckles: ['much', 'medium', 'less', 'none'], + sexualpreference: ['straight', 'gay', 'bi', 'pan', 'asexual'], + pubichair: ['none', 'short', 'medium', 'long', 'hairy', 'waxed', 'landingstrip', 'bikinizone', 'other'], }; Object.keys(valuesList).forEach(async(key) => { const values = valuesList[key]; diff --git a/backend/utils/userdata.js b/backend/utils/userdata.js new file mode 100644 index 0000000..eca211b --- /dev/null +++ b/backend/utils/userdata.js @@ -0,0 +1,10 @@ +export const calculateAge = (birthdate) => { + const today = new Date(); + const birthDate = new Date(birthdate); + let age = today.getFullYear() - birthDate.getFullYear(); + const monthDiff = today.getMonth() - birthDate.getMonth(); + if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { + age--; + } + return age; +}; \ No newline at end of file diff --git a/frontend/src/components/AppNavigation.vue b/frontend/src/components/AppNavigation.vue index d425f87..ed03a01 100644 --- a/frontend/src/components/AppNavigation.vue +++ b/frontend/src/components/AppNavigation.vue @@ -1,13 +1,14 @@