Allow saving empty secrets via secret manager and slash command (#4580) * Allow saving empty secrets via secret manager and slash command Closes #4577 * Fix logic on cancel * Fix /secrets-read with empty secret value

1d279e500e99ffd6fab955a780433c474f3a1ec8

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
2 files changed, +29 -10Ignore whitespace
public/scripts/secrets.js+26 -8
@@ -2,7 +2,7 @@ import { DOMPurify, moment } from '../lib.js';
2import { event_types, eventSource, getRequestHeaders } from '../script.js';2import { event_types, eventSource, getRequestHeaders } from '../script.js';
3import { t } from './i18n.js';3import { t } from './i18n.js';
4import { chat_completion_sources } from './openai.js';4import { chat_completion_sources } from './openai.js';
5import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js';5import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
6import { SlashCommand } from './slash-commands/SlashCommand.js';6import { SlashCommand } from './slash-commands/SlashCommand.js';
7import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';7import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
8import { enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';8import { enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
@@ -293,11 +293,13 @@ export let secret_state = {};
293 * @param {string} key Secret key293 * @param {string} key Secret key
294 * @param {string} value Secret value to write294 * @param {string} value Secret value to write
295 * @param {string} [label] (Optional) Label for the key. If not provided, generated automatically.295 * @param {string} [label] (Optional) Label for the key. If not provided, generated automatically.
296 * @param {Object} [options] Additional options
297 * @param {boolean} [options.allowEmpty] Whether to allow writing empty values. If false and value is empty, the secret will be deleted.
296 * @return {Promise<string?>} The ID of the newly created secret key, or null if no value is provided.298 * @return {Promise<string?>} The ID of the newly created secret key, or null if no value is provided.
297 */299 */
298export async function writeSecret(key, value, label) {300export async function writeSecret(key, value, label, { allowEmpty } = {}) {
299 try {301 try {
300 if (!value) {302 if (!value && !allowEmpty) {
301 console.warn(`No value provided for ${key} in writeSecret, redirecting to deleteSecret`);303 console.warn(`No value provided for ${key} in writeSecret, redirecting to deleteSecret`);
302 await deleteSecret(key);304 await deleteSecret(key);
303 return null;305 return null;
@@ -558,7 +560,8 @@ async function openKeyManagerDialog(key) {
558 const template = $(await renderTemplateAsync('secretKeyManager', { name, key }));560 const template = $(await renderTemplateAsync('secretKeyManager', { name, key }));
559 template.find('button[data-action="add-secret"]').on('click', async function () {561 template.find('button[data-action="add-secret"]').on('click', async function () {
560 let label = '';562 let label = '';
561 const value = await Popup.show.input(t`Add Secret`, t`Enter the secret value:`, '', {563 let result = POPUP_RESULT.CANCELLED;
564 const value = await Popup.show.input(t`Add Secret`, t`Enter the secret value (can be empty):`, '', {
562 customInputs: [{565 customInputs: [{
563 id: 'newSecretLabel',566 id: 'newSecretLabel',
564 type: 'text',567 type: 'text',
@@ -567,13 +570,20 @@ async function openKeyManagerDialog(key) {
567 onClose: popup => {570 onClose: popup => {
568 if (popup.result) {571 if (popup.result) {
569 label = popup.inputResults.get('newSecretLabel').toString().trim();572 label = popup.inputResults.get('newSecretLabel').toString().trim();
573 result = popup.result;
570 }574 }
571 },575 },
572 });576 });
573 if (!value) {577 if (!value) {
574 return;578 if (result !== POPUP_RESULT.AFFIRMATIVE) {
579 return;
580 }
581 const allowEmpty = await Popup.show.confirm(t`No value entered`, t`No value was entered for the secret. Do you want to add an empty secret?`);
582 if (!allowEmpty) {
583 return;
584 }
575 }585 }
576 await writeSecret(key, value, label);586 await writeSecret(key, value, label, { allowEmpty: true });
577 await renderSecretsList();587 await renderSecretsList();
578 });588 });
579589
@@ -821,6 +831,13 @@ function registerSecretSlashCommands() {
821 isRequired: false,831 isRequired: false,
822 typeList: [ARGUMENT_TYPE.STRING],832 typeList: [ARGUMENT_TYPE.STRING],
823 }),833 }),
834 SlashCommandNamedArgument.fromProps({
835 name: 'empty',
836 description: t`Whether to allow empty values.`,
837 isRequired: false,
838 typeList: [ARGUMENT_TYPE.BOOLEAN],
839 defaultValue: String(false),
840 }),
824 ],841 ],
825 unnamedArgumentList: [842 unnamedArgumentList: [
826 SlashCommandArgument.fromProps({843 SlashCommandArgument.fromProps({
@@ -831,6 +848,7 @@ function registerSecretSlashCommands() {
831 ],848 ],
832 callback: async (args, value) => {849 callback: async (args, value) => {
833 const quiet = isTrueBoolean(args?.quiet?.toString());850 const quiet = isTrueBoolean(args?.quiet?.toString());
851 const allowEmpty = isTrueBoolean(args?.empty?.toString());
834 const key = args?.key?.toString()?.trim() || resolveSecretKey();852 const key = args?.key?.toString()?.trim() || resolveSecretKey();
835853
836 if (!key) {854 if (!key) {
@@ -849,7 +867,7 @@ function registerSecretSlashCommands() {
849 }867 }
850868
851 const valueStr = value?.toString()?.trim();869 const valueStr = value?.toString()?.trim();
852 if (!valueStr) {870 if (!valueStr && !allowEmpty) {
853 if (!quiet) {871 if (!quiet) {
854 toastr.error(t`No value provided for the secret key: ${key}`);872 toastr.error(t`No value provided for the secret key: ${key}`);
855 }873 }
@@ -857,7 +875,7 @@ function registerSecretSlashCommands() {
857 }875 }
858876
859 const label = args?.label?.toString()?.trim() || getLabel();877 const label = args?.label?.toString()?.trim() || getLabel();
860 const id = await writeSecret(key, valueStr, label);878 const id = await writeSecret(key, valueStr, label, { allowEmpty });
861879
862 if (!quiet) {880 if (!quiet) {
863 toastr.success(t`Secret has been written for the key: ${key}`);881 toastr.success(t`Secret has been written for the key: ${key}`);
src/endpoints/secrets.js+3 -2
@@ -561,12 +561,13 @@ router.post('/find', (request, response) => {
561 }561 }
562562
563 const manager = new SecretManager(request.user.directories);563 const manager = new SecretManager(request.user.directories);
564 const secretValue = manager.readSecret(key, id);564 const state = manager.getSecretState();
565565
566 if (!secretValue) {566 if (!state[key]) {
567 return response.sendStatus(404);567 return response.sendStatus(404);
568 }568 }
569569
570 const secretValue = manager.readSecret(key, id);
570 return response.send({ value: secretValue });571 return response.send({ value: secretValue });
571 } catch (error) {572 } catch (error) {
572 console.error('Error finding secret:', error);573 console.error('Error finding secret:', error);