Fix config backup/restore failures (related to #646)

Addresses two issues preventing configuration backup/restore:

1. Export passphrase validation mismatch: UI only validated 12+ char
   requirement when using custom passphrase, but backend always enforced
   it. Users with shorter login passwords saw unexplained failures.
   - Frontend now validates all passphrases meet 12-char minimum
   - Clear error message suggests custom passphrase if login password too short

2. Import data parsing failed silently: Frontend sent `exportData.data`
   which was undefined for legacy/CLI backups (raw base64 strings).
   Backend rejected these with no logs.
   - Frontend now handles both formats: {status, data} and raw strings
   - Backend logs validation failures for easier troubleshooting

Related to #646 where user reported "error after entering password" with
no container logs. These changes ensure proper validation feedback and
make the backup system resilient to different export formats.
This commit is contained in:
rcourtman 2025-11-06 17:53:54 +00:00
parent b50dba577f
commit 7ed9203e4b
2 changed files with 28 additions and 5 deletions

View file

@ -1902,10 +1902,14 @@ const Settings: Component<SettingsProps> = (props) => {
return;
}
// Just require a passphrase to be entered
const hasAuth = securityStatus()?.hasAuthentication;
if ((!hasAuth || useCustomPassphrase()) && !exportPassphrase()) {
showError('Please enter a passphrase');
// Backend requires at least 12 characters for encryption security
if (exportPassphrase().length < 12) {
const hasAuth = securityStatus()?.hasAuthentication;
showError(
hasAuth && !useCustomPassphrase()
? 'Your password must be at least 12 characters. Please use a custom passphrase instead.'
: 'Passphrase must be at least 12 characters long',
);
return;
}
@ -2029,6 +2033,21 @@ const Settings: Component<SettingsProps> = (props) => {
return;
}
// Support both formats:
// 1. New format: {status: "success", data: "base64string"}
// 2. Legacy/CLI format: raw base64 string or {data: "base64string"}
let encryptedData: string;
if (typeof exportData === 'string') {
// Raw base64 string from CLI export
encryptedData = exportData;
} else if (exportData.data) {
// Standard format with data field
encryptedData = exportData.data;
} else {
showError('Invalid backup file format. Expected encrypted data in "data" field.');
return;
}
// Get CSRF token from cookie
const csrfToken = document.cookie
.split('; ')
@ -2056,7 +2075,7 @@ const Settings: Component<SettingsProps> = (props) => {
credentials: 'include', // Include cookies for session auth
body: JSON.stringify({
passphrase: importPassphrase(),
data: exportData.data,
data: encryptedData,
}),
});

View file

@ -2910,12 +2910,14 @@ func (h *ConfigHandlers) HandleExportConfig(w http.ResponseWriter, r *http.Reque
}
if req.Passphrase == "" {
log.Warn().Msg("Export rejected: passphrase is required")
http.Error(w, "Passphrase is required", http.StatusBadRequest)
return
}
// Require strong passphrase (at least 12 characters)
if len(req.Passphrase) < 12 {
log.Warn().Int("length", len(req.Passphrase)).Msg("Export rejected: passphrase too short (minimum 12 characters)")
http.Error(w, "Passphrase must be at least 12 characters long", http.StatusBadRequest)
return
}
@ -2947,11 +2949,13 @@ func (h *ConfigHandlers) HandleImportConfig(w http.ResponseWriter, r *http.Reque
}
if req.Passphrase == "" {
log.Warn().Msg("Import rejected: passphrase is required")
http.Error(w, "Passphrase is required", http.StatusBadRequest)
return
}
if req.Data == "" {
log.Warn().Msg("Import rejected: encrypted data is required (ensure backup file has 'data' field)")
http.Error(w, "Import data is required", http.StatusBadRequest)
return
}