import { readRuntimeConfig, setConfigValue, writeRuntimeConfig } from '../../../lib/config-store.mjs'; import { getSecret, hasSecret, setSecret } from '../../../lib/secret-store.mjs'; import { isMailerConfigured, reinitMailer } from '../../../lib/mailer.mjs'; import { PlatformError } from '../../kernel/errors.mjs'; import { readApplicationSettings, writeApplicationSettings } from './repository.mjs'; const SECRET_FIELDS = Object.freeze({ anthropic_key: 'ANTHROPIC_API_KEY', openai_key: 'OPENAI_API_KEY', openrouter_key: 'OPENROUTER_API_KEY', etsy_key: 'ETSY_API_KEY', ebay_client_secret: 'EBAY_CLIENT_SECRET', ai_model_photo_local_token: 'LOCAL_IMAGE_TOKEN', }); const RUNTIME_FIELDS = Object.freeze({ ebay_client_id: 'EBAY_CLIENT_ID', ebay_ru_name: 'EBAY_RU_NAME', ebay_sandbox: 'EBAY_SANDBOX', ollama_url: 'OLLAMA_URL', public_base_url: 'PUBLIC_BASE_URL', ai_model_photo_local_url: 'LOCAL_IMAGE_URL', ai_model_photo_local_workflow_path: 'LOCAL_IMAGE_WORKFLOW_PATH', ai_model_photo_local_install_path: 'LOCAL_IMAGE_INSTALL_PATH', }); const DB_FIELDS = new Set([ 'default_platform','default_ai','default_language','seller_notes','ai_model_photos_enabled','ai_model_photo_provider', 'ai_model_photo_default_mode','ai_model_photo_default_variants','ai_model_photo_default_preset','ai_model_photo_openrouter_free_only', 'ai_model_photo_block_underwear','ai_model_photo_block_kids','ai_model_photo_preserve_logos','ai_model_photo_openrouter_model', 'ai_model_photo_openai_model','ai_model_photo_local_enabled','ai_model_photo_local_require_token','ai_model_photo_use_source_reference', 'ai_model_photo_reference_strength','ai_model_photo_local_resolution','flux_runtime_profile','flux_default_profile', 'flux_prompt_assistant','flux_default_style','flux_default_format','flux_default_variants','flux_seed_lock','flux_max_parallel_jobs', 'smtp_host','smtp_port','smtp_user','smtp_from', ]); const ALL_FIELDS = new Set([...Object.keys(SECRET_FIELDS), ...Object.keys(RUNTIME_FIELDS), ...DB_FIELDS]); function fail(message, status = 400, code = 'SETTINGS_INVALID') { throw new PlatformError(message, { status, code, expose: true }); } function cleanString(value, { max = 4096, multiline = false } = {}) { const result = String(value ?? '').trim(); if (result.length > max) fail('Einstellungswert ist zu lang.'); if (!multiline && /[\r\n\0]/.test(result)) fail('Einstellungswert enthält unzulässige Steuerzeichen.'); return result; } export function createSettingsService({ etsyApi, ebayApi }) { function decorate(settings) { const result = { ...settings }; result.has_anthropic_key = hasSecret('ANTHROPIC_API_KEY'); result.has_openai_key = hasSecret('OPENAI_API_KEY'); result.has_openrouter_key = hasSecret('OPENROUTER_API_KEY'); result.has_etsy_key = hasSecret('ETSY_API_KEY'); result.etsy_connected = Boolean(etsyApi?.isConnected?.()); result.has_ebay_credentials = Boolean(ebayApi?.hasCredentials?.()); result.ebay_connected = Boolean(ebayApi?.isConnected?.()); result.ebay_sandbox = process.env.EBAY_SANDBOX === 'true'; result.ollama_url = process.env.OLLAMA_URL || 'http://localhost:11434'; result.public_base_url = process.env.PUBLIC_BASE_URL || ''; result.ai_model_photo_local_url = process.env.LOCAL_IMAGE_URL || result.ai_model_photo_local_url || 'http://127.0.0.1:8188'; result.ai_model_photo_local_workflow_path = process.env.LOCAL_IMAGE_WORKFLOW_PATH || result.ai_model_photo_local_workflow_path || 'local-ai/workflows/vendoo_flux_schnell_api.json'; result.ai_model_photo_local_install_path = process.env.LOCAL_IMAGE_INSTALL_PATH || result.ai_model_photo_local_install_path || 'local-ai/comfyui'; result.ai_model_photo_local_enabled = result.ai_model_photo_local_enabled || '1'; result.ai_model_photo_local_require_token = result.ai_model_photo_local_require_token || '1'; result.ai_model_photo_use_source_reference = result.ai_model_photo_use_source_reference || '1'; result.ai_model_photo_reference_strength = result.ai_model_photo_reference_strength || '70'; result.ai_model_photo_local_resolution = result.ai_model_photo_local_resolution || '768'; result.ai_model_photo_preserve_logos = result.ai_model_photo_preserve_logos || '1'; result.flux_runtime_profile = result.flux_runtime_profile || 'auto'; result.flux_default_profile = result.flux_default_profile || 'balanced'; result.flux_prompt_assistant = result.flux_prompt_assistant || '1'; result.flux_default_style = result.flux_default_style || 'photorealistic'; result.flux_default_format = result.flux_default_format || '768x768'; result.flux_default_variants = result.flux_default_variants || '1'; result.flux_seed_lock = result.flux_seed_lock || '0'; result.flux_max_parallel_jobs = result.flux_max_parallel_jobs || '1'; result.local_image_token_set = hasSecret('LOCAL_IMAGE_TOKEN'); return result; } function validatePatch(input) { const source = input && typeof input === 'object' && !Array.isArray(input) ? input : {}; for (const key of Object.keys(source)) if (!ALL_FIELDS.has(key)) fail(`Unbekanntes Einstellungsfeld: ${key}`); return source; } function smtpStatus() { const settings = readApplicationSettings(); return { smtp_host: settings.smtp_host || process.env.SMTP_HOST || '', smtp_port: settings.smtp_port || process.env.SMTP_PORT || '587', smtp_user: settings.smtp_user || process.env.SMTP_USER || '', smtp_pass: hasSecret('SMTP_PASS') ? '••••••••' : '', smtp_from: settings.smtp_from || process.env.SMTP_FROM || '', configured: isMailerConfigured(), source: hasSecret('SMTP_PASS') ? 'encrypted-secret-store' : (process.env.SMTP_HOST ? 'environment' : 'none'), }; } return Object.freeze({ get() { return decorate(readApplicationSettings()); }, update(input) { const source = validatePatch(input); let envContent = readRuntimeConfig(); const dbPatch = {}; for (const [field, secretName] of Object.entries(SECRET_FIELDS)) { if (source[field] !== undefined && source[field] !== '') setSecret(secretName, cleanString(source[field], { max: 16384 }), { source: 'settings-ui' }); } for (const [field, envName] of Object.entries(RUNTIME_FIELDS)) { if (source[field] === undefined) continue; let value = cleanString(source[field], { max: 4096 }); if (['public_base_url','ai_model_photo_local_url'].includes(field)) value = value.replace(/\/$/, ''); envContent = setConfigValue(envContent, envName, value); process.env[envName] = value; } for (const field of DB_FIELDS) { if (source[field] === undefined) continue; dbPatch[field] = cleanString(source[field], { max: field === 'seller_notes' ? 16000 : 4096, multiline: field === 'seller_notes' }); } if (dbPatch.ai_model_photo_local_enabled !== undefined) { const value = String(dbPatch.ai_model_photo_local_enabled) === '0' ? 'false' : 'true'; envContent = setConfigValue(envContent, 'LOCAL_IMAGE_ENABLED', value); process.env.LOCAL_IMAGE_ENABLED = value; } if (dbPatch.ai_model_photo_local_require_token !== undefined) { const value = String(dbPatch.ai_model_photo_local_require_token) === '0' ? 'false' : 'true'; envContent = setConfigValue(envContent, 'LOCAL_IMAGE_REQUIRE_TOKEN', value); process.env.LOCAL_IMAGE_REQUIRE_TOKEN = value; } writeRuntimeConfig(envContent); return decorate(writeApplicationSettings(dbPatch)); }, smtpStatus, updateSmtp(input) { const patch = {}; for (const field of ['smtp_host','smtp_port','smtp_user','smtp_from']) if (input[field] !== undefined) patch[field] = cleanString(input[field], { max: 512 }); if (input.smtp_pass !== undefined && input.smtp_pass !== '••••••••' && input.smtp_pass !== '') setSecret('SMTP_PASS', cleanString(input.smtp_pass, { max: 4096 }), { source: 'smtp-ui' }); patch.smtp_pass = ''; const settings = writeApplicationSettings(patch); const smtpSecret = getSecret('SMTP_PASS'); if (settings.smtp_host && settings.smtp_user && smtpSecret) { reinitMailer({ host: settings.smtp_host, port: settings.smtp_port || '587', user: settings.smtp_user, pass: smtpSecret }); if (settings.smtp_from) process.env.SMTP_FROM = settings.smtp_from; } return smtpStatus(); }, health() { return { ok: true, settings: Object.keys(readApplicationSettings()).length, smtp_configured: isMailerConfigured() }; }, }); }