diff --git a/backend/scripts/create-bisaya-course-content.js b/backend/scripts/create-bisaya-course-content.js index df5ac9c..45ea041 100644 --- a/backend/scripts/create-bisaya-course-content.js +++ b/backend/scripts/create-bisaya-course-content.js @@ -1367,20 +1367,25 @@ async function resolveExerciseTypeId(exercise) { return exercise.exerciseTypeId; } - if (!exercise.exerciseTypeName) { - throw new Error(`Kein exerciseTypeId oder exerciseTypeName für "${exercise.title}" definiert`); + const trimmedName = + exercise.exerciseTypeName != null && exercise.exerciseTypeName !== '' + ? String(exercise.exerciseTypeName).trim() + : ''; + + if (!trimmedName) { + throw new Error(`Kein exerciseTypeId oder exerciseTypeName für Übung "${exercise.title || 'unbenannt'}" definiert`); } const [type] = await sequelize.query( `SELECT id FROM community.vocab_grammar_exercise_type WHERE name = :name LIMIT 1`, { - replacements: { name: exercise.exerciseTypeName }, + replacements: { name: trimmedName }, type: sequelize.QueryTypes.SELECT } ); if (!type) { - throw new Error(`Übungstyp "${exercise.exerciseTypeName}" nicht gefunden`); + throw new Error(`Übungstyp "${trimmedName}" nicht gefunden`); } return Number(type.id); diff --git a/backend/scripts/update-week1-bisaya-exercises.js b/backend/scripts/update-week1-bisaya-exercises.js index 4448cdb..b637c21 100644 --- a/backend/scripts/update-week1-bisaya-exercises.js +++ b/backend/scripts/update-week1-bisaya-exercises.js @@ -48,16 +48,21 @@ async function resolveExerciseTypeId(exercise) { return exercise.exerciseTypeId; } + const name = exercise.exerciseTypeName; + if (name === undefined || name === null || String(name).trim() === '') { + throw new Error(`Kein exerciseTypeId oder exerciseTypeName für Übung "${exercise.title || 'unbenannt'}"`); + } + const [type] = await sequelize.query( `SELECT id FROM community.vocab_grammar_exercise_type WHERE name = :name LIMIT 1`, { - replacements: { name: exercise.exerciseTypeName }, + replacements: { name: String(name).trim() }, type: sequelize.QueryTypes.SELECT } ); if (!type) { - throw new Error(`Übungstyp "${exercise.exerciseTypeName}" nicht gefunden`); + throw new Error(`Übungstyp "${String(name).trim()}" nicht gefunden`); } return Number(type.id); diff --git a/backend/services/settingsService.js b/backend/services/settingsService.js index 8c6ddab..1d3bafe 100644 --- a/backend/services/settingsService.js +++ b/backend/services/settingsService.js @@ -10,6 +10,18 @@ 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'; + +/** 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 getUserParams(userId, paramDescriptions) { @@ -451,55 +463,61 @@ class SettingsService extends BaseService{ const { apiKey, clearKey, baseUrl, model, enabled } = payload; - if (clearKey) { - const keyRow = await UserParam.findOne({ - where: { userId: user.id, paramTypeId: apiKeyType.id } - }); - if (keyRow) { - await keyRow.destroy(); + 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 }); } - 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 [keyRow, keyCreated] = await UserParam.findOrCreate({ - where: { userId: user.id, paramTypeId: apiKeyType.id }, + + 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: apiKeyType.id, - value: plain - } + paramTypeId: settingsType.id, + value: ' ' + }, + transaction }); - if (!keyCreated) { - await keyRow.update({ value: plain }); - } - } - - 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 [metaRow, metaCreated] = await UserParam.findOrCreate({ - where: { userId: user.id, paramTypeId: settingsType.id }, - defaults: { - userId: user.id, - paramTypeId: settingsType.id, - value: jsonStr - } + metaRow.setDataValue('value', encMeta); + await metaRow.save({ fields: ['value'], transaction }); }); - if (!metaCreated) { - await metaRow.update({ value: jsonStr }); - } return { success: true }; } diff --git a/frontend/src/components/AppNavigation.vue b/frontend/src/components/AppNavigation.vue index 703ba3e..4d7008f 100644 --- a/frontend/src/components/AppNavigation.vue +++ b/frontend/src/components/AppNavigation.vue @@ -516,7 +516,7 @@ export default {