[feat] add random password refresh to the main window

This commit is contained in:
dijunkun
2026-09-22 10:33:28 +08:00
parent b610a0ec7e
commit 7e3e050edf
7 changed files with 135 additions and 74 deletions
@@ -3,6 +3,7 @@
#include <remote_action.h>
#include <SDL3/SDL.h>
#include <openssl/rand.h>
#include <slint.h>
#include <tinyfiledialogs.h>
@@ -65,6 +66,28 @@ namespace {
using namespace std::chrono_literals;
std::optional<std::string> GenerateRandomPassword(std::string_view current) {
constexpr std::string_view alphabet =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
constexpr auto unbiased_limit = 256 - 256 % alphabet.size();
std::string password;
do {
password.clear();
while (password.size() < 6) {
unsigned char value;
if (RAND_bytes(&value, sizeof(value)) != 1) {
return std::nullopt;
}
// Reject the incomplete bucket so every character is equally likely.
if (value < unbiased_limit) {
password.push_back(alphabet[value % alphabet.size()]);
}
}
} while (password == current ||
password.find_first_not_of("0123456789") == std::string::npos);
return password;
}
std::string CreatePasswordChangeRequestId(uint64_t sequence) {
const auto timestamp = std::chrono::system_clock::now()
.time_since_epoch()
@@ -1294,78 +1317,17 @@ void GuiApplication::BindMainCallbacks() {
ui_->main->set_password_visible(show_password_);
});
main->on_reset_password([this](slint::SharedString value) {
const std::string password(value);
if (password.size() != 6) {
return false;
}
if (!peer_ || !signal_connected_) {
offline_warning_text_ =
localization::signal_disconnected[localization_language_index_];
show_offline_warning_window_ = true;
return true;
}
std::string request_id;
{
std::lock_guard<std::mutex> lock(password_change_mutex_);
if (password_change_pending_) {
return true;
}
request_id =
CreatePasswordChangeRequestId(++next_password_change_request_id_);
password_change_pending_ = true;
password_change_result_ready_ = false;
password_change_succeeded_ = false;
password_change_result_uncertain_ = false;
password_change_requested_at_ = std::chrono::steady_clock::now();
pending_password_change_request_id_ = request_id;
pending_local_password_ = password;
password_change_error_.clear();
}
const bool self_hosted = config_center_->IsSelfHosted();
const std::string server_host =
self_hosted ? config_center_->GetSignalServerHost()
: config_center_->GetDefaultServerHost();
const int server_port =
self_hosted ? config_center_->GetSignalServerPort()
: config_center_->GetDefaultSignalServerPort();
const std::string pending_identity =
std::string(client_id_) + "@" + password;
if (!settings_.StagePendingPasswordChange(
pending_identity, self_hosted, server_host, server_port,
request_id)) {
std::lock_guard<std::mutex> lock(password_change_mutex_);
password_change_pending_ = false;
pending_password_change_request_id_.clear();
pending_local_password_.clear();
password_change_error_.clear();
return RequestPasswordChange(std::string(value));
});
main->on_refresh_password([this] {
const auto password = GenerateRandomPassword(password_saved_);
if (!password) {
LOG_ERROR("Could not generate a random password");
offline_warning_text_ = localization::failed[localization_language_index_];
show_offline_warning_window_ = true;
LOG_ERROR("Could not durably stage password change");
return true;
return;
}
const nlohmann::json request = {{"type", "change_password"},
{"request_id", request_id},
{"new_password", password}};
const std::string message = request.dump();
if (SendSignalMessage(peer_, message.data(), message.size()) != 0) {
{
std::lock_guard<std::mutex> lock(password_change_mutex_);
password_change_pending_ = false;
pending_password_change_request_id_.clear();
pending_local_password_.clear();
offline_warning_text_ =
localization::signal_disconnected[localization_language_index_];
show_offline_warning_window_ = true;
}
if (!settings_.ClearPendingPasswordChange()) {
LOG_WARN("Could not clear unsent pending password change");
}
}
return true;
RequestPasswordChange(*password);
});
main->on_connect_requested(
[this](slint::SharedString id) { ConnectFromUi(std::string(id)); });
@@ -2039,6 +2001,78 @@ void GuiApplication::ShareLocalCursorState() {
}
}
bool GuiApplication::RequestPasswordChange(const std::string &password) {
if (password.size() != 6) {
return false;
}
if (!peer_ || !signal_connected_) {
offline_warning_text_ =
localization::signal_disconnected[localization_language_index_];
show_offline_warning_window_ = true;
return true;
}
std::string request_id;
{
std::lock_guard<std::mutex> lock(password_change_mutex_);
if (password_change_pending_) {
return true;
}
request_id =
CreatePasswordChangeRequestId(++next_password_change_request_id_);
password_change_pending_ = true;
password_change_result_ready_ = false;
password_change_succeeded_ = false;
password_change_result_uncertain_ = false;
password_change_requested_at_ = std::chrono::steady_clock::now();
pending_password_change_request_id_ = request_id;
pending_local_password_ = password;
password_change_error_.clear();
}
const bool self_hosted = config_center_->IsSelfHosted();
const std::string server_host =
self_hosted ? config_center_->GetSignalServerHost()
: config_center_->GetDefaultServerHost();
const int server_port =
self_hosted ? config_center_->GetSignalServerPort()
: config_center_->GetDefaultSignalServerPort();
const std::string pending_identity = std::string(client_id_) + "@" + password;
if (!settings_.StagePendingPasswordChange(
pending_identity, self_hosted, server_host, server_port, request_id)) {
std::lock_guard<std::mutex> lock(password_change_mutex_);
password_change_pending_ = false;
pending_password_change_request_id_.clear();
pending_local_password_.clear();
password_change_error_.clear();
offline_warning_text_ = localization::failed[localization_language_index_];
show_offline_warning_window_ = true;
LOG_ERROR("Could not durably stage password change");
return true;
}
const nlohmann::json request = {{"type", "change_password"},
{"request_id", request_id},
{"new_password", password}};
const std::string message = request.dump();
if (SendSignalMessage(peer_, message.data(), message.size()) != 0) {
{
std::lock_guard<std::mutex> lock(password_change_mutex_);
password_change_pending_ = false;
pending_password_change_request_id_.clear();
pending_local_password_.clear();
offline_warning_text_ =
localization::signal_disconnected[localization_language_index_];
show_offline_warning_window_ = true;
}
if (!settings_.ClearPendingPasswordChange()) {
LOG_WARN("Could not clear unsent pending password change");
}
}
return true;
}
void GuiApplication::HandlePasswordChangeResult() {
bool succeeded = false;
bool uncertain = false;
@@ -2188,6 +2222,10 @@ void GuiApplication::SyncMainWindow() {
ui_->main->set_local_id(UiText(FormatPeerId(client_id_)));
ui_->main->set_local_password(password_saved_);
ui_->main->set_password_visible(show_password_);
{
std::lock_guard<std::mutex> lock(password_change_mutex_);
ui_->main->set_password_change_pending(password_change_pending_);
}
ui_->main->set_signal_connected(signal_connected_);
ui_->main->set_signal_tls_error(signal_status_ ==
SignalStatus::SignalTlsCertError);
@@ -49,6 +49,7 @@ private:
void BindServerCallbacks();
void Tick();
void ShareLocalCursorState();
bool RequestPasswordChange(const std::string &password);
void HandlePasswordChangeResult();
void HandleCredentialRecovery();
void SyncMainWindow();
@@ -100,4 +101,4 @@ private:
} // namespace crossdesk
#endif
#endif
@@ -31,6 +31,8 @@ struct TranslationRow {
X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \
u8"Скопировано в буфер обмена") \
X(password, u8"密码", "Password", u8"Пароль") \
X(refresh_password, u8"刷新随机密码", "Refresh random password", \
u8"Обновить случайный пароль") \
X(max_password_len, u8"最大6个字符", "Max 6 chars", u8"Макс. 6 символов") \
X(remote_desktop, u8"远程桌面", "Remote Desktop", \
u8"Удаленный рабочий стол") \
+4
View File
@@ -41,6 +41,7 @@ export global FontAwesomeIcons {
out property <string> keyboard: "\u{f11c}";
out property <string> minus: "\u{f068}";
out property <string> pen: "\u{f304}";
out property <string> rotate: "\u{f2f1}";
out property <string> signal: "\u{f012}";
out property <string> shield-halved: "\u{f3ed}";
out property <string> square-full: "\u{f45c}";
@@ -54,6 +55,7 @@ export global FontAwesomeIcons {
export component IconButton inherits Rectangle {
in property <string> icon;
in property <string> tooltip;
in property <bool> enabled: true;
in property <bool> danger: false;
in property <length> icon-size: 16px;
in property <color> icon-color: #24272d;
@@ -69,6 +71,7 @@ export component IconButton inherits Rectangle {
: touch.has-hover ? (danger ? #f04438 : root.hover-background)
: root.normal-background;
border-radius: 4px;
opacity: root.enabled ? 1 : 0.4;
Text {
text: root.icon;
@@ -90,6 +93,7 @@ export component IconButton inherits Rectangle {
}
touch := TouchArea {
enabled: root.enabled;
clicked => { root.clicked(); }
}
+18 -3
View File
@@ -24,6 +24,7 @@ export global UiStrings {
in-out property <string> online: "Online";
in-out property <string> offline: "Offline";
in-out property <string> password: "Password";
in-out property <string> refresh-password: "Refresh random password";
in-out property <string> copied: "Local ID copied to clipboard";
in-out property <string> connect: "Connect";
in-out property <string> settings: "Settings";
@@ -208,6 +209,7 @@ export component MainWindow inherits Window {
in property <string> local-password: "";
in-out property <string> remote-id-input: "";
in property <bool> password-visible: true;
in property <bool> password-change-pending: false;
in property <bool> signal-connected: false;
in property <bool> signal-tls-error: false;
in property <[RecentConnection]> recent-connections;
@@ -262,6 +264,7 @@ export component MainWindow inherits Window {
callback copy-local-id;
callback toggle-password-visibility;
callback refresh-password;
callback reset-password(string) -> bool;
callback format-remote-id(string) -> string;
callback connect-requested(string);
@@ -676,19 +679,31 @@ export component MainWindow inherits Window {
IconButton {
x: 174px;
y: 133px;
width: 29px;
width: 24px;
height: 30px;
icon: root.password-visible ? FontAwesomeIcons.eye : FontAwesomeIcons.eye-slash;
icon-size: 17px;
clicked => { root.toggle-password-visibility(); }
}
IconButton {
x: 214px;
x: 198px;
y: 133px;
width: 29px;
width: 24px;
height: 30px;
icon: FontAwesomeIcons.rotate;
icon-size: 15px;
tooltip: UiStrings.refresh-password;
enabled: !root.password-change-pending;
clicked => { root.refresh-password(); }
}
IconButton {
x: 222px;
y: 133px;
width: 24px;
height: 30px;
icon: FontAwesomeIcons.pen;
icon-size: 15px;
enabled: !root.password-change-pending;
clicked => { root.reset-password-open = true; }
}
}
@@ -36,6 +36,7 @@ inline int ApplyMainWindowStrings(
strings.set_online(Text(localization::online[language]));
strings.set_offline(Text(localization::offline[language]));
strings.set_password(Text(localization::password[language]));
strings.set_refresh_password(Text(localization::refresh_password[language]));
strings.set_copied(
Text(localization::local_id_copied_to_clipboard[language]));
strings.set_connect(Text(localization::connect[language]));
+1 -1
View File
@@ -392,7 +392,7 @@ function setup_targets()
-- version and use fmt's supported runtime-parser fallback here.
add_defines("FMT_CONSTEVAL=")
add_packages("slint", {public = true})
add_packages("libyuv", "tinyfiledialogs")
add_packages("libyuv", "tinyfiledialogs", "openssl3")
add_rules("slint")
if is_config("CROSSDESK_PORTABLE", true) then
-- RuntimeState changes layout in portable builds. Consumers of GUI