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
Signed| @@ -2,7 +2,7 @@ import { DOMPurify, moment } from '../lib.js'; | ||
| 2 | 2 | import { event_types, eventSource, getRequestHeaders } from '../script.js'; |
| 3 | 3 | import { t } from './i18n.js'; |
| 4 | 4 | import { chat_completion_sources } from './openai.js'; |
| 5 | 5 | import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js'; |
| 6 | 6 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| 7 | 7 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; |
| 8 | 8 | import { enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; |
| @@ -293,11 +293,13 @@ export let secret_state = {}; | ||
| 293 | 293 | * @param {string} key Secret key |
| 294 | 294 | * @param {string} value Secret value to write |
| 295 | 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 | 298 | * @return {Promise<string?>} The ID of the newly created secret key, or null if no value is provided. |
| 297 | 299 | */ |
| 298 | 300 | export async function writeSecret(key, value, label, { allowEmpty } = {}) { |
| 299 | 301 | try { |
| 300 | 302 | if (!value && !allowEmpty) { |
| 301 | 303 | console.warn(`No value provided for ${key} in writeSecret, redirecting to deleteSecret`); |
| 302 | 304 | await deleteSecret(key); |
| 303 | 305 | return null; |
| @@ -558,7 +560,8 @@ async function openKeyManagerDialog(key) { | ||
| 558 | 560 | const template = $(await renderTemplateAsync('secretKeyManager', { name, key })); |
| 559 | 561 | template.find('button[data-action="add-secret"]').on('click', async function () { |
| 560 | 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 | 565 | customInputs: [{ |
| 563 | 566 | id: 'newSecretLabel', |
| 564 | 567 | type: 'text', |
| @@ -567,13 +570,20 @@ async function openKeyManagerDialog(key) { | ||
| 567 | 570 | onClose: popup => { |
| 568 | 571 | if (popup.result) { |
| 569 | 572 | label = popup.inputResults.get('newSecretLabel').toString().trim(); |
| 573 | + result = popup.result; | |
| 570 | 574 | } |
| 571 | 575 | }, |
| 572 | 576 | }); |
| 573 | 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 | 586 | await writeSecret(key, value, label, { allowEmpty: true }); |
| 577 | 587 | await renderSecretsList(); |
| 578 | 588 | }); |
| 579 | 589 | |
| @@ -821,6 +831,13 @@ function registerSecretSlashCommands() { | ||
| 821 | 831 | isRequired: false, |
| 822 | 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 | 842 | unnamedArgumentList: [ |
| 826 | 843 | SlashCommandArgument.fromProps({ |
| @@ -831,6 +848,7 @@ function registerSecretSlashCommands() { | ||
| 831 | 848 | ], |
| 832 | 849 | callback: async (args, value) => { |
| 833 | 850 | const quiet = isTrueBoolean(args?.quiet?.toString()); |
| 851 | + const allowEmpty = isTrueBoolean(args?.empty?.toString()); | |
| 834 | 852 | const key = args?.key?.toString()?.trim() || resolveSecretKey(); |
| 835 | 853 | |
| 836 | 854 | if (!key) { |
| @@ -849,7 +867,7 @@ function registerSecretSlashCommands() { | ||
| 849 | 867 | } |
| 850 | 868 | |
| 851 | 869 | const valueStr = value?.toString()?.trim(); |
| 852 | 870 | if (!valueStr && !allowEmpty) { |
| 853 | 871 | if (!quiet) { |
| 854 | 872 | toastr.error(t`No value provided for the secret key: ${key}`); |
| 855 | 873 | } |
| @@ -857,7 +875,7 @@ function registerSecretSlashCommands() { | ||
| 857 | 875 | } |
| 858 | 876 | |
| 859 | 877 | const label = args?.label?.toString()?.trim() || getLabel(); |
| 860 | 878 | const id = await writeSecret(key, valueStr, label, { allowEmpty }); |
| 861 | 879 | |
| 862 | 880 | if (!quiet) { |
| 863 | 881 | toastr.success(t`Secret has been written for the key: ${key}`); |
| @@ -561,12 +561,13 @@ router.post('/find', (request, response) => { | ||
| 561 | 561 | } |
| 562 | 562 | |
| 563 | 563 | const manager = new SecretManager(request.user.directories); |
| 564 | 564 | const secretValuestate = manager.readSecretgetSecretState(key, id); |
| 565 | 565 | |
| 566 | 566 | if (!secretValuestate[key]) { |
| 567 | 567 | return response.sendStatus(404); |
| 568 | 568 | } |
| 569 | 569 | |
| 570 | + const secretValue = manager.readSecret(key, id); | |
| 570 | 571 | return response.send({ value: secretValue }); |
| 571 | 572 | } catch (error) { |
| 572 | 573 | console.error('Error finding secret:', error); |