diff --git a/default/config.yaml b/default/config.yaml index f8c0f210d..817aebcd8 100644 --- a/default/config.yaml +++ b/default/config.yaml @@ -165,6 +165,8 @@ rateLimiting: ## BACKUP CONFIGURATION backups: + # Allow users to create a full backup archive of their data + allowFullDataBackup: true # Common settings for all backup types common: # Number of backups to keep for each chat and settings file diff --git a/index.d.ts b/index.d.ts index 712253bed..2e55437c1 100644 --- a/index.d.ts +++ b/index.d.ts @@ -40,7 +40,7 @@ declare global { /** * Authenticated user handle. */ - handle: string; + handle: string | null; /** * Last time the session was extended. */ diff --git a/public/scripts/secrets.js b/public/scripts/secrets.js index 03a056b6c..64846c216 100644 --- a/public/scripts/secrets.js +++ b/public/scripts/secrets.js @@ -277,6 +277,29 @@ function getActiveSecretLabel(key) { return ''; } +/** + * Checks if secrets can be viewed based on server configuration. + * @returns {Promise} A boolean value, or null if the request fails. + */ +export async function canViewSecrets() { + try { + const response = await fetch('/api/secrets/settings', { + method: 'POST', + headers: getRequestHeaders({ omitContentType: true }), + }); + + if (!response.ok) { + return null; + } + + const data = await response.json(); + return data?.allowKeysExposure === true; + } catch (error) { + console.error('Error getting secrets settings:', error); + return null; + } +} + async function viewSecrets() { const response = await fetch('/api/secrets/view', { method: 'POST', diff --git a/public/scripts/user.js b/public/scripts/user.js index 52650dd80..2ceaf4332 100644 --- a/public/scripts/user.js +++ b/public/scripts/user.js @@ -1,5 +1,6 @@ import { getRequestHeaders } from '../script.js'; import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js'; +import { canViewSecrets } from './secrets.js'; import { renderTemplateAsync } from './templates.js'; import { ensureImageFormatSupported, getBase64Async, humanFileSize } from './utils.js'; @@ -266,6 +267,11 @@ async function backupUserData(handle, callback) { throw new Error('Failed to backup user data'); } + const includesSecrets = await canViewSecrets(); + if (includesSecrets === false) { + toastr.warning('The backup will not include secrets due to a server configuration.', 'Secrets Not Included'); + } + const blob = await response.blob(); const header = response.headers.get('Content-Disposition'); const parts = header.split(';'); diff --git a/src/endpoints/secrets.js b/src/endpoints/secrets.js index 2f511759c..35e1c57bc 100644 --- a/src/endpoints/secrets.js +++ b/src/endpoints/secrets.js @@ -104,7 +104,7 @@ const EXPORTABLE_KEYS = [ SECRET_KEYS.DEEPLX_URL, ]; -const allowKeysExposure = !!getConfigValue('allowKeysExposure', false, 'boolean'); +export const allowKeysExposure = !!getConfigValue('allowKeysExposure', false, 'boolean'); /** * SecretManager class to handle all secret operations @@ -636,3 +636,7 @@ router.post('/rename', (request, response) => { return response.sendStatus(500); } }); + +router.post('/settings', async (_request, response) => { + return response.send({ allowKeysExposure }); +}); diff --git a/src/endpoints/users-private.js b/src/endpoints/users-private.js index 7aa3d4349..6c2ea9510 100644 --- a/src/endpoints/users-private.js +++ b/src/endpoints/users-private.js @@ -8,7 +8,7 @@ import express from 'express'; import { getUserAvatar, toKey, getPasswordHash, getPasswordSalt, createBackupArchive, ensurePublicDirectoriesExist, toAvatarKey } from '../users.js'; import { SETTINGS_FILE } from '../constants.js'; import { checkForNewContent, CONTENT_TYPES } from './content-manager.js'; -import { color, Cache } from '../util.js'; +import { color, Cache, getConfigValue } from '../util.js'; const RESET_CACHE = new Cache(5 * 60 * 1000); @@ -138,6 +138,13 @@ router.post('/change-password', async (request, response) => { router.post('/backup', async (request, response) => { try { + const allowFullDataBackup = !!getConfigValue('backups.allowFullDataBackup', true, 'boolean'); + + if (!allowFullDataBackup) { + console.warn('Backup failed: Full data backup is disabled in configuration'); + return response.status(403).json({ error: 'Full data backup is disabled' }); + } + const handle = request.body.handle; if (!handle) { diff --git a/src/users.js b/src/users.js index 466cb507f..10216b679 100644 --- a/src/users.js +++ b/src/users.js @@ -17,7 +17,7 @@ import sanitize from 'sanitize-filename'; import { USER_DIRECTORY_TEMPLATE, DEFAULT_USER, PUBLIC_DIRECTORIES, SETTINGS_FILE, UPLOADS_DIRECTORY } from './constants.js'; import { getConfigValue, color, delay, generateTimestamp, invalidateFirefoxCache, isPathUnderParent } from './util.js'; -import { readSecret, writeSecret } from './endpoints/secrets.js'; +import { allowKeysExposure, readSecret, writeSecret, SECRETS_FILE } from './endpoints/secrets.js'; import { getContentOfType } from './endpoints/content-manager.js'; import { serverDirectory } from './server-directory.js'; @@ -1052,7 +1052,14 @@ export async function createBackupArchive(handle, response) { archive.pipe(response); // Append files from a sub-directory, putting its contents at the root of archive - archive.directory(directories.root, false); + const ignore = allowKeysExposure ? [] : [SECRETS_FILE]; + archive.glob('**/*', { + cwd: directories.root, + follow: false, + stat: true, + dot: true, + ignore, + }); archive.finalize(); }