Improve exercise type validation and encryption in settings service: Enhance error handling for exercise type name checks in both create-bisaya-course-content and update-week1-bisaya-exercises scripts. Implement encryption for API keys and user settings in settingsService, ensuring sensitive data is securely stored. Update localization files to include new terms related to model patterns in English, German, and Spanish.

This commit is contained in:
Torsten Schulz (local)
2026-03-25 16:09:04 +01:00
parent d50d3c4016
commit 6be816fe48
7 changed files with 82 additions and 51 deletions

View File

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

View File

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

View File

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

View File

@@ -516,7 +516,7 @@ export default {
</script>
<style lang="scss" scoped>
@import '../assets/styles.scss';
@use '../assets/styles.scss' as *;
.app-navigation,
.nav-primary > ul {

View File

@@ -424,6 +424,7 @@
"modelSentence": "Modellsatz",
"modelDialogLine": "Mögliche Dialogzeile",
"modelResponse": "Mögliche Antwort",
"modelPattern": "Möglicher Mustersatz",
"patternPrompt": "Muster",
"readingAloudInstruction": "Lies den Text laut vor. Klicke auf 'Aufnahme starten' und beginne zu sprechen.",
"speakingFromMemoryInstruction": "Sprich frei aus dem Kopf. Verwende die angezeigten Schlüsselwörter.",

View File

@@ -424,6 +424,7 @@
"modelSentence": "Model sentence",
"modelDialogLine": "Possible dialog line",
"modelResponse": "Possible response",
"modelPattern": "Possible pattern example",
"patternPrompt": "Pattern",
"readingAloudInstruction": "Read the text aloud. Click 'Start Recording' and begin speaking.",
"speakingFromMemoryInstruction": "Speak freely from memory. Use the displayed keywords.",

View File

@@ -421,6 +421,7 @@
"modelSentence": "Frase modelo",
"modelDialogLine": "Línea posible del diálogo",
"modelResponse": "Respuesta posible",
"modelPattern": "Ejemplo posible del patrón",
"patternPrompt": "Patrón",
"readingAloudInstruction": "Lee el texto en voz alta. Haz clic en 'Iniciar grabación' y comienza a hablar.",
"speakingFromMemoryInstruction": "Habla de memoria. Usa las palabras clave mostradas.",