WebView and Connect integration: implemented login ceremony via /slicer/login request (and alternative disabled one with fetch function overriding); all stored JWT tokens merged into single secret store record to reduce number of login dialogs appearances when new app is run for first time on Mac.

This commit is contained in:
Jan Bařtipán 2024-06-06 13:36:20 +02:00 committed by Lukas Matena
parent 5601a6e75f
commit 850ac19167
10 changed files with 136 additions and 32 deletions

View File

@ -342,7 +342,7 @@ template<class T>
struct NilValueTempl<T, std::enable_if_t<std::is_enum_v<T>, void>>
{
using NilType = T;
static constexpr auto value = static_cast<T>(std::numeric_limits<std::underlying_type_t<T>>::max());
static constexpr auto value = std::numeric_limits<std::underlying_type_t<T>>::max();
};
template<class T> struct NilValueTempl<T, std::enable_if_t<std::is_floating_point_v<T>, void>> {

View File

@ -653,7 +653,7 @@ void PhysicalPrinterDialog::update(bool printer_change)
text_ctrl* printhost_win = printhost_field ? dynamic_cast<text_ctrl*>(printhost_field->getWindow()) : nullptr;
if (!m_opened_as_connect && printhost_win && m_last_host_type != htPrusaConnect){
m_stored_host = printhost_win->GetValue();
printhost_win->SetValue(L"https://connect.prusa3d.com");
printhost_win->SetValue(L"https://dev.connect.prusa3d.com");
}
} else {
m_printhost_browse_btn->Show();
@ -888,10 +888,10 @@ void PhysicalPrinterDialog::OnOK(wxEvent& event)
text_ctrl* printhost_win = printhost_field ? dynamic_cast<text_ctrl*>(printhost_field->getWindow()) : nullptr;
const auto opt = m_config->option<ConfigOptionEnum<PrintHostType>>("host_type");
if (opt->value == htPrusaConnect) {
if (printhost_win && printhost_win->GetValue() != L"https://connect.prusa3d.com"){
InfoDialog msg(this, _L("Warning"), _L("URL of PrusaConnect is different from https://connect.prusa3d.com. Do you want to continue?"), true, wxYES_NO);
if (printhost_win && printhost_win->GetValue() != L"https://dev.connect.prusa3d.com"){
InfoDialog msg(this, _L("Warning"), _L("URL of PrusaConnect is different from https://dev.connect.prusa3d.com. Do you want to continue?"), true, wxYES_NO);
if(msg.ShowModal() != wxID_YES){
printhost_win->SetValue(L"https://connect.prusa3d.com");
printhost_win->SetValue(L"https://dev.connect.prusa3d.com");
return;
}
}

View File

@ -889,6 +889,8 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
});
this->q->Bind(EVT_UA_ID_USER_SUCCESS, [this](UserAccountSuccessEvent& evt) {
// There are multiple handlers and we want to notify all
evt.Skip();
std::string username;
if (user_account->on_user_id_success(evt.data, username)) {
// login notification

View File

@ -143,12 +143,22 @@ UserAccountCommunication::UserAccountCommunication(wxEvtHandler* evt_handler, Ap
std::string access_token, refresh_token, shared_session_key, next_timeout;
if (is_secret_store_ok()) {
std::string key0, key1, key2;
load_secret("access_token", key0, access_token);
load_secret("refresh_token", key1, refresh_token);
load_secret("access_token_timeout", key2, next_timeout);
assert(key0 == key1);
std::string key0, key1, key2, tokens;
if (load_secret("tokens", key0, tokens)) {
std::vector<std::string> token_list;
boost::split(token_list, tokens, boost::is_any_of("|"), boost::token_compress_off);
assert(token_list.empty() || token_list.size() == 3);
access_token = token_list.size() > 0 ? token_list[0] : std::string();
refresh_token = token_list.size() > 1 ? token_list[1] : std::string();
next_timeout = token_list.size() > 2 ? token_list[2] : std::string();
} else {
load_secret("access_token", key0, access_token);
load_secret("refresh_token", key1, refresh_token);
load_secret("access_token_timeout", key2, next_timeout);
assert(key0 == key1);
}
shared_session_key = key0;
} else {
access_token = m_app_config->get("access_token");
refresh_token = m_app_config->get("refresh_token");
@ -173,7 +183,7 @@ UserAccountCommunication::UserAccountCommunication(wxEvtHandler* evt_handler, Ap
UserAccountCommunication::~UserAccountCommunication()
{
if (m_thread.joinable()) {
Stop the worker thread, if running.
// Stop the worker thread, if running.
{
// Notify the worker thread to cancel wait on detection polling.
std::lock_guard<std::mutex> lck(m_thread_stop_mutex);
@ -191,9 +201,13 @@ void UserAccountCommunication::set_username(const std::string& username)
{
std::lock_guard<std::mutex> lock(m_session_mutex);
if (is_secret_store_ok()) {
save_secret("access_token", m_session->get_shared_session_key(), m_remember_session ? m_session->get_access_token() : std::string());
save_secret("refresh_token", m_session->get_shared_session_key(), m_remember_session ? m_session->get_refresh_token() : std::string());
save_secret("access_token_timeout", m_session->get_shared_session_key(), m_remember_session ? GUI::format("%1%",m_session->get_next_token_timeout()) : "0");
std::string tokens;
if (m_remember_session) {
tokens = m_session->get_access_token() +
"|" + m_session->get_refresh_token() +
"|" + std::to_string(m_session->get_next_token_timeout());
}
save_secret("tokens", m_session->get_shared_session_key(), tokens);
}
else {
m_app_config->set("access_token", m_remember_session ? m_session->get_access_token() : std::string());
@ -245,7 +259,7 @@ void UserAccountCommunication::on_uuid_map_success()
void UserAccountCommunication::login_redirect()
{
const std::string AUTH_HOST = "https://account.prusa3d.com";
const std::string AUTH_HOST = "https://test-account.prusa3d.com";
const std::string CLIENT_ID = client_id();
const std::string REDIRECT_URI = "prusaslicer://login";
CodeChalengeGenerator ccg;

View File

@ -94,8 +94,7 @@ private:
void wakeup_session_thread();
void init_session_thread();
void login_redirect();
std::string client_id() const { return "oamhmhZez7opFosnwzElIgE2oGgI2iJORSkw587O"; }
std::string client_id() const { return "UfTRUm5QjWwaQEGpWQBHGHO3reAyuzgOdBaiqO52"; }
};

View File

@ -111,15 +111,15 @@ public:
// do not forget to add delete to destructor
m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_DUMMY] = std::make_unique<DummyUserAction>();
m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_REFRESH_TOKEN] = std::make_unique<UserActionPost>("EXCHANGE_TOKENS", "https://account.prusa3d.com/o/token/");
m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_CODE_FOR_TOKEN] = std::make_unique<UserActionPost>("EXCHANGE_TOKENS", "https://account.prusa3d.com/o/token/");
m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_USER_ID] = std::make_unique<UserActionGetWithEvent>("USER_ID", "https://account.prusa3d.com/api/v1/me/", EVT_UA_ID_USER_SUCCESS, EVT_UA_RESET);
m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_TEST_ACCESS_TOKEN] = std::make_unique<UserActionGetWithEvent>("TEST_ACCESS_TOKEN", "https://account.prusa3d.com/api/v1/me/", EVT_UA_ID_USER_SUCCESS, EVT_UA_FAIL);
m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_TEST_CONNECTION] = std::make_unique<UserActionGetWithEvent>("TEST_CONNECTION", "https://account.prusa3d.com/api/v1/me/", wxEVT_NULL, EVT_UA_RESET);
m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_STATUS] = std::make_unique<UserActionGetWithEvent>("CONNECT_STATUS", "https://connect.prusa3d.com/slicer/status", EVT_UA_PRUSACONNECT_STATUS_SUCCESS, EVT_UA_FAIL);
m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_PRINTER_MODELS] = std::make_unique<UserActionGetWithEvent>("CONNECT_PRINTER_MODELS", "https://connect.prusa3d.com/slicer/printer_list", EVT_UA_PRUSACONNECT_PRINTER_MODELS_SUCCESS, EVT_UA_FAIL);
m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_REFRESH_TOKEN] = std::make_unique<UserActionPost>("EXCHANGE_TOKENS", "https://test-account.prusa3d.com/o/token/");
m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_CODE_FOR_TOKEN] = std::make_unique<UserActionPost>("EXCHANGE_TOKENS", "https://test-account.prusa3d.com/o/token/");
m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_USER_ID] = std::make_unique<UserActionGetWithEvent>("USER_ID", "https://test-account.prusa3d.com/api/v1/me/", EVT_UA_ID_USER_SUCCESS, EVT_UA_RESET);
m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_TEST_ACCESS_TOKEN] = std::make_unique<UserActionGetWithEvent>("TEST_ACCESS_TOKEN", "https://test-account.prusa3d.com/api/v1/me/", EVT_UA_ID_USER_SUCCESS, EVT_UA_FAIL);
m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_TEST_CONNECTION] = std::make_unique<UserActionGetWithEvent>("TEST_CONNECTION", "https://test-account.prusa3d.com/api/v1/me/", wxEVT_NULL, EVT_UA_RESET);
m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_STATUS] = std::make_unique<UserActionGetWithEvent>("CONNECT_STATUS", "https://dev.connect.prusa3d.com/slicer/status", EVT_UA_PRUSACONNECT_STATUS_SUCCESS, EVT_UA_FAIL);
m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_CONNECT_PRINTER_MODELS] = std::make_unique<UserActionGetWithEvent>("CONNECT_PRINTER_MODELS", "https://dev.connect.prusa3d.com/slicer/printer_list", EVT_UA_PRUSACONNECT_PRINTER_MODELS_SUCCESS, EVT_UA_FAIL);
m_actions[UserAccountActionID::USER_ACCOUNT_ACTION_AVATAR] = std::make_unique<UserActionGetWithEvent>("AVATAR", "https://media.printables.com/media/", 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", "https://connect.prusa3d.com/app/printers/", 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", "https://dev.connect.prusa3d.com/app/printers/", EVT_UA_PRUSACONNECT_PRINTER_DATA_SUCCESS, EVT_UA_FAIL);
}
~UserAccountSession()
{
@ -162,7 +162,7 @@ private:
void cancel_queue();
void code_exchange_fail_callback(const std::string& body);
void token_success_callback(const std::string& body);
std::string client_id() const { return "oamhmhZez7opFosnwzElIgE2oGgI2iJORSkw587O"; }
std::string client_id() const { return "UfTRUm5QjWwaQEGpWQBHGHO3reAyuzgOdBaiqO52"; }
// false prevents action queu to be processed - no communication is done
// sets to true by init_with_code or enqueue_action call

View File

@ -15,6 +15,11 @@
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
// if set to 1 the fetch() JS function gets override to include JWT in authorization header
// if set to 0, the /slicer/login is invoked from WebKit (passing JWT token only to this request)
// to set authorization cookie for all WebKit requests to Connect
#define AUTH_VIA_FETCH_OVERRIDE 0
namespace pt = boost::property_tree;
@ -542,8 +547,83 @@ void ConnectRequestHandler::on_connect_action_request_config()
}
ConnectWebViewPanel::ConnectWebViewPanel(wxWindow* parent)
: WebViewPanel(parent, L"https://connect.prusa3d.com/", { "_prusaSlicer" }, "connect_loading")
: WebViewPanel(parent, L"https://dev.connect.prusa3d.com/", { "_prusaSlicer" }, "connect_loading")
{
//m_browser->RegisterHandler(wxSharedPtr<wxWebViewHandler>(new WebViewHandler("https")));
Plater* plater = wxGetApp().plater();
m_browser->AddUserScript(wxString::Format(
#if AUTH_VIA_FETCH_OVERRIDE
/*
* Notes:
* - The fetch() function has two distinct prototypes (i.e. input args):
* 1. fetch(url: string, options: object | undefined)
* 2. fetch(req: Request, options: object | undefined)
* - For some reason I can't explain the headers can be extended only via Request object
* i.e. the fetch prototype (2). So we need to convert (1) call into (2) before
*
*/
R"(
if (window.__fetch === undefined) {
window.__fetch = fetch;
window.fetch = function(req, opts = {}) {
if (typeof req === 'string') {
req = new Request(req, opts);
opts = {};
}
if (window.__access_token && (req.url[0] == '/' || req.url.indexOf('prusa3d.com') > 0)) {
req.headers.set('Authorization', 'Bearer ' + window.__access_token);
console.log('Header updated: ', req.headers.get('Authorization'));
console.log('AT Version: ', __access_token_version);
}
//console.log('Injected fetch used', req, opts);
return __fetch(req, opts);
};
}
window.__access_token = '%s';
window.__access_token_version = 0;
)",
#else
R"(
console.log('Preparing login');
window.fetch('/slicer/login', {method: 'POST', headers: {Authorization: 'Bearer %s'}})
.then((resp) => {
console.log('Login resp', resp);
resp.text().then((json) => console.log('Login resp body', json));
});
)",
#endif
plater->get_user_account()->get_access_token()
));
plater->Bind(EVT_UA_ID_USER_SUCCESS, &ConnectWebViewPanel::on_user_token, this);
}
ConnectWebViewPanel::~ConnectWebViewPanel()
{
m_browser->Unbind(EVT_UA_ID_USER_SUCCESS, &ConnectWebViewPanel::on_user_token, this);
}
void ConnectWebViewPanel::on_user_token(UserAccountSuccessEvent& e)
{
e.Skip();
wxString javascript = wxString::Format(
#if AUTH_VIA_FETCH_OVERRIDE
"window.__access_token = '%s';window.__access_token_version = (window.__access_token_version || 0) + 1;console.log('Updated Auth token', window.__access_token);",
#else
R"(
console.log('Preparing login');
window.fetch('/slicer/login', {method: 'POST', headers: {Authorization: 'Bearer %s'}})
.then((resp) => {
console.log('Login resp', resp);
resp.text().then((json) => console.log('Login resp body', json));
});
)",
#endif
wxGetApp().plater()->get_user_account()->get_access_token()
);
//m_browser->AddUserScript(javascript, wxWEBVIEW_INJECT_AT_DOCUMENT_END);
m_browser->RunScript(javascript);
}
void ConnectWebViewPanel::on_script_message(wxWebViewEvent& evt)
@ -1040,7 +1120,7 @@ void WebViewDialog::EndModal(int retCode)
PrinterPickWebViewDialog::PrinterPickWebViewDialog(wxWindow* parent, std::string& ret_val)
: WebViewDialog(parent
, L"https://connect.prusa3d.com/slicer-select-printer"
, L"https://dev.connect.prusa3d.com/slicer-select-printer"
, _L("Choose a printer")
, wxSize(std::max(parent->GetClientSize().x / 2, 100 * wxGetApp().em_unit()), std::max(parent->GetClientSize().y / 2, 50 * wxGetApp().em_unit()))
,{"_prusaSlicer"}

View File

@ -1,18 +1,24 @@
#ifndef slic3r_WebViewDialog_hpp_
#define slic3r_WebViewDialog_hpp_
#define DEBUG_URL_PANEL
#include <map>
#include <wx/wx.h>
#include <wx/event.h>
#include "UserAccountSession.hpp"
#ifdef DEBUG_URL_PANEL
#include <wx/infobar.h>
#endif
class wxWebView;
class wxWebViewEvent;
namespace Slic3r {
namespace GUI {
//#define DEBUG_URL_PANEL
class WebViewPanel : public wxPanel
{
public:
@ -183,6 +189,7 @@ class ConnectWebViewPanel : public WebViewPanel, public ConnectRequestHandler
{
public:
ConnectWebViewPanel(wxWindow* parent);
~ConnectWebViewPanel() override;
void on_script_message(wxWebViewEvent& evt) override;
void logout();
void sys_color_changed() override;
@ -191,6 +198,8 @@ protected:
void on_connect_action_print() override;
void on_connect_action_webapp_ready() override {}
void run_script_bridge(const wxString& script) override {run_script(script); }
private:
void on_user_token(UserAccountSuccessEvent& e);
};
class PrinterWebViewPanel : public WebViewPanel

View File

@ -60,7 +60,7 @@ bool PrusaConnectNew::test(wxString& curl_msg) const
{
// Test is not used by upload and gets list of files on a device.
const std::string name = get_name();
std::string url = GUI::format("https://connect.prusa3d.com/app/teams/%1%/files?printer_uuid=%2%", m_team_id, m_uuid);
std::string url = GUI::format("https://dev.connect.prusa3d.com/app/teams/%1%/files?printer_uuid=%2%", m_team_id, m_uuid);
const std::string access_token = GUI::wxGetApp().plater()->get_user_account()->get_access_token();
BOOST_LOG_TRIVIAL(info) << GUI::format("%1%: Get files/raw at: %2%", name, url);
bool res = true;

View File

@ -32,7 +32,7 @@ public:
bool has_auto_discovery() const override { return true; }
bool can_test() const override { return true; }
PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint | PrintHostPostUploadAction::QueuePrint; }
std::string get_host() const override { return "https://connect.prusa3d.com"; }
std::string get_host() const override { return "https://dev.connect.prusa3d.com"; }
bool get_storage(wxArrayString& storage_path, wxArrayString& storage_name) const override;
//const std::string& get_apikey() const { return m_apikey; }
//const std::string& get_cafile() const { return m_cafile; }