diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0f88880 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "CodeGPT.query.language": "German" +} \ No newline at end of file diff --git a/backend/app.js b/backend/app.js index d62d93a..be448a1 100644 --- a/backend/app.js +++ b/backend/app.js @@ -5,6 +5,8 @@ import chatRouter from './routers/chatRouter.js'; import authRouter from './routers/authRouter.js'; import navigationRouter from './routers/navigationRouter.js'; import settingsRouter from './routers/settingsRouter.js'; +import adminRouter from './routers/adminRouter.js'; +import contactRouter from './routers/contactRouter.js'; import cors from 'cors'; const __filename = fileURLToPath(import.meta.url); @@ -26,7 +28,9 @@ app.use('/api/chat', chatRouter); app.use('/api/auth', authRouter); app.use('/api/navigation', navigationRouter); app.use('/api/settings', settingsRouter); +app.use('/api/admin', adminRouter); app.use('/images', express.static(path.join(__dirname, '../frontend/public/images'))); +app.use('/api/contact', contactRouter); app.use((req, res) => { res.status(404).send('404 Not Found'); diff --git a/backend/controllers/adminController.js b/backend/controllers/adminController.js new file mode 100644 index 0000000..54b65b4 --- /dev/null +++ b/backend/controllers/adminController.js @@ -0,0 +1,44 @@ +import AdminService from '../services/adminService.js'; + +export const getOpenInterests = async (req, res) => { + try { + const { userid: userId} = req.headers; + const openInterests = await AdminService.getOpenInterests(userId); + res.status(200).json(openInterests); + } catch (error) { + res.status(403).json({error: error.message }); + } +} + +export const changeInterest = async (req, res) => { + try { + const { userid: userId } = req.headers; + const { id: interestId, active, adult: adultOnly} = req.body; + AdminService.changeInterest(userId, interestId, active, adultOnly); + res.status(200).json(AdminService.getOpenInterests(userId)); + } catch (error) { + res.status(403).json({ error: error.message }); + } +} + +export const deleteInterest = async (req, res) => { + try { + const { userid: userId } = req.headers; + const { id: interestId } = req.params; + AdminService.deleteInterest(userId, interestId); + res.status(200).json(AdminService.getOpenInterests(userId)); + } catch (error) { + res.status(403).json({ error: error.message }); + } +} + +export const changeTranslation = async (req, res) => { + try { + const { userid: userId } = req.headers; + const { id: interestId, translations } = req.body; + AdminService.changeTranslation(userId, interestId, translations); + res.status(200).json(AdminService.getOpenInterests(userId)) + } catch(error) { + res.status(403).json({ error: error.message }); + } +} \ No newline at end of file diff --git a/backend/controllers/contactController.js b/backend/controllers/contactController.js new file mode 100644 index 0000000..dbff2c2 --- /dev/null +++ b/backend/controllers/contactController.js @@ -0,0 +1,11 @@ +import ContactService from '../services/ContactService.js'; + +export const addContactMessage = async(req, res) => { + try { + const { email, name, message, acceptDataSave } = req.body; + await ContactService.addContactMessage(email, name, message, acceptDataSave); + res.status(200).json({ status: 'ok' }); + } catch (error) { + res.status(409).json({ error: error }); + } +} \ No newline at end of file diff --git a/backend/controllers/navigationController.js b/backend/controllers/navigationController.js index 5fed27e..38e93e2 100644 --- a/backend/controllers/navigationController.js +++ b/backend/controllers/navigationController.js @@ -157,9 +157,9 @@ const menuStructure = { visible: ["all"], path: "/settings/view" }, - interrests: { + interests: { visible: ["all"], - path: "/settings/interrests" + path: "/settings/interests" }, sexuality: { visible: ["over14"], @@ -190,9 +190,9 @@ const menuStructure = { visible: ["mainadmin", "rights"], path: "/admin/rights" }, - interrests: { - visible: ["mainadmin", "interrests"], - path: "/admin/interrests" + interests: { + visible: ["mainadmin", "interests"], + path: "/admin/interests" }, falukant: { visible: ["mainadmin", "falukant"], @@ -255,7 +255,8 @@ export const menu = async (req, res) => { where: { userId: user.id }, include: [{ model: UserRightType, - as: 'rightType' + as: 'rightType', + required: false }] }); const userBirthdateParams = await UserParam.findAll({ diff --git a/backend/controllers/settingsController.js b/backend/controllers/settingsController.js index fc3fdbc..5e31aa1 100644 --- a/backend/controllers/settingsController.js +++ b/backend/controllers/settingsController.js @@ -1,84 +1,9 @@ -import UserParamType from '../models/type/user_param.js'; -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'; +import settingsService from '../services/settingsService.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: [ - { - model: SettingsType, - as: 'settings_type', - where: { name: type } - }, - { - model: UserParam, - as: 'user_params', - required: false, - include: [ - { - model: User, - as: 'user', - where: { hashedId: userid } - } - ] - } - ], - 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 } - }) : []; - - return { - id: field.id, - name: field.description, - minAge: field.minAge, - gender: field.gender, - datatype: field.datatype, - value: field.user_params.length > 0 ? field.user_params[0].value : null, - options: options.map(opt => ({ id: opt.id, value: opt.value })) - }; - })); + const responseFields = await settingsService.filterSettings(userid, type); res.status(200).json(responseFields); } catch (error) { console.error('Error filtering settings:', error); @@ -89,16 +14,7 @@ export const filterSettings = async (req, res) => { export const updateSetting = async (req, res) => { const { userid, settingId, value } = req.body; try { - const user = await User.findOne({ where: { hashedId: userid } }); - if (!user) { - return res.status(404).json({ error: 'User not found' }); - } - - const paramType = await UserParamType.findOne({ where: { id: settingId } }); - if (!paramType) { - return res.status(404).json({ error: 'Parameter type not found' }); - } - await UserParam.upsertParam(user.id, paramType.id, value); + await settingsService.updateSetting(userid, settingId, value); res.status(200).json({ message: 'Setting updated successfully' }); } catch (error) { console.error('Error updating user setting:', error); @@ -106,69 +22,114 @@ export const updateSetting = async (req, res) => { } }; -export const getTypeParamValueId = async(req, res) => { +export const getTypeParamValueId = async (req, res) => { const { paramValue } = req.body; - const userParamValueObject = await UserParamValue.findOne({ - where: { value: paramValue } - }); - if (!userParamValueObject) { + try { + const paramValueId = await settingsService.getTypeParamValueId(paramValue); + res.status(200).json({ paramValueId }); + } catch (error) { + console.error('Error retrieving parameter value ID:', error); res.status(404).json({ error: "notfound" }); - return; } - res.status(200).json({ paramValueId: userParamValueObject.id }); }; -export const getTypeParamValues = async(req, res) => { +export const getTypeParamValues = async (req, res) => { const { type } = req.body; - const userParamValues = await UserParamValue.findAll({ - include: [ - { - model: UserParamType, - as: 'user_param_type', - where: { description: type } - } - ] - }); - res.status(200).json(userParamValues.map(type => { return { id: type.dataValues.id, name: type.dataValues.value}})); + try { + const paramValues = await settingsService.getTypeParamValues(type); + res.status(200).json(paramValues); + } catch (error) { + console.error('Error retrieving parameter values:', error); + res.status(500).json({ error: 'An error occurred while retrieving the parameter values' }); + } } -export const getTypeParamValue = async(req, res) => { - const { id } = req.param; - const userParamValueObject = await UserParamValue.findOne({ - where: { id: id } - }); - if (!userParamValueObject) { +export const getTypeParamValue = async (req, res) => { + const { id } = req.params; + try { + const paramValue = await settingsService.getTypeParamValue(id); + res.status(200).json({ paramValue }); + } catch (error) { + console.error('Error retrieving parameter value:', error); res.status(404).json({ error: "notfound" }); - return; } - 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 }); + const { userId } = req.body; + const accountSettings = await settingsService.getAccountSettings(userId); + res.status(200).json(accountSettings); } 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; +export const setAccountSettings = async (req, res) => { + try { + await settingsService.setAccountSettings(req.body); + res.status(200).json({ message: 'Account settings updated successfully' }); + } catch (error) { + console.error('Error updating account settings:', error); + res.status(500).json({ error: 'Internal server error' }); } +}; -} \ No newline at end of file +export const getPossibleInterests = async (req, res) => { + try { + const { userid: userId } = req.headers; + const interests = await settingsService.getPossibleInterests(userId); + res.status(200).json(interests); + } catch (error) { + console.error('Error retrieving possible interests:', error); + res.status(500).json({ error: 'An error occurred while retrieving the possible interests' }); + } +} + +export const getInterests = async (req, res) => { + try { + const { userid: userId } = req.headers; + const interests = await settingsService.getInterests(userId); + res.status(200).json(interests); + } catch (error) { + console.error('Error retrieving interests:', error); + res.status(500).json({ error: 'Internal server error' }); + } +} + +export const addInterest = async (req, res) => { + try { + const { userid: userId } = req.headers; + const { name } = req.body; + const interest = await settingsService.addInterest(userId, name); + res.status(200).json({ interest }); + } catch (error) { + console.error('Error adding interest:', error); + res.status(500).json({ error: 'Internal server error' }); + } +} + +export const addUserInterest = async (req, res) => { + try { + const { userid: userId } = req.headers; + const { interestid: interestId } = req.body; + await settingsService.addUserInterest(userId, interestId); + res.status(200).json({ message: 'User interest added successfully' }); + } catch (error) { + console.error('Error adding user interest:', error); + res.status(500).json({ error: 'Internal server error' }); + } +} + +export const removeInterest = async (req, res) => { + try { + const { userid: userId } = req.headers; + const { id: interestId } = req.params; + await settingsService.removeInterest(userId, interestId); + res.status(200).json({ message: 'Interest removed successfully' }); + } catch (error) { + console.error('Error removing interest:', error); + res.status(500).json({ error: 'Internal server error' }); + } +} diff --git a/backend/models/associations.js b/backend/models/associations.js index c9d57f5..550bef5 100644 --- a/backend/models/associations.js +++ b/backend/models/associations.js @@ -5,6 +5,9 @@ import UserRightType from './type/user_right.js'; import UserRight from './community/user_right.js'; import SettingsType from './type/settings.js'; import UserParamValue from './type/user_param_value.js'; +import InterestType from './type/interest.js'; +import InterestTranslationType from './type/interest_translation.js'; +import Interest from './community/interest.js'; export default function setupAssociations() { SettingsType.hasMany(UserParamType, { foreignKey: 'settingsId', as: 'user_param_types' }); @@ -18,7 +21,23 @@ export default function setupAssociations() { UserRight.belongsTo(User, { foreignKey: 'userId' }); UserRight.belongsTo(UserRightType, { foreignKey: 'rightTypeId', as: 'rightType' }); + UserRightType.hasMany(UserRight, { foreignKey: 'rightTypeId', as: 'rightType' }); UserParamType.hasMany(UserParamValue, { foreignKey: 'userParamTypeId', as: 'user_param_values' }); UserParamValue.belongsTo(UserParamType, { foreignKey: 'userParamTypeId', as: 'user_param_type' }); + + InterestType.hasMany(InterestTranslationType, { foreignKey: 'interestsId', as: 'interest_translations' }); + InterestTranslationType.belongsTo(InterestType, { foreignKey: 'interestsId', as: 'interest_translations' }); + + InterestType.hasMany(Interest, { foreignKey: 'userinterestId', as: 'user_interest_type'} ); + User.hasMany(Interest, { foreignKey: 'userId', as: 'user_interest' }); + Interest.belongsTo(InterestType, { foreignKey: 'userinterestId', as: 'user_interest_type' }); + Interest.belongsTo(User, { foreignKey: 'userId', as: 'user_interest' }); + + InterestTranslationType.belongsTo(UserParamValue, { + foreignKey: 'language', + targetKey: 'id', + as: 'user_param_value' + }); + } diff --git a/backend/models/community/interest.js b/backend/models/community/interest.js new file mode 100644 index 0000000..5173b26 --- /dev/null +++ b/backend/models/community/interest.js @@ -0,0 +1,11 @@ +import { sequelize } from '../../utils/sequelize.js'; +import { DataTypes } from 'sequelize'; + +const interest = sequelize.define('interest_type', { +}, { + tableName: 'interest', + schema: 'community', + underscored: true +}); + +export default interest; \ No newline at end of file diff --git a/backend/models/index.js b/backend/models/index.js index d60bce6..d0861a9 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -6,6 +6,10 @@ import User from './community/user.js'; import UserParam from './community/user_param.js'; import Login from './logs/login.js'; import UserRight from './community/user_right.js'; +import InterestType from './type/interest.js'; +import InterestTranslationType from './type/interest_translation.js'; +import Interest from './community/interest.js'; +import ContactMessage from './service/contactmessage.js'; const models = { SettingsType, @@ -16,6 +20,10 @@ const models = { UserParam, Login, UserRight, + InterestType, + InterestTranslationType, + Interest, + ContactMessage, }; export default models; diff --git a/backend/models/service/contactmessage.js b/backend/models/service/contactmessage.js new file mode 100644 index 0000000..9061034 --- /dev/null +++ b/backend/models/service/contactmessage.js @@ -0,0 +1,65 @@ +import { sequelize } from '../../utils/sequelize.js'; +import { DataTypes } from 'sequelize'; +import { encrypt, decrypt } from '../../utils/encryption.js'; + +const ContactMessage = sequelize.define('contact_message', { + email: { + type: DataTypes.STRING, + allowNull: false, + set(value) { + if (value) { + const encryptedValue = encrypt(value); + this.setDataValue('email', encryptedValue.toString('hex')); + } + }, + get() { + const value = this.getDataValue('email'); + if (value) { + return decrypt(Buffer.from(value, 'hex')); + } + } + }, + message: { + type: DataTypes.STRING, + allowNull: false, + set(value) { + if (value) { + const encryptedValue = encrypt(value); + this.setDataValue('message', encryptedValue.toString('hex')); + } + }, + get() { + const value = this.getDataValue('message'); + if (value) { + return decrypt(Buffer.from(value, 'hex')); + } + } + }, + name: { + type: DataTypes.STRING, + allowNull: false, + set(value) { + if (value) { + const encryptedValue = encrypt(value); + this.setDataValue('name', encryptedValue.toString('hex')); + } + }, + get() { + const value = this.getDataValue('name'); + if (value) { + return decrypt(Buffer.from(value, 'hex')); + } + } + }, + allowDataSave: { + type: DataTypes.BOOLEAN, + allowNull: false + }, +}, { + tableName: 'contact_message', + timestamps: true, + schema: 'service', + underscored: true +}); + +export default ContactMessage; diff --git a/backend/models/type/interest.js b/backend/models/type/interest.js new file mode 100644 index 0000000..ab7ea6e --- /dev/null +++ b/backend/models/type/interest.js @@ -0,0 +1,25 @@ +import { sequelize } from '../../utils/sequelize.js'; +import { DataTypes } from 'sequelize'; + +const Interest = sequelize.define('interest_type', { + name: { + type: DataTypes.STRING, + allowNull: false + }, + allowed: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false + }, + adultOnly: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: true, + } +}, { + tableName: 'interest', + schema: 'type', + underscored: true +}); + +export default Interest; \ No newline at end of file diff --git a/backend/models/type/interest_translation.js b/backend/models/type/interest_translation.js new file mode 100644 index 0000000..7e4b028 --- /dev/null +++ b/backend/models/type/interest_translation.js @@ -0,0 +1,30 @@ +import { sequelize } from '../../utils/sequelize.js'; +import { DataTypes } from 'sequelize'; +import Interest from '../type/interest.js'; + +const interestTranslation = sequelize.define('interest_translation_type', { + translation: { + type: DataTypes.STRING, + allowNull: false + }, + language: { + type: DataTypes.INTEGER, + allowNull: false + }, + interestsId: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: Interest, + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE' + }, +}, { + tableName: 'interest_translation', + schema: 'type', + underscored: true +}); + +export default interestTranslation; \ No newline at end of file diff --git a/backend/routers/adminRouter.js b/backend/routers/adminRouter.js new file mode 100644 index 0000000..a8abef8 --- /dev/null +++ b/backend/routers/adminRouter.js @@ -0,0 +1,12 @@ +import { Router } from 'express'; +import { authenticate } from '../middleware/authMiddleware.js'; +import { getOpenInterests, changeInterest, deleteInterest, changeTranslation } from '../controllers/adminController.js'; + +const router = Router(); + +router.get('/interests/open', authenticate, getOpenInterests); +router.post('/interest', authenticate, changeInterest); +router.post('/interest/translation', authenticate, changeTranslation); +router.delete('/interest/:id', authenticate, deleteInterest); + +export default router; \ No newline at end of file diff --git a/backend/routers/contactRouter.js b/backend/routers/contactRouter.js new file mode 100644 index 0000000..2c6d06c --- /dev/null +++ b/backend/routers/contactRouter.js @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import { addContactMessage } from '../controllers/contactController.js'; + +const router = Router(); + +router.post('/', addContactMessage); + +export default router; \ No newline at end of file diff --git a/backend/routers/settingsRouter.js b/backend/routers/settingsRouter.js index 7dafa7d..c739a87 100644 --- a/backend/routers/settingsRouter.js +++ b/backend/routers/settingsRouter.js @@ -1,5 +1,6 @@ import { Router } from 'express'; -import { filterSettings, updateSetting, getTypeParamValueId, getTypeParamValues, getTypeParamValue, getAccountSettings } from '../controllers/settingsController.js'; +import { filterSettings, updateSetting, getTypeParamValueId, getTypeParamValues, getTypeParamValue, getAccountSettings, + getPossibleInterests, getInterests, addInterest, addUserInterest, removeInterest } from '../controllers/settingsController.js'; import { authenticate } from '../middleware/authMiddleware.js'; const router = Router(); @@ -11,5 +12,10 @@ router.post('/account', authenticate, getAccountSettings); router.post('/getparamvalues', getTypeParamValues); router.post('/getparamvalueid', getTypeParamValueId); router.post('/getparamvalue/:id', getTypeParamValue); +router.get('/getpossibleinterests', authenticate, getPossibleInterests); +router.get('/getuserinterests', authenticate, getInterests); +router.post('/addinterest', authenticate, addInterest); +router.post('/setinterest', authenticate, addUserInterest); +router.get('/removeinterest/:id', authenticate, removeInterest); export default router; diff --git a/backend/services/ContactService.js b/backend/services/ContactService.js new file mode 100644 index 0000000..4d12830 --- /dev/null +++ b/backend/services/ContactService.js @@ -0,0 +1,22 @@ +import ContactMessage from "../models/service/contactmessage.js"; + +class ContactService { + + async addContactMessage(email, name, message, acceptDataSave) { + if (acceptDataSave && !email) { + throw new Error('emailrequired'); + } + if (!acceptDataSave) { + name = ''; + email = ''; + } + ContactMessage.create({ + email, + name, + message, + allowDataSave: acceptDataSave + }); + } +} + +export default new ContactService(); \ No newline at end of file diff --git a/backend/services/adminService.js b/backend/services/adminService.js new file mode 100644 index 0000000..ab9e30f --- /dev/null +++ b/backend/services/adminService.js @@ -0,0 +1,124 @@ +import UserRight from "../models/community/user_right.js"; +import UserRightType from "../models/type/user_right.js"; +import InterestType from "../models/type/interest.js" +import InterestTranslationType from "../models/type/interest_translation.js" +import User from "../models/community/user.js"; +import UserParamValue from "../models/type/user_param_value.js"; + +class AdminService { + async hasUserAccess(userId, section) { + const userRights = await UserRight.findAll({ +/* where: { + userId: userId, + },*/ + include: [{ + model: UserRightType, + as: 'rightType', + where: { + title: [section, 'mainadmin'], + } + }, + { + model: User, + as: 'user', + where: { + hashedId: userId, + } + } + ] + + }); + return userRights.length > 0; + } + + async getOpenInterests(userId) { + if (!this.hasUserAccess(userId, 'interests')) { + throw new Error('noaccess'); + } + const openInterests = await InterestType.findAll({ + where: { + allowed: false + }, + include: { + model: InterestTranslationType, + as: 'interest_translations', + } + }) + return openInterests; + } + + async changeInterest(userId, interestId, active, adultOnly) { + if (!this.hasUserAccess(userId, 'interests')) { + throw new Error('noaccess'); + } + const interest = await InterestType.findOne({ + where: { + id: interestId + } + }); + if (interest) { + interest.allowed = active; + interest.adultOnly = adultOnly; + await interest.save(); + } + } + + async deleteInterest(userId, interestId) { + if (!this.hasUserAccess(userId, 'interests')) { + throw new Error('noaccess'); + } + const interest = await InterestType.findOne({ + where: { + id: interestId + } + }); + if (interest) { + await interest.destroy(); + } + } + + async changeTranslation(userId, interestId, translations) { + if (!this.hasUserAccess(userId, 'interests')) { + throw new Error('noaccess'); + } + const interest = await InterestType.findOne({ + id: interestId + }); + if (!interest) { + throw new Error('notexisting'); + } + for (const languageId of Object.keys(translations)) { + const languageObject = await UserParamValue.findOne( + { + where: { + id: languageId + } + } + ); + if (!languageObject) { + throw new Error('wronglanguage'); + } + const translation = await InterestTranslationType.findOne( + { + where: { + interestsId: interestId, + language: languageObject.id + } + } + ); + if (translation) { + translation.translation = translations[languageId]; + translation.save(); + } else { + await InterestTranslationType.create({ + interestsId: interestId, + language: languageObject.id, + translation: translations[languageId] + + }); + } + } + } +} + +export default new AdminService(); \ No newline at end of file diff --git a/backend/services/settingsService.js b/backend/services/settingsService.js new file mode 100644 index 0000000..2c0f61d --- /dev/null +++ b/backend/services/settingsService.js @@ -0,0 +1,313 @@ +import UserParamType from '../models/type/user_param.js'; +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 Interest from '../models/type/interest.js'; +import UserInterest from '../models/community/interest.js' +import InterestTranslation from '../models/type/interest_translation.js'; +import { calculateAge } from '../utils/userdata.js'; +import { Op } from 'sequelize'; + +class SettingsService { + async getUser(userId) { + const user = await User.findOne({ where: { hashedId: userId } }); + if (!user) { + throw new Error('User not found'); + } + return user; + } + + async getUserParams(userId, paramDescriptions) { + return await UserParam.findAll({ + where: { userId }, + include: [ + { + model: UserParamType, + as: 'paramType', + where: { description: { [Op.in]: paramDescriptions } } + } + ] + }); + } + + async getFieldOptions(field) { + if (['singleselect', 'multiselect'].includes(field.datatype)) { + return await UserParamValue.findAll({ + where: { userParamTypeId: field.id } + }); + } + return []; + } + + async filterSettings(userId, type) { + const user = await this.getUser(userId); + const userParams = await this.getUserParams(user.id, ['birthdate', 'gender']); + + let birthdate = null; + let gender = null; + for (const param of userParams) { + if (param.paramType.description === 'birthdate') { + birthdate = param.value; + } + if (param.paramType.description === 'gender') { + const genderResult = await UserParamValue.findOne({ where: { id: param.value } }); + gender = genderResult ? genderResult.dataValues?.value : null; + } + } + + const age = birthdate ? calculateAge(birthdate) : null; + const fields = await UserParamType.findAll({ + include: [ + { + model: SettingsType, + as: 'settings_type', + where: { name: type } + }, + { + model: UserParam, + as: 'user_params', + required: false, + include: [ + { + model: User, + as: 'user', + where: { hashedId: userId } + } + ] + } + ], + where: { + [Op.and]: [ + { minAge: { [Op.or]: [null, { [Op.lte]: age }] } }, + { gender: { [Op.or]: [null, gender] } } + ] + } + }); + + return await Promise.all(fields.map(async (field) => { + const options = await this.getFieldOptions(field); + return { + id: field.id, + name: field.description, + minAge: field.minAge, + gender: field.gender, + datatype: field.datatype, + value: field.user_params.length > 0 ? field.user_params[0].value : null, + options: options.map(opt => ({ id: opt.id, value: opt.value })) + }; + })); + } + + async updateSetting(userId, settingId, value) { + const user = await this.getUser(userId); + const paramType = await UserParamType.findOne({ where: { id: settingId } }); + if (!paramType) { + throw new Error('Parameter type not found'); + } + await UserParam.upsertParam(user.id, paramType.id, value); + } + + async getTypeParamValueId(paramValue) { + const userParamValueObject = await UserParamValue.findOne({ + where: { value: paramValue } + }); + if (!userParamValueObject) { + throw new Error('Parameter value not found'); + } + return userParamValueObject.id; + } + + async getTypeParamValues(type) { + const userParamValues = await UserParamValue.findAll({ + include: [ + { + model: UserParamType, + as: 'user_param_type', + where: { description: type } + } + ] + }); + return userParamValues.map(type => ({ id: type.dataValues.id, name: type.dataValues.value })); + } + + async getTypeParamValue(id) { + const userParamValueObject = await UserParamValue.findOne({ + where: { id } + }); + if (!userParamValueObject) { + throw new Error('Parameter value not found'); + } + return userParamValueObject.value; + } + + async getAccountSettings(userId) { + const user = await this.getUser(userId); + const email = user.email; + return { username: user.username, email, showinsearch: user.searchable }; + } + + async setAccountSettings(data) { + const { userId, username, email, searchable, oldpassword, newpassword, newpasswordrepeat } = data; + const user = await this.getUser(userId); + + user.searchable = searchable; + + if (user.password !== oldpassword) { + throw new Error('Wrong password'); + } + + const updateUser = {}; + if (username.toLowerCase() !== user.username.toLowerCase()) { + const isUsernameTaken = (await User.findAll({ where: { username: username } })).length > 0; + if (isUsernameTaken) { + throw new Error('Username already taken'); + } + updateUser.username = username; + } + + if (newpassword.trim().length > 0) { + if (newpassword.length < 6) { + throw new Error('Password too short'); + } + if (newpassword !== newpasswordrepeat) { + throw new Error('Passwords do not match'); + } + updateUser.password = newpassword; + } + await user.update(updateUser); + } + + async getPossibleInterests(userId) { + const user = await this.getUser(userId); + const userParams = await this.getUserParams(user.id, ['birthdate']); + let birthdate = null; + for (const param of userParams) { + if (param.paramType.description === 'birthdate') { + birthdate = param.value; + } + } + const age = birthdate ? calculateAge(birthdate) : 0; + const filter = { + where: age >= 18 ? { + allowed: true, + } : { + allowed: true, + [Op.or]: [ + { adultOnly: false }, + { adultOnly: { [Op.eq]: null } } + ] + }, + include: [ + { + model: InterestTranslation, + as: 'interest_translations', + required: false, + include: [ + { + model: UserParamValue, + as: 'user_param_value', + required: false + } + ] + } + ] + }; + + return await Interest.findAll(filter); + } + + async getInterests(userId) { + const user = await this.getUser(userId); + return await UserInterest.findAll({ + where: { userId: user.id }, + include: [ + { + model: Interest, + as: 'user_interest_type', + include: [ + { + model: InterestTranslation, + as: 'interest_translations', + include: [ + { + model: UserParamValue, + as: 'user_param_value', + required: false + } + ] + } + ] + } + ] + }); + } + + async addInterest(userId, name) { + const user = await this.getUser(userId); + const existingInterests = await Interest.findAll({ where: { name: name.toLowerCase() } }); + if (existingInterests.length > 0) { + throw new Error('Interest already exists'); + } + const userParam = await this.getUserParams(user.id, ['language']); + let language = 'en'; + if (userParam) { + const userParamValue = await UserParamValue.findOne({ + where: { + id: userParam[0].value + } + }); + language = userParamValue && userParamValue.value ? userParamValue.value : 'en'; + } + const languageParam = await UserParamValue.findOne({ where: { value: language } }); + const languageId = languageParam.id; + const interest = await Interest.create({ name: name.toLowerCase(), allowed: false, adultOnly: true }); + await InterestTranslation.create({ interestsId: interest.id, language: languageId, translation: name }); + return interest; + } + + async addUserInterest(userId, interestId) { + const user = await this.getUser(userId); + const interestsFilter = { + id: interestId, + allowed: true, + }; + const userParams = await this.getUserParams(user.id, ['birthdate']); + let birthdate = null; + for (const param of userParams) { + if (param.paramType.description === 'birthdate') { + birthdate = param.value; + } + } + const age = birthdate ? calculateAge(birthdate) : 0; + if (age < 18) { + interestsFilter[Op.or] = [ + { adultOnly: false }, + { adultOnly: { [Op.eq]: null } } + ]; + } + const existingInterests = await Interest.findAll({ where: interestsFilter }); + if (existingInterests.length === 0) { + throw new Error('Interest not found'); + }; + const interest = await UserInterest.findAll({ + where: { userId: user.id, userinterestId: interestId } + }); + if (interest.length > 0) { + throw new Error('Interest already exists'); + } + await UserInterest.create({ userId: user.id, userinterestId: interestId }); + } + + async removeInterest(userId, interestId) { + const user = await this.getUser(userId); + const interests = await UserInterest.findAll({ + where: { userId: user.id, userinterestId: interestId } + }); + for (const interest of interests) { + await interest.destroy(); + } + } +} + +export default new SettingsService(); diff --git a/backend/utils/initializeTypes.js b/backend/utils/initializeTypes.js index 750eab7..e910dbb 100644 --- a/backend/utils/initializeTypes.js +++ b/backend/utils/initializeTypes.js @@ -1,6 +1,10 @@ import UserParamType from '../models/type/user_param.js'; import SettingsType from '../models/type/settings.js'; import UserParamValue from '../models/type/user_param_value.js'; +import Interest from '../models/type/interest.js'; +import { Op, } from 'sequelize'; +import InterestTranslation from '../models/type/interest_translation.js'; +import { sequelize } from '../utils/sequelize.js'; const initializeTypes = async () => { const settingsTypes = await SettingsType.findAll(); @@ -10,7 +14,7 @@ const initializeTypes = async () => { }, {}); const getSettingsTypeId = (name) => settingsTypeMap[name]; - const getUserParamTypeId = async(name) => { + const getUserParamTypeId = async (name) => { const userParamType = await UserParamType.findOne({ where: { description: name @@ -19,26 +23,26 @@ const initializeTypes = async () => { return userParamType.id; }; const userParams = { - language: {type: 'singleselect', setting: 'personal'}, - birthdate: { type: 'date', setting: 'personal' }, - zip: { type: 'string', setting: 'personal' }, - town: { type: 'string', setting: 'personal' }, - bodyheight: { type: 'float', setting: 'view' }, - weight: { type: 'float', setting: 'view' }, - eyecolor: { type: 'singleselect', setting: 'view' }, - haircolor: { type: 'singleselect', setting: 'view' }, - hairlength: { type: 'singleselect', setting: 'view' }, - skincolor: { type: 'singleselect', setting: 'view' }, - freckles: { type: 'singleselect', setting: 'view' }, - piercings: { type: 'bool', setting: 'view' }, - tattoos: { type: 'bool', setting: 'view' }, - 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: 'male' }, + language: { type: 'singleselect', setting: 'personal' }, + birthdate: { type: 'date', setting: 'personal' }, + zip: { type: 'string', setting: 'personal' }, + town: { type: 'string', setting: 'personal' }, + bodyheight: { type: 'float', setting: 'view' }, + weight: { type: 'float', setting: 'view' }, + eyecolor: { type: 'singleselect', setting: 'view' }, + haircolor: { type: 'singleselect', setting: 'view' }, + hairlength: { type: 'singleselect', setting: 'view' }, + skincolor: { type: 'singleselect', setting: 'view' }, + freckles: { type: 'singleselect', setting: 'view' }, + piercings: { type: 'bool', setting: 'view' }, + tattoos: { type: 'bool', setting: 'view' }, + 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: 'male' }, 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 createItem = { description: key, datatype: item.type, settingsId: getSettingsTypeId(item.setting) }; if (item.minAge) createItem.minAge = item.minAge; @@ -59,12 +63,12 @@ const initializeTypes = async () => { 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 userParamTypeId = await getUserParamTypeId(key); - values.forEach(async(value) => { + values.forEach(async (value) => { await UserParamValue.findOrCreate({ - where: { + where: { userParamTypeId: userParamTypeId, value: value }, @@ -72,6 +76,91 @@ const initializeTypes = async () => { }) }); }); -} + const interestsList = { + music: { adult: false, translation: { value: 'Musik', language: 'de' } }, + piano: { adult: false, translation: { value: 'Klavier', language: 'de' } }, + art: { adult: false, translation: { value: 'Kunst', language: 'de' } }, + photography: { adult: false, translation: { value: 'Fotografie', language: 'de' } }, + gaming: { adult: false, translation: { value: 'Spielen', language: 'de' } }, + sports: { adult: false, translation: { value: 'Sport', language: 'de' } }, + tabletennis: { adult: false, translation: { value: 'Tischtennis', language: 'de' } }, + soccer: { adult: false, translation: { value: 'Fußball', language: 'de' } }, + tennis: { adult: false, translation: { value: 'Tennis', language: 'de' } }, + swimming: { adult: false, translation: { value: 'Schwimmen', language: 'de' } }, + hiking: { adult: false, translation: { value: 'Wandern', language: 'de' } }, + cooking: { adult: false, translation: { value: 'Kochen', language: 'de' } }, + travel: { adult: false, translation: { value: 'Reisen', language: 'de' } }, + movies: { adult: false, translation: { value: 'Filme', language: 'de' } }, + books: { adult: false, translation: { value: 'Bücher', language: 'de' } }, + reading: { adult: false, translation: { value: 'Lesen', language: 'de' } }, + pets: { adult: false, translation: { value: 'Haustiere', language: 'de' } }, + dogs: { adult: false, translation: { value: 'Hunde', language: 'de' } }, + cats: { adult: false, translation: { value: 'Katzen', language: 'de' } }, + plants: { adult: false, translation: { value: 'Pflanzen', language: 'de' } }, + gardening: { adult: false, translation: { value: 'Gartenarbeit', language: 'de' } }, + yoga: { adult: false, translation: { value: 'Yoga', language: 'de' } }, + meditation: { adult: false, translation: { value: 'Meditation', language: 'de' } }, + spirituality: { adult: false, translation: { value: 'Spiritualität', language: 'de' } }, + religion: { adult: false, translation: { value: 'Religion', language: 'de' } }, + politics: { adult: false, translation: { value: 'Politik', language: 'de' } }, + history: { adult: false, translation: { value: 'Geschichte', language: 'de' } }, + science: { adult: false, translation: { value: 'Wissenschaft', language: 'de' } }, + technology: { adult: false, translation: { value: 'Technologie', language: 'de' } }, + fashion: { adult: false, translation: { value: 'Mode', language: 'de' } }, + beauty: { adult: false, translation: { value: 'Schönheit', language: 'de' } }, + fitness: { adult: false, translation: { value: 'Fitness', language: 'de' } }, + health: { adult: false, translation: { value: 'Gesundheit', language: 'de' } }, + nutrition: { adult: false, translation: { value: 'Ernährung', language: 'de' } }, + wellness: { adult: false, translation: { value: 'Wellness', language: 'de' } }, + finance: { adult: false, translation: { value: 'Finanzen', language: 'de' } }, + business: { adult: false, translation: { value: 'Geschäft', language: 'de' } }, + entrepreneurship: { adult: false, translation: { value: 'Unternehmertum', language: 'de' } }, + education: { adult: false, translation: { value: 'Bildung', language: 'de' } }, + learning: { adult: false, translation: { value: 'Lernen', language: 'de' } }, + language: { adult: false, translation: { value: 'Sprache', language: 'de' } }, + culture: { adult: false, translation: { value: 'Kultur', language: 'de' } }, + family: { adult: false, translation: { value: 'Familie', language: 'de' } }, + friends: { adult: false, translation: { value: 'Freunde', language: 'de' } }, + social: { adult: false, translation: { value: 'Soziales', language: 'de' } }, + nightlife: { adult: false, translation: { value: 'Nachtleben', language: 'de' } }, + partying: { adult: false, translation: { value: 'Feiern', language: 'de' } }, + comedy: { adult: false, translation: { value: 'Komödie', language: 'de' } }, + humor: { adult: false, translation: { value: 'Humor', language: 'de' } }, + sex: { adult: true, translation: { value: 'Sex', language: 'de' } }, + romance: { adult: false, translation: { value: 'Romantik', language: 'de' } }, + dating: { adult: true, translation: { value: 'Dating', language: 'de' } }, + relationships: { adult: false, translation: { value: 'Beziehungen', language: 'de' } }, + adventure: { adult: true, translation: { value: 'Abenteuer', language: 'de' } }, + escapade: { adult: true, translation: { value: 'Eskapade', language: 'de' } }, + sexuality: { adult: true, translation: { value: 'Sexualität', language: 'de' } } + }; + const languages = await UserParamValue.findAll({where: { value: {[Op.in]: ['de', 'en']} } }); + const languageId = (language) => { + const lang = languages.find((lang) => lang.value === language); + return lang ? lang.id : null; + }; + + for (const key of Object.keys(interestsList)) { + try { + const value = interestsList[key]; + const [item, created] = await Interest.findOrCreate({ + where: { name: key }, + defaults: { name: key, allowed: true, adultOnly: value.adult }, + }); + + if (created) { + const langId = languageId('de'); + console.log(item.id, langId, value.translation.value); + await InterestTranslation.create({ + interestsId: item.id, + language: langId, + translation: value.translation.value + }); + } + } catch (error) { + throw error; + } + } +}; export default initializeTypes; diff --git a/backend/utils/initializeUserRights.js b/backend/utils/initializeUserRights.js index a7b7c1f..67867c3 100644 --- a/backend/utils/initializeUserRights.js +++ b/backend/utils/initializeUserRights.js @@ -23,8 +23,8 @@ const initializeUserRights = async() => { defaults: { title: "rights"} }); await UserRightType.findOrCreate({ - where: { title: "interrests"}, - defaults: { title: "interrests"} + where: { title: "interests"}, + defaults: { title: "interests"} }); await UserRightType.findOrCreate({ where: { title: "falukant"}, diff --git a/backend/utils/sequelize.js b/backend/utils/sequelize.js index 75264e8..6fcd71c 100644 --- a/backend/utils/sequelize.js +++ b/backend/utils/sequelize.js @@ -8,13 +8,14 @@ const sequelize = new Sequelize(process.env.DB_NAME, process.env.DB_USER, proces dialect: 'postgres', define: { timestamps: false - } + }, }); const createSchemas = async () => { await sequelize.query('CREATE SCHEMA IF NOT EXISTS community'); await sequelize.query('CREATE SCHEMA IF NOT EXISTS logs'); await sequelize.query('CREATE SCHEMA IF NOT EXISTS type'); + await sequelize.query('CREATE SCHEMA IF NOT EXISTS service'); }; const initializeDatabase = async () => { @@ -24,21 +25,10 @@ const initializeDatabase = async () => { }; const syncModels = async (models) => { - // Stellen Sie sicher, dass alle Modelle vorhanden sind - if (!models.SettingsType || !models.UserParamValue || !models.UserParamType || !models.UserRightType || - !models.User || !models.UserParam || !models.Login || !models.UserRight) { - throw new Error('Models are not properly loaded.'); + // Nur einmaliges sync ohne alter/force + for (const model of Object.values(models)) { + await model.sync(); } - - // Synchronisieren Sie die Modelle in der gewünschten Reihenfolge - await models.SettingsType.sync({ alter: true }); - await models.UserParamValue.sync({ alter: true }); - await models.UserParamType.sync({ alter: true }); - await models.UserRightType.sync({ alter: true }); - await models.User.sync({ alter: true }); - await models.UserParam.sync({ alter: true }); - await models.Login.sync({ alter: true }); - await models.UserRight.sync({ alter: true }); }; export { sequelize, initializeDatabase }; diff --git a/backend/utils/syncDatabase.js b/backend/utils/syncDatabase.js index d43e681..422f034 100644 --- a/backend/utils/syncDatabase.js +++ b/backend/utils/syncDatabase.js @@ -8,13 +8,11 @@ import models from '../models/index.js'; const syncDatabase = async () => { try { await initializeDatabase(); - for (const model of Object.values(models)) { - await model.sync({ alter: true }); - } setupAssociations(); for (const model of Object.values(models)) { - await model.sync({ alter: true }); + await model.sync(); } + await initializeSettings(); await initializeTypes(); await initializeUserRights(); diff --git a/frontend/src/components/AppFooter.vue b/frontend/src/components/AppFooter.vue index cac4ed6..7f65450 100644 --- a/frontend/src/components/AppFooter.vue +++ b/frontend/src/components/AppFooter.vue @@ -10,11 +10,13 @@
- Impressum - Datenschutzerklärung + {{ $t('imprint.button') }} + {{ $t('dataPrivacy.button') }} + {{ $t('contact.button') }}
+ @@ -22,12 +24,14 @@ import { mapGetters } from 'vuex'; import ImprintDialog from '../dialogues/standard/ImprintDialog.vue'; import DataPrivacyDialog from '../dialogues/standard/DataPrivacyDialog.vue'; +import ContactDialog from '../dialogues/standard/ContactDialog.vue'; export default { name: 'AppFooter', components: { ImprintDialog, DataPrivacyDialog, + ContactDialog, }, computed: { ...mapGetters('dialogs', ['openDialogs']) @@ -39,6 +43,9 @@ export default { openDataPrivacyDialog() { this.$refs.dataPrivacyDialog.open(); }, + openContactDialog() { + this.$refs.contactDialog.open(); + }, toggleDialogMinimize(dialogName) { this.$store.dispatch('dialogs/toggleDialogMinimize', dialogName); } diff --git a/frontend/src/components/AppHeader.vue b/frontend/src/components/AppHeader.vue index 2dc8d84..17ba1a6 100644 --- a/frontend/src/components/AppHeader.vue +++ b/frontend/src/components/AppHeader.vue @@ -16,7 +16,7 @@ header { display: flex; justify-content: space-between; padding: 10px; - background-color: #f8f9fa; + background-color: #f8a22b; } .logo, .title, .advertisement { text-align: center; diff --git a/frontend/src/components/AppNavigation.vue b/frontend/src/components/AppNavigation.vue index ed03a01..ddcc65d 100644 --- a/frontend/src/components/AppNavigation.vue +++ b/frontend/src/components/AppNavigation.vue @@ -59,6 +59,7 @@ nav>ul { padding: 0; flex-direction: row; margin: 0; + cursor: pointer; } ul { diff --git a/frontend/src/components/DialogWidget.vue b/frontend/src/components/DialogWidget.vue index 708f542..0857a14 100644 --- a/frontend/src/components/DialogWidget.vue +++ b/frontend/src/components/DialogWidget.vue @@ -1,8 +1,9 @@