Bugs in settings fixed, profile added

This commit is contained in:
Torsten Schulz
2024-09-21 00:25:42 +02:00
parent c5a72d57d8
commit e494fe41db
65 changed files with 3121 additions and 7478 deletions

View File

@@ -8,6 +8,7 @@ import settingsRouter from './routers/settingsRouter.js';
import adminRouter from './routers/adminRouter.js'; import adminRouter from './routers/adminRouter.js';
import contactRouter from './routers/contactRouter.js'; import contactRouter from './routers/contactRouter.js';
import cors from 'cors'; import cors from 'cors';
import socialnetworkRouter from './routers/socialnetworkRouter.js';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@@ -31,6 +32,7 @@ app.use('/api/settings', settingsRouter);
app.use('/api/admin', adminRouter); app.use('/api/admin', adminRouter);
app.use('/images', express.static(path.join(__dirname, '../frontend/public/images'))); app.use('/images', express.static(path.join(__dirname, '../frontend/public/images')));
app.use('/api/contact', contactRouter); app.use('/api/contact', contactRouter);
app.use('/api/socialnetwork', socialnetworkRouter);
app.use((req, res) => { app.use((req, res) => {
res.status(404).send('404 Not Found'); res.status(404).send('404 Not Found');

View File

@@ -1,4 +1,5 @@
import AdminService from '../services/adminService.js'; import AdminService from '../services/adminService.js';
import Joi from 'joi';
export const getOpenInterests = async (req, res) => { export const getOpenInterests = async (req, res) => {
try { try {
@@ -51,4 +52,28 @@ export const getOpenContacts = async (req, res) => {
} catch (error) { } catch (error) {
res.status(403).json({ error: error.message }); res.status(403).json({ error: error.message });
} }
} }
export const answerContact = async (req, res) => {
try {
const schema = Joi.object({
id: Joi.number().integer().required(),
answer: Joi.string().min(1).required()
});
const { error, value } = schema.validate(req.body);
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
const { id, answer } = value;
await AdminService.answerContact(id, answer);
res.status(200).json({ status: 'ok' });
} catch (error) {
console.error('Error in answerContact:', error);
res.status(error.status || 500).json({ error: error.message || 'Internal Server Error' });
}
};

View File

@@ -162,6 +162,10 @@ const menuStructure = {
visible: ["all"], visible: ["all"],
path: "/settings/interests" path: "/settings/interests"
}, },
flirt: {
visible: ["over14"],
path: "/settings/flirt"
},
sexuality: { sexuality: {
visible: ["over14"], visible: ["over14"],
path: "/settings/sexuality" path: "/settings/sexuality"

View File

@@ -133,3 +133,25 @@ export const removeInterest = async (req, res) => {
res.status(500).json({ error: 'Internal server error' }); res.status(500).json({ error: 'Internal server error' });
} }
} }
export const getVisibilities = async (req, res) => {
try {
const visibilities = await settingsService.getVisibilities();
res.status(200).json(visibilities);
} catch (error) {
console.error('Error retrieving visibilities:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
export const updateVisibility = async (req, res) => {
const { userParamTypeId, visibilityId } = req.body;
const hashedUserId = req.headers.userid;
try {
await settingsService.updateVisibility(hashedUserId, userParamTypeId, visibilityId);
res.status(200).json({ message: 'Visibility updated successfully' });
} catch (error) {
console.error('Error updating visibility:', error);
res.status(500).json({ error: 'Internal server error' });
}
};

View File

@@ -0,0 +1,37 @@
import SocialNetworkService from '../services/socialnetworkService.js';
class SocialNetworkController {
constructor() {
this.socialNetworkService = new SocialNetworkService();
this.userSearch = this.userSearch.bind(this);
this.profile = this.profile.bind(this);
}
async userSearch(req, res) {
try {
const { username, ageFrom, ageTo, genders } = req.body;
const users = await this.socialNetworkService.searchUsers({ username, ageFrom, ageTo, genders });
res.status(200).json(users);
} catch (error) {
console.error('Error in userSearch:', error);
res.status(500).json({ error: error.message });
}
}
async profile(req, res) {
try {
const { userId } = req.params;
const requestingUserId = req.headers.userid;
if (!userId || !requestingUserId) {
return res.status(400).json({ error: 'Invalid user or requesting user ID.' });
}
const profile = await this.socialNetworkService.getProfile(userId, requestingUserId);
res.status(200).json(profile);
} catch (error) {
console.error('Error in profile:', error);
res.status(500).json({ error: error.message });
}
}
}
export default SocialNetworkController;

View File

@@ -2,5 +2,19 @@
"account_activation_subject": "Aktivierung Deines yourPart-Zugangs", "account_activation_subject": "Aktivierung Deines yourPart-Zugangs",
"account_activation_html": "<p>Hallo {{username}},</p><p>Herzlichen Dank für Deine Registrierung bei yourPart. Um Deinen Zugang zu erhalten, musst Du Ihn noch aktivieren. Dazu klicke bitte folgenden Link an:</p><p><a href='{{activationLink}}'>{{activationLink}}</a></p><p>Alternativ kannst Du auch nachfolgenden Code eingeben, wenn Du danach gefragt wirst:</p><p>{{resetToken}}</p><p>Dein yourPart-Team</p>", "account_activation_html": "<p>Hallo {{username}},</p><p>Herzlichen Dank für Deine Registrierung bei yourPart. Um Deinen Zugang zu erhalten, musst Du Ihn noch aktivieren. Dazu klicke bitte folgenden Link an:</p><p><a href='{{activationLink}}'>{{activationLink}}</a></p><p>Alternativ kannst Du auch nachfolgenden Code eingeben, wenn Du danach gefragt wirst:</p><p>{{resetToken}}</p><p>Dein yourPart-Team</p>",
"account_activation_text": "Hallo {{username}},\n\nHerzlichen Dank für Deine Registrierung bei yourPart. Um Deinen Zugang zu erhalten, musst Du Ihn noch aktivieren. Dazu klicke bitte folgenden Link an:\n\n{{activationLink}}\n\nAlternativ kannst Du auch nachfolgenden Code eingeben, wenn Du danach gefragt wirst:\n\n{{resetToken}}\n\nDein yourPart-Team", "account_activation_text": "Hallo {{username}},\n\nHerzlichen Dank für Deine Registrierung bei yourPart. Um Deinen Zugang zu erhalten, musst Du Ihn noch aktivieren. Dazu klicke bitte folgenden Link an:\n\n{{activationLink}}\n\nAlternativ kannst Du auch nachfolgenden Code eingeben, wenn Du danach gefragt wirst:\n\n{{resetToken}}\n\nDein yourPart-Team",
"welcome": "welcome" "welcome": "Willkommen",
"your_contact_request_answered_subject": "Ihre Kontaktanfrage wurde beantwortet",
"your_contact_request_answered_text": "Hallo,\n\nIhre Kontaktanfrage wurde beantwortet:\n\n{{answer}}\n\nMit freundlichen Grüßen,\nSupport Team",
"error": {
"emailrequired": "E-Mail ist erforderlich.",
"Contact not found": "Kontaktanfrage nicht gefunden.",
"id and answer are required": "ID und Antwort sind erforderlich.",
"Validation error": "Validierungsfehler: {{message}}"
},
"admin": {
"editcontactrequest": {
"title": "Kontaktanfrage bearbeiten"
}
},
"error.title": "Fehler"
} }

View File

@@ -2,5 +2,7 @@
"account_activation_subject": "account_activation_subject", "account_activation_subject": "account_activation_subject",
"account_activation_text": "account_activation_text", "account_activation_text": "account_activation_text",
"welcome": "welcome", "welcome": "welcome",
"account_activation_html": "account_activation_html" "account_activation_html": "account_activation_html",
"your_contact_request_answered_subject": "your_contact_request_answered_subject",
"your_contact_request_answered_text": "your_contact_request_answered_text"
} }

View File

@@ -0,0 +1,43 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn({
tableName: 'contact_message',
schema: 'service'
}, 'answer', {
type: Sequelize.TEXT,
allowNull: true
});
await queryInterface.addColumn({
tableName: 'contact_message',
schema: 'service'
}, 'answered_at', {
type: Sequelize.DATE,
allowNull: true
});
await queryInterface.addColumn({
tableName: 'contact_message',
schema: 'service'
}, 'is_answered', {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.removeColumn({
tableName: 'contact_message',
schema: 'service'
}, 'answer');
await queryInterface.removeColumn({
tableName: 'contact_message',
schema: 'service'
}, 'answered_at');
await queryInterface.removeColumn({
tableName: 'contact_message',
schema: 'service'
}, 'is_answered');
}
};

View File

@@ -8,6 +8,8 @@ import UserParamValue from './type/user_param_value.js';
import InterestType from './type/interest.js'; import InterestType from './type/interest.js';
import InterestTranslationType from './type/interest_translation.js'; import InterestTranslationType from './type/interest_translation.js';
import Interest from './community/interest.js'; import Interest from './community/interest.js';
import UserParamVisibilityType from './type/user_param_visibility.js';
import UserParamVisibility from './community/user_param_visibility.js';
export default function setupAssociations() { export default function setupAssociations() {
SettingsType.hasMany(UserParamType, { foreignKey: 'settingsId', as: 'user_param_types' }); SettingsType.hasMany(UserParamType, { foreignKey: 'settingsId', as: 'user_param_types' });
@@ -16,11 +18,12 @@ export default function setupAssociations() {
UserParamType.hasMany(UserParam, { foreignKey: 'paramTypeId', as: 'user_params' }); UserParamType.hasMany(UserParam, { foreignKey: 'paramTypeId', as: 'user_params' });
UserParam.belongsTo(UserParamType, { foreignKey: 'paramTypeId', as: 'paramType' }); UserParam.belongsTo(UserParamType, { foreignKey: 'paramTypeId', as: 'paramType' });
User.hasMany(UserParam, { foreignKey: 'userId', as: 'user_params' });
UserParam.belongsTo(User, { foreignKey: 'userId', as: 'user' }); UserParam.belongsTo(User, { foreignKey: 'userId', as: 'user' });
UserRight.belongsTo(User, { foreignKey: 'userId' }); UserRight.belongsTo(User, { foreignKey: 'userId', as: 'user_with_rights' });
UserRight.belongsTo(UserRightType, { foreignKey: 'rightTypeId', as: 'rightType' }); UserRight.belongsTo(UserRightType, { foreignKey: 'rightTypeId', as: 'rightType' });
UserRightType.hasMany(UserRight, { foreignKey: 'rightTypeId', as: 'rightType' }); UserRightType.hasMany(UserRight, { foreignKey: 'rightTypeId', as: 'user_rights' });
UserParamType.hasMany(UserParamValue, { foreignKey: 'userParamTypeId', as: 'user_param_values' }); UserParamType.hasMany(UserParamValue, { foreignKey: 'userParamTypeId', as: 'user_param_values' });
UserParamValue.belongsTo(UserParamType, { foreignKey: 'userParamTypeId', as: 'user_param_type' }); UserParamValue.belongsTo(UserParamType, { foreignKey: 'userParamTypeId', as: 'user_param_type' });
@@ -28,15 +31,16 @@ export default function setupAssociations() {
InterestType.hasMany(InterestTranslationType, { foreignKey: 'interestsId', as: 'interest_translations' }); InterestType.hasMany(InterestTranslationType, { foreignKey: 'interestsId', as: 'interest_translations' });
InterestTranslationType.belongsTo(InterestType, { foreignKey: 'interestsId', as: 'interest_translations' }); InterestTranslationType.belongsTo(InterestType, { foreignKey: 'interestsId', as: 'interest_translations' });
InterestType.hasMany(Interest, { foreignKey: 'userinterestId', as: 'user_interest_type'} ); InterestType.hasMany(Interest, { foreignKey: 'userinterestId', as: 'user_interest_type' });
User.hasMany(Interest, { foreignKey: 'userId', as: 'user_interest' }); User.hasMany(Interest, { foreignKey: 'userId', as: 'user_interests' });
Interest.belongsTo(InterestType, { foreignKey: 'userinterestId', as: 'user_interest_type' }); Interest.belongsTo(InterestType, { foreignKey: 'userinterestId', as: 'interest_type' });
Interest.belongsTo(User, { foreignKey: 'userId', as: 'user_interest' }); Interest.belongsTo(User, { foreignKey: 'userId', as: 'interest_owner' });
InterestTranslationType.belongsTo(UserParamValue, { InterestTranslationType.belongsTo(UserParamValue, { foreignKey: 'language', targetKey: 'id', as: 'user_param_value' });
foreignKey: 'language',
targetKey: 'id',
as: 'user_param_value'
});
UserParam.hasMany(UserParamVisibility, { foreignKey: 'param_id', as: 'param_visibilities' });
UserParamVisibility.belongsTo(UserParam, { foreignKey: 'param_id', as: 'param' });
UserParamVisibility.belongsTo(UserParamVisibilityType, { foreignKey: 'visibility', as: 'visibility_type' });
UserParamVisibilityType.hasMany(UserParamVisibility, { foreignKey: 'visibility', as: 'user_param_visibilities' });
} }

View File

@@ -5,7 +5,7 @@ import crypto from 'crypto';
const User = sequelize.define('user', { const User = sequelize.define('user', {
email: { email: {
type: DataTypes.BLOB, // Verwende BLOB, um die E-Mail als bytea zu speichern type: DataTypes.BLOB,
allowNull: false, allowNull: false,
unique: true, unique: true,
set(value) { set(value) {

View File

@@ -61,22 +61,4 @@ 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: val }
});
if (!created) {
userParam.value = value !== null && value !== undefined ? value.toString() : '';
await userParam.save();
}
} catch (error) {
console.error('Error in upsertParam:', error);
throw error;
}
};
export default UserParam; export default UserParam;

View File

@@ -0,0 +1,26 @@
import { sequelize } from '../../utils/sequelize.js';
import { DataTypes } from 'sequelize';
const UserParamVisibility = sequelize.define('user_param_visibility', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
param_id: {
type: DataTypes.INTEGER,
allowNull: false
},
visibility: {
type: DataTypes.INTEGER,
allowNull: false
}
}, {
tableName: 'user_param_visibility',
timestamps: false,
underscored: true,
schema: 'community'
});
export default UserParamVisibility;

View File

@@ -10,6 +10,8 @@ import InterestType from './type/interest.js';
import InterestTranslationType from './type/interest_translation.js'; import InterestTranslationType from './type/interest_translation.js';
import Interest from './community/interest.js'; import Interest from './community/interest.js';
import ContactMessage from './service/contactmessage.js'; import ContactMessage from './service/contactmessage.js';
import UserParamVisibilityType from './type/user_param_visibility.js';
import UserParamVisibility from './community/user_param_visibility.js';
const models = { const models = {
SettingsType, SettingsType,
@@ -24,6 +26,8 @@ const models = {
InterestTranslationType, InterestTranslationType,
Interest, Interest,
ContactMessage, ContactMessage,
UserParamVisibilityType,
UserParamVisibility,
}; };
export default models; export default models;

View File

@@ -59,6 +59,32 @@ const ContactMessage = sequelize.define('contact_message', {
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,
allowNull: false, allowNull: false,
defaultValue: false defaultValue: false
},
// Neue Felder für die Antwort
answer: {
type: DataTypes.TEXT,
allowNull: true,
set(value) {
if (value) {
const encryptedValue = encrypt(value);
this.setDataValue('answer', encryptedValue.toString('hex'));
}
},
get() {
const value = this.getDataValue('answer');
if (value) {
return decrypt(Buffer.from(value, 'hex'));
}
}
},
answeredAt: {
type: DataTypes.DATE,
allowNull: true
},
isAnswered: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
} }
}, { }, {
tableName: 'contact_message', tableName: 'contact_message',
@@ -68,4 +94,3 @@ const ContactMessage = sequelize.define('contact_message', {
}); });
export default ContactMessage; export default ContactMessage;

47
backend/models/trigger.js Normal file
View File

@@ -0,0 +1,47 @@
import { sequelize } from '../utils/sequelize.js';
export async function createTriggers() {
const createTriggerFunction = `
CREATE OR REPLACE FUNCTION create_user_param_visibility_trigger()
RETURNS TRIGGER AS $$
BEGIN
-- Check if UserParamVisibility already exists for this UserParam
IF NOT EXISTS (
SELECT 1 FROM community.user_param_visibility
WHERE param_id = NEW.id
) THEN
-- Insert the default visibility (Invisible)
INSERT INTO community.user_param_visibility (param_id, visibility)
VALUES (NEW.id, (
SELECT id FROM type.user_param_visibility WHERE description = 'Invisible'
));
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
`;
const createInsertTrigger = `
CREATE TRIGGER trigger_create_user_param_visibility
AFTER INSERT ON community.user_param
FOR EACH ROW
EXECUTE FUNCTION create_user_param_visibility_trigger();
`;
const createUpdateTrigger = `
CREATE TRIGGER trigger_update_user_param_visibility
AFTER UPDATE ON community.user_param
FOR EACH ROW
EXECUTE FUNCTION create_user_param_visibility_trigger();
`;
try {
await sequelize.query(createTriggerFunction);
await sequelize.query(createInsertTrigger);
await sequelize.query(createUpdateTrigger);
console.log('Triggers created successfully');
} catch (error) {
console.error('Error creating triggers:', error);
}
}

View File

@@ -26,6 +26,15 @@ const UserParamType = sequelize.define('user_param_type', {
model: 'settings', model: 'settings',
key: 'id' key: 'id'
} }
},
orderId: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
},
unit: {
type: DataTypes.STRING,
allowNull: true
} }
}, { }, {
tableName: 'user_param', tableName: 'user_param',

View File

@@ -10,6 +10,11 @@ const UserParamValue = sequelize.define('user_param_value', {
value: { value: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false allowNull: false
},
orderId: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
} }
}, },
{ {

View File

@@ -0,0 +1,22 @@
import { sequelize } from '../../utils/sequelize.js';
import { DataTypes } from 'sequelize';
const UserParamVisibilityType = sequelize.define('user_param_visibility_type', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
description: {
type: DataTypes.STRING,
allowNull: false
}
}, {
tableName: 'user_param_visibility_type',
timestamps: false,
underscored: true,
schema: 'type'
});
export default UserParamVisibilityType;

View File

@@ -15,6 +15,7 @@
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"i18n": "^0.15.1", "i18n": "^0.15.1",
"joi": "^17.13.3",
"mysql2": "^3.10.3", "mysql2": "^3.10.3",
"nodemailer": "^6.9.14", "nodemailer": "^6.9.14",
"pg": "^8.12.0", "pg": "^8.12.0",
@@ -40,6 +41,19 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
"integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="
},
"node_modules/@hapi/topo": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
"integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@isaacs/cliui": { "node_modules/@isaacs/cliui": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -210,6 +224,24 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@sideway/address": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
"integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==",
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@sideway/formula": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="
},
"node_modules/@sideway/pinpoint": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
},
"node_modules/@socket.io/component-emitter": { "node_modules/@socket.io/component-emitter": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
@@ -432,9 +464,9 @@
"dev": true "dev": true
}, },
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.2", "version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"dependencies": { "dependencies": {
"bytes": "3.1.2", "bytes": "3.1.2",
"content-type": "~1.0.5", "content-type": "~1.0.5",
@@ -444,7 +476,7 @@
"http-errors": "2.0.0", "http-errors": "2.0.0",
"iconv-lite": "0.4.24", "iconv-lite": "0.4.24",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"qs": "6.11.0", "qs": "6.13.0",
"raw-body": "2.5.2", "raw-body": "2.5.2",
"type-is": "~1.6.18", "type-is": "~1.6.18",
"unpipe": "1.0.0" "unpipe": "1.0.0"
@@ -842,9 +874,9 @@
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
}, },
"node_modules/encodeurl": { "node_modules/encodeurl": {
"version": "1.0.2", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} }
@@ -1004,36 +1036,36 @@
} }
}, },
"node_modules/express": { "node_modules/express": {
"version": "4.19.2", "version": "4.21.0",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
"body-parser": "1.20.2", "body-parser": "1.20.3",
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"content-type": "~1.0.4", "content-type": "~1.0.4",
"cookie": "0.6.0", "cookie": "0.6.0",
"cookie-signature": "1.0.6", "cookie-signature": "1.0.6",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"etag": "~1.8.1", "etag": "~1.8.1",
"finalhandler": "1.2.0", "finalhandler": "1.3.1",
"fresh": "0.5.2", "fresh": "0.5.2",
"http-errors": "2.0.0", "http-errors": "2.0.0",
"merge-descriptors": "1.0.1", "merge-descriptors": "1.0.3",
"methods": "~1.1.2", "methods": "~1.1.2",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"path-to-regexp": "0.1.7", "path-to-regexp": "0.1.10",
"proxy-addr": "~2.0.7", "proxy-addr": "~2.0.7",
"qs": "6.11.0", "qs": "6.13.0",
"range-parser": "~1.2.1", "range-parser": "~1.2.1",
"safe-buffer": "5.2.1", "safe-buffer": "5.2.1",
"send": "0.18.0", "send": "0.19.0",
"serve-static": "1.15.0", "serve-static": "1.16.2",
"setprototypeof": "1.2.0", "setprototypeof": "1.2.0",
"statuses": "2.0.1", "statuses": "2.0.1",
"type-is": "~1.6.18", "type-is": "~1.6.18",
@@ -1097,12 +1129,12 @@
} }
}, },
"node_modules/finalhandler": { "node_modules/finalhandler": {
"version": "1.2.0", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"dependencies": { "dependencies": {
"debug": "2.6.9", "debug": "2.6.9",
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
@@ -1515,6 +1547,18 @@
"@pkgjs/parseargs": "^0.11.0" "@pkgjs/parseargs": "^0.11.0"
} }
}, },
"node_modules/joi": {
"version": "17.13.3",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
"integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==",
"dependencies": {
"@hapi/hoek": "^9.3.0",
"@hapi/topo": "^5.1.0",
"@sideway/address": "^4.1.5",
"@sideway/formula": "^3.0.1",
"@sideway/pinpoint": "^2.0.0"
}
},
"node_modules/js-beautify": { "node_modules/js-beautify": {
"version": "1.15.1", "version": "1.15.1",
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz", "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz",
@@ -1722,9 +1766,12 @@
} }
}, },
"node_modules/merge-descriptors": { "node_modules/merge-descriptors": {
"version": "1.0.1", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
}, },
"node_modules/methods": { "node_modules/methods": {
"version": "1.1.2", "version": "1.1.2",
@@ -2075,9 +2122,9 @@
} }
}, },
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "0.1.7", "version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
}, },
"node_modules/pg": { "node_modules/pg": {
"version": "8.12.0", "version": "8.12.0",
@@ -2225,11 +2272,11 @@
} }
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.11.0", "version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"dependencies": { "dependencies": {
"side-channel": "^1.0.4" "side-channel": "^1.0.6"
}, },
"engines": { "engines": {
"node": ">=0.6" "node": ">=0.6"
@@ -2354,9 +2401,9 @@
} }
}, },
"node_modules/send": { "node_modules/send": {
"version": "0.18.0", "version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"dependencies": { "dependencies": {
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
@@ -2389,6 +2436,14 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
}, },
"node_modules/send/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/send/node_modules/ms": { "node_modules/send/node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -2499,14 +2554,14 @@
} }
}, },
"node_modules/serve-static": { "node_modules/serve-static": {
"version": "1.15.0", "version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"dependencies": { "dependencies": {
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"send": "0.18.0" "send": "0.19.0"
}, },
"engines": { "engines": {
"node": ">= 0.8.0" "node": ">= 0.8.0"

View File

@@ -17,6 +17,7 @@
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"i18n": "^0.15.1", "i18n": "^0.15.1",
"joi": "^17.13.3",
"mysql2": "^3.10.3", "mysql2": "^3.10.3",
"nodemailer": "^6.9.14", "nodemailer": "^6.9.14",
"pg": "^8.12.0", "pg": "^8.12.0",

View File

@@ -1,6 +1,6 @@
import { Router } from 'express'; import { Router } from 'express';
import { authenticate } from '../middleware/authMiddleware.js'; import { authenticate } from '../middleware/authMiddleware.js';
import { getOpenInterests, changeInterest, deleteInterest, changeTranslation, getOpenContacts } from '../controllers/adminController.js'; import { getOpenInterests, changeInterest, deleteInterest, changeTranslation, getOpenContacts, answerContact } from '../controllers/adminController.js';
const router = Router(); const router = Router();
@@ -10,5 +10,7 @@ router.post('/interest/translation', authenticate, changeTranslation);
router.delete('/interest/:id', authenticate, deleteInterest); router.delete('/interest/:id', authenticate, deleteInterest);
router.get('/opencontacts', authenticate, getOpenContacts); router.get('/opencontacts', authenticate, getOpenContacts);
router.post('/contacts/answer', answerContact);
export default router; export default router;

View File

@@ -1,6 +1,7 @@
import { Router } from 'express'; import { Router } from 'express';
import { filterSettings, updateSetting, getTypeParamValueId, getTypeParamValues, getTypeParamValue, getAccountSettings, import { filterSettings, updateSetting, getTypeParamValueId, getTypeParamValues, getTypeParamValue, getAccountSettings,
getPossibleInterests, getInterests, addInterest, addUserInterest, removeInterest } from '../controllers/settingsController.js'; getPossibleInterests, getInterests, addInterest, addUserInterest, removeInterest, getVisibilities, updateVisibility }
from '../controllers/settingsController.js';
import { authenticate } from '../middleware/authMiddleware.js'; import { authenticate } from '../middleware/authMiddleware.js';
const router = Router(); const router = Router();
@@ -17,5 +18,7 @@ router.get('/getuserinterests', authenticate, getInterests);
router.post('/addinterest', authenticate, addInterest); router.post('/addinterest', authenticate, addInterest);
router.post('/setinterest', authenticate, addUserInterest); router.post('/setinterest', authenticate, addUserInterest);
router.get('/removeinterest/:id', authenticate, removeInterest); router.get('/removeinterest/:id', authenticate, removeInterest);
router.get('/visibilities', authenticate, getVisibilities);
router.post('/update-visibility', authenticate, updateVisibility);
export default router; export default router;

View File

@@ -0,0 +1,11 @@
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import SocialNetworkController from '../controllers/socialnetworkController.js';
const router = express.Router();
const socialNetworkController = new SocialNetworkController();
router.post('/usersearch', authenticate, socialNetworkController.userSearch);
router.get('/profile/:userId', authenticate, socialNetworkController.profile);
export default router;

View File

@@ -32,7 +32,6 @@ amqp.connect(RABBITMQ_URL, (err, connection) => {
}); });
}); });
// Sync database before starting the server
syncDatabase().then(() => { syncDatabase().then(() => {
server.listen(3001, () => { server.listen(3001, () => {
console.log('Server is running on port 3001'); console.log('Server is running on port 3001');

View File

@@ -0,0 +1,56 @@
import User from '../models/community/user.js';
import UserParam from '../models/community/user_param.js';
import UserParamType from '../models/type/user_param.js';
import UserParamVisibility from '../models/community/user_param_visibility.js';
import UserParamVisibilityType from '../models/type/user_param_visibility.js';
import { Op } from 'sequelize';
class BaseService {
async getUserByHashedId(hashedId) {
const user = await User.findOne({ where: { hashedId } });
if (!user) {
throw new Error('User not found');
}
return user;
}
async getUserById(userId) {
const user = await User.findOne({ where: { id: 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 } }
},
{
model: UserParamVisibility,
as: 'param_visibilities',
include: [
{
model: UserParamVisibilityType,
as: 'visibility_type'
}
]
}
]
});
}
calculateAge(birthdate) {
const birthDate = new Date(birthdate);
const ageDifMs = Date.now() - birthDate.getTime();
const ageDate = new Date(ageDifMs);
return Math.abs(ageDate.getUTCFullYear() - 1970);
}
}
export default BaseService;

View File

@@ -10,13 +10,30 @@ class ContactService {
name = ''; name = '';
email = ''; email = '';
} }
ContactMessage.create({ await ContactMessage.create({
email, email,
name, name,
message, message,
allowDataSave: acceptDataSave allowDataSave: acceptDataSave
}); });
} }
async getContactById(id) {
const contact = await ContactMessage.findByPk(id);
if (!contact) {
const error = new Error('Contact not found');
error.status = 404;
throw error;
}
return contact;
}
async saveAnswer(contact, answer) {
contact.answer = answer;
contact.answeredAt = new Date();
contact.isFinished = true;
await contact.save();
}
} }
export default new ContactService(); export default new ContactService();

View File

@@ -5,6 +5,8 @@ import InterestTranslationType from "../models/type/interest_translation.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 ContactMessage from "../models/service/contactmessage.js"; import ContactMessage from "../models/service/contactmessage.js";
import ContactService from "./ContactService.js";
import { sendAnswerEmail } from './emailService.js';
class AdminService { class AdminService {
async hasUserAccess(userId, section) { async hasUserAccess(userId, section) {
@@ -18,7 +20,7 @@ class AdminService {
}, },
{ {
model: User, model: User,
as: 'user', as: 'user_with_rights',
where: { where: {
hashedId: userId, hashedId: userId,
} }
@@ -129,6 +131,13 @@ class AdminService {
}) })
return openContacts; return openContacts;
} }
async answerContact(contactId, answer) {
const contact = await ContactService.getContactById(contactId);
await ContactService.saveAnswer(contact, answer);
await sendAnswerEmail(contact.email, answer, contact.language || 'en');
}
} }
export default new AdminService(); export default new AdminService();

View File

@@ -42,3 +42,16 @@ export const sendAccountActivationEmail = async (email, activationLink, username
await transporter.sendMail(mailOptions); await transporter.sendMail(mailOptions);
}; };
export const sendAnswerEmail = async (toEmail, answer, language) => {
i18n.setLocale(language);
const mailOptions = {
from: process.env.SMTP_FROM,
to: toEmail,
subject: 'yourPart',
text: answer,
html: `<p>${ answer }</p>`
};
await transporter.sendMail(mailOptions);
};

View File

@@ -1,23 +1,18 @@
import BaseService from './BaseService.js';
import UserParamType from '../models/type/user_param.js'; import UserParamType from '../models/type/user_param.js';
import SettingsType from '../models/type/settings.js'; 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 Interest from '../models/type/interest.js'; import Interest from '../models/type/interest.js';
import UserInterest from '../models/community/interest.js' import UserInterest from '../models/community/interest.js';
import InterestTranslation from '../models/type/interest_translation.js'; import InterestTranslation from '../models/type/interest_translation.js';
import { calculateAge } from '../utils/userdata.js';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
import UserParamVisibilityType from '../models/type/user_param_visibility.js';
import UserParamVisibility from '../models/community/user_param_visibility.js';
import { generateIv } from '../utils/encryption.js';
class SettingsService { class SettingsService extends BaseService{
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) { async getUserParams(userId, paramDescriptions) {
return await UserParam.findAll({ return await UserParam.findAll({
where: { userId }, where: { userId },
@@ -25,7 +20,8 @@ class SettingsService {
{ {
model: UserParamType, model: UserParamType,
as: 'paramType', as: 'paramType',
where: { description: { [Op.in]: paramDescriptions } } where: { description: { [Op.in]: paramDescriptions } },
order: [[ 'order_id', 'asc' ]]
} }
] ]
}); });
@@ -34,16 +30,16 @@ class SettingsService {
async getFieldOptions(field) { async getFieldOptions(field) {
if (['singleselect', 'multiselect'].includes(field.datatype)) { if (['singleselect', 'multiselect'].includes(field.datatype)) {
return await UserParamValue.findAll({ return await UserParamValue.findAll({
where: { userParamTypeId: field.id } where: { userParamTypeId: field.id },
order: [[ 'order_id', 'asc' ]]
}); });
} }
return []; return [];
} }
async filterSettings(userId, type) { async filterSettings(hashedUserId, type) {
const user = await this.getUser(userId); const user = await this.getUserByHashedId(hashedUserId);
const userParams = await this.getUserParams(user.id, ['birthdate', 'gender']); const userParams = await this.getUserParams(user.id, ['birthdate', 'gender']);
let birthdate = null; let birthdate = null;
let gender = null; let gender = null;
for (const param of userParams) { for (const param of userParams) {
@@ -55,8 +51,7 @@ class SettingsService {
gender = genderResult ? genderResult.dataValues?.value : null; gender = genderResult ? genderResult.dataValues?.value : null;
} }
} }
const age = birthdate ? this.calculateAge(birthdate) : null;
const age = birthdate ? calculateAge(birthdate) : null;
const fields = await UserParamType.findAll({ const fields = await UserParamType.findAll({
include: [ include: [
{ {
@@ -72,7 +67,18 @@ class SettingsService {
{ {
model: User, model: User,
as: 'user', as: 'user',
where: { hashedId: userId } where: { id: user.id }
},
{
model: UserParamVisibility,
as: 'param_visibilities',
required: false,
include: [
{
model: UserParamVisibilityType,
as: 'visibility_type'
}
]
} }
] ]
} }
@@ -84,28 +90,55 @@ class SettingsService {
] ]
} }
}); });
return await Promise.all(fields.map(async (field) => { return await Promise.all(fields.map(async (field) => {
const options = await this.getFieldOptions(field); const options = await this.getFieldOptions(field);
const visibilityData = field.user_params[0]?.param_visibilities?.[0];
const visibility = visibilityData
? { id: visibilityData.visibility_type?.id, description: visibilityData.visibility_type?.description }
: { id: null, description: 'Invisible' };
return { return {
id: field.id, id: field.id,
name: field.description, name: field.description,
minAge: field.minAge, minAge: field.minAge,
gender: field.gender, gender: field.gender,
datatype: field.datatype, datatype: field.datatype,
unit: field.unit,
value: field.user_params.length > 0 ? field.user_params[0].value : null, value: field.user_params.length > 0 ? field.user_params[0].value : null,
options: options.map(opt => ({ id: opt.id, value: opt.value })) options: options.map(opt => ({ id: opt.id, value: opt.value })),
visibility
}; };
})); }));
} }
async updateSetting(userId, settingId, value) { async updateSetting(hashedUserId, settingId, value) {
const user = await this.getUser(userId); try {
const paramType = await UserParamType.findOne({ where: { id: settingId } }); const user = await this.getUserByHashedId(hashedUserId);
if (!paramType) { const paramType = await UserParamType.findOne({ where: { id: settingId } });
throw new Error('Parameter type not found'); if (!paramType) {
throw new Error('Parameter type not found');
}
const userParam = await UserParam.findOne({
where: { userId: user.id, paramTypeId: settingId }
});
if (userParam) {
console.log('update param with ', value)
if (typeof value === 'boolean') {
value = value ? 'true' : 'false';
}
await userParam.update({value: value});
} else {
await UserParam.create(
{
userId: user.id,
paramTypeId: settingId,
value: value
}
);
}
} catch (error) {
console.error('Error updating setting:', hashedUserId, settingId, value, error);
throw error;
} }
await UserParam.upsertParam(user.id, paramType.id, value);
} }
async getTypeParamValueId(paramValue) { async getTypeParamValueId(paramValue) {
@@ -141,171 +174,123 @@ class SettingsService {
return userParamValueObject.value; return userParamValueObject.value;
} }
async getAccountSettings(userId) { async addInterest(hashedUserId, name) {
const user = await this.getUser(userId); try {
const email = user.email; const user = await this.getUserByHashedId(hashedUserId);
return { username: user.username, email, showinsearch: user.searchable };
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.length > 0) {
const userParamValue = await UserParamValue.findOne({
where: { id: userParam[0].value }
});
language = userParamValue ? 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;
} catch (error) {
console.error('Error adding interest:', error);
throw error;
}
} }
async setAccountSettings(data) { async addUserInterest(hashedUserId, interestId) {
const { userId, username, email, searchable, oldpassword, newpassword, newpasswordrepeat } = data; try {
const user = await this.getUser(userId); const user = await this.getUserByHashedId(hashedUserId);
user.searchable = searchable; const userParams = await this.getUserParams(user.id, ['birthdate']);
let birthdate = null;
if (user.password !== oldpassword) { for (const param of userParams) {
throw new Error('Wrong password'); if (param.paramType.description === 'birthdate') {
} birthdate = param.value;
}
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) { const age = birthdate ? this.calculateAge(birthdate) : 0;
if (newpassword.length < 6) { const interestsFilter = { id: interestId, allowed: true };
throw new Error('Password too short'); if (age < 18) {
} interestsFilter[Op.or] = [
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: false },
{ adultOnly: { [Op.eq]: null } } { 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 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 });
} catch (error) {
console.error('Error adding user interest:', error);
throw error;
} }
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) { async removeInterest(hashedUserId, interestId) {
const user = await this.getUser(userId); try {
const interests = await UserInterest.findAll({ const user = await this.getUserByHashedId(hashedUserId);
where: { userId: user.id, userinterestId: interestId } const interests = await UserInterest.findAll({
}); where: { userId: user.id, userinterestId: interestId }
for (const interest of interests) { });
await interest.destroy(); for (const interest of interests) {
await interest.destroy();
}
} catch (error) {
console.error('Error removing interest:', error);
throw error;
}
}
async getVisibilities() {
return UserParamVisibilityType.findAll();
}
async updateVisibility(hashedUserId, userParamTypeId, visibilityId) {
try {
const user = await this.getUserByHashedId(hashedUserId);
console.log(JSON.stringify(user));
if (!user) {
throw new Error('User not found');
}
const userParam = await UserParam.findOne({
where: { paramTypeId: userParamTypeId, userId: user.id }
});
if (!userParam) {
console.error(`UserParam not found for settingId: ${userParamTypeId} and userId: ${user.id}`);
throw new Error('User parameter not found or does not belong to the user');
}
let userParamVisibility = await UserParamVisibility.findOne({
where: { param_id: userParam.id }
});
if (userParamVisibility) {
userParamVisibility.visibility = visibilityId;
await userParamVisibility.save();
} else {
await UserParamVisibility.create({
param_id: userParam.id,
visibility: visibilityId
});
}
console.log(`Visibility updated for settingId: ${userParamTypeId} with visibilityId: ${visibilityId}`);
} catch (error) {
console.error('Error updating visibility:', error.message);
throw error;
} }
} }
} }

View File

@@ -0,0 +1,187 @@
import BaseService from './BaseService.js';
import { Op, col } from 'sequelize';
import User from '../models/community/user.js';
import UserParam from '../models/community/user_param.js';
import UserParamType from '../models/type/user_param.js';
import UserParamValue from '../models/type/user_param_value.js';
import UserParamVisibility from '../models/community/user_param_visibility.js';
import UserParamVisibilityType from '../models/type/user_param_visibility.js';
class SocialNetworkService extends BaseService {
async searchUsers({ username, ageFrom, ageTo, genders }) {
try {
const whereClause = {
active: true,
searchable: true
};
if (username) {
whereClause.username = { [Op.iLike]: `%${username}%` };
}
const users = await User.findAll({
where: whereClause,
include: [
{
model: UserParam,
as: 'user_params',
include: [
{
model: UserParamType,
as: 'paramType',
where: {
description: {
[Op.in]: ['gender', 'birthdate']
}
},
required: true
}
],
required: true
}
]
});
const results = [];
for (const user of users) {
const id = user.hashedId;
const birthdateParam = user.user_params.find(param => param.paramType.description === 'birthdate');
const genderParam = user.user_params.find(param => param.paramType.description === 'gender');
const age = birthdateParam ? this.calculateAge(birthdateParam.value) : null;
const decryptedGenderValue = genderParam ? genderParam.value : null;
let gender = null;
if (decryptedGenderValue) {
const genderValue = await UserParamValue.findOne({
where: {
id: decryptedGenderValue
}
});
gender = genderValue ? genderValue.value : null;
}
const isWithinAgeRange = (!ageFrom || age >= ageFrom) && (!ageTo || age <= ageTo);
if (isWithinAgeRange && (!genders || !genders.length || (gender && genders.includes(gender))) && age >= 14) {
results.push({
id: id,
username: user.username,
email: user.email,
gender: gender,
age: age
});
}
}
return results;
} catch (error) {
console.error('Error in searchUsers:', error);
throw new Error('Error searching users');
}
}
async getProfile(hashedUserId, requestingUserId) {
try {
const requestingUser = await this.getUserByHashedId(requestingUserId);
const requestingUserParams = await this.getUserParams(requestingUser.id, ['birthdate']);
let requestingUserAge = 0;
for (const param of requestingUserParams) {
if (param.paramType.description === 'birthdate') {
requestingUserAge = this.calculateAge(param.value);
break;
}
}
const user = await User.findOne({
where: {
hashedId: hashedUserId,
active: true,
searchable: true,
},
include: [
{
model: UserParam,
as: 'user_params',
include: [
{
model: UserParamType,
as: 'paramType',
},
{
model: UserParamVisibility,
as: 'param_visibilities',
include: [
{
model: UserParamVisibilityType,
as: 'visibility_type'
}
]
}
],
order: [[ 'order_id', 'asc']]
}
]
});
if (user) {
const userParams = {};
await Promise.all(user.user_params.map(async (param) => {
const visibilityData = param.param_visibilities?.[0]?.visibility_type;
const visibility = visibilityData ? visibilityData.description : 'Invisible';
let paramValue = param.value;
let paramValueChanged = false;
try {
const parsedValue = JSON.parse(paramValue);
if (Array.isArray(parsedValue)) {
paramValue = await Promise.all(parsedValue.map(async (value) => {
if (/^\d+$/.test(value)) {
const userParamValue = await UserParamValue.findOne({
where: {
id: parseInt(value, 10),
userParamTypeId: param.paramTypeId
}
});
paramValueChanged = true;
return userParamValue ? userParamValue.value : value;
}
return value;
}));
}
} catch (e) {
}
if (!paramValueChanged) {
if (/^\d+$/.test(paramValue)) {
const userParamValue = await UserParamValue.findOne({
where: {
id: parseInt(paramValue, 10),
userParamTypeId: param.paramTypeId
}
});
if (userParamValue) {
paramValue = userParamValue.value;
}
}
}
const paramTypeDescription = param.paramType.description;
if (visibility === 'Invisible') {
return;
}
if (visibility === 'All' || (visibility === 'FriendsAndAdults' && requestingUserAge >= 18) || (visibility === 'AdultsOnly' && requestingUserAge >= 18)) {
userParams[paramTypeDescription] = {
type: param.paramType.datatype,
value: paramValue
};
if (paramTypeDescription === 'birthdate') {
userParams['age'] = { value: this.calculateAge(Date.parse(paramValue)), type: "int"};
}
}
}));
const userProfile = {
username: user.username,
registrationDate: user.registrationDate,
params: userParams
};
return userProfile;
}
return null;
} catch (error) {
console.error('Error in getProfile:', error);
throw new Error('Error getting profile');
}
}
}
export default SocialNetworkService;

View File

@@ -13,6 +13,10 @@ const initializeSettings = async () => {
where: { name: 'sexuality' }, where: { name: 'sexuality' },
defaults: { name: 'sexuality' } defaults: { name: 'sexuality' }
}); });
await SettingsType.findOrCreate({
where: { name: 'flirt' },
defaults: { name: 'flirt' }
});
}; };
export default initializeSettings; export default initializeSettings;

View File

@@ -4,7 +4,7 @@ import UserParamValue from '../models/type/user_param_value.js';
import Interest from '../models/type/interest.js'; import Interest from '../models/type/interest.js';
import { Op, } from 'sequelize'; import { Op, } from 'sequelize';
import InterestTranslation from '../models/type/interest_translation.js'; import InterestTranslation from '../models/type/interest_translation.js';
import { sequelize } from '../utils/sequelize.js'; import UserParamVisibilityType from '../models/type/user_param_visibility.js';
const initializeTypes = async () => { const initializeTypes = async () => {
const settingsTypes = await SettingsType.findAll(); const settingsTypes = await SettingsType.findAll();
@@ -27,8 +27,8 @@ const initializeTypes = async () => {
birthdate: { type: 'date', setting: 'personal' }, birthdate: { type: 'date', setting: 'personal' },
zip: { type: 'string', setting: 'personal' }, zip: { type: 'string', setting: 'personal' },
town: { type: 'string', setting: 'personal' }, town: { type: 'string', setting: 'personal' },
bodyheight: { type: 'float', setting: 'view' }, bodyheight: { type: 'float', setting: 'view', unit: 'cm' },
weight: { type: 'float', setting: 'view' }, weight: { type: 'float', setting: 'view', unit: 'kg' },
eyecolor: { type: 'singleselect', setting: 'view' }, eyecolor: { type: 'singleselect', setting: 'view' },
haircolor: { type: 'singleselect', setting: 'view' }, haircolor: { type: 'singleselect', setting: 'view' },
hairlength: { type: 'singleselect', setting: 'view' }, hairlength: { type: 'singleselect', setting: 'view' },
@@ -39,19 +39,26 @@ 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: 'male' }, penislength: { type: 'int', setting: 'sexuality', minAge: 14, gender: 'male', unit: 'cm' },
brasize: { type: 'string', setting: 'sexuality', minAge: 14, gender: 'female' } brasize: { type: 'string', setting: 'sexuality', minAge: 14, gender: 'female' },
interestedInGender: { type: 'multiselect', setting: 'flirt', minAge: 14},
hasChildren: { type: 'bool', setting: 'flirt', minAge: 14 },
willChildren: { type: 'bool', setting: 'flirt', minAge: 14 },
smokes: { type: 'singleselect', setting: 'flirt', minAge: 14},
drinks: { type: 'singleselect', setting: 'flirt', minAge: 14 },
}; };
Object.keys(userParams).forEach(async (key) => { let orderId = 1;
for (const key of Object.keys(userParams)) {
const item = userParams[key]; const item = userParams[key];
const createItem = { description: key, datatype: item.type, settingsId: getSettingsTypeId(item.setting) }; const createItem = { description: key, datatype: item.type, settingsId: getSettingsTypeId(item.setting), orderId: orderId++ };
if (item.minAge) createItem.minAge = item.minAge; if (item.minAge) createItem.minAge = item.minAge;
if (item.gender) createItem.gender = item.gender; if (item.gender) createItem.gender = item.gender;
await UserParamType.findOrCreate({ if (item.unit) createItem.unit = item.unit;
await UserParamType.findOrCreate({
where: { description: key }, where: { description: key },
defaults: createItem defaults: createItem
}); });
}); }
const valuesList = { const valuesList = {
gender: ['male', 'female', 'transfemale', 'transmale', 'nonbinary'], gender: ['male', 'female', 'transfemale', 'transmale', 'nonbinary'],
language: ['de', 'en'], language: ['de', 'en'],
@@ -62,17 +69,22 @@ const initializeTypes = async () => {
freckles: ['much', 'medium', 'less', 'none'], freckles: ['much', 'medium', 'less', 'none'],
sexualpreference: ['straight', 'gay', 'bi', 'pan', 'asexual'], sexualpreference: ['straight', 'gay', 'bi', 'pan', 'asexual'],
pubichair: ['none', 'short', 'medium', 'long', 'hairy', 'waxed', 'landingstrip', 'bikinizone', 'other'], pubichair: ['none', 'short', 'medium', 'long', 'hairy', 'waxed', 'landingstrip', 'bikinizone', 'other'],
interestedInGender: ['male', 'female'],
smokes: ['never', 'socially', 'often', 'daily'],
drinks: ['never', 'socially', 'often', 'daily'],
brasize: ['Keine', 'AA', 'A', 'B', 'C', 'D', 'E (DD)', 'F (E)', 'G (F)', 'H (FF)', 'I (G)', 'J (GG)', 'K (H)']
}; };
Object.keys(valuesList).forEach(async (key) => { Object.keys(valuesList).forEach(async (key) => {
const values = valuesList[key]; const values = valuesList[key];
const userParamTypeId = await getUserParamTypeId(key); const userParamTypeId = await getUserParamTypeId(key);
let orderId = 1;
values.forEach(async (value) => { values.forEach(async (value) => {
await UserParamValue.findOrCreate({ await UserParamValue.findOrCreate({
where: { where: {
userParamTypeId: userParamTypeId, userParamTypeId: userParamTypeId,
value: value value: value
}, },
defaults: { userParamTypeId: userParamTypeId, value: value } defaults: { userParamTypeId: userParamTypeId, value: value, orderId: orderId++ }
}) })
}); });
}); });
@@ -161,6 +173,18 @@ const initializeTypes = async () => {
throw error; throw error;
} }
} }
const visibilityTypes = ['Invisible', 'OnlyFriends', 'FriendsAndAdults', 'AdultsOnly', 'All'];
for (const type of visibilityTypes) {
try {
await UserParamVisibilityType.findOrCreate({
where: { description: type },
defaults: { description: type }
});
} catch (error) {
throw error;
}
}
}; };
export default initializeTypes; export default initializeTypes;

View File

@@ -4,6 +4,7 @@ import initializeSettings from './initializeSettings.js';
import initializeUserRights from './initializeUserRights.js'; import initializeUserRights from './initializeUserRights.js';
import setupAssociations from '../models/associations.js'; import setupAssociations from '../models/associations.js';
import models from '../models/index.js'; import models from '../models/index.js';
import { createTriggers } from '../models/trigger.js';
const syncDatabase = async () => { const syncDatabase = async () => {
try { try {
@@ -12,6 +13,7 @@ const syncDatabase = async () => {
for (const model of Object.values(models)) { for (const model of Object.values(models)) {
await model.sync(); await model.sync();
} }
createTriggers();
await initializeSettings(); await initializeSettings();
await initializeTypes(); await initializeTypes();

14
frontend/index.html Normal file
View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<title>YourPart</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,32 @@
{ {
"name": "frontend", "name": "frontend",
"version": "1.0.0", "version": "1.0.0",
"type": "module",
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "dev": "vite",
"build": "vue-cli-service build", "build": "vite build",
"lint": "vue-cli-service lint" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@tinymce/tinymce-vue": "^6.0.1",
"axios": "^1.7.2", "axios": "^1.7.2",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"tinymce": "^7.3.0",
"vue": "~3.4.31", "vue": "~3.4.31",
"vue-i18n": "^10.0.0-beta.2", "vue-i18n": "^10.0.0-beta.2",
"vue-multiselect": "^3.1.0",
"vue-router": "^4.0.13", "vue-router": "^4.0.13",
"vuex": "^4.1.0" "vuex": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-service": "^5.0.8", "@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@vitejs/plugin-vue": "^5.1.3",
"assert": "^2.1.0",
"sass": "^1.77.8", "sass": "^1.77.8",
"sass-loader": "^10.5.2" "stream-browserify": "^3.0.0",
"util": "^0.12.5",
"vite": "^5.4.4"
} }
} }

View File

@@ -22,7 +22,7 @@ button {
cursor: pointer; cursor: pointer;
background: #F9A22C; background: #F9A22C;
color: #000000; color: #000000;
border: none; border: 1px solid #F9A22C;
border-radius: 4px; border-radius: 4px;
transition: background 0.05s; transition: background 0.05s;
border: 1px solid transparent; border: 1px solid transparent;
@@ -51,4 +51,19 @@ button:hover {
.link { .link {
color: #F9A22C; color: #F9A22C;
cursor: pointer; cursor: pointer;
}
h1, h2, h3 {
margin: 0;
}
.multiselect__option--highlight,
.multiselect__option--highlight::after,
.multiselect__tag,
.multiselect__option--highlight[data-select],
.multiselect__option--highlight[data-selected],
.multiselect__option--highlight[data-deselect] {
background: none;
background-color: #F9A22C;
color: #000;
} }

View File

@@ -3,10 +3,10 @@
<div class="logo"><img src="/images/icons/logo_color.png"></div> <div class="logo"><img src="/images/icons/logo_color.png"></div>
<div class="window-bar"> <div class="window-bar">
<button v-for="dialog in openDialogs" :key="dialog.dialog.name" class="dialog-button" <button v-for="dialog in openDialogs" :key="dialog.dialog.name" class="dialog-button"
@click="toggleDialogMinimize(dialog.dialog.name)" :title="dialog.dialog.title"> @click="toggleDialogMinimize(dialog.dialog.name)" :title="dialog.dialog.localTitle">
<img v-if="dialog.dialog.icon" :src="'/images/icons/' + dialog.dialog.icon" /> <img v-if="dialog.dialog.icon" :src="'/images/icons/' + dialog.dialog.icon" />
<span class="button-text">{{ dialog.dialog.isTitleTranslated ? $t(dialog.dialog.title) : dialog.dialog.title <span class="button-text">{{ dialog.dialog.isTitleTranslated ? $t(dialog.dialog.localTitle) :
}}</span> dialog.dialog.localTitle }}</span>
</button> </button>
</div> </div>
<div class="static-block"> <div class="static-block">

View File

@@ -60,6 +60,7 @@ nav>ul {
flex-direction: row; flex-direction: row;
margin: 0; margin: 0;
cursor: pointer; cursor: pointer;
z-index: 999;
} }
ul { ul {

View File

@@ -7,7 +7,7 @@
<span v-if="icon" class="dialog-icon"> <span v-if="icon" class="dialog-icon">
<img :src="'/images/icons/' + icon" alt="Icon" /> <img :src="'/images/icons/' + icon" alt="Icon" />
</span> </span>
<span class="dialog-title">{{ isTitleTranslated ? $t(title) : title }}</span> <span class="dialog-title">{{ localIsTitleTranslated ? $t(localTitle) : localTitle }}</span>
<span v-if="!modal" class="dialog-minimize" @click="minimize">_</span> <span v-if="!modal" class="dialog-minimize" @click="minimize">_</span>
<span v-if="showClose" class="dialog-close" @click="close"></span> <span v-if="showClose" class="dialog-close" @click="close"></span>
</div> </div>
@@ -73,12 +73,13 @@ export default {
isDragging: false, isDragging: false,
dragOffsetX: 0, dragOffsetX: 0,
dragOffsetY: 0, dragOffsetY: 0,
localTitle: this.title,
localIsTitleTranslated: this.isTitleTranslated,
}; };
}, },
computed: { computed: {
dialogWidth() { dialogWidth() {
const val = this.width || '70%'; const val = this.width || '70%';
console.log(val);
return val; return val;
}, },
dialogHeight() { dialogHeight() {
@@ -90,6 +91,9 @@ export default {
if (!newValue) { if (!newValue) {
this.minimized = false; this.minimized = false;
} }
},
title(newValue) {
this.updateTitle(newValue);
} }
}, },
methods: { methods: {
@@ -107,9 +111,13 @@ export default {
this.$store.dispatch('dialogs/removeOpenDialog', this.name); this.$store.dispatch('dialogs/removeOpenDialog', this.name);
}, },
buttonClick(action) { buttonClick(action) {
this.$emit(action); if (typeof action === 'function') {
if (action === 'close') { action(); // Wenn action eine Funktion ist, rufe sie direkt auf
this.close(); } else {
this.$emit(action);
if (action === 'close') {
this.close();
}
} }
}, },
handleOverlayClick() { handleOverlayClick() {
@@ -131,7 +139,6 @@ export default {
this.dragOffsetY = event.clientY - dialog.offsetTop; this.dragOffsetY = event.clientY - dialog.offsetTop;
document.addEventListener('mousemove', this.onDrag); document.addEventListener('mousemove', this.onDrag);
document.addEventListener('mouseup', this.stopDragging); document.addEventListener('mouseup', this.stopDragging);
console.log('dragging started');
}, },
onDrag(event) { onDrag(event) {
if (!this.isDragging) return; if (!this.isDragging) return;
@@ -143,13 +150,15 @@ export default {
document.removeEventListener('mousemove', this.onDrag); document.removeEventListener('mousemove', this.onDrag);
document.removeEventListener('mouseup', this.stopDragging); document.removeEventListener('mouseup', this.stopDragging);
}, },
}, updateTitle(newTitle, newIsTitleTranslated) {
mounted() { this.localTitle = newTitle;
this.$store.subscribe((mutation) => { this.localIsTitleTranslated = newIsTitleTranslated;
if (mutation.type === 'dialogs/toggleDialogMinimize' && mutation.payload === this.name) { this.$store.dispatch('dialogs/updateDialogTitle', {
this.minimized = !this.minimized; name: this.name,
} newTitle: newTitle,
}); isTitleTranslated: this.localIsTitleTranslated
});
},
} }
}; };
</script> </script>

View File

@@ -0,0 +1,76 @@
<template>
<div ref="quillEditor" class="quill-editor"></div>
</template>
<script>
import Quill from 'quill/core';
import Toolbar from 'quill/modules/toolbar';
import Snow from 'quill/themes/snow';
import Bold from 'quill/formats/bold';
import Italic from 'quill/formats/italic';
import Underline from 'quill/formats/underline';
import List from 'quill/formats/list';
Quill.register({
'modules/toolbar': Toolbar,
'themes/snow': Snow,
'formats/bold': Bold,
'formats/italic': Italic,
'formats/underline': Underline,
'formats/list': List
});
export default {
name: 'QuillEditor',
props: {
content: {
type: String,
default: ''
},
placeholder: {
type: String,
default: 'Compose an epic...'
}
},
data() {
return {
editor: null,
};
},
mounted() {
this.editor = new Quill(this.$refs.quillEditor, {
theme: 'snow',
placeholder: this.placeholder,
modules: {
toolbar: [
[{ 'header': [1, 2, false] }],
['bold', 'italic', 'underline'],
['link', 'image'],
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
[{ 'align': [] }],
[{ 'color': [] }, { 'background': [] }],
['clean']
]
}
});
this.editor.on('text-change', () => {
this.$emit('update:content', this.editor.root.innerHTML);
});
this.editor.root.innerHTML = this.content;
},
beforeUnmount() {
if (this.editor) {
this.editor.off('text-change');
this.editor = null;
}
}
};
</script>
<style scoped>
.quill-editor {
min-height: 200px;
}
</style>

View File

@@ -1,30 +1,58 @@
<template> <template>
<div class="settings-widget"> <div class="settings-widget">
<template v-for="setting in settings"> <table>
<InputStringWidget v-if="setting.datatype == 'string'" :labelTr="`settings.personal.label.${setting.name}`" <tr v-for="setting in settings" :key="setting.id">
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value=setting.value :list="languagesList()" <td>
@input="handleInput(setting.id, $event)" /> <InputStringWidget v-if="setting.datatype == 'string'"
<DateInputWidget v-else-if="setting.datatype == 'date'" :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=setting.value
@input="handleInput(setting.id, $event)" /> :list="languagesList()" @input="handleInput(setting.id, $event)" />
<SelectDropdownWidget v-else-if="setting.datatype == 'singleselect'"
:labelTr="`settings.personal.label.${setting.name}`" <DateInputWidget v-else-if="setting.datatype == 'date'"
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value=setting.value :labelTr="`settings.personal.label.${setting.name}`"
:list="getSettingOptions(setting.name, setting.options)" @input="handleInput(setting.id, $event)" /> :tooltipTr="`settings.personal.tooltip.${setting.name}`" :value=setting.value
<InputNumberWidget v-else-if="setting.datatype == 'int'" @input="handleInput(setting.id, $event)" />
:labelTr="`settings.personal.label.${setting.name}`"
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value="convertToInt(setting.value)" min="0" <SelectDropdownWidget v-else-if="setting.datatype == 'singleselect'"
max="200" @input="handleInput(setting.id, $event)" /> :labelTr="`settings.personal.label.${setting.name}`"
<FloatInputWidget v-else-if="setting.datatype == 'float'" :tooltipTr="`settings.personal.tooltip.${setting.name}`" :value=setting.value
:labelTr="`settings.personal.label.${setting.name}`" :list="getSettingOptions(setting.name, setting.options)"
: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'" :labelTr="`settings.personal.label.${setting.name}`" <InputNumberWidget v-else-if="setting.datatype == 'int'"
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value="convertToBool(setting.value)" :labelTr="`settings.personal.label.${setting.name}`"
@input="handleInput(setting.id, $event)" /> :tooltipTr="`settings.personal.tooltip.${setting.name}`" :value="convertToInt(setting.value)"
<div v-else>{{ setting }} min="0" max="200" @input="handleInput(setting.id, $event)" />
</div>
</template> <FloatInputWidget v-else-if="setting.datatype == 'float'"
:labelTr="`settings.personal.label.${setting.name}`"
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value="convertToFloat(setting.value)"
@input="handleInput(setting.id, $event)" />
<CheckboxWidget v-else-if="setting.datatype == 'bool'"
:labelTr="`settings.personal.label.${setting.name}`"
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value="convertToBool(setting.value)"
@input="handleInput(setting.id, $event)" />
<MultiselectWidget v-else-if="setting.datatype == 'multiselect'"
:labelTr="`settings.personal.label.${setting.name}`"
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value="setting.value"
:list="getSettingOptions(setting.name, setting.options)"
@input="handleInput(setting.id, $event)" />
<div v-else>{{ setting }}</div>
<span v-if="setting.unit">&nbsp;{{ setting.unit }}</span>
</td>
<td>
<select v-model="setting.visibility.id"
@change="handleVisibilityChange(setting.id, setting.visibility.id)">
<option v-for="visibility in possibleVisibilities" :key="visibility.id" :value="visibility.id">
{{ $t(`settings.visibility.${visibility.description}`) }}
</option>
</select>
</td>
</tr>
</table>
</div> </div>
</template> </template>
@@ -33,10 +61,11 @@ import apiClient from '@/utils/axios.js';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import InputStringWidget from '@/components/form/InputStringWidget.vue'; import InputStringWidget from '@/components/form/InputStringWidget.vue';
import DateInputWidget from '@/components/form/DateInputWidget.vue'; import DateInputWidget from '@/components/form/DateInputWidget.vue';
import SelectDropdownWidget from '@/components/form/SelectDropdownWidget'; import SelectDropdownWidget from '@/components/form/SelectDropdownWidget.vue';
import InputNumberWidget from '@/components/form/InputNumberWidget'; import InputNumberWidget from '@/components/form/InputNumberWidget.vue';
import FloatInputWidget from '@/components/form/FloatInputWidget'; import FloatInputWidget from '@/components/form/FloatInputWidget.vue';
import CheckboxWidget from '@/components/form/CheckboxWidget'; import CheckboxWidget from '@/components/form/CheckboxWidget.vue';
import MultiselectWidget from '@/components/form/MultiselectWidget.vue';
export default { export default {
name: "SettingsWidget", name: "SettingsWidget",
@@ -46,7 +75,8 @@ export default {
SelectDropdownWidget, SelectDropdownWidget,
InputNumberWidget, InputNumberWidget,
FloatInputWidget, FloatInputWidget,
CheckboxWidget CheckboxWidget,
MultiselectWidget
}, },
props: { props: {
settingsType: { settingsType: {
@@ -54,6 +84,10 @@ export default {
required: true required: true
} }
}, },
data: {
settings: [],
possibleVisibilities: [],
},
computed: { computed: {
...mapGetters(['user']), ...mapGetters(['user']),
}, },
@@ -64,6 +98,8 @@ export default {
async fetchSettings() { async fetchSettings() {
if (this.user && this.user.id) { if (this.user && this.user.id) {
try { try {
const visibilityResponse = await apiClient.get('/api/settings/visibilities');
this.possibleVisibilities = visibilityResponse.data;
const userid = this.user.id; const userid = this.user.id;
const response = await apiClient.post('/api/settings/filter', { const response = await apiClient.post('/api/settings/filter', {
userid: userid, userid: userid,
@@ -73,7 +109,7 @@ export default {
} catch (err) { } catch (err) {
this.settings = []; this.settings = [];
} }
} }
}, },
getSettingOptions(fieldName, options) { getSettingOptions(fieldName, options) {
return options.map((option) => { return options.map((option) => {
@@ -94,6 +130,7 @@ export default {
settingId: settingId, settingId: settingId,
value: value value: value
}); });
this.fetchSettings();
} catch (err) { } catch (err) {
console.error('Error updating setting:', err); console.error('Error updating setting:', err);
} }
@@ -120,7 +157,17 @@ export default {
} else { } else {
return false; return false;
} }
} },
async handleVisibilityChange(settingId, visibilityId) {
try {
await apiClient.post('/api/settings/update-visibility', {
userParamTypeId: settingId,
visibilityId: visibilityId
});
} catch (err) {
console.error('Error updating visibility:', err);
}
},
}, },
data() { data() {
return { return {
@@ -129,3 +176,9 @@ export default {
} }
}; };
</script> </script>
<style lang="scss" scoped>
label {
float: left;
}
</style>

View File

@@ -30,7 +30,7 @@ export default {
}, },
methods: { methods: {
updateValue(checked) { updateValue(checked) {
this.$emit("input", checked); this.$emit("input", checked || false);
} }
} }
}; };

View File

@@ -2,7 +2,7 @@
<label> <label>
<span :style="{ width: width + 'em' }">{{ $t(labelTr) }}</span> <span :style="{ width: width + 'em' }">{{ $t(labelTr) }}</span>
<input type="number" :value="formattedValue" :placeholder="$t(labelTr)" :title="$t(tooltipTr)" <input type="number" :value="formattedValue" :placeholder="$t(labelTr)" :title="$t(tooltipTr)"
@input="updateValue($event.target.value)" :step="step" /> @change="updateValue($event.target.value)" :step="step" />
<span v-if="postfix">{{ postfix }}</span> <span v-if="postfix">{{ postfix }}</span>
</label> </label>
</template> </template>
@@ -41,7 +41,7 @@ export default {
}, },
computed: { computed: {
formattedValue() { formattedValue() {
return this.value != null && typeof this.value === 'float' ? this.value.toFixed(this.decimals) : ''; return this.value != null ? this.value.toFixed(this.decimals) : '';
}, },
step() { step() {
return Math.pow(10, -this.decimals); return Math.pow(10, -this.decimals);

View File

@@ -1,8 +1,8 @@
<template> <template>
<label> <label>
<span :style="{ width: width + 'em' }">{{ $t(labelTr) }}</span> <span :style="{ width: width + 'em' }">{{ $t(labelTr) }}</span>
<input type="number" :value="value" :title="$t(tooltipTr)" :min="min" :max="max" <input type="number" :value="value" :title="$t(tooltipTr)" :min="min" :max="max"
@input="updateValue($event.target.value)" /> @change="updateValue($event.target.value)" />
</label> </label>
</template> </template>
@@ -38,7 +38,8 @@ export default {
}, },
methods: { methods: {
updateValue(value) { updateValue(value) {
this.$emit("input", parseFloat(value)); console.log('changed to ', value)
this.$emit("input", parseInt(value));
} }
} }
}; };

View File

@@ -2,7 +2,7 @@
<label> <label>
<span :style="{ width: width + 'em' }">{{ $t(labelTr) }}</span> <span :style="{ width: width + 'em' }">{{ $t(labelTr) }}</span>
<input type="text" :value="value" :placeholder="$t(labelTr)" :title="$t(tooltipTr)" <input type="text" :value="value" :placeholder="$t(labelTr)" :title="$t(tooltipTr)"
@input="validateAndUpdate($event.target.value)" /> @change="validateAndUpdate($event.target.value)" />
</label> </label>
</template> </template>

View File

@@ -0,0 +1,139 @@
<template>
<label>
<span :style="{ width: width + 'em' }">{{ $t(labelTr) }}</span>
<Multiselect
v-model="selectedOptions"
:options="validList"
:multiple="true"
:close-on-select="false"
:clear-on-select="false"
:preserve-search="true"
:placeholder="$t('select_option')"
:track-by="'value'"
>
<template #option="{ option }">
<span v-if="option && option.value">Option: {{ getTranslation(option) }}</span>
</template>
<template #tag="{ option, remove }">
<span v-if="option && option.captionTr" class="custom-tag">
{{ $t(option.captionTr) }}
<span @click="remove(option)">×</span>
</span>
<span v-else>@e</span>
</template>
</Multiselect>
</label>
</template>
<script>
import Multiselect from 'vue-multiselect';
import 'vue-multiselect/dist/vue-multiselect.min.css';
export default {
name: "MultiselectWidget",
components: { Multiselect },
props: {
labelTr: { type: String, required: true },
value: { type: String, required: false, default: '[]' },
tooltipTr: { type: String, required: true },
width: { type: Number, required: false, default: 10 },
list: {
type: Array,
required: true,
default: () => [] // Standardwert hinzufügen, um undefined zu vermeiden
},
},
data() {
return {
internalValues: this.stringToArray(this.value), // Speichert nur die IDs (Werte)
selectedOptions: this.getOptionsFromIds(this.stringToArray(this.value)) // Hilfsvariable, speichert die vollständigen Objekte
};
},
computed: {
validList() {
return this.validatedList(); // Immer ein Array zurückgeben
}
},
watch: {
value(newValue) {
const ids = this.stringToArray(newValue);
this.internalValues = ids; // Nur die IDs speichern
this.selectedOptions = this.getOptionsFromIds(ids); // Optionen basierend auf IDs setzen
},
selectedOptions(newOptions) {
this.internalValues = newOptions.map(option => option.value); // Nur die IDs extrahieren
this.updateValue();
}
},
methods: {
stringToArray(str) {
try {
const array = JSON.parse(str);
return array.filter(item => item !== null && item !== undefined);
} catch (error) {
console.error('Invalid JSON string in value:', str);
return [];
}
},
updateValue() {
const stringValue = JSON.stringify(this.internalValues); // In JSON-String umwandeln
this.$emit("input", stringValue); // String an das Parent-Element übermitteln
},
getTranslation(option) {
return option.captionTr ? this.$t(option.captionTr) : option.caption;
},
findOption(optionId) {
return this.validatedList().find(opt => opt.value === optionId);
},
getOptionsFromIds(ids) {
return ids.map(id => this.findOption(id)).filter(option => option); // Vollständige Objekte basierend auf IDs abrufen
},
validatedList() {
// Überprüfen, ob die Liste valide ist
if (!this.list || !Array.isArray(this.list)) {
return [];
}
return this.list.filter(option => option && option.value !== null && option.value !== undefined && (option.captionTr || option.caption));
}
}
};
</script>
<style scoped>
label {
display: block;
margin-bottom: 1em;
}
label>span {
display: inline-block;
margin-bottom: 0.5em;
}
.multiselect {
margin-left: 0.5em;
}
.custom-tag {
background-color: #f0f0f0;
border: 1px solid #ccc;
padding: 5px;
border-radius: 4px;
margin-right: 5px;
display: inline-block;
}
.custom-tag span {
margin-left: 8px;
cursor: pointer;
}
.multiselect {
display: inline-block;
width: 7em;
}
.multiselect__tags {
white-space: nowrap;
}
</style>

View File

@@ -0,0 +1,110 @@
<template>
<DialogWidget ref="dialog" :title="$t('admin.editcontactrequest.title')" :show-close="true" :buttons="buttons"
@close="closeDialog" name="AnswerContact" :modal="true" :isTitleTranslated="true">
<div class="contact-details">
<h3>Von: {{ contact.email }}</h3>
<p>{{ contact.message }}</p>
</div>
<div class="editor-container">
<Editor v-model="answer" :init="tinymceInitOptions" :api-key="apiKey" />
</div>
</DialogWidget>
<DialogWidget ref="errorDialog" :title="$t('error.title')" :show-close="true" :buttons="errorButtons"
@close="closeErrorDialog" name="ErrorDialog" :modal="true" :isTitleTranslated="false">
<div>
<p>{{ errorMessage }}</p>
</div>
</DialogWidget>
</template>
<script>
import { ref, onBeforeUnmount } from 'vue'
import Editor from '@tinymce/tinymce-vue'
import apiClient from '@/utils/axios.js'
import DialogWidget from '@/components/DialogWidget.vue'
export default {
name: 'AnswerContact',
components: {
DialogWidget,
Editor,
},
data() {
return {
apiKey: import.meta.env.VITE_TINYMCE_API_KEY,
dialog: null,
errorDialog: null,
contact: null,
answer: '',
errorMessage: '',
tinymceInitOptions: {
height: 300,
menubar: false,
plugins: [
'advlist autolink lists link image charmap print preview anchor',
'searchreplace visualblocks code fullscreen',
'insertdatetime media table paste code help wordcount'
],
toolbar:
'undo redo cut copy paste | bold italic forecolor fontfamily fontsize | \
alignleft aligncenter alignright alignjustify | \
bullist numlist outdent indent | removeformat | help'
},
buttons: [
{ text: 'OK', action: this.sendAnswer },
{ text: 'Cancel', action: this.closeDialog }
],
errorButtons: [
{ text: 'OK', action: this.closeErrorDialog }
]
}
},
methods: {
open(contactData) {
this.contact = contactData;
this.dialog.open();
this.answer = '';
},
closeDialog() {
this.dialog.close();
this.answer = '';
},
closeErrorDialog() {
this.errorDialog.close();
},
async sendAnswer() {
try {
await apiClient.post('/api/admin/contacts/answer', {
id: this.contact.id,
answer: this.answer,
});
this.dialog.close();
this.$emit('refresh');
this.answer = '';
} catch (error) {
const errorText = error.response?.data?.error || 'An unexpected error occurred.';
this.errorMessage = errorText;
this.errorDialog.open();
}
}
},
mounted() {
this.dialog = this.$refs.dialog;
this.errorDialog = this.$refs.errorDialog;
},
beforeUnmount() {
// Aufräumarbeiten falls nötig
}
}
</script>
<style scoped>
.contact-details {
margin-bottom: 20px;
}
.editor-container {
margin-top: 20px;
}
</style>

View File

@@ -0,0 +1,149 @@
<template>
<DialogWidget ref="dialog" :title="$t('socialnetwork.profile.pretitle')" :isTitleTranslated="isTitleTranslated"
:show-close="true" :buttons="[{ text: 'Ok', action: 'close' }]" :modal="false" @close="closeDialog">
<div class="dialog-body">
<div>
<ul class="tab-list">
<li v-for="tab in tabs" :key="tab.name" :class="{ active: activeTab === tab.name }"
@click="selectTab(tab.name)">
{{ tab.label }}
</li>
</ul>
<div class="tab-content" v-if="activeTab === 'general'">
<table>
<tr v-for="(value, key) in userProfile.params" :key="key">
<td>{{ $t(`socialnetwork.profile.${key}`) }}</td>
<td>{{ generateValue(key, value) }}</td>
</tr>
</table>
</div>
</div>
</div>
</DialogWidget>
</template>
<script>
import DialogWidget from '@/components/DialogWidget.vue';
import apiClient from '@/utils/axios.js';
export default {
name: 'UserProfileDialog',
components: {
DialogWidget
},
props: {
userId: {
type: String,
required: true
}
},
data() {
return {
isTitleTranslated: true,
userProfile: {},
activeTab: 'general',
userId: '',
tabs: [
{ name: 'general', label: this.$t('socialnetwork.profile.tab.general') },
{ name: 'images', label: this.$t('socialnetwork.profile.tab.images') },
{ name: 'guestbook', label: this.$t('socialnetwork.profile.tab.guestbook') }
],
};
},
methods: {
open() {
this.$refs.dialog.open();
this.loadUserProfile();
},
async loadUserProfile() {
try {
const response = await apiClient.get(`/api/socialnetwork/profile/${this.userId}`);
this.userProfile = response.data;
const newTitle = this.$t('socialnetwork.profile.title').replace('<username>', this.userProfile.username);
this.$refs.dialog.updateTitle(newTitle, false);
} catch (error) {
this.$refs.dialog.updateTitle('socialnetwork.profile.error_title', true);
console.error('Fehler beim Laden des Benutzerprofils:', error);
}
},
closeDialog() {
this.$refs.dialog.close();
},
selectTab(tabName) {
this.activeTab = tabName;
},
generateValue(key, value) {
if (Array.isArray(value.value)) {
const strings = [];
for (const val of value.value) {
strings.push(this.generateValue(key, {type: value.type, value: val}));
}
return strings.join(', ');
}
switch (value.type) {
case 'bool':
return this.$t(`socialnetwork.profile.values.bool.${value.value}`);
case 'multiselect':
case 'singleselect':
return this.$t(`socialnetwork.profile.values.${key}.${value.value}`);
case 'date':
const date = new Date(value.value);
return date.toLocaleDateString();
case 'string':
case 'int':
return value.value;
case 'float':
return new Intl.NumberFormat(navigator.language, {
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(parseFloat(value.value));
default:
return value.value;
}
}
}
};
</script>
<style scoped>
.tab-list {
list-style-type: none;
padding: 0;
display: flex;
margin-bottom: 20px;
border-bottom: 2px solid #ccc;
}
.tab-list li {
padding: 10px 20px;
cursor: pointer;
margin-right: 5px;
border: 1px solid #ccc;
border-bottom: none;
background: #f9f9f9;
}
.tab-list li.active {
background: #ffffff;
border-bottom: 2px solid #ffffff;
font-weight: bold;
}
.tab-content {
padding: 20px;
border: 1px solid #ccc;
background: #ffffff;
overflow: auto;
}
.dialog-body,
.dialog-body > div {
height: 100%;
}
.dialog-body > div {
display: flex;
flex-direction: column;
}
</style>

View File

@@ -11,9 +11,10 @@ import enError from './locales/en/error.json';
import enActivate from './locales/en/activate.json'; import enActivate from './locales/en/activate.json';
import enSettings from './locales/en/settings.json'; import enSettings from './locales/en/settings.json';
import enAdmin from './locales/en/admin.json'; import enAdmin from './locales/en/admin.json';
import enSocialNetwork from './locales/en/socialnetwork.json';
import deGeneral from './locales/de/general.json'; import deGeneral from './locales/de/general.json';
import deHeader from './locales/de/header.json'; import deHeader from './locales/de/header.json';
import deNavigation from './locales/de/navigation.json'; import deNavigation from './locales/de/navigation.json';
import deHome from './locales/de/home.json'; import deHome from './locales/de/home.json';
import deChat from './locales/de/chat.json'; import deChat from './locales/de/chat.json';
@@ -22,6 +23,7 @@ import deError from './locales/de/error.json';
import deActivate from './locales/de/activate.json'; import deActivate from './locales/de/activate.json';
import deSettings from './locales/de/settings.json'; import deSettings from './locales/de/settings.json';
import deAdmin from './locales/de/admin.json'; import deAdmin from './locales/de/admin.json';
import deSocialNetwork from './locales/de/socialnetwork.json';
const messages = { const messages = {
en: { en: {
@@ -35,8 +37,10 @@ const messages = {
...enActivate, ...enActivate,
...enSettings, ...enSettings,
...enAdmin, ...enAdmin,
...enSocialNetwork,
}, },
de: { de: {
'Ok': 'Ok',
...deGeneral, ...deGeneral,
...deHeader, ...deHeader,
...deNavigation, ...deNavigation,
@@ -47,6 +51,7 @@ const messages = {
...deActivate, ...deActivate,
...deSettings, ...deSettings,
...deAdmin, ...deAdmin,
...deSocialNetwork,
} }
}; };

View File

@@ -19,6 +19,9 @@
"actions": "Aktionen", "actions": "Aktionen",
"open": "Bearbeiten", "open": "Bearbeiten",
"finished": "Abschließen" "finished": "Abschließen"
},
"editcontactrequest": {
"title": "[Admin] - Kontaktanfrage bearbeiten"
} }
} }
} }

View File

@@ -27,5 +27,7 @@
}, },
"general": { "general": {
"datetimelong": "dd.MM.yyyy HH:mm:ss" "datetimelong": "dd.MM.yyyy HH:mm:ss"
} },
"OK": "Ok",
"Cancel": "Abbrechen"
} }

View File

@@ -27,6 +27,7 @@
"account": "Account", "account": "Account",
"personal": "Persönliches", "personal": "Persönliches",
"view": "Aussehen", "view": "Aussehen",
"flirt": "Flirt",
"interests": "Interessen", "interests": "Interessen",
"notifications": "Benachrichtigungen", "notifications": "Benachrichtigungen",
"sexuality": "Sexualität" "sexuality": "Sexualität"

View File

@@ -19,8 +19,13 @@
"tattoos": "Tattoos", "tattoos": "Tattoos",
"sexualpreference": "Ausrichtung", "sexualpreference": "Ausrichtung",
"pubichair": "Schamhaare", "pubichair": "Schamhaare",
"penislenght": "Penislänge", "penislength": "Penislänge",
"brasize": "BH-Größe" "brasize": "BH-Größe",
"willChildren": "Ich möchte Kinder",
"smokes": "Rauchen",
"drinks": "Ich trinke Alkohol",
"hasChildren": "Ich habe Kinder",
"interestedInGender": "Interessiert an"
}, },
"tooltip": { "tooltip": {
"language": "Sprache", "language": "Sprache",
@@ -39,7 +44,7 @@
"tattoos": "Tattoos", "tattoos": "Tattoos",
"sexualpreference": "Ausrichtung", "sexualpreference": "Ausrichtung",
"pubichair": "Schamhaare", "pubichair": "Schamhaare",
"penislenght": "Penislänge", "penislength": "Penislänge",
"brasize": "BH-Größe" "brasize": "BH-Größe"
}, },
"gender": { "gender": {
@@ -109,6 +114,22 @@
"landingstrip": "Landebahn", "landingstrip": "Landebahn",
"bikinizone": "Nur Bikinizone", "bikinizone": "Nur Bikinizone",
"other": "Andere" "other": "Andere"
},
"interestedInGender": {
"male": "Männer",
"female": "Frauen"
},
"smokes": {
"often": "Oft",
"socially": "In Gesellschaft",
"daily": "Täglich",
"never": "Nie"
},
"drinks": {
"often": "Oft",
"socially": "In Gesellschaft",
"daily": "Täglich",
"never": "Nie"
} }
}, },
"view": { "view": {
@@ -136,6 +157,16 @@
"added": "Das neue Interesse wurde hinzugefügt und wird bearbeitet. Bis zum Abschluss ist es nicht in der Liste der Interessen sichtbar.", "added": "Das neue Interesse wurde hinzugefügt und wird bearbeitet. Bis zum Abschluss ist es nicht in der Liste der Interessen sichtbar.",
"adderror": "Beim hinzufügen des Interesses ist ein Fehler aufgetreten.", "adderror": "Beim hinzufügen des Interesses ist ein Fehler aufgetreten.",
"errorsetinterest": "Das Interest konnte für Dich nicht gebucht werden." "errorsetinterest": "Das Interest konnte für Dich nicht gebucht werden."
} },
"visibility": {
"Invisible": "Nicht anzeigen",
"OnlyFriends": "Nur Freunden anzeigen",
"FriendsAndAdults": "Freunden und Erwachsenen anzeigen",
"AdultsOnly": "Nur Erwachsenen anzeigen",
"All": "Jedem zeigen"
},
"flirt": {
"title": "Flirt"
}
} }
} }

View File

@@ -0,0 +1,143 @@
{
"socialnetwork": {
"usersearch": {
"title": "Benutzersuche",
"username": "Benutzername",
"age_from": "Alter von",
"age_to": "bis",
"gender": "Geschlecht",
"search_button": "Suchen",
"no_results": "Keine Ergebnisse gefunden",
"results_title": "Suchergebnisse:",
"result": {
"nick": "Spitzname",
"gender": "Geschlecht",
"age": "Alter"
}
},
"profile": {
"pretitle": "Lade Daten. Bitte warten...",
"error_title": "User nicht gefunden",
"title": "Profil von <username>",
"tab": {
"general": "Allgemeines",
"sexuality": "Sexualität",
"images": "Bilder",
"guestbook": "Gästebuch"
},
"values": {
"bool": {
"true": "Ja",
"false": "Nein"
},
"smokes": {
"never": "Nie",
"socially": "In Gesellschaft",
"often": "Oft",
"daily": "Täglich"
},
"drinks": {
"never": "Nie",
"socially": "In Gesellschaft",
"often": "Oft",
"daily": "Täglich"
},
"interestedInGender": {
"male": "Männern",
"female": "Frauen"
},
"sexualpreference": {
"straight": "Heterosexuell",
"gay": "Homosexuell",
"bi": "Bisexuell",
"pan": "Pansexuell",
"asexual": "Asexuell"
},
"pubichair": {
"none": "Keine",
"short": "Kurz",
"medium": "Mittel",
"long": "Lang",
"hairy": "Unrasiert",
"waxed": "Gewachst",
"landingstrip": "Landebahn",
"other": "Anderes",
"bikinizone": "Bikinizone"
},
"gender": {
"male": "Männlich",
"female": "Weiblich",
"transmale": "Trans-Frau",
"transfemale": "Trans-Mann",
"nonbinary": "Nonbinär"
},
"language": {
"de": "Deutsch",
"en": "Englisch"
},
"eyecolor": {
"blue": "Blau",
"green": "Grün",
"brown": "Braun",
"black": "Schwarz",
"grey": "Grau",
"hazel": "Haselnuss",
"amber": "Bernstein",
"red": "Rot",
"other": "Andere"
},
"haircolor": {
"black": "Schwarz",
"brown": "Braun",
"blonde": "Blond",
"red": "Rot",
"grey": "Grau",
"white": "Weiß",
"other": "Andere"
},
"hairlength": {
"short": "Kurz",
"medium": "Mittel",
"long": "Lang",
"bald": "Glatze",
"other": "Andere"
},
"skincolor": {
"light": "Hell",
"medium": "Mittel",
"dark": "Dunkel",
"other": "Andere"
},
"freckles": {
"much": "Viele",
"medium": "Mittel",
"less": "Wenige",
"none": "Keine"
}
},
"interestedInGender": "Interessiert an",
"hasChildren": "Hat Kinder",
"smokes": "Rauchen",
"drinks": "Alkohol",
"willChildren": "Will Kinder",
"sexualpreference": "Sexuelle Ausrichtung",
"pubichair": "Schamhaare",
"penislength": "Penislänge",
"brasize": "BH-Größe",
"piercings": "Piercings",
"tattoos": "Tattoos",
"language": "Sprache",
"gender": "Geschlecht",
"eyecolor": "Augenfarbe",
"haircolor": "Haarfarbe",
"hairlength": "Haarlänge",
"freckles": "Sommersprossen",
"skincolor": "Hautfarbe",
"birthdate": "Geburtsdatum",
"age": "Alter",
"town": "Stadt",
"bodyheight": "Größe",
"weight": "Gewicht"
}
}
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -4,23 +4,31 @@ 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 FlirtSettingsView from '../views/settings/FlirtView.vue';
import SexualitySettingsView from '../views/settings/SexualityView.vue'; import SexualitySettingsView from '../views/settings/SexualityView.vue';
import AccountSettingsView from '../views/settings/AccountView.vue'; import AccountSettingsView from '../views/settings/AccountView.vue';
import InterestsView from '../views/settings/InterestsView.vue'; import InterestsView from '../views/settings/InterestsView.vue';
import AdminInterestsView from '../views/admin/InterestsView.vue'; import AdminInterestsView from '../views/admin/InterestsView.vue';
import AdminContactsView from '../views/admin/ContactsView.vue'; import AdminContactsView from '../views/admin/ContactsView.vue';
import SearchView from '../views/social/SearchView.vue';
const routes = [ const routes = [
{ {
path: '/', path: '/',
name: 'Home', name: 'Home',
component: HomeView component: HomeView
}, },
{ {
path: '/activate', path: '/activate',
name: 'Activate page', name: 'Activate page',
component: ActivateView component: ActivateView
}, },
{
path: '/socialnetwork/search',
name: 'Search users',
component: SearchView,
meta: { requiresAuth: true }
},
{ {
path: '/settings/personal', path: '/settings/personal',
name: 'Personal settings', name: 'Personal settings',
@@ -39,6 +47,12 @@ const routes = [
component: SexualitySettingsView, component: SexualitySettingsView,
meta: { requiresAuth: true } meta: { requiresAuth: true }
}, },
{
path: '/settings/flirt',
name: 'Flirt settings',
component: FlirtSettingsView,
meta: { requiresAuth: true }
},
{ {
path: '/settings/account', path: '/settings/account',
name: 'Account settings', name: 'Account settings',
@@ -67,7 +81,7 @@ const routes = [
const router = createRouter({ const router = createRouter({
history: createWebHistory(process.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes routes
}); });

View File

@@ -26,6 +26,23 @@ const mutations = {
dialog.dialog.toggleMinimize(); dialog.dialog.toggleMinimize();
} }
minimizing = false; minimizing = false;
},
updateDialogTitle(state, { name, newTitle, isTitleTranslated }) {
const dialogIndex = state.openDialogs.findIndex((d) => d.dialog.name === name);
if (dialogIndex !== -1) {
// Update dialog object reactively
const updatedDialog = {
...state.openDialogs[dialogIndex],
dialog: {
...state.openDialogs[dialogIndex].dialog,
localTitle: newTitle,
isTitleTranslated: isTitleTranslated
}
};
// Replace the old dialog with the updated one
state.openDialogs.splice(dialogIndex, 1, updatedDialog);
}
} }
}; };
@@ -38,7 +55,10 @@ const actions = {
}, },
toggleDialogMinimize({ commit }, dialogName) { toggleDialogMinimize({ commit }, dialogName) {
commit('toggleDialogMinimize', dialogName); commit('toggleDialogMinimize', dialogName);
} },
updateDialogTitle({ commit }, { name, newTitle, isTitleTranslated }) {
commit('updateDialogTitle', { name, newTitle, isTitleTranslated });
},
}; };
export default { export default {

View File

@@ -2,7 +2,7 @@ import axios from 'axios';
import store from '../store'; import store from '../store';
const apiClient = axios.create({ const apiClient = axios.create({
baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:3001', baseURL: import.meta.env.VUE_APP_API_BASE_URL || 'http://localhost:3001',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }

View File

@@ -14,14 +14,15 @@
<td>{{ formatDateTimeLong(contact.createdAt) }}</td> <td>{{ formatDateTimeLong(contact.createdAt) }}</td>
<td>{{ contact.email }}</td> <td>{{ contact.email }}</td>
<td> <td>
<button @clicked="openRequest(contact)">{{ $t('admin.contacts.open') }}</button> <button @click="openRequest(contact)">{{ $t('admin.contacts.open') }}</button>
<button @clicked="finishRequest(contact)">{{ $t('admin.contacts.finished') }}</button> <button @click="finishRequest(contact)">{{ $t('admin.contacts.finished') }}</button>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<ErrorDialog ref="errorDialog" /> <ErrorDialog ref="errorDialog" />
<AnswerContact ref="answerContactDialog" />
</template> </template>
<script> <script>
@@ -29,11 +30,13 @@ import apiClient from '@/utils/axios.js';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import ErrorDialog from '@/dialogues/standard/ErrorDialog.vue'; import ErrorDialog from '@/dialogues/standard/ErrorDialog.vue';
import { formatDateTimeLong } from '@/utils/datetime.js'; import { formatDateTimeLong } from '@/utils/datetime.js';
import AnswerContact from '../../dialogues/admin/AnswerContact.vue';
export default { export default {
name: 'AdminContactsView', name: 'AdminContactsView',
components: { components: {
ErrorDialog ErrorDialog,
AnswerContact,
}, },
data() { data() {
return { return {
@@ -54,10 +57,10 @@ export default {
} }
}, },
async openRequest(contact) { async openRequest(contact) {
this.$refs.answerContactDialog.open(contact);
}, },
async finishRequest(contact) { async finishRequest(contact) {
await apiClient.get('/api/admin/opencontacts/finish/${contact.id}');
} }
} }
} }

View File

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

View File

@@ -0,0 +1,206 @@
<template>
<div class="search-view">
<h2>{{ $t('socialnetwork.usersearch.title') }}</h2>
<form @submit.prevent="performSearch">
<div class="form-group">
<label for="username">{{ $t('socialnetwork.usersearch.username') }}:</label>
<input type="text" id="username" v-model="searchCriteria.username"
:placeholder="$t('socialnetwork.usersearch.username')" />
</div>
<div class="form-group">
<label for="ageFrom">{{ $t('socialnetwork.usersearch.age_from') }}:</label>
<input type="number" id="ageFrom" v-model="searchCriteria.ageFrom" :min="14" :max="150"
:placeholder="$t('socialnetwork.usersearch.age_from')" class="age-input" />
<label for="ageTo">{{ $t('socialnetwork.usersearch.age_to') }}:</label>
<input type="number" id="ageTo" v-model="searchCriteria.ageTo" :min="14" :max="150"
:placeholder="$t('socialnetwork.usersearch.age_to')" class="age-input" />
</div>
<div class="form-group">
<label for="gender">{{ $t('socialnetwork.usersearch.gender') }}:</label>
<multiselect v-model="searchCriteria.gender" :options="genderOptions" :multiple="true"
:close-on-select="false" :placeholder="$t('socialnetwork.usersearch.gender')" label="name"
track-by="name" />
</div>
<div class="form-group">
<button type="submit" class="search-button">{{ $t('socialnetwork.usersearch.search_button') }}</button>
</div>
</form>
<div class="search-results" v-if="searchResults.length">
<h3>{{ $t('socialnetwork.usersearch.results_title') }}</h3>
<table>
<thead>
<tr>
<th>{{ $t("socialnetwork.usersearch.result.nick") }}</th>
<th>{{ $t("socialnetwork.usersearch.result.gender") }}</th>
<th>{{ $t("socialnetwork.usersearch.result.age") }}</th>
</tr>
</thead>
<tbody>
<tr v-for="result in searchResults" :key="result.id">
<td><span @click.prevent="openUserProfile(result.id)" :class="'clickable g-' + result.gender">{{ result.username }}</span></td>
<td>{{ result.gender }}</td>
<td>{{ result.age }}</td>
</tr>
</tbody>
</table>
</div>
<div v-else class="no-results">
{{ $t('socialnetwork.usersearch.no_results') }}
</div>
</div>
<UserProfileDialog ref="userProfileDialog" :username="selectedUsername" />
</template>
<script>
import Multiselect from 'vue-multiselect';
import 'vue-multiselect/dist/vue-multiselect.min.css';
import apiClient from '@/utils/axios.js';
import UserProfileDialog from '@/dialogues/socialnetwork/UserProfileDialog.vue';
export default {
components: {
Multiselect,
UserProfileDialog
},
data() {
return {
searchCriteria: {
username: '',
ageFrom: 14,
ageTo: 150,
gender: []
},
genderOptions: [],
searchResults: []
};
},
async mounted() {
await this.loadGenderOptions();
},
methods: {
async loadGenderOptions() {
try {
const response = await apiClient.post('/api/settings/getparamvalues', {
type: 'gender'
});
this.genderOptions = response.data.map(g => ({ name: g.name, value: g.value }));
} catch (error) {
console.error('Fehler beim Laden der Geschlechtsoptionen:', error);
}
},
async performSearch() {
const searchCriteria = {
username: this.searchCriteria.username,
ageFrom: this.searchCriteria.ageFrom,
ageTo: this.searchCriteria.ageTo,
gender: this.searchCriteria.gender.map(g => g.value)
};
try {
const response = await apiClient.post('/api/socialnetwork/usersearch', searchCriteria);
this.searchResults = response.data;
} catch (error) {
console.error('Fehler bei der Suche:', error);
}
},
openUserProfile(id) {
this.$refs.userProfileDialog.userId = id;
this.$refs.userProfileDialog.open();
}
}
};
</script>
<style scoped>
.search-view {
max-width: 600px;
margin: 0 auto;
padding: 0;
}
h2 {
margin-bottom: 10px;
text-align: center;
}
.form-group {
display: flex;
align-items: center;
margin-bottom: 10px;
}
label {
width: 120px;
font-weight: bold;
margin-right: 10px;
text-align: right;
}
input,
.multiselect__input {
flex: 1;
padding: 5px;
border-radius: 4px;
border: 1px solid #ccc;
}
.age-input {
width: 70px;
margin-right: 10px;
}
.search-results {
margin-top: 20px;
}
.search-results ul {
list-style-type: none;
padding: 0;
}
.search-results li {
padding: 8px;
background: #f9f9f9;
border-bottom: 1px solid #ddd;
}
table {
margin: 0.5em 0;
padding: 0;
border-collapse: collapse;
}
thead {
color: #7BBE55;
}
th, td {
padding-right: 1em;
}
th, td:not:last-child {
border-bottom: 1px solid #7E471B;
}
.clickable {
cursor: pointer;
}
.no-results {
margin-top: 20px;
text-align: center;
color: #888;
}
.g-male {
color: #3377ff;
}
.g-female {
color: #ff3377;
}
</style>

27
frontend/vite.config.js Normal file
View File

@@ -0,0 +1,27 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill';
import { NodeModulesPolyfillPlugin } from '@esbuild-plugins/node-modules-polyfill';
import path from 'path';
export default defineConfig({
plugins: [vue()],
optimizeDeps: {
esbuildOptions: {
plugins: [
NodeGlobalsPolyfillPlugin({
buffer: true
}),
NodeModulesPolyfillPlugin()
]
}
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
stream: 'stream-browserify',
util: 'util',
assert: 'assert',
}
}
})

View File

@@ -1,13 +0,0 @@
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true
}
},
client: {
webSocketURL: 'ws://localhost:8080/ws',
},
}
};

View File

@@ -7,7 +7,7 @@
"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 dev"
}, },
"devDependencies": { "devDependencies": {
"concurrently": "^7.0.0", "concurrently": "^7.0.0",
@@ -18,4 +18,4 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"sequelize-cli": "^6.6.2" "sequelize-cli": "^6.6.2"
} }
} }