Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 52 additions & 6 deletions api/controllers/settings.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,44 @@
// settings.js

const Settings = require('../models/Settings');

module.exports = {
updateSettings: updateSettings,
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
.status(400)
.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) {
Expand All @@ -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];
}
Expand All @@ -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',
Expand All @@ -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));
}
51 changes: 45 additions & 6 deletions api/models/Settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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.
}
}
};
Expand All @@ -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) {
Expand All @@ -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;
Expand All @@ -60,4 +99,4 @@ settingsSchema.statics = {

const Settings = mongoose.model('Settings', settingsSchema);

module.exports = Settings;
module.exports = Settings;