En-/decryption fixed

This commit is contained in:
Torsten Schulz
2024-07-28 16:12:48 +02:00
parent 4c12303edc
commit 4b6ad3aefe
27 changed files with 3315 additions and 97 deletions

View File

@@ -3,7 +3,7 @@ import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import chatRouter from './routers/chatRouter.js'; import chatRouter from './routers/chatRouter.js';
import authRouter from './routers/authRouter.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 settingsRouter from './routers/settingsRouter.js';
import cors from 'cors'; import cors from 'cors';
@@ -11,8 +11,15 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const app = express(); 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(express.json()); // To handle JSON request bodies
app.use('/api/chat', chatRouter); app.use('/api/chat', chatRouter);

View File

@@ -7,6 +7,7 @@ export const register = async (req, res) => {
const result = await userService.registerUser({ email, username, password, language }); const result = await userService.registerUser({ email, username, password, language });
res.status(201).json(result); res.status(201).json(result);
} catch (error) { } catch (error) {
console.log(error);
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
} }
}; };

View File

@@ -8,20 +8,23 @@ const menuStructure = {
home: { home: {
visible: ["all"], visible: ["all"],
children: {}, children: {},
path: "/" path: "/",
icon: "logo_mono.png"
}, },
friends: { friends: {
visible: ["all"], visible: ["all"],
children: { children: {
manageFriends : { manageFriends : {
visible: ["all"], visible: ["all"],
path: "/socialnetwork/friends" path: "/socialnetwork/friends",
icon: "friends24.png"
} }
}, },
showLoggedinFriends: 1 showLoggedinFriends: 1
}, },
socialnetwork: { socialnetwork: {
visible: ["all"], visible: ["all"],
icon: "socialnetwork.png",
children: { children: {
guestbook: { guestbook: {
visible: ["all"], visible: ["all"],
@@ -56,6 +59,7 @@ const menuStructure = {
}, },
chats: { chats: {
visible: ["over12"], visible: ["over12"],
icon: "chat.png",
children: { children: {
multiChat: { multiChat: {
visible: ["over12"], visible: ["over12"],
@@ -69,6 +73,7 @@ const menuStructure = {
}, },
falukant: { falukant: {
visible: ["all"], visible: ["all"],
icon: "falukant24.png",
children: { children: {
create: { create: {
visible: ["nofalukantaccount"], visible: ["nofalukantaccount"],
@@ -130,9 +135,11 @@ const menuStructure = {
}, },
minigames: { minigames: {
visible: ["all"], visible: ["all"],
icon: "minigames24.png",
}, },
settings: { settings: {
visible: ["all"], visible: ["all"],
icon: "settings24.png",
children: { children: {
homepage: { homepage: {
visible: ["all"], visible: ["all"],
@@ -156,7 +163,7 @@ const menuStructure = {
}, },
sexuality: { sexuality: {
visible: ["over14"], visible: ["over14"],
path: "/setting/sexuality" path: "/settings/sexuality"
}, },
notifications: { notifications: {
visible: ["all"], visible: ["all"],

View File

@@ -3,10 +3,40 @@ import SettingsType from '../models/type/settings.js';
import UserParam from '../models/community/user_param.js'; import UserParam from '../models/community/user_param.js';
import User from '../models/community/user.js'; import User from '../models/community/user.js';
import UserParamValue from '../models/type/user_param_value.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) => { export const filterSettings = async (req, res) => {
const { userid, type } = req.body; const { userid, type } = req.body;
try { 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({ const fields = await UserParamType.findAll({
include: [ 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 responseFields = await Promise.all(fields.map(async (field) => {
const options = ['singleselect', 'multiselect'].includes(field.datatype) ? await UserParamValue.findAll({ const options = ['singleselect', 'multiselect'].includes(field.datatype) ? await UserParamValue.findAll({
where: { userParamTypeId: field.id } where: { userParamTypeId: field.id }
@@ -44,11 +79,10 @@ export const filterSettings = async (req, res) => {
options: options.map(opt => ({ id: opt.id, value: opt.value })) options: options.map(opt => ({ id: opt.id, value: opt.value }))
}; };
})); }));
res.status(200).json(responseFields); res.status(200).json(responseFields);
} catch (error) { } catch (error) {
console.error('Error fetching settings:', error); console.error('Error filtering settings:', error);
res.status(500).json({ error: 'Internal server error' }); res.status(500).json({ error: 'An error occurred while filtering the settings' });
} }
}; };
@@ -108,4 +142,33 @@ export const getTypeParamValue = async(req, res) => {
return; return;
} }
res.status(200).json({ paramValueId: userParamValueObject.value }); res.status(200).json({ paramValueId: userParamValueObject.value });
}; };
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;
}
}

View File

@@ -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();
};

View File

@@ -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
},
};

View File

@@ -1,25 +1,18 @@
import { sequelize } from '../../utils/sequelize.js'; import { sequelize } from '../../utils/sequelize.js';
import { DataTypes } from 'sequelize'; import { DataTypes } from 'sequelize';
import bcrypt from 'bcrypt'; import { encrypt, decrypt } from '../../utils/encryption.js';
import { encrypt, generateIv } from '../../utils/encryption.js';
import crypto from 'crypto'; import crypto from 'crypto';
const User = sequelize.define('user', { const User = sequelize.define('user', {
email: { email: {
type: DataTypes.STRING, type: DataTypes.BLOB, // Verwende BLOB, um die E-Mail als bytea zu speichern
allowNull: false, allowNull: false,
unique: true, unique: true,
set(value) { set(value) {
if (value) { if (value) {
const iv = generateIv(); this.setDataValue('email', Buffer.from(encrypt(value), 'hex'));
this.setDataValue('iv', iv.toString('hex'));
this.setDataValue('email', encrypt(value, iv));
} }
} },
},
iv: {
type: DataTypes.STRING,
allowNull: false
}, },
username: { username: {
type: DataTypes.STRING, type: DataTypes.STRING,
@@ -46,6 +39,14 @@ const User = sequelize.define('user', {
hashedId: { hashedId: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true allowNull: true
},
searchable: {
type: DataTypes.BOOLEAN,
defaultValue: true
},
authCode: {
type: DataTypes.STRING,
allowNull: true
} }
}, { }, {
tableName: 'user', tableName: 'user',
@@ -57,6 +58,11 @@ const User = sequelize.define('user', {
user.hashedId = hashedId; user.hashedId = hashedId;
await user.save(); await user.save();
} }
},
getterMethods: {
email() {
return decrypt(this.getDataValue('email').toString('hex'));
}
} }
}); });

View File

@@ -63,13 +63,14 @@ const UserParam = sequelize.define('user_param', {
UserParam.upsertParam = async function (userId, paramTypeId, value) { UserParam.upsertParam = async function (userId, paramTypeId, value) {
try { try {
const val = value !== null && value !== undefined ? value.toString() : '';
const [userParam, created] = await UserParam.findOrCreate({ const [userParam, created] = await UserParam.findOrCreate({
where: { userId, paramTypeId }, where: { userId, paramTypeId },
defaults: { value } defaults: { value: val }
}); });
if (!created) { if (!created) {
userParam.value = value; userParam.value = value !== null && value !== undefined ? value.toString() : '';
await userParam.save(); await userParam.save();
} }
} catch (error) { } catch (error) {

View File

@@ -1,7 +1,10 @@
import { Router } from 'express'; import { Router } from 'express';
import { menu } from '../controllers/navigationController.js'; import { menu } from '../controllers/navigationController.js';
import { authenticate } from '../middleware/authMiddleware.js';
const router = Router(); const router = Router();
router.use(authenticate);
router.get('/:userid', menu); router.get('/:userid', menu);
export default router; export default router;

View File

@@ -1,11 +1,15 @@
import { Router } from 'express'; 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(); const router = Router();
settingsRouter.post('/filter', filterSettings);
settingsRouter.post('/update', updateSetting);
settingsRouter.post('/getparamvalueid', getTypeParamValueId);
settingsRouter.post('/getparamvalues', getTypeParamValues);
settingsRouter.post('/getparamvalue/:id', getTypeParamValue);
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;

View File

@@ -1,51 +1,53 @@
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import crypto from 'crypto';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import User from '../models/community/user.js'; import User from '../models/community/user.js';
import UserParam from '../models/community/user_param.js'; import UserParam from '../models/community/user_param.js';
import UserParamType from '../models/type/user_param.js'; import UserParamType from '../models/type/user_param.js';
import { sendAccountActivationEmail, sendPasswordResetEmail } from './emailService.js'; import { sendAccountActivationEmail, sendPasswordResetEmail } from './emailService.js';
import { sequelize } from '../utils/sequelize.js'; import { sequelize } from '../utils/sequelize.js';
import { encrypt, generateIv } from '../utils/encryption.js';
const saltRounds = 10; const saltRounds = 10;
export const registerUser = async ({ email, username, password, language }) => { export const registerUser = async ({ email, username, password, language }) => {
const iv = generateIv(); const encryptionKey = process.env.SECRET_KEY;
const encryptedEmail = encrypt(email, iv);
const results = await sequelize.query( 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 type: sequelize.QueryTypes.SELECT
} }
); );
if (results.length && results.length > 0) { if (results.length > 0) {
throw new Error('Email already in use'); throw new Error('emailinuse');
} }
const hashedPassword = await bcrypt.hash(password, saltRounds); const hashedPassword = await bcrypt.hash(password, saltRounds);
const resetToken = uuidv4(); const resetToken = uuidv4();
const user = await User.create({ const user = await User.create({
email: encryptedEmail, email: email,
iv: iv.toString('hex'),
username, username,
password: hashedPassword, password: hashedPassword,
resetToken: resetToken, resetToken: resetToken,
active: false, active: false,
registration_date: new Date() registration_date: new Date()
}); });
const languageType = await UserParamType.findOne({ where: { description: 'language' } }); const languageType = await UserParamType.findOne({ where: { description: 'language' } });
if (!languageType) { if (!languageType) {
throw new Error('Language type not found'); throw new Error('languagenotfound');
} }
await UserParam.create({
const languageParam = await UserParam.create({
userId: user.id, userId: user.id,
paramTypeId: languageType.id, paramTypeId: languageType.id,
value: language value: language
}); });
const activationLink = `${process.env.FRONTEND_URL}/activate?token=${resetToken}`; const activationLink = `${process.env.FRONTEND_URL}/activate?token=${resetToken}`;
await sendAccountActivationEmail(email, activationLink, username, resetToken, language); 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 }) => { export const loginUser = async ({ username, password }) => {
@@ -57,6 +59,9 @@ export const loginUser = async ({ username, password }) => {
if (!match) { if (!match) {
throw new Error('credentialsinvalid'); throw new Error('credentialsinvalid');
} }
const authCode = crypto.randomBytes(20).toString('hex');
user.authCode = authCode;
await user.save();
const params = await UserParam.findAll({ const params = await UserParam.findAll({
where: { where: {
userId: user.id userId: user.id
@@ -69,9 +74,16 @@ export const loginUser = async ({ username, password }) => {
} }
} }
}); });
const mappedParams = params.map(param => { const mappedParams = params.map(param => {
return { 'name': param.paramType.description, 'value': param.value }; }); return { 'name': param.paramType.description, 'value': param.value };
return { id: user.hashedId, username: user.username, active: user.active, param: mappedParams }; });
return {
id: user.hashedId,
username: user.username,
active: user.active,
param: mappedParams,
authCode
};
}; };
export const handleForgotPassword = async ({ email }) => { export const handleForgotPassword = async ({ email }) => {

View File

@@ -1,16 +1,18 @@
import crypto from 'crypto'; import crypto from 'crypto';
const algorithm = 'aes-256-ctr'; const algorithm = 'aes-256-ecb'; // Verwende ECB-Modus, der keinen IV benötigt
const secretKey = process.env.SECRET_KEY; 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) => { export const encrypt = (text) => {
const cipher = crypto.createCipheriv(algorithm, secretKey, Buffer.alloc(16, 0)); const cipher = crypto.createCipheriv(algorithm, key, null); // Kein IV verwendet
const encrypted = Buffer.concat([cipher.update(text), cipher.final()]); let encrypted = cipher.update(text, 'utf8', 'hex');
return encrypted.toString('hex'); encrypted += cipher.final('hex');
return encrypted;
}; };
export const decrypt = (hash) => { export const decrypt = (text) => {
const decipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.alloc(16, 0)); const decipher = crypto.createDecipheriv(algorithm, key, null); // Kein IV verwendet
const decrpyted = Buffer.concat([decipher.update(Buffer.from(hash, 'hex')), decipher.final()]); let decrypted = decipher.update(text, 'hex', 'utf8');
return decrpyted.toString(); decrypted += decipher.final('utf8');
return decrypted;
}; };

View File

@@ -1,21 +1,21 @@
import crypto from 'crypto'; import crypto from 'crypto';
const algorithm = 'aes-256-cbc'; const algorithm = 'aes-256-ecb';
const secretKey = process.env.SECRET_KEY; const key = crypto.scryptSync(process.env.SECRET_KEY, 'salt', 32);
export const generateIv = () => { export const generateIv = () => {
return crypto.randomBytes(16); return crypto.randomBytes(16);
}; };
export const encrypt = (text, iv) => { export const encrypt = (text) => {
const cipher = crypto.createCipheriv(algorithm, Buffer.from(secretKey, 'utf-8'), iv); const cipher = crypto.createCipheriv(algorithm, key, null);
let encrypted = cipher.update(text, 'utf8', 'hex'); let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex'); encrypted += cipher.final('hex');
return encrypted; return encrypted;
}; };
export const decrypt = (text, iv) => { export const decrypt = (text) => {
const decipher = crypto.createDecipheriv(algorithm, Buffer.from(secretKey, 'utf-8'), iv); const decipher = crypto.createDecipheriv(algorithm, key, null);
let decrypted = decipher.update(text, 'hex', 'utf8'); let decrypted = decipher.update(text, 'hex', 'utf8');
decrypted += decipher.final('utf8'); decrypted += decipher.final('utf8');
return decrypted; return decrypted;

View File

@@ -35,8 +35,8 @@ const initializeTypes = async () => {
sexualpreference: { type: 'singleselect', 'setting': 'sexuality', minAge: 14 }, sexualpreference: { type: 'singleselect', 'setting': 'sexuality', minAge: 14 },
gender: { type: 'singleselect', setting: 'personal' }, gender: { type: 'singleselect', setting: 'personal' },
pubichair: { type: 'singleselect', setting: 'sexuality', minAge: 14 }, pubichair: { type: 'singleselect', setting: 'sexuality', minAge: 14 },
penislenght: { type: 'int', setting: 'sexuality', minAge: 14, gender: 'm' }, penislenght: { type: 'int', setting: 'sexuality', minAge: 14, gender: 'male' },
brasize: { type: 'string', setting: 'sexuality', minAge: 14, gender: 'f' } brasize: { type: 'string', setting: 'sexuality', minAge: 14, gender: 'female' }
}; };
Object.keys(userParams).forEach(async(key) => { Object.keys(userParams).forEach(async(key) => {
const item = userParams[key]; const item = userParams[key];
@@ -56,6 +56,8 @@ const initializeTypes = async () => {
hairlength: ['short', 'medium', 'long', 'bald', 'other'], hairlength: ['short', 'medium', 'long', 'bald', 'other'],
skincolor: ['light', 'medium', 'dark', 'other'], skincolor: ['light', 'medium', 'dark', 'other'],
freckles: ['much', 'medium', 'less', 'none'], 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) => { Object.keys(valuesList).forEach(async(key) => {
const values = valuesList[key]; const values = valuesList[key];

10
backend/utils/userdata.js Normal file
View File

@@ -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;
};

View File

@@ -1,13 +1,14 @@
<template> <template>
<nav> <nav>
<ul> <ul>
<li v-for="(item, key) in menu" :key="key" class="mainmenuitem"> <li v-for="(item, key) in menu" :key="key" class="mainmenuitem" @click="openPage(item.path ?? null)">
<a :href="item.path" v-if="item.path" class="menuitem">{{ $t(`navigation.${key}`) }}</a> <span v-if="item.icon" :style="`background-image:url('/images/icons/${item.icon}')`"
<span v-if="!item.path" class="menuitem">{{ $t(`navigation.${key}`) }}</span> class="menu-icon">&nbsp;</span>
<span>{{ $t(`navigation.${key}`) }}</span>
<ul v-if="item.children" class="submenu1"> <ul v-if="item.children" class="submenu1">
<li v-for="(subitem, subkey) in item.children" :key="subitem.text"> <li v-for="(subitem, subkey) in item.children" :key="subitem.text" @click="openPage(subitem.path ?? null)">
<a :href="subitem.path" v-if="subitem.path">{{ $t(`navigation.m-${key}.${subkey}`) }}</a> <span v-if="subitem.icon" :style="`background-image:url('/images/icons/${subitem.icon}')`" class="submenu-icon">&nbsp;</span>
<span v-if="!subitem.path">{{ $t(`navigation.m-${key}.${subkey}`) }}</span> <span>{{ $t(`navigation.m-${key}.${subkey}`) }}</span>
</li> </li>
</ul> </ul>
</li> </li>
@@ -37,6 +38,11 @@ export default {
}, },
methods: { methods: {
...mapActions(['loadMenu', 'logout']), ...mapActions(['loadMenu', 'logout']),
openPage(url) {
if (url) {
this.$router.push(url);
}
}
} }
}; };
</script> </script>
@@ -71,11 +77,12 @@ nav>ul>li:hover {
background-color: #D37C06; background-color: #D37C06;
white-space: nowrap; white-space: nowrap;
} }
nav>ul>li:hover > span {
nav>ul>li:hover>span {
color: #000000; color: #000000;
} }
nav>ul>li:hover > ul{ nav>ul>li:hover>ul {
display: inline-block; display: inline-block;
} }
@@ -119,13 +126,33 @@ a {
left: 0; left: 0;
top: 2.5em; top: 2.5em;
} }
.submenu1 > li {
.submenu1>li {
padding: 0.5em; padding: 0.5em;
line-height: 1em; line-height: 1em;
color: #7E471B; color: #7E471B;
} }
.submenu1>li:hover { .submenu1>li:hover {
color: #000000; color: #000000;
background-color: #D37C06; background-color: #D37C06;
} }
.menu-icon {
width: 24px;
height: 24px;
margin-right: 3px;
background-repeat: no-repeat;
display: inline-block;
line-height: 1.6em;
}
.submenu-icon {
width: 1.2em;
height: 1em;
margin-right: 3px;
background-repeat: no-repeat;
display: inline-block;
line-height: 1em;
background-size: 1.2em 1.2em;
}
</style> </style>

View File

@@ -13,15 +13,14 @@
:list="getSettingOptions(setting.name, setting.options)" @input="handleInput(setting.id, $event)" /> :list="getSettingOptions(setting.name, setting.options)" @input="handleInput(setting.id, $event)" />
<InputNumberWidget v-else-if="setting.datatype == 'int'" <InputNumberWidget v-else-if="setting.datatype == 'int'"
:labelTr="`settings.personal.label.${setting.name}`" :labelTr="`settings.personal.label.${setting.name}`"
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value=setting.value min="0" max="200" :tooltipTr="`settings.personal.tooltip.${setting.name}`" :value="convertToInt(setting.value)" min="0"
@input="handleInput(setting.id, $event)" /> max="200" @input="handleInput(setting.id, $event)" />
<FloatInputWidget v-else-if="setting.datatype == 'float'" <FloatInputWidget v-else-if="setting.datatype == 'float'"
:labelTr="`settings.personal.label.${setting.name}`" :labelTr="`settings.personal.label.${setting.name}`"
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value=setting.value :tooltipTr="`settings.personal.tooltip.${setting.name}`" :value="convertToFloat(setting.value)"
@input="handleInput(setting.id, $event)" /> @input="handleInput(setting.id, $event)" />
<CheckboxWidget v-else-if="setting.datatype == 'bool'" <CheckboxWidget v-else-if="setting.datatype == 'bool'" :labelTr="`settings.personal.label.${setting.name}`"
:labelTr="`settings.personal.label.${setting.name}`" :tooltipTr="`settings.personal.tooltip.${setting.name}`" :value="convertToBool(setting.value)"
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value=setting.value
@input="handleInput(setting.id, $event)" /> @input="handleInput(setting.id, $event)" />
<div v-else>{{ setting }} <div v-else>{{ setting }}
</div> </div>
@@ -104,6 +103,23 @@ export default {
{ value: 'en', captionTr: 'settings.personal.languages.en' }, { value: 'en', captionTr: 'settings.personal.languages.en' },
{ value: 'de', captionTr: 'settings.personal.languages.de' }, { value: 'de', captionTr: 'settings.personal.languages.de' },
]; ];
},
convertToInt(value) {
const intValue = parseInt(value, 10);
return isNaN(intValue) ? 0 : intValue;
},
convertToFloat(value) {
const floatValue = parseFloat(value);
return isNaN(floatValue) ? 0.0 : floatValue;
},
convertToBool(value) {
if (value === 'true' || value === true) {
return true;
} else if (value === 'false' || value === false) {
return false;
} else {
return false;
}
} }
}, },
data() { data() {

View File

@@ -41,7 +41,6 @@ label {
display: flex; display: flex;
align-items: center; align-items: center;
} }
input[type="checkbox"] { input[type="checkbox"] {
margin-right: 0.5em; margin-right: 0.5em;
} }

View File

@@ -41,7 +41,7 @@ export default {
}, },
computed: { computed: {
formattedValue() { formattedValue() {
return this.value != null ? this.value.toFixed(this.decimals) : ''; return this.value != null && typeof this.value === 'float' ? this.value.toFixed(this.decimals) : '';
}, },
step() { step() {
return Math.pow(10, -this.decimals); return Math.pow(10, -this.decimals);

View File

@@ -16,7 +16,11 @@
"weight": "Gewicht", "weight": "Gewicht",
"bodyheight": "Größe", "bodyheight": "Größe",
"piercings": "Piercings", "piercings": "Piercings",
"tattoos": "Tattoos" "tattoos": "Tattoos",
"sexualpreference": "Ausrichtung",
"pubichair": "Schamhaare",
"penislenght": "Penislänge",
"brasize": "BH-Größe"
}, },
"tooltip": { "tooltip": {
"language": "Sprache", "language": "Sprache",
@@ -32,7 +36,11 @@
"weight": "Gewicht", "weight": "Gewicht",
"bodyheight": "Größe", "bodyheight": "Größe",
"piercings": "Piercings", "piercings": "Piercings",
"tattoos": "Tattoos" "tattoos": "Tattoos",
"sexualpreference": "Ausrichtung",
"pubichair": "Schamhaare",
"penislenght": "Penislänge",
"brasize": "BH-Größe"
}, },
"gender": { "gender": {
"male": "Männlich", "male": "Männlich",
@@ -83,10 +91,42 @@
"medium": "Mittel", "medium": "Mittel",
"less": "Wenige", "less": "Wenige",
"none": "Keine" "none": "Keine"
},
"sexualpreference": {
"straight": "Heterosexuell",
"gay": "Homosexuell",
"bi": "Bisexuell",
"asexual": "Asexuell",
"pan": "Pansexuell"
},
"pubichair": {
"none": "Keine",
"short": "Kurz",
"medium": "Mittel",
"long": "Lang",
"hairy": "Natürlich",
"waxed": "Heißwachsentfernung",
"landingstrip": "Landebahn",
"bikinizone": "Nur Bikinizone",
"other": "Andere"
} }
}, },
"view": { "view": {
"title": "Aussehen" "title": "Aussehen"
},
"sexuality": {
"title": "Sexualität"
},
"account": {
"title": "Account",
"username": "Benutzername",
"email": "Email-Adresse",
"newpassword": "Passwort",
"newpasswordretype": "Passwort wiederholen",
"deleteAccount": "Account löschen",
"language": "Sprache",
"showinsearch": "In Usersuchen anzeigen",
"changeaction": "Benutzerdaten ändern"
} }
} }
} }

View File

@@ -4,6 +4,8 @@ import HomeView from '../views/HomeView.vue';
import ActivateView from '../views/auth/ActivateView.vue'; import ActivateView from '../views/auth/ActivateView.vue';
import PeronalSettingsView from '../views/settings/PersonalView.vue'; import PeronalSettingsView from '../views/settings/PersonalView.vue';
import ViewSettingsView from '../views/settings/ViewView.vue'; import ViewSettingsView from '../views/settings/ViewView.vue';
import SexualitySettingsView from '../views/settings/SexualityView.vue';
import AccountSettingsView from '../views/settings/AccountView.vue';
const routes = [ const routes = [
{ {
@@ -28,6 +30,18 @@ const routes = [
component: ViewSettingsView, component: ViewSettingsView,
meta: { requiresAuth: true } meta: { requiresAuth: true }
}, },
{
path: '/settings/sexuality',
name: 'Sexuality settings',
component: SexualitySettingsView,
meta: { requiresAuth: true }
},
{
path: '/settings/account',
name: 'Account settings',
component: AccountSettingsView,
meta: { requiresAuth: true }
}
]; ];

View File

@@ -1,4 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import store from '../store';
const apiClient = axios.create({ const apiClient = axios.create({
baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:3001', baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:3001',
@@ -7,4 +8,18 @@ const apiClient = axios.create({
} }
}); });
apiClient.interceptors.request.use(config => {
console.log('loading user');
const user = store.getters.user;
console.log(user);
if (user && user.authCode) {
console.log('set auth');
config.headers['userId'] = user.id;
config.headers['authCode'] = user.authCode;
}
return config;
}, error => {
return Promise.reject(error);
});
export default apiClient; export default apiClient;

View File

@@ -1,4 +1,4 @@
import axios from 'axios'; import apiClient from './axios';
import store from '../store'; import store from '../store';
const loadMenu = async () => { const loadMenu = async () => {
@@ -7,7 +7,7 @@ const loadMenu = async () => {
if (!userId) { if (!userId) {
throw new Error('User ID not found'); throw new Error('User ID not found');
} }
const response = await axios.get('/api/navigation/' + userId); const response = await apiClient.get('/api/navigation/' + userId);
return response.data; return response.data;
} catch (err) { } catch (err) {
console.error(err); console.error(err);

View File

@@ -0,0 +1,73 @@
<template>
<div>
<h2>{{ $t("settings.account.title") }}</h2>
<div>
<label><span>{{ $t("settings.account.username") }} </span><input type="text" v-model="username"
:placeholder="$t('settings.account.username')" /></label>
</div>
<div>
<label><span>{{ $t("settings.account.email") }} </span><input type="text" v-model="email"
:placeholder="$t('settings.account.email')" /></label>
</div>
<div>
<label><span>{{ $t("settings.account.newpassword") }} </span><input type="password" v-model="newpassword"
:placeholder="$t('settings.account.newpassword')" /></label>
</div>
<div>
<label><span>{{ $t("settings.account.newpasswordretype") }} </span><input type="password"
v-model="newpasswordretype" :placeholder="$t('settings.account.newpasswordretype')" /></label>
</div>
<div>
<label><span>{{ $t("settings.account.oldpassword") }} </span><input type="password"
v-model="oldpassword" :placeholder="$t('settings.account.oldpassword')" /></label>
</div>
<div>
<button @click="changeAccount">{{ $t("settings.account.changeaction") }}</button>
</div>
<div>
<label><input type="checkbox" v-model="showInSearch" /> {{ $t("settings.account.showinsearch") }}</label>
</div>
</div>
</template>
<script>
import apiClient from '@/utils/axios.js';
import { mapGetters } from 'vuex';
export default {
name: "AccountSettingsView",
components: {},
computed: {
...mapGetters(['user']),
},
data() {
return {
username: "",
email: "",
newpassword: "",
newpasswordretype: "",
showInSearch: false,
oldpassword: "",
};
},
methods: {},
async mounted() {
const response = await apiClient.post('/api/settings/account', { userId: this.user.id });
console.log(response.data);
this.username = response.data.username;
this.showInSearch = response.data.showinsearch;
this.email = response.data.email;
console.log(this.showInSearch);
},
};
</script>
<style lang="scss" scoped>
label {
white-space: nowrap;
}
label > span {
width: 15em;
display: inline-block;
}
</style>

View File

@@ -0,0 +1,17 @@
<template>
<div>
<h2>{{ $t("settings.sexuality.title") }}</h2>
<SettingsWidget :settingsType="'sexuality'" />
</div>
</template>
<script>
import SettingsWidget from '@/components/SettingsWidget.vue';
export default {
name: 'SexualitySettingsView',
components: {
SettingsWidget,
}
}
</script>

2840
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2,17 +2,20 @@
"name": "yourpart", "name": "yourpart",
"version": "3.0.0-pre-alpha.0.1", "version": "3.0.0-pre-alpha.0.1",
"scripts": { "scripts": {
"start": "npm-run-all --parallel build start:backend", "start": "npm-run-all --parallel build start:backend",
"build": "cd frontend && npm run build", "build": "cd frontend && npm run build",
"start:backend": "cd backend && node server.js", "start:backend": "cd backend && node server.js",
"dev": "concurrently \"npm run dev:backend\" \"npm run dev:frontend\"", "dev": "concurrently \"npm run dev:backend\" \"npm run dev:frontend\"",
"dev:backend": "cd backend && nodemon server.js", "dev:backend": "cd backend && nodemon server.js",
"dev:frontend": "cd frontend && npm run serve" "dev:frontend": "cd frontend && npm run serve"
}, },
"devDependencies": { "devDependencies": {
"concurrently": "^7.0.0", "concurrently": "^7.0.0",
"npm-run-all": "^4.1.5", "nodemon": "^2.0.15",
"nodemon": "^2.0.15" "npm-run-all": "^4.1.5"
},
"dependencies": {
"cors": "^2.8.5",
"sequelize-cli": "^6.6.2"
} }
} }