From 757fad137fe74292dd2d40760df132dab69cd206 Mon Sep 17 00:00:00 2001 From: vikas Date: Thu, 27 Nov 2025 19:27:15 +0530 Subject: [PATCH] feat: Add ElevenLabs API key support to Settings model (#422) --- api/controllers/settings.js | 58 +++++++++++++++++++++++++++++++++---- api/models/Settings.js | 51 ++++++++++++++++++++++++++++---- 2 files changed, 97 insertions(+), 12 deletions(-) diff --git a/api/controllers/settings.js b/api/controllers/settings.js index 949973b6..1e8c2aa0 100644 --- a/api/controllers/settings.js +++ b/api/controllers/settings.js @@ -1,3 +1,5 @@ +// settings.js + const Settings = require('../models/Settings'); module.exports = { @@ -5,6 +7,28 @@ module.exports = { getSettings: getSettings }; +// --- NEW HELPER FUNCTION FOR SECURITY --- +// This ensures the secret key is never sent to the frontend. +function getSafeSettingsResponse(settings) { + // Convert to a plain object to ensure access to all fields + const response = settings.toJSON ? settings.toJSON() : settings; + + // The key is present here because we forced Mongoose to select it. + const elevenlabsKey = response.elevenlabsApiKey; + + return { + ...response, + // 1. Explicitly remove the key from the response object + elevenlabsApiKey: undefined, + + // 2. Add safe flags/previews for the frontend + elevenlabsKeySet: !!elevenlabsKey, + elevenlabsKeyPreview: elevenlabsKey ? elevenlabsKey.slice(-4) : null + }; +} +// ---------------------------------------- + + async function updateSettings(req, res) { if (!req.user) { return res @@ -12,7 +36,9 @@ async function updateSettings(req, res) { .json({ message: 'Are you logged in? Is bearer token present?' }); } - const userSettings = await Settings.getOrCreate(req.user); + // NOTE: Assuming getOrCreate now accepts a second argument (true) + // to force Mongoose to select the secret elevenlabsApiKey field. + const userSettings = await Settings.getOrCreate(req.user, true); const { body } = req; if (body.user && body.user !== req.user.id) { @@ -23,6 +49,18 @@ async function updateSettings(req, res) { delete body.id; } + // --- START CHANGES FOR ELEVENLABS KEY --- + if (body.elevenlabsApiKey !== undefined) { + const trimmedKey = body.elevenlabsApiKey.trim(); + // Save empty strings/cleared fields as null in the database. + userSettings.elevenlabsApiKey = trimmedKey || null; + + // Remove from the body so the generic loop below doesn't overwrite it + delete body.elevenlabsApiKey; + } + // --- END CHANGES FOR ELEVENLABS KEY --- + + // This loop handles all other settings properties generically for (let key in body) { userSettings[key] = body[key]; } @@ -31,9 +69,15 @@ async function updateSettings(req, res) { const settings = await Settings.findByIdAndUpdate( userSettings.id, userSettings, - { new: true } + { + new: true, + // CRUCIAL: Force the key to be selected in the final response document + select: '+elevenlabsApiKey' + } ).exec(); - return res.status(200).json(settings.toJSON()); + + // --- USE THE SAFE RESPONSE HELPER --- + return res.status(200).json(getSafeSettingsResponse(settings)); } catch (err) { return res.status(409).json({ message: 'Error saving settings', @@ -49,7 +93,9 @@ async function getSettings(req, res) { .json({ message: 'Are you logged in? Is bearer token present?' }); } - const response = await Settings.getOrCreate(req.user); + // NOTE: Fetch the document, passing 'true' to ensure the secret key is selected. + const response = await Settings.getOrCreate(req.user, true); - return res.status(200).json(response); -} + // --- USE THE SAFE RESPONSE HELPER --- + return res.status(200).json(getSafeSettingsResponse(response)); +} \ No newline at end of file diff --git a/api/models/Settings.js b/api/models/Settings.js index fc35446f..0365aaae 100644 --- a/api/models/Settings.js +++ b/api/models/Settings.js @@ -7,6 +7,17 @@ const SETTINGS_SCHEMA_DEFINITION = { display: {}, scanning: {}, navigation: {}, + + // --- START CHANGES FOR ELEVENLABS API KEY --- + elevenlabsApiKey: { + type: String, + required: false, + trim: true, + // CRITICAL: Mongoose will hide this field in queries by default for security + select: false + }, + // --- END CHANGES FOR ELEVENLABS API KEY --- + user: { type: Schema.Types.ObjectId, ref: 'User', @@ -24,6 +35,9 @@ const SETTINGS_SCHEMA_OPTIONS = { transform: function(doc, ret) { ret.id = ret._id; delete ret._id; + + // We do not need to explicitly delete elevenlabsApiKey here if it's set + // with `select: false` in the schema definition, but this is fine. } } }; @@ -34,11 +48,23 @@ const settingsSchema = new Schema( ); settingsSchema.statics = { - getOrCreate: async function(user) { + // --- MODIFIED getOrCreate STATIC METHOD --- + // Added optional 'includeSecrets' flag to force selection of secret fields (like API keys) + getOrCreate: async function(user, includeSecrets = false) { + let settingsQuery = this.findOne({ user: user.id }); + + // Check if we need to include secret fields (used by the settings.js controller) + if (includeSecrets) { + // Force selection of the secret key before executing the query + settingsQuery = settingsQuery.select('+elevenlabsApiKey'); + } + let settings = null; try { - settings = await Settings.findOne({ user: user.id }).exec(); - } catch (e) {} + settings = await settingsQuery.exec(); + } catch (e) { + // Handle error finding settings + } // No settings yet? We need to create them if (!settings) { @@ -47,11 +73,24 @@ settingsSchema.statics = { try { settings = await settings.save().exec(); - } catch (e) {} + } catch (e) { + // Handle error saving new settings + } + + // If the settings were just created, and we need the secrets, + // we must run a new find query to ensure the secrets are attached. + if (settings && includeSecrets) { + settings = await this.findOne({ user: user.id }).select('+elevenlabsApiKey').exec(); + } } if (settings) { - settings = settings.toJSON(); + // The settings.js controller will handle sanitization via getSafeSettingsResponse, + // so we should only call toJSON() if the controller doesn't need the secret + // fields (which is unlikely if includeSecrets is true). + // Given the previous code calls toJSON, we keep it here for now, but + // the controller will be responsible for the final safety check. + settings = settings.toJSON(); } return settings; @@ -60,4 +99,4 @@ settingsSchema.statics = { const Settings = mongoose.model('Settings', settingsSchema); -module.exports = Settings; +module.exports = Settings; \ No newline at end of file