SPE-2570: Refresh access token if Printables fire accessTokenExpired event

Add fancy loading overlay during refresh period

missing include
This commit is contained in:
David Kocik 2024-11-18 14:52:35 +01:00
parent ce5008cc8d
commit 5ba8f21c77
11 changed files with 181 additions and 59 deletions

View File

@ -915,11 +915,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
this->q->Bind(EVT_OPEN_EXTERNAL_LOGIN_WIZARD, open_external_login);
this->q->Bind(EVT_OPEN_EXTERNAL_LOGIN, open_external_login);
// void on_account_login(const std::string& token);
// void on_account_will_refresh();
// void on_account_did_refresh(const std::string& token);
// void on_account_logout();
this->q->Bind(EVT_UA_LOGGEDOUT, [this](UserAccountSuccessEvent& evt) {
user_account->clear();
std::string text = _u8L("Logged out from Prusa Account.");
@ -1032,7 +1028,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
wxGetApp().handle_connect_request_printer_select_inner(evt.data);
});
this->q->Bind(EVT_UA_PRUSACONNECT_PRINTER_DATA_FAIL, [this](UserAccountFailEvent& evt) {
BOOST_LOG_TRIVIAL(error) << "Failed communication with Prusa Account: " << evt.data;
BOOST_LOG_TRIVIAL(error) << "Failed communication with Connect Printer endpoint: " << evt.data;
user_account->on_communication_fail();
std::string msg = _u8L("Failed to select printer from Prusa Connect.");
this->notification_manager->close_notification_of_type(NotificationType::SelectFilamentFromConnect);

View File

@ -103,6 +103,10 @@ void UserAccount::enqueue_printer_data_action(const std::string& uuid)
{
m_communication->enqueue_printer_data_action(uuid);
}
void UserAccount::request_refresh()
{
m_communication->request_refresh();
}
bool UserAccount::on_login_code_recieved(const std::string& url_message)
{

View File

@ -48,6 +48,7 @@ public:
void enqueue_connect_printer_models_action();
void enqueue_avatar_action();
void enqueue_printer_data_action(const std::string& uuid);
void request_refresh();
// Clears all data and connections, called on logout or EVT_UA_RESET
void clear();

View File

@ -470,6 +470,13 @@ void UserAccountCommunication::enqueue_printer_data_action(const std::string& uu
}
wakeup_session_thread();
}
void UserAccountCommunication::request_refresh()
{
m_token_timer->Stop();
enqueue_refresh();
}
void UserAccountCommunication::enqueue_refresh()
{
{
@ -529,8 +536,7 @@ void UserAccountCommunication::on_activate_app(bool active)
#endif
if (active && m_next_token_refresh_at > 0 && m_next_token_refresh_at - now < refresh_threshold) {
BOOST_LOG_TRIVIAL(info) << "Enqueue access token refresh on activation";
m_token_timer->Stop();
enqueue_refresh();
request_refresh();
}
}
@ -548,7 +554,7 @@ void UserAccountCommunication::set_refresh_time(int seconds)
assert(m_token_timer);
m_token_timer->Stop();
const auto prior_expiration_secs = 5 * 60;
int milliseconds = std::max((seconds - prior_expiration_secs) * 1000, 60000);
int milliseconds = std::max((seconds - prior_expiration_secs) * 1000, 50000);
m_next_token_refresh_at = std::time(nullptr) + milliseconds / 1000;
m_token_timer->StartOnce(milliseconds);
}

View File

@ -53,6 +53,7 @@ public:
void enqueue_test_connection();
void enqueue_printer_data_action(const std::string& uuid);
void enqueue_refresh();
void request_refresh();
// Callbacks - called from UI after receiving Event from Session thread. Some might use Session thread.
//

View File

@ -9,7 +9,6 @@
#include <boost/regex.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/beast/core/detail/base64.hpp>
#include <curl/curl.h>
#include <string>
@ -155,7 +154,6 @@ void UserAccountSession::token_success_callback(const std::string& body)
BOOST_LOG_TRIVIAL(debug) << "Access token refreshed";
// Data we need
std::string access_token, refresh_token, shared_session_key;
int expires_in = 300;
try {
std::stringstream ss(body);
pt::ptree ptree;
@ -164,25 +162,20 @@ void UserAccountSession::token_success_callback(const std::string& body)
const auto access_token_optional = ptree.get_optional<std::string>("access_token");
const auto refresh_token_optional = ptree.get_optional<std::string>("refresh_token");
const auto shared_session_key_optional = ptree.get_optional<std::string>("shared_session_key");
const auto expires_in_optional = ptree.get_optional<int>("expires_in");
if (access_token_optional)
access_token = *access_token_optional;
if (refresh_token_optional)
refresh_token = *refresh_token_optional;
if (shared_session_key_optional)
shared_session_key = *shared_session_key_optional;
assert(expires_in_optional);
if (expires_in_optional)
expires_in = *expires_in_optional;
}
catch (const std::exception&) {
std::string msg = "Could not parse server response after code exchange.";
wxQueueEvent(p_evt_handler, new UserAccountFailEvent(EVT_UA_RESET, std::move(msg)));
return;
}
if (access_token.empty() || refresh_token.empty() || shared_session_key.empty()) {
int expires_in = Utils::get_exp_seconds(access_token);
if (access_token.empty() || refresh_token.empty() || shared_session_key.empty() || expires_in <= 0) {
// just debug msg, no need to translate
std::string msg = GUI::format("Failed read tokens after POST.\nAccess token: %1%\nRefresh token: %2%\nShared session token: %3%\nbody: %4%", access_token, refresh_token, shared_session_key, body);
{

View File

@ -122,7 +122,7 @@ public:
m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_STATUS] = std::make_unique<UserActionGetWithEvent>("CONNECT_STATUS", sc.connect_status_url(), EVT_UA_PRUSACONNECT_STATUS_SUCCESS, EVT_UA_FAIL);
m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_PRINTER_MODELS] = std::make_unique<UserActionGetWithEvent>("CONNECT_PRINTER_MODELS", sc.connect_printer_list_url(), EVT_UA_PRUSACONNECT_PRINTER_MODELS_SUCCESS, EVT_UA_FAIL);
m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_AVATAR] = std::make_unique<UserActionGetWithEvent>("AVATAR", sc.media_url(), EVT_UA_AVATAR_SUCCESS, EVT_UA_FAIL);
m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_DATA_FROM_UUID] = std::make_unique<UserActionGetWithEvent>("USER_ACCOUNT_ACTION_CONNECT_DATA_FROM_UUID", sc.connect_printers_url(), EVT_UA_PRUSACONNECT_PRINTER_DATA_SUCCESS, EVT_UA_FAIL);
m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_DATA_FROM_UUID] = std::make_unique<UserActionGetWithEvent>("USER_ACCOUNT_ACTION_CONNECT_DATA_FROM_UUID", sc.connect_printers_url(), EVT_UA_PRUSACONNECT_PRINTER_DATA_SUCCESS, EVT_UA_PRUSACONNECT_PRINTER_DATA_FAIL);
}
~UserAccountSession()
{

View File

@ -782,6 +782,9 @@ void ConnectWebViewPanel::on_navigation_request(wxWebViewEvent &evt)
if (m_reached_default_url && !evt.GetURL().StartsWith(m_default_url)) {
BOOST_LOG_TRIVIAL(info) << evt.GetURL() << " does not start with default url. Vetoing.";
evt.Veto();
} else if (m_reached_default_url && evt.GetURL().Find(GUI::format_wxstr("/web/%1%.html", m_loading_html)) != wxNOT_FOUND) {
// Do not allow back button to loading screen
evt.Veto();
}
}
@ -859,9 +862,13 @@ void PrinterWebViewPanel::on_navigation_request(wxWebViewEvent &evt)
{
const wxString url = evt.GetURL();
if (url.StartsWith(m_default_url) && !m_api_key_sent) {
m_reached_default_url = true;
if (!m_usr.empty() && !m_psk.empty()) {
send_credentials();
}
} else if (m_reached_default_url && evt.GetURL().Find(GUI::format_wxstr("/web/%1%.html", m_loading_html)) != wxNOT_FOUND) {
// Do not allow back button to loading screen
evt.Veto();
}
}
@ -977,6 +984,9 @@ void PrintablesWebViewPanel::on_navigation_request(wxWebViewEvent &evt)
} else if (m_reached_default_url && url.StartsWith("http")) {
BOOST_LOG_TRIVIAL(info) << evt.GetURL() << " does not start with default url. Vetoing.";
evt.Veto();
} else if (m_reached_default_url && evt.GetURL().Find(GUI::format_wxstr("/web/%1%.html", m_loading_html)) != wxNOT_FOUND) {
// Do not allow back button to loading screen
evt.Veto();
}
}
@ -992,6 +1002,11 @@ void PrintablesWebViewPanel::on_loaded(wxWebViewEvent& evt)
load_default_url();
return;
}
if (evt.GetURL().StartsWith(m_default_url)) {
define_css();
} else {
m_styles_defined = false;
}
#ifdef _WIN32
// This is needed only once after add_request_authorization
if (m_remove_request_auth) {
@ -1074,6 +1089,8 @@ void PrintablesWebViewPanel::logout(const std::string& override_url/* = std::str
if (!m_shown || !m_browser) {
return;
}
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__;
hide_loading_overlay();
delete_cookies(m_browser, Utils::ServiceConfig::instance().printables_url());
m_browser->RunScript("localStorage.clear();");
@ -1086,17 +1103,18 @@ void PrintablesWebViewPanel::logout(const std::string& override_url/* = std::str
// We cannot do simple reload here, it would keep the access token in the header
load_request(m_browser, next_url, std::string());
#endif //
}
void PrintablesWebViewPanel::login(const std::string& access_token, const std::string& override_url/* = std::string()*/)
{
if (!m_shown) {
return;
}
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__;
hide_loading_overlay();
// We cannot add token to header as when making the first request.
// In fact, we shall not do request here, only run scripts.
// postMessage accessTokenWillChange -> postMessage accessTokenChange -> window.location.reload();
wxString script = "window.postMessage(JSON.stringify({ event: 'accessTokenWillChange' }))";
run_script(script);
@ -1119,16 +1137,19 @@ void PrintablesWebViewPanel::load_default_url()
if (!m_browser) {
return;
}
hide_loading_overlay();
std::string actual_default_url = get_url_lang_theme(from_u8(Utils::ServiceConfig::instance().printables_url() + "/homepage"));
const std::string access_token = wxGetApp().plater()->get_user_account()->get_access_token();
// in case of opening printables logged out - delete cookies and localstorage to get rid of last login
if (access_token.empty()) {
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " logout";
delete_cookies(m_browser, Utils::ServiceConfig::instance().printables_url());
m_browser->AddUserScript("localStorage.clear();");
load_url(actual_default_url);
return;
}
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " login";
// add token to first request
#ifdef _WIN32
add_request_authorization(m_browser, m_default_url, access_token);
@ -1144,6 +1165,7 @@ void PrintablesWebViewPanel::send_refreshed_token(const std::string& access_toke
if (m_load_default_url) {
return;
}
hide_loading_overlay();
wxString script = GUI::format_wxstr("window.postMessage(JSON.stringify({"
"event: 'accessTokenChange',"
"token: '%1%'"
@ -1162,7 +1184,7 @@ void PrintablesWebViewPanel::send_will_refresh()
void PrintablesWebViewPanel::on_script_message(wxWebViewEvent& evt)
{
BOOST_LOG_TRIVIAL(error) << "received message from Printables: " << evt.GetString();
BOOST_LOG_TRIVIAL(debug) << "received message from Printables: " << evt.GetString();
handle_message(into_u8(evt.GetString()));
}
@ -1176,10 +1198,17 @@ void PrintablesWebViewPanel::sys_color_changed()
void PrintablesWebViewPanel::on_printables_event_access_token_expired(const std::string& message_data)
{
// accessTokenExpired
// Printables pozaduje refresh access tokenu, muze byt volano nekolikrat.Nechme na Mobilni aplikaci at zaridi to ze zareaguje jen jednou
// We do no react on this event now - Our Acount managment should know when to renew our tokens.
// { "event": "accessTokenExpired:)
// There seems to be a situation where we get accessTokenExpired when there is active token from Slicer POW
// We need get new token and freeze webview until its not refreshed
if (m_refreshing_token) {
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " already refreshing";
return;
}
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__;
m_refreshing_token = true;
show_loading_overlay();
wxGetApp().plater()->get_user_account()->request_refresh();
}
void PrintablesWebViewPanel::on_reload_event(const std::string& message_data)
@ -1286,22 +1315,47 @@ void PrintablesWebViewPanel::on_printables_event_required_login(const std::strin
wxGetApp().printables_login_request();
}
void PrintablesWebViewPanel::show_download_notification(const std::string& filename)
void PrintablesWebViewPanel::define_css()
{
// Here we create a javascript that is injected to the webpage.
// The script contains styles and everything.
// There was a trouble with passing wide characters to the script (it was displayed wrong)
// Solution is to URL-encode the strings here and pass it.
// Then inside javascript decode it.
const std::string message_filename = Http::url_encode(GUI::format(_u8L("Downloading %1%"),filename));
const std::string message_dest = Http::url_encode(GUI::format(_u8L("To %1%"), wxGetApp().app_config->get("url_downloader_dest")));
std::string script = GUI::format(R"(
// Inject custom CSS
var style = document.createElement('style');
style.innerHTML = `
if (m_styles_defined) {
return;
}
m_styles_defined = true;
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__;
const std::string script = R"(
var style = document.createElement('style');
style.innerHTML = `
body {
/* Add your body styles here */
}
.slic3r-loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(127 127 127 / 50%);
z-index: 50;
display: flex;
align-items: center;
justify-content: center;
}
.slic3r-loading-anim {
width: 60px;
aspect-ratio: 4;
--_g: no-repeat radial-gradient(circle closest-side,#000 90%,#0000);
background:
var(--_g) 0% 50%,
var(--_g) 50% 50%,
var(--_g) 100% 50%;
background-size: calc(100%/3) 100%;
animation: slic3r-loading-anim 1s infinite linear;
}
@keyframes slic3r-loading-anim {
33%{background-size:calc(100%/3) 0% ,calc(100%/3) 100%,calc(100%/3) 100%}
50%{background-size:calc(100%/3) 100%,calc(100%/3) 0% ,calc(100%/3) 100%}
66%{background-size:calc(100%/3) 100%,calc(100%/3) 100%,calc(100%/3) 0% }
}
.notification-popup {
position: fixed;
right: 10px;
@ -1353,11 +1407,28 @@ void PrintablesWebViewPanel::show_download_notification(const std::string& filen
color: #ffa500; /* Orange "X" */
font-weight: bold;
}
`;
document.head.appendChild(style);
`;
document.head.appendChild(style);
)";
run_script(script);
}
// Define the notification functions
function appendNotification() {
void PrintablesWebViewPanel::show_download_notification(const std::string& filename)
{
// There was a trouble with passing wide characters to the script (it was displayed wrong)
// Solution is to URL-encode the strings here and pass it.
// Then inside javascript decodes it.
const std::string message_filename = Http::url_encode(GUI::format(_u8L("Downloading %1%"),filename));
const std::string message_dest = Http::url_encode(GUI::format(_u8L("To %1%"), wxGetApp().app_config->get("url_downloader_dest")));
//std::string message_filename = GUI::format(_u8L("Downloading %1%"),filename);
//std::string message_dest = GUI::format(_u8L("To %1%"), escape_string_cstyle(wxGetApp().app_config->get("url_downloader_dest")));
std::string script = GUI::format(R"(
function removeNotification() {
const notifDiv = document.getElementById('slicer-notification');
if (notifDiv)
notifDiv.remove();
}
function appendNotification() {
const body = document.getElementsByTagName('body')[0];
const notifDiv = document.createElement('div');
notifDiv.innerHTML = `
@ -1372,16 +1443,42 @@ void PrintablesWebViewPanel::show_download_notification(const std::string& filen
window.setTimeout(removeNotification, 5000);
}
function removeNotification() {
const notifDiv = document.getElementById('slicer-notification');
if (notifDiv)
notifDiv.remove();
}
appendNotification();
)", message_filename, message_dest);
appendNotification();
)", message_filename, message_dest);
run_script(script);
}
void PrintablesWebViewPanel::show_loading_overlay()
{
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__;
std::string script = R"(
function slic3r_showLoadingOverlay() {
const body = document.getElementsByTagName('body')[0];
const overlayDiv = document.createElement('div');
overlayDiv.className = 'slic3r-loading-overlay'
overlayDiv.id = 'slic3r-loading-overlay';
overlayDiv.innerHTML = '<div class="slic3r-loading-anim"></div>';
body.appendChild(overlayDiv);
}
slic3r_showLoadingOverlay();
)";
run_script(script);
}
void PrintablesWebViewPanel::hide_loading_overlay()
{
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__;
m_refreshing_token = false;
std::string script = R"(
function slic3r_hideLoadingOverlay() {
const overlayDiv = document.getElementById('slic3r-loading-overlay');
if (overlayDiv)
overlayDiv.remove();
}
slic3r_hideLoadingOverlay();
)";
run_script(script);
}
} // namespace slic3r::GUI

View File

@ -203,10 +203,16 @@ private:
void on_printables_event_required_login(const std::string& message_data);
void load_default_url() override;
std::string get_url_lang_theme(const wxString& url) const;
void define_css();
void show_download_notification(const std::string& filename);
void show_loading_overlay();
void hide_loading_overlay();
std::map<std::string, std::function<void(const std::string&)>> m_events;
std::string m_next_show_url;
bool m_refreshing_token {false};
bool m_styles_defined {false};
#ifdef _WIN32
bool m_remove_request_auth { false };
#endif

View File

@ -8,19 +8,21 @@
#include <sstream>
#include <tuple>
#include <algorithm>
#include <chrono>
namespace Slic3r::Utils {
bool verify_exp(const std::string& token)
namespace {
boost::optional<double> get_exp(const std::string& token)
{
size_t payload_start = token.find('.');
if (payload_start == std::string::npos)
return false;
return boost::none;
payload_start += 1; // payload starts after dot
const size_t payload_end = token.find('.', payload_start);
if (payload_end == std::string::npos)
return false;
return boost::none;
size_t encoded_length = payload_end - payload_start;
size_t decoded_length = boost::beast::detail::base64::decoded_size(encoded_length);
@ -40,7 +42,7 @@ bool verify_exp(const std::string& token)
std::tie(written_bytes, read_bytes) = boost::beast::detail::base64::decode(json.data(), json_b64.data(), json_b64.length());
json.resize(written_bytes);
if (written_bytes == 0)
return false;
return boost::none;
namespace pt = boost::property_tree;
@ -48,10 +50,26 @@ bool verify_exp(const std::string& token)
std::istringstream iss(json);
pt::json_parser::read_json(iss, payload);
auto exp_opt = payload.get_optional<double>("exp");
return payload.get_optional<double>("exp");
}
}
int get_exp_seconds(const std::string& token)
{
auto exp_opt = get_exp(token);
if (!exp_opt)
return 0;
auto now = std::chrono::system_clock::now();
auto now_in_seconds = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
double remaining_time = *exp_opt - now_in_seconds;
return (int)remaining_time;
}
bool verify_exp(const std::string& token)
{
auto exp_opt = get_exp(token);
if (!exp_opt)
return false;
auto now = time(nullptr);
return exp_opt.get() > now;
}

View File

@ -5,6 +5,6 @@
namespace Slic3r::Utils {
bool verify_exp(const std::string& token);
int get_exp_seconds(const std::string& token);
}