diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 7c8fa465d05..e8f8a18f3bf 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -68,6 +68,7 @@ #include #include #include +#include #include #include @@ -1106,6 +1107,11 @@ void GUI_App::post_init() hms_query->check_hms_info(); }); + // restore a previously persisted Snapmaker login (revalidates the stored token) + CallAfter([this] { + sm_restore_login_from_config(); + }); + DeviceManager::load_filaments_blacklist_config(); @@ -4143,6 +4149,140 @@ void GUI_App::sm_request_user_logout() } catch (std::exception&) { ; } + sm_clear_login_from_config(); +} + +// --- Snapmaker login persistence --------------------------------------------- +// Goal: stop forcing a fresh OAuth login on every restart (issues #116/#226/#266). +// +// Only the bearer token is persisted, and only to the OS secret store via +// wxSecretStore (libsecret on Linux, Credential Manager on Windows, Keychain on +// macOS) -- never to plaintext config. On startup we revalidate the token +// against accounts/current and re-fetch the profile (id/name/account/icon) from +// the server, so no user data is written to disk. If no secret service is +// available we simply do not persist the token and fall back to the previous +// "log in again" behaviour, rather than writing a credential somewhere insecure. +namespace { +#if wxUSE_SECRETSTORE +const wxString SM_LOGIN_SECRET_SERVICE = "Snapmaker_Orca/login"; + +bool sm_secret_store_save(const std::string& account, const std::string& token) +{ + wxSecretStore store = wxSecretStore::GetDefault(); + wxString errmsg; + if (!store.IsOk(&errmsg)) { + BOOST_LOG_TRIVIAL(warning) << "sm_login: secret store unavailable, token not persisted: " << errmsg.ToStdString(); + return false; + } + const wxString user = account.empty() ? wxString("snapmaker") : wxString::FromUTF8(account.c_str()); + return store.Save(SM_LOGIN_SECRET_SERVICE, user, wxSecretValue(wxString::FromUTF8(token.c_str()))); +} + +std::string sm_secret_store_load() +{ + wxSecretStore store = wxSecretStore::GetDefault(); + wxString errmsg; + if (!store.IsOk(&errmsg)) { + BOOST_LOG_TRIVIAL(warning) << "sm_login: secret store unavailable: " << errmsg.ToStdString(); + return std::string(); + } + wxString user; + wxSecretValue value; + if (!store.Load(SM_LOGIN_SECRET_SERVICE, user, value)) + return std::string(); + return std::string(value.GetAsString(wxConvUTF8).ToUTF8()); +} + +void sm_secret_store_clear() +{ + wxSecretStore store = wxSecretStore::GetDefault(); + if (store.IsOk()) + store.Delete(SM_LOGIN_SECRET_SERVICE); +} +#else // !wxUSE_SECRETSTORE +// No OS secret store on this toolchain (e.g. MinGW, which lacks wincred.h). +// Degrade gracefully: never persist the token, so the user logs in again each +// launch -- but we never write the credential to plaintext. +bool sm_secret_store_save(const std::string&, const std::string&) { return false; } +std::string sm_secret_store_load() { return std::string(); } +void sm_secret_store_clear() {} +#endif // wxUSE_SECRETSTORE +} // namespace + +void GUI_App::sm_save_login_to_config() +{ + const std::string token = m_login_userinfo.get_user_token(); + if (token.empty()) { + sm_clear_login_from_config(); + return; + } + // Only the token is persisted (to the OS secret store). Profile fields are + // re-fetched from the server on restore, so nothing lands in plaintext. + sm_secret_store_save(m_login_userinfo.get_user_account(), token); +} + +void GUI_App::sm_clear_login_from_config() +{ + sm_secret_store_clear(); + // Scrub any fields an older build may have written to plaintext config. + app_config->erase("sm_login", "user_id"); + app_config->erase("sm_login", "user_name"); + app_config->erase("sm_login", "user_account"); + app_config->erase("sm_login", "user_icon_url"); + app_config->erase("sm_login", "token"); + app_config->save(); +} + +void GUI_App::sm_restore_login_from_config() +{ + const std::string token = sm_secret_store_load(); + if (token.empty()) + return; + + const std::string region = app_config->get_country_code(); + const std::string user_info_url = (region == "CN") + ? "https://api.snapmaker.cn/api/common/accounts/current" + : "https://id.snapmaker.com/api/common/accounts/current"; + + auto http = Http::get(user_info_url); + http.header("Authorization", token); + // on_complete fires only for 2xx; an expired/invalid token (401/403) and any + // network failure route to on_error below, which clears the stored session. + http.on_complete([this, token](std::string body, unsigned /*status*/) { + // Parse on this worker thread, then apply all state changes on the + // main thread (SMUserInfo::set_user_login notifies the UI). + std::string user_id, user_name, user_icon_url, user_account; + try { + json response = json::parse(body); + if (response.count("data")) { + json data = response["data"]; + if (data.count("id")) + user_id = std::to_string(data["id"].get()); + if (data.count("nickname")) + user_name = data["nickname"].get(); + if (data.count("icon")) + user_icon_url = data["icon"].get(); + if (data.count("account")) + user_account = data["account"].get(); + } + } catch (std::exception&) { + CallAfter([this]() { sm_clear_login_from_config(); }); + return; + } + CallAfter([this, token, user_id, user_name, user_icon_url, user_account]() { + if (!user_id.empty()) m_login_userinfo.set_user_id(user_id); + if (!user_name.empty()) m_login_userinfo.set_user_name(user_name); + if (!user_icon_url.empty()) m_login_userinfo.set_user_icon_url(user_icon_url); + if (!user_account.empty()) m_login_userinfo.set_user_account(user_account); + m_login_userinfo.set_user_token(token); + m_login_userinfo.set_user_login(true); + sm_save_login_to_config(); + }); + }) + .on_error([this](std::string, std::string, unsigned) { + CallAfter([this]() { sm_clear_login_from_config(); }); + }) + .perform(); } //BBS diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 6d79ccdd18e..94c43c89d56 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -591,7 +591,13 @@ class GUI_App : public wxApp void sm_request_login(bool show_user_info = false); void sm_ShowUserLogin(bool show = true); void sm_request_user_logout(); - + // Snapmaker login persistence across restarts. Non-secret profile fields are + // kept in AppConfig's [sm_login] section; the bearer token itself is stored in + // the OS secret store (never in plaintext). See GUI_App.cpp for details. + void sm_save_login_to_config(); + void sm_clear_login_from_config(); + void sm_restore_login_from_config(); + void request_user_logout(); int request_user_unbind(std::string dev_id); std::string handle_web_request(std::string cmd); diff --git a/src/slic3r/GUI/SSWCP.cpp b/src/slic3r/GUI/SSWCP.cpp index 3e6da9f9ffb..42822d54dbc 100644 --- a/src/slic3r/GUI/SSWCP.cpp +++ b/src/slic3r/GUI/SSWCP.cpp @@ -4754,6 +4754,24 @@ void SSWCP_UserLogin_Instance::sw_SubscribeUserLoginState() try { std::weak_ptr weak_ptr = shared_from_this(); wxGetApp().m_user_login_subscribers[m_webview] = weak_ptr; + + // A persisted session is restored asynchronously at startup (the stored + // token is revalidated over the network). That can finish before the web + // UI subscribes here, in which case the one-shot notify() push already + // fired and was lost. So if we are already logged in at subscribe time, + // push the current state now; otherwise a later notify() will deliver it. + auto pInfo = wxGetApp().sm_get_userinfo(); + if (pInfo && pInfo->is_user_login()) { + json data; + data["status"] = "online"; + data["nickname"] = pInfo->get_user_name(); + data["icon"] = pInfo->get_user_icon_url(); + data["token"] = pInfo->get_user_token(); + data["userid"] = pInfo->get_user_id(); + data["account"] = pInfo->get_user_account(); + m_res_data = data; + send_to_js(); + } } catch (std::exception& e) { handle_general_fail(); diff --git a/src/slic3r/GUI/WebSMUserLoginDialog.cpp b/src/slic3r/GUI/WebSMUserLoginDialog.cpp index 9b23e17535b..14013f4fdf2 100644 --- a/src/slic3r/GUI/WebSMUserLoginDialog.cpp +++ b/src/slic3r/GUI/WebSMUserLoginDialog.cpp @@ -221,6 +221,8 @@ void SMUserLogin::OnNavigationRequest(wxWebViewEvent &evt) sentryReportLog(SENTRY_LOG_TRACE, userInfo, BP_LOGIN); wxGetApp().sm_get_userinfo()->set_user_token(token); wxGetApp().sm_get_userinfo()->set_user_login(true); + // Persist the session so the user stays logged in across restarts. + wxGetApp().sm_save_login_to_config(); } }) .on_error([&](std::string body, std::string error, unsigned status) {