Files
duplicati/Duplicati/GUI/Duplicati.GUI.TrayIcon/PasswordPrompt.cs
T
Kenneth Skovhede cf42426d1b Store password with TrayIcon
When using the TrayIcon with no hosted server (i.e., connecting to a running server), it is now possible to save the password and url in the secret provider.

There is a checkbox that allows saving the settings, and if checked, will save the settings to the secret provider (using the OS default if none is specifically provided).

This fixes #6379
2025-12-07 22:26:37 +01:00

237 lines
7.4 KiB
C#

// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
#nullable enable
using System;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Threading;
using Duplicati.Library.Utility;
namespace Duplicati.GUI.TrayIcon;
internal static class PasswordPrompt
{
public static bool IsShowingDialog { get; private set; } = false;
/// <summary>
/// Shows a password prompt dialog within an already-running Avalonia application.
/// Must be called from the UI thread or will dispatch to it.
/// </summary>
/// <param name="hostUrl">The host URL to show in the prompt</param>
/// <param name="password">The current password to pre-fill, if any</param
/// <param name="isChangePassword">Whether this is a change password prompt</param>
/// <returns>A task that completes with the password, or null if cancelled</returns>
public static Task<bool> ShowPasswordDialogAsync(bool isChangePassword)
{
if (IsShowingDialog)
throw new InvalidOperationException("A password prompt dialog is already showing.");
IsShowingDialog = true;
var tcs = new TaskCompletionSource<bool>();
if (Dispatcher.UIThread.CheckAccess())
{
ShowDialogInternal(tcs, isChangePassword);
}
else
{
Dispatcher.UIThread.Post(() => ShowDialogInternal(tcs, isChangePassword));
}
return tcs.Task.ContinueWith(t =>
{
IsShowingDialog = false;
return t.Result;
});
}
private static void ShowDialogInternal(TaskCompletionSource<bool> tcs, bool isChangePassword)
{
try
{
var window = new PasswordPromptWindow(isChangePassword);
window.PasswordSubmitted += (_, pwd) =>
{
tcs.TrySetResult(true);
window.Close();
};
window.Cancelled += (_, _) =>
{
tcs.TrySetResult(false);
window.Close();
};
window.Closed += (_, _) =>
{
// Ensure the task completes even if window is closed another way
tcs.TrySetResult(false);
};
window.Show();
}
catch (Exception ex)
{
Console.Error.WriteLine($"Failed to show password prompt window: {ex.Message}");
tcs.TrySetResult(false);
}
}
}
internal class PasswordPromptWindow : Window
{
private readonly TextBox passwordBox;
private readonly TextBox hostUrlBox;
private readonly CheckBox saveConfigurationCheckBox;
public event EventHandler<string?>? PasswordSubmitted;
public event EventHandler? Cancelled;
public PasswordPromptWindow(bool isChangePassword)
{
Title = "Duplicati TrayIcon Connect";
Width = 420;
Height = 210;
CanResize = false;
WindowStartupLocation = WindowStartupLocation.CenterScreen;
var description = new TextBlock
{
Text = isChangePassword
? "Enter the new Duplicati server password:"
: "Enter the Duplicati server password to connect:",
Margin = new Thickness(0, 0, 0, 10),
TextWrapping = TextWrapping.Wrap
};
passwordBox = new TextBox
{
Watermark = "Server password",
PasswordChar = '•',
Text = Program.PasswordStorage.Password ?? string.Empty,
HorizontalAlignment = HorizontalAlignment.Stretch,
Margin = new Thickness(0, 0, 0, 10)
};
var hostUrlLabel = new TextBlock
{
Text = "Duplicati server URL:",
Margin = new Thickness(0, 5, 0, 0),
TextWrapping = TextWrapping.Wrap
};
hostUrlBox = new TextBox
{
Text = Program.PasswordStorage.HostUrl ?? string.Empty,
Watermark = "http://localhost:8200",
HorizontalAlignment = HorizontalAlignment.Stretch,
Margin = new Thickness(0, 0, 0, 10)
};
saveConfigurationCheckBox = new CheckBox
{
Content = "Save configuration",
HorizontalAlignment = HorizontalAlignment.Left,
Margin = new Thickness(0, 0, 0, 10),
IsChecked = Program.PasswordStorage.ShouldSavePassword,
IsVisible = Program.PasswordStorage.IsSetSupported == true,
};
var okButton = new Button
{
Content = "Connect",
IsDefault = true,
Width = 100
};
okButton.Click += (_, _) => SubmitPassword();
var cancelButton = new Button
{
Content = "Cancel",
IsCancel = true,
Width = 100
};
cancelButton.Click += (_, _) => Cancel();
var buttonPanel = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right,
Spacing = 10
};
buttonPanel.Children.Add(cancelButton);
buttonPanel.Children.Add(okButton);
var contentPanel = new Grid
{
RowDefinitions = new RowDefinitions("*,Auto")
};
var inputPanel = new StackPanel
{
Children =
{
description,
passwordBox,
hostUrlLabel,
hostUrlBox,
saveConfigurationCheckBox
}
};
Grid.SetRow(inputPanel, 0);
Grid.SetRow(buttonPanel, 1);
contentPanel.Children.Add(inputPanel);
contentPanel.Children.Add(buttonPanel);
Content = new Border
{
Padding = new Thickness(20),
Child = contentPanel
};
Opened += (_, _) => passwordBox.Focus();
}
private void SubmitPassword()
{
var password = passwordBox.Text;
var hostUrlText = hostUrlBox.Text?.Trim();
Program.PasswordStorage.UpdatePasswordAsync(hostUrlText, password, saveConfigurationCheckBox.IsChecked ?? false, CancellationToken.None)
.Await();
PasswordSubmitted?.Invoke(this, password);
}
private void Cancel()
{
Cancelled?.Invoke(this, EventArgs.Empty);
}
}