[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 <remote_action.h>
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <openssl/rand.h>
#include <slint.h> #include <slint.h>
#include <tinyfiledialogs.h> #include <tinyfiledialogs.h>
@@ -65,6 +66,28 @@ namespace {
using namespace std::chrono_literals; 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) { std::string CreatePasswordChangeRequestId(uint64_t sequence) {
const auto timestamp = std::chrono::system_clock::now() const auto timestamp = std::chrono::system_clock::now()
.time_since_epoch() .time_since_epoch()
@@ -1294,78 +1317,17 @@ void GuiApplication::BindMainCallbacks() {
ui_->main->set_password_visible(show_password_); ui_->main->set_password_visible(show_password_);
}); });
main->on_reset_password([this](slint::SharedString value) { main->on_reset_password([this](slint::SharedString value) {
const std::string password(value); return RequestPasswordChange(std::string(value));
if (password.size() != 6) { });
return false; main->on_refresh_password([this] {
} const auto password = GenerateRandomPassword(password_saved_);
if (!password) {
if (!peer_ || !signal_connected_) { LOG_ERROR("Could not generate a random password");
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_]; offline_warning_text_ = localization::failed[localization_language_index_];
show_offline_warning_window_ = true; show_offline_warning_window_ = true;
LOG_ERROR("Could not durably stage password change"); return;
return true;
} }
RequestPasswordChange(*password);
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;
}); });
main->on_connect_requested( main->on_connect_requested(
[this](slint::SharedString id) { ConnectFromUi(std::string(id)); }); [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() { void GuiApplication::HandlePasswordChangeResult() {
bool succeeded = false; bool succeeded = false;
bool uncertain = false; bool uncertain = false;
@@ -2188,6 +2222,10 @@ void GuiApplication::SyncMainWindow() {
ui_->main->set_local_id(UiText(FormatPeerId(client_id_))); ui_->main->set_local_id(UiText(FormatPeerId(client_id_)));
ui_->main->set_local_password(password_saved_); ui_->main->set_local_password(password_saved_);
ui_->main->set_password_visible(show_password_); 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_connected(signal_connected_);
ui_->main->set_signal_tls_error(signal_status_ == ui_->main->set_signal_tls_error(signal_status_ ==
SignalStatus::SignalTlsCertError); SignalStatus::SignalTlsCertError);
@@ -49,6 +49,7 @@ private:
void BindServerCallbacks(); void BindServerCallbacks();
void Tick(); void Tick();
void ShareLocalCursorState(); void ShareLocalCursorState();
bool RequestPasswordChange(const std::string &password);
void HandlePasswordChangeResult(); void HandlePasswordChangeResult();
void HandleCredentialRecovery(); void HandleCredentialRecovery();
void SyncMainWindow(); void SyncMainWindow();
@@ -100,4 +101,4 @@ private:
} // namespace crossdesk } // namespace crossdesk
#endif #endif
@@ -31,6 +31,8 @@ struct TranslationRow {
X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \ X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \
u8"Скопировано в буфер обмена") \ u8"Скопировано в буфер обмена") \
X(password, u8"密码", "Password", 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(max_password_len, u8"最大6个字符", "Max 6 chars", u8"Макс. 6 символов") \
X(remote_desktop, u8"远程桌面", "Remote Desktop", \ X(remote_desktop, u8"远程桌面", "Remote Desktop", \
u8"Удаленный рабочий стол") \ u8"Удаленный рабочий стол") \
+4
View File
@@ -41,6 +41,7 @@ export global FontAwesomeIcons {
out property <string> keyboard: "\u{f11c}"; out property <string> keyboard: "\u{f11c}";
out property <string> minus: "\u{f068}"; out property <string> minus: "\u{f068}";
out property <string> pen: "\u{f304}"; out property <string> pen: "\u{f304}";
out property <string> rotate: "\u{f2f1}";
out property <string> signal: "\u{f012}"; out property <string> signal: "\u{f012}";
out property <string> shield-halved: "\u{f3ed}"; out property <string> shield-halved: "\u{f3ed}";
out property <string> square-full: "\u{f45c}"; out property <string> square-full: "\u{f45c}";
@@ -54,6 +55,7 @@ export global FontAwesomeIcons {
export component IconButton inherits Rectangle { export component IconButton inherits Rectangle {
in property <string> icon; in property <string> icon;
in property <string> tooltip; in property <string> tooltip;
in property <bool> enabled: true;
in property <bool> danger: false; in property <bool> danger: false;
in property <length> icon-size: 16px; in property <length> icon-size: 16px;
in property <color> icon-color: #24272d; in property <color> icon-color: #24272d;
@@ -69,6 +71,7 @@ export component IconButton inherits Rectangle {
: touch.has-hover ? (danger ? #f04438 : root.hover-background) : touch.has-hover ? (danger ? #f04438 : root.hover-background)
: root.normal-background; : root.normal-background;
border-radius: 4px; border-radius: 4px;
opacity: root.enabled ? 1 : 0.4;
Text { Text {
text: root.icon; text: root.icon;
@@ -90,6 +93,7 @@ export component IconButton inherits Rectangle {
} }
touch := TouchArea { touch := TouchArea {
enabled: root.enabled;
clicked => { root.clicked(); } clicked => { root.clicked(); }
} }
+18 -3
View File
@@ -24,6 +24,7 @@ export global UiStrings {
in-out property <string> online: "Online"; in-out property <string> online: "Online";
in-out property <string> offline: "Offline"; in-out property <string> offline: "Offline";
in-out property <string> password: "Password"; 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> copied: "Local ID copied to clipboard";
in-out property <string> connect: "Connect"; in-out property <string> connect: "Connect";
in-out property <string> settings: "Settings"; in-out property <string> settings: "Settings";
@@ -208,6 +209,7 @@ export component MainWindow inherits Window {
in property <string> local-password: ""; in property <string> local-password: "";
in-out property <string> remote-id-input: ""; in-out property <string> remote-id-input: "";
in property <bool> password-visible: true; in property <bool> password-visible: true;
in property <bool> password-change-pending: false;
in property <bool> signal-connected: false; in property <bool> signal-connected: false;
in property <bool> signal-tls-error: false; in property <bool> signal-tls-error: false;
in property <[RecentConnection]> recent-connections; in property <[RecentConnection]> recent-connections;
@@ -262,6 +264,7 @@ export component MainWindow inherits Window {
callback copy-local-id; callback copy-local-id;
callback toggle-password-visibility; callback toggle-password-visibility;
callback refresh-password;
callback reset-password(string) -> bool; callback reset-password(string) -> bool;
callback format-remote-id(string) -> string; callback format-remote-id(string) -> string;
callback connect-requested(string); callback connect-requested(string);
@@ -676,19 +679,31 @@ export component MainWindow inherits Window {
IconButton { IconButton {
x: 174px; x: 174px;
y: 133px; y: 133px;
width: 29px; width: 24px;
height: 30px; height: 30px;
icon: root.password-visible ? FontAwesomeIcons.eye : FontAwesomeIcons.eye-slash; icon: root.password-visible ? FontAwesomeIcons.eye : FontAwesomeIcons.eye-slash;
icon-size: 17px; icon-size: 17px;
clicked => { root.toggle-password-visibility(); } clicked => { root.toggle-password-visibility(); }
} }
IconButton { IconButton {
x: 214px; x: 198px;
y: 133px; 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; height: 30px;
icon: FontAwesomeIcons.pen; icon: FontAwesomeIcons.pen;
icon-size: 15px; icon-size: 15px;
enabled: !root.password-change-pending;
clicked => { root.reset-password-open = true; } clicked => { root.reset-password-open = true; }
} }
} }
@@ -36,6 +36,7 @@ inline int ApplyMainWindowStrings(
strings.set_online(Text(localization::online[language])); strings.set_online(Text(localization::online[language]));
strings.set_offline(Text(localization::offline[language])); strings.set_offline(Text(localization::offline[language]));
strings.set_password(Text(localization::password[language])); strings.set_password(Text(localization::password[language]));
strings.set_refresh_password(Text(localization::refresh_password[language]));
strings.set_copied( strings.set_copied(
Text(localization::local_id_copied_to_clipboard[language])); Text(localization::local_id_copied_to_clipboard[language]));
strings.set_connect(Text(localization::connect[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. -- version and use fmt's supported runtime-parser fallback here.
add_defines("FMT_CONSTEVAL=") add_defines("FMT_CONSTEVAL=")
add_packages("slint", {public = true}) add_packages("slint", {public = true})
add_packages("libyuv", "tinyfiledialogs") add_packages("libyuv", "tinyfiledialogs", "openssl3")
add_rules("slint") add_rules("slint")
if is_config("CROSSDESK_PORTABLE", true) then if is_config("CROSSDESK_PORTABLE", true) then
-- RuntimeState changes layout in portable builds. Consumers of GUI -- RuntimeState changes layout in portable builds. Consumers of GUI