fix: conditionally include secrets in user data backup (#5360) * fix: conditionally include secrets in user data backup * feat: add full data backup toggle * 418 -> 403 I'm not a teapot * Distinguish fails from disabled

c78f978ede3b94351f70ed8c6df40a89073b53a0

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

Signed
7 files changed, +54 -5Ignore whitespace
default/config.yaml+2 -0
@@ -165,6 +165,8 @@ rateLimiting:
165165
166## BACKUP CONFIGURATION166## BACKUP CONFIGURATION
167backups:167backups:
168 # Allow users to create a full backup archive of their data
169 allowFullDataBackup: true
168 # Common settings for all backup types170 # Common settings for all backup types
169 common:171 common:
170 # Number of backups to keep for each chat and settings file172 # Number of backups to keep for each chat and settings file
index.d.ts+1 -1
@@ -40,7 +40,7 @@ declare global {
40 /**40 /**
41 * Authenticated user handle.41 * Authenticated user handle.
42 */42 */
43 handle: string;43 handle: string | null;
44 /**44 /**
45 * Last time the session was extended.45 * Last time the session was extended.
46 */46 */
public/scripts/secrets.js+23 -0
@@ -277,6 +277,29 @@ function getActiveSecretLabel(key) {
277 return '';277 return '';
278}278}
279279
280/**
281 * Checks if secrets can be viewed based on server configuration.
282 * @returns {Promise<boolean|null>} A boolean value, or null if the request fails.
283 */
284export async function canViewSecrets() {
285 try {
286 const response = await fetch('/api/secrets/settings', {
287 method: 'POST',
288 headers: getRequestHeaders({ omitContentType: true }),
289 });
290
291 if (!response.ok) {
292 return null;
293 }
294
295 const data = await response.json();
296 return data?.allowKeysExposure === true;
297 } catch (error) {
298 console.error('Error getting secrets settings:', error);
299 return null;
300 }
301}
302
280async function viewSecrets() {303async function viewSecrets() {
281 const response = await fetch('/api/secrets/view', {304 const response = await fetch('/api/secrets/view', {
282 method: 'POST',305 method: 'POST',
public/scripts/user.js+6 -0
@@ -1,5 +1,6 @@
1import { getRequestHeaders } from '../script.js';1import { getRequestHeaders } from '../script.js';
2import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';2import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
3import { canViewSecrets } from './secrets.js';
3import { renderTemplateAsync } from './templates.js';4import { renderTemplateAsync } from './templates.js';
4import { ensureImageFormatSupported, getBase64Async, humanFileSize } from './utils.js';5import { ensureImageFormatSupported, getBase64Async, humanFileSize } from './utils.js';
56
@@ -266,6 +267,11 @@ async function backupUserData(handle, callback) {
266 throw new Error('Failed to backup user data');267 throw new Error('Failed to backup user data');
267 }268 }
268269
270 const includesSecrets = await canViewSecrets();
271 if (includesSecrets === false) {
272 toastr.warning('The backup will not include secrets due to a server configuration.', 'Secrets Not Included');
273 }
274
269 const blob = await response.blob();275 const blob = await response.blob();
270 const header = response.headers.get('Content-Disposition');276 const header = response.headers.get('Content-Disposition');
271 const parts = header.split(';');277 const parts = header.split(';');
src/endpoints/secrets.js+5 -1
@@ -104,7 +104,7 @@ const EXPORTABLE_KEYS = [
104 SECRET_KEYS.DEEPLX_URL,104 SECRET_KEYS.DEEPLX_URL,
105];105];
106106
107const allowKeysExposure = !!getConfigValue('allowKeysExposure', false, 'boolean');107export const allowKeysExposure = !!getConfigValue('allowKeysExposure', false, 'boolean');
108108
109/**109/**
110 * SecretManager class to handle all secret operations110 * SecretManager class to handle all secret operations
@@ -636,3 +636,7 @@ router.post('/rename', (request, response) => {
636 return response.sendStatus(500);636 return response.sendStatus(500);
637 }637 }
638});638});
639
640router.post('/settings', async (_request, response) => {
641 return response.send({ allowKeysExposure });
642});
src/endpoints/users-private.js+8 -1
@@ -8,7 +8,7 @@ import express from 'express';
8import { getUserAvatar, toKey, getPasswordHash, getPasswordSalt, createBackupArchive, ensurePublicDirectoriesExist, toAvatarKey } from '../users.js';8import { getUserAvatar, toKey, getPasswordHash, getPasswordSalt, createBackupArchive, ensurePublicDirectoriesExist, toAvatarKey } from '../users.js';
9import { SETTINGS_FILE } from '../constants.js';9import { SETTINGS_FILE } from '../constants.js';
10import { checkForNewContent, CONTENT_TYPES } from './content-manager.js';10import { checkForNewContent, CONTENT_TYPES } from './content-manager.js';
11import { color, Cache } from '../util.js';11import { color, Cache, getConfigValue } from '../util.js';
1212
13const RESET_CACHE = new Cache(5 * 60 * 1000);13const RESET_CACHE = new Cache(5 * 60 * 1000);
1414
@@ -138,6 +138,13 @@ router.post('/change-password', async (request, response) => {
138138
139router.post('/backup', async (request, response) => {139router.post('/backup', async (request, response) => {
140 try {140 try {
141 const allowFullDataBackup = !!getConfigValue('backups.allowFullDataBackup', true, 'boolean');
142
143 if (!allowFullDataBackup) {
144 console.warn('Backup failed: Full data backup is disabled in configuration');
145 return response.status(403).json({ error: 'Full data backup is disabled' });
146 }
147
141 const handle = request.body.handle;148 const handle = request.body.handle;
142149
143 if (!handle) {150 if (!handle) {
src/users.js+9 -2
@@ -17,7 +17,7 @@ import sanitize from 'sanitize-filename';
1717
18import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE, UPLOADS_DIRECTORY } from './constants.js';18import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE, UPLOADS_DIRECTORY } from './constants.js';
19import { getConfigValue, color, delay, generateTimestamp, invalidateFirefoxCache, isPathUnderParent } from './util.js';19import { getConfigValue, color, delay, generateTimestamp, invalidateFirefoxCache, isPathUnderParent } from './util.js';
20import { readSecret, writeSecret } from './endpoints/secrets.js';20import { allowKeysExposure, readSecret, writeSecret, SECRETS_FILE } from './endpoints/secrets.js';
21import { getContentOfType } from './endpoints/content-manager.js';21import { getContentOfType } from './endpoints/content-manager.js';
22import { serverDirectory } from './server-directory.js';22import { serverDirectory } from './server-directory.js';
2323
@@ -1052,7 +1052,14 @@ export async function createBackupArchive(handle, response) {
1052 archive.pipe(response);1052 archive.pipe(response);
10531053
1054 // Append files from a sub-directory, putting its contents at the root of archive1054 // Append files from a sub-directory, putting its contents at the root of archive
1055 archive.directory(directories.root, false);1055 const ignore = allowKeysExposure ? [] : [SECRETS_FILE];
1056 archive.glob('**/*', {
1057 cwd: directories.root,
1058 follow: false,
1059 stat: true,
1060 dot: true,
1061 ignore,
1062 });
1056 archive.finalize();1063 archive.finalize();
1057}1064}
10581065