Files
yourpart3/backend/services/settingsService.js
Torsten Schulz (local) cf6d72385e fix(settings): enhance user parameter handling and add special user parameter types
- Introduced a new method to ensure special user parameter types for adult verification settings, improving data integrity and handling.
- Updated the upsertUserParam method to utilize the new special parameter type handling, ensuring robust user parameter management.
- Updated package dependencies in package.json and package-lock.json for consistency and to address potential vulnerabilities.
2026-03-27 10:38:42 +01:00

699 lines
26 KiB
JavaScript

import BaseService from './BaseService.js';
import UserParamType from '../models/type/user_param.js';
import SettingsType from '../models/type/settings.js';
import UserParam from '../models/community/user_param.js';
import User from '../models/community/user.js';
import UserParamValue from '../models/type/user_param_value.js';
import Interest from '../models/type/interest.js';
import UserInterest from '../models/community/interest.js';
import InterestTranslation from '../models/type/interest_translation.js';
import { Op } from 'sequelize';
import UserParamVisibilityType from '../models/type/user_param_visibility.js';
import UserParamVisibility from '../models/community/user_param_visibility.js';
import { encrypt } from '../utils/encryption.js';
import { sequelize } from '../utils/sequelize.js';
import fsPromises from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import { v4 as uuidv4 } from 'uuid';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/** Wie UserParam.value-Setter: bei Verschlüsselungsfehler leeren String speichern, nicht crashen. */
function encryptUserParamValue(plain) {
try {
return encrypt(plain);
} catch (error) {
console.error('Error encrypting user_param value:', error);
return '';
}
}
class SettingsService extends BaseService{
async ensureSpecialUserParamType(description) {
const specialTypes = {
adult_verification_status: { datatype: 'string', setting: 'account', orderId: 910, minAge: 18 },
adult_verification_request: { datatype: 'string', setting: 'account', orderId: 911, minAge: 18 },
adult_upload_blocked: { datatype: 'bool', setting: 'account', orderId: 912, minAge: 18 },
};
const definition = specialTypes[description];
if (!definition) {
return null;
}
const settingsType = await SettingsType.findOne({
where: { name: definition.setting }
});
if (!settingsType) {
throw new Error(`Missing settings type: ${definition.setting}`);
}
const [paramType] = await UserParamType.findOrCreate({
where: { description },
defaults: {
description,
datatype: definition.datatype,
settingsId: settingsType.id,
orderId: definition.orderId,
minAge: definition.minAge,
immutable: false
}
});
return paramType;
}
parseAdultVerificationRequest(value) {
if (!value) return null;
try {
return JSON.parse(value);
} catch {
return null;
}
}
normalizeAdultVerificationStatus(value) {
if (['pending', 'approved', 'rejected'].includes(value)) {
return value;
}
return 'none';
}
async getAdultAccessStateByUserId(userId) {
const userParams = await this.getUserParams(userId, ['birthdate', 'adult_verification_status', 'adult_verification_request']);
let birthdate = null;
let adultVerificationStatus = 'none';
let adultVerificationRequest = null;
for (const param of userParams) {
if (param.paramType.description === 'birthdate') {
birthdate = param.value;
}
if (param.paramType.description === 'adult_verification_status') {
adultVerificationStatus = this.normalizeAdultVerificationStatus(param.value);
}
if (param.paramType.description === 'adult_verification_request') {
adultVerificationRequest = this.parseAdultVerificationRequest(param.value);
}
}
const age = birthdate ? this.calculateAge(birthdate) : null;
const isAdult = age !== null && age >= 18;
return {
age,
isAdult,
adultVerificationStatus: isAdult ? adultVerificationStatus : 'none',
adultVerificationRequest: isAdult ? adultVerificationRequest : null,
adultAccessEnabled: isAdult && adultVerificationStatus === 'approved'
};
}
buildAdultVerificationFilePath(fileName) {
return path.join(__dirname, '..', 'images', 'adult-verification', fileName);
}
async saveAdultVerificationDocument(file) {
const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
if (!file || !file.buffer) {
throw new Error('No verification document provided');
}
if (!allowedMimeTypes.includes(file.mimetype)) {
throw new Error('Unsupported verification document type');
}
const ext = path.extname(file.originalname || '').toLowerCase();
const safeExt = ext && ext.length <= 8 ? ext : (file.mimetype === 'application/pdf' ? '.pdf' : '.bin');
const fileName = `${uuidv4()}${safeExt}`;
const filePath = this.buildAdultVerificationFilePath(fileName);
await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
await fsPromises.writeFile(filePath, file.buffer);
return { fileName, filePath };
}
async upsertUserParam(userId, description, value) {
let paramType = await UserParamType.findOne({ where: { description } });
if (!paramType) {
paramType = await this.ensureSpecialUserParamType(description);
}
if (!paramType) {
throw new Error(`Missing user param type: ${description}`);
}
const existingParam = await UserParam.findOne({
where: { userId, paramTypeId: paramType.id }
});
if (existingParam) {
await existingParam.update({ value });
return existingParam;
}
return UserParam.create({
userId,
paramTypeId: paramType.id,
value
});
}
async getUserParams(userId, paramDescriptions) {
return await UserParam.findAll({
where: { userId },
include: [
{
model: UserParamType,
as: 'paramType',
where: { description: { [Op.in]: paramDescriptions } },
order: [[ 'order_id', 'asc' ]]
}
]
});
}
async getFieldOptions(field) {
if (['singleselect', 'multiselect'].includes(field.datatype)) {
return await UserParamValue.findAll({
where: { userParamTypeId: field.id },
order: [[ 'order_id', 'asc' ]]
});
}
return [];
}
async filterSettings(hashedUserId, type) {
const user = await this.getUserByHashedId(hashedUserId);
const userParams = await this.getUserParams(user.id, ['birthdate', 'gender']);
let birthdate = null;
let gender = null;
for (const param of userParams) {
if (param.paramType.description === 'birthdate') {
birthdate = param.value;
}
if (param.paramType.description === 'gender') {
const genderResult = await UserParamValue.findOne({ where: { id: param.value } });
gender = genderResult ? genderResult.dataValues?.value : null;
}
}
const age = birthdate ? this.calculateAge(birthdate) : null;
const fields = await UserParamType.findAll({
include: [
{
model: SettingsType,
as: 'settings_type',
where: { name: type }
},
{
model: UserParam,
as: 'user_params',
required: false,
include: [
{
model: User,
as: 'user',
where: { id: user.id }
},
{
model: UserParamVisibility,
as: 'param_visibilities',
required: false,
include: [
{
model: UserParamVisibilityType,
as: 'visibility_type'
}
]
}
]
}
],
where: {
[Op.and]: [
{ minAge: { [Op.or]: [null, { [Op.lte]: age }] } },
{ gender: { [Op.or]: [null, gender] } }
]
}
});
return await Promise.all(fields.map(async (field) => {
const options = await this.getFieldOptions(field);
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 {
id: field.id,
name: field.description,
minAge: field.minAge,
gender: field.gender,
datatype: field.datatype,
unit: field.unit,
immutable: field.immutable,
value: field.user_params.length > 0 ? field.user_params[0].value : null,
options: options.map(opt => ({ id: opt.id, value: opt.value })),
visibility
};
}));
}
async updateSetting(hashedUserId, settingId, value) {
try {
const user = await this.getUserByHashedId(hashedUserId);
const paramType = await UserParamType.findOne({ where: { id: settingId } });
if (!paramType) {
throw new Error('Parameter type not found');
}
// Prüfe ob das Feld unveränderlich ist
if (paramType.immutable) {
const userParam = await UserParam.findOne({
where: { userId: user.id, paramTypeId: settingId }
});
// Wenn bereits ein Wert existiert, ist das Feld unveränderlich
if (userParam && userParam.value) {
throw new Error('This field cannot be changed. Please contact support for modifications.');
}
}
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;
}
}
async getTypeParamValueId(paramValue) {
const userParamValueObject = await UserParamValue.findOne({
where: { value: paramValue }
});
if (!userParamValueObject) {
throw new Error('Parameter value not found');
}
return userParamValueObject.id;
}
async getTypeParamValues(type) {
const userParamValues = await UserParamValue.findAll({
include: [
{
model: UserParamType,
as: 'user_param_value_type',
where: { description: type }
}
]
});
return userParamValues.map(type => ({ id: type.dataValues.id, name: type.dataValues.value }));
}
async getTypeParamValue(id) {
const userParamValueObject = await UserParamValue.findOne({
where: { id }
});
if (!userParamValueObject) {
throw new Error('Parameter value not found');
}
return userParamValueObject.value;
}
async addInterest(hashedUserId, name) {
try {
const user = await this.getUserByHashedId(hashedUserId);
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 addUserInterest(hashedUserId, interestId) {
try {
const user = await this.getUserByHashedId(hashedUserId);
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 ? this.calculateAge(birthdate) : 0;
const interestsFilter = { id: interestId, allowed: true };
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 });
} catch (error) {
console.error('Error adding user interest:', error);
throw error;
}
}
async removeInterest(hashedUserId, interestId) {
try {
const user = await this.getUserByHashedId(hashedUserId);
const interests = await UserInterest.findAll({
where: { userId: user.id, userinterestId: interestId }
});
for (const interest of interests) {
await interest.destroy();
}
} catch (error) {
console.error('Error removing interest:', error);
throw error;
}
}
async getAccountSettings(hashedUserId) {
try {
const user = await this.getUserByHashedId(hashedUserId);
if (!user) {
throw new Error('User not found');
}
// Die Email wird automatisch durch den Getter entschlüsselt
// Falls die Entschlüsselung fehlschlägt, verwende null
let email = null;
try {
email = user.email; // Getter entschlüsselt automatisch
} catch (decryptError) {
console.warn('Email decryption failed, using null:', decryptError.message);
email = null;
}
const adultAccess = await this.getAdultAccessStateByUserId(user.id);
return {
username: user.username,
email: email,
showinsearch: user.searchable,
...adultAccess
};
} catch (error) {
console.error('Error getting account settings:', error);
throw error;
}
}
async setAccountSettings({ userId, settings }) {
try {
const user = await this.getUserByHashedId(userId);
if (!user) {
throw new Error('User not found');
}
const adultAccess = await this.getAdultAccessStateByUserId(user.id);
// Update username if provided
if (settings.username !== undefined) {
await user.update({ username: settings.username });
}
// Update email if provided
if (settings.email !== undefined) {
await user.update({ email: settings.email });
}
// Update searchable flag if provided
if (settings.showinsearch !== undefined) {
await user.update({ searchable: settings.showinsearch });
}
if (settings.requestAdultVerification) {
if (!adultAccess.isAdult) {
throw new Error('Adult verification can only be requested by adult users');
}
const normalizedValue = adultAccess.adultVerificationStatus === 'approved'
? 'approved'
: 'pending';
await this.upsertUserParam(user.id, 'adult_verification_status', normalizedValue);
}
// Update password if provided and not empty
if (settings.newpassword && settings.newpassword.trim() !== '') {
if (!settings.oldpassword || settings.oldpassword.trim() === '') {
throw new Error('Old password is required to change password');
}
// Verify old password
const bcrypt = await import('bcryptjs');
const match = await bcrypt.compare(settings.oldpassword, user.password);
if (!match) {
throw new Error('Old password is incorrect');
}
// Hash new password
const hashedPassword = await bcrypt.hash(settings.newpassword, 10);
await user.update({ password: hashedPassword });
}
return { success: true };
} catch (error) {
console.error('Error setting account settings:', error);
throw error;
}
}
async submitAdultVerificationRequest(hashedUserId, { note }, file) {
const user = await this.getUserByHashedId(hashedUserId);
if (!user) {
throw new Error('User not found');
}
const adultAccess = await this.getAdultAccessStateByUserId(user.id);
if (!adultAccess.isAdult) {
throw new Error('Adult verification can only be requested by adult users');
}
if (!file) {
throw new Error('No verification document provided');
}
const savedFile = await this.saveAdultVerificationDocument(file);
const requestPayload = {
fileName: savedFile.fileName,
originalName: file.originalname,
mimeType: file.mimetype,
note: note || '',
submittedAt: new Date().toISOString()
};
await this.upsertUserParam(user.id, 'adult_verification_request', JSON.stringify(requestPayload));
await this.upsertUserParam(user.id, 'adult_verification_status', adultAccess.adultVerificationStatus === 'approved' ? 'approved' : 'pending');
return requestPayload;
}
async getVisibilities() {
return UserParamVisibilityType.findAll();
}
async updateVisibility(hashedUserId, userParamTypeId, visibilityId) {
try {
const user = await this.getUserByHashedId(hashedUserId);
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;
}
}
/**
* LLM-/Sprachassistent: Werte in community.user_param, Typen in type.user_param,
* Gruppe type.settings.name = languageAssistant. API-Key separat (llm_api_key), Metadaten als JSON in llm_settings.
* Kein Klartext-Key an den Client.
*/
async getLlmSettings(hashedUserId) {
const user = await this.getUserByHashedId(hashedUserId);
const settingsType = await UserParamType.findOne({ where: { description: 'llm_settings' } });
const apiKeyType = await UserParamType.findOne({ where: { description: 'llm_api_key' } });
if (!settingsType || !apiKeyType) {
return {
enabled: true,
baseUrl: '',
model: 'gpt-4o-mini',
hasKey: false,
keyLast4: null
};
}
const settingsRow = await UserParam.findOne({
where: { userId: user.id, paramTypeId: settingsType.id }
});
const keyRow = await UserParam.findOne({
where: { userId: user.id, paramTypeId: apiKeyType.id }
});
let parsed = {};
if (settingsRow?.value) {
try {
parsed = JSON.parse(settingsRow.value);
} catch {
parsed = {};
}
}
const hasStoredKey = Boolean(keyRow && keyRow.getDataValue('value') && String(keyRow.getDataValue('value')).trim());
const hasReadableKey = Boolean(keyRow && keyRow.value && String(keyRow.value).trim());
return {
enabled: parsed.enabled !== false,
baseUrl: parsed.baseUrl || '',
model: parsed.model || 'gpt-4o-mini',
hasKey: hasStoredKey,
keyLast4: parsed.keyLast4 || null,
keyStatus: hasStoredKey ? (hasReadableKey ? 'stored' : 'invalid') : 'missing'
};
}
async saveLlmSettings(hashedUserId, payload) {
const user = await this.getUserByHashedId(hashedUserId);
const settingsType = await UserParamType.findOne({ where: { description: 'llm_settings' } });
const apiKeyType = await UserParamType.findOne({ where: { description: 'llm_api_key' } });
if (!settingsType || !apiKeyType) {
throw new Error(
'LLM-Einstellungstypen fehlen (languageAssistant / llm_settings / llm_api_key). initializeSettings & initializeTypes ausführen.'
);
}
const settingsRow = await UserParam.findOne({
where: { userId: user.id, paramTypeId: settingsType.id }
});
let parsed = {};
if (settingsRow?.value) {
try {
parsed = JSON.parse(settingsRow.value);
} catch {
parsed = {};
}
}
const { apiKey, clearKey, baseUrl, model, enabled } = payload;
await sequelize.transaction(async (transaction) => {
if (clearKey) {
const keyRow = await UserParam.findOne({
where: { userId: user.id, paramTypeId: apiKeyType.id },
transaction
});
if (keyRow) {
await keyRow.destroy({ transaction });
}
delete parsed.keyLast4;
} else if (apiKey !== undefined && String(apiKey).trim() !== '') {
const plain = String(apiKey).trim();
parsed.keyLast4 = plain.length >= 4 ? plain.slice(-4) : plain;
const encKey = encryptUserParamValue(plain);
const [keyRow] = await UserParam.findOrCreate({
where: { userId: user.id, paramTypeId: apiKeyType.id },
defaults: {
userId: user.id,
paramTypeId: apiKeyType.id,
// Platzhalter: Setter verschlüsselt; wird sofort durch encKey überschrieben.
value: ' '
},
transaction
});
keyRow.setDataValue('value', encKey);
await keyRow.save({ fields: ['value'], transaction });
}
if (baseUrl !== undefined) {
parsed.baseUrl = String(baseUrl).trim();
}
if (model !== undefined) {
parsed.model = String(model).trim() || 'gpt-4o-mini';
}
if (enabled !== undefined) {
parsed.enabled = Boolean(enabled);
}
if (!parsed.model) {
parsed.model = 'gpt-4o-mini';
}
const jsonStr = JSON.stringify(parsed);
const encMeta = encryptUserParamValue(jsonStr);
const [metaRow] = await UserParam.findOrCreate({
where: { userId: user.id, paramTypeId: settingsType.id },
defaults: {
userId: user.id,
paramTypeId: settingsType.id,
value: ' '
},
transaction
});
metaRow.setDataValue('value', encMeta);
await metaRow.save({ fields: ['value'], transaction });
});
return { success: true };
}
}
export default new SettingsService();