diff --git a/resources/icons/blank.png b/resources/icons/blank.png new file mode 100644 index 0000000000..a4c14c6338 Binary files /dev/null and b/resources/icons/blank.png differ diff --git a/source/main/Application.cpp b/source/main/Application.cpp index b25c55e36c..8822d4a120 100644 --- a/source/main/Application.cpp +++ b/source/main/Application.cpp @@ -128,6 +128,7 @@ CVar* mp_cyclethru_net_actors; // New remote API CVar* remote_query_url; +CVar* remote_login_token; // Diagnostic CVar* diag_auto_spawner_report; diff --git a/source/main/Application.h b/source/main/Application.h index ef0a52d4d3..1eaac1a37b 100644 --- a/source/main/Application.h +++ b/source/main/Application.h @@ -103,6 +103,11 @@ enum MsgType MSG_NET_DISCONNECT_REQUESTED, MSG_NET_USER_DISCONNECT, MSG_NET_RECV_ERROR, + MSG_NET_USERAUTH_SUCCESS, + MSG_NET_USERAUTH_FAILURE, + MSG_NET_USERAUTH_TFA_REQUESTED, + MSG_NET_USERAUTH_TFA_FAILURE, + MSG_NET_USERAUTH_TFA_TRIGGERED, MSG_NET_REFRESH_SERVERLIST_SUCCESS, //!< Payload = GUI::MpServerInfoVec* (owner) MSG_NET_REFRESH_SERVERLIST_FAILURE, //!< Payload = RoR::CurlFailInfo* (owner) MSG_NET_REFRESH_REPOLIST_SUCCESS, //!< Payload = GUI::ResourcesCollection* (owner) @@ -364,6 +369,7 @@ extern CVar* mp_cyclethru_net_actors; //!< Include remote actors when cycling th // New remote API extern CVar* remote_query_url; +extern CVar* remote_login_token; // Diagnostic extern CVar* diag_auto_spawner_report; diff --git a/source/main/CMakeLists.txt b/source/main/CMakeLists.txt index aed5361ad4..399eb54c67 100644 --- a/source/main/CMakeLists.txt +++ b/source/main/CMakeLists.txt @@ -119,6 +119,7 @@ set(SOURCE_FILES gui/panels/GUI_ConsoleWindow.{h,cpp} gui/panels/GUI_DirectionArrow.{h,cpp} gui/panels/GUI_LoadingWindow.{h,cpp} + gui/panels/GUI_LoginBox.{h,cpp} gui/panels/GUI_FlexbodyDebug.{h,cpp} gui/panels/GUI_FrictionSettings.{h,cpp} gui/panels/GUI_TopMenubar.{h,cpp} diff --git a/source/main/ForwardDeclarations.h b/source/main/ForwardDeclarations.h index d9f6deca60..9e14b142de 100644 --- a/source/main/ForwardDeclarations.h +++ b/source/main/ForwardDeclarations.h @@ -2,7 +2,7 @@ This source file is part of Rigs of Rods Copyright 2005-2012 Pierre-Michel Ricordel Copyright 2007-2012 Thomas Fischer - Copyright 2013-2020 Petr Ohlidal + Copyright 2013-2024 Petr Ohlidal For more information, see http://www.rigsofrods.org/ @@ -227,6 +227,7 @@ namespace RoR class SurveyMap; class TopMenubar; class VehicleButtons; + class LoginBox; } } // namespace RoR diff --git a/source/main/GameContext.cpp b/source/main/GameContext.cpp index 9abaf08048..974744e609 100644 --- a/source/main/GameContext.cpp +++ b/source/main/GameContext.cpp @@ -948,6 +948,10 @@ void GameContext::UpdateGlobalInputEvents() { App::GetGuiManager()->RepositorySelector.SetVisible(false); } + else if (App::GetGuiManager()->LoginBox.IsVisible()) + { + App::GetGuiManager()->LoginBox.SetVisible(false); + } else { this->PushMessage(Message(MSG_APP_SHUTDOWN_REQUESTED)); diff --git a/source/main/gui/GUIManager.cpp b/source/main/gui/GUIManager.cpp index da68f0c614..2570944797 100644 --- a/source/main/gui/GUIManager.cpp +++ b/source/main/gui/GUIManager.cpp @@ -435,6 +435,11 @@ void GUIManager::DrawMainMenuGui() { this->RepositorySelector.Draw(); } + + if (this->LoginBox.IsVisible()) + { + this->LoginBox.Draw(); + } } void GUIManager::ShowMessageBox(const char* title, const char* text, bool allow_close, const char* btn1_text, const char* btn2_text) diff --git a/source/main/gui/GUIManager.h b/source/main/gui/GUIManager.h index 90f82ae0a7..55d6e01ba7 100644 --- a/source/main/gui/GUIManager.h +++ b/source/main/gui/GUIManager.h @@ -53,6 +53,7 @@ #include "GUI_TopMenubar.h" #include "GUI_VehicleDescription.h" #include "GUI_VehicleButtons.h" +#include "GUI_LoginBox.h" // Deps #include @@ -126,6 +127,7 @@ class GUIManager GUI::DirectionArrow DirectionArrow; GUI::VehicleButtons VehicleButtons; GUI::FlexbodyDebug FlexbodyDebug; + GUI::LoginBox LoginBox; Ogre::Overlay* MenuWallpaper = nullptr; // GUI manipulation diff --git a/source/main/gui/panels/GUI_GameMainMenu.cpp b/source/main/gui/panels/GUI_GameMainMenu.cpp index 7b929f3182..d2c3803720 100644 --- a/source/main/gui/panels/GUI_GameMainMenu.cpp +++ b/source/main/gui/panels/GUI_GameMainMenu.cpp @@ -43,6 +43,7 @@ using namespace GUI; void GameMainMenu::Draw() { this->DrawMenuPanel(); + this->DrawProfileBox(); if (App::app_state->getEnum() == AppState::MAIN_MENU) { this->DrawVersionBox(); @@ -224,6 +225,42 @@ void GameMainMenu::DrawMenuPanel() m_kb_enter_index = -1; } +void GameMainMenu::DrawProfileBox() +{ + ImVec2 image_size = ImVec2(50, 50); + ImVec2 button_size = ImVec2(60, 30); + ImVec2 display_size = ImGui::GetIO().DisplaySize; + + const float window_height = 15.0f; + const float margin = display_size.y / 15.0f; + + ImGui::SetNextWindowPos(ImVec2(margin, margin)); + ImGui::PushStyleColor(ImGuiCol_WindowBg, WINDOW_BG_COLOR); + ImGuiWindowFlags flags = + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar; + if (ImGui::Begin(_LC("MainMenu", "Profile box"), nullptr, flags)) + { + ImGui::Image( + reinterpret_cast(FetchIcon("blank.png")->getHandle()), + image_size); + + ImGui::SameLine(); + if (ImGui::Button("Log in", button_size)) { + App::GetGuiManager()->LoginBox.SetVisible(true); + this->SetVisible(false); + } + + ImGui::SameLine(); + if (ImGui::Button("Regster", button_size)) { + // TODO open as a link + } + + ImGui::End(); + } + ImGui::PopStyleColor(1); +} + void GameMainMenu::DrawVersionBox() { const float margin = ImGui::GetIO().DisplaySize.y / 30.f; diff --git a/source/main/gui/panels/GUI_GameMainMenu.h b/source/main/gui/panels/GUI_GameMainMenu.h index 573d553b40..e84a759afa 100644 --- a/source/main/gui/panels/GUI_GameMainMenu.h +++ b/source/main/gui/panels/GUI_GameMainMenu.h @@ -49,6 +49,7 @@ class GameMainMenu void DrawMenuPanel(); void DrawVersionBox(); void DrawNoticeBox(); + void DrawProfileBox(); bool HighlightButton(const std::string &text, ImVec2 btn_size, int index) const; void HandleInputEvents(); bool m_is_visible = false; diff --git a/source/main/gui/panels/GUI_LoginBox.cpp b/source/main/gui/panels/GUI_LoginBox.cpp new file mode 100644 index 0000000000..49f8447065 --- /dev/null +++ b/source/main/gui/panels/GUI_LoginBox.cpp @@ -0,0 +1,451 @@ +/* + This source file is part of Rigs of Rods + Copyright 2005-2012 Pierre-Michel Ricordel + Copyright 2007-2012 Thomas Fischer + Copyright 2013-2024 Petr Ohlidal + + For more information, see http://www.rigsofrods.org/ + + Rigs of Rods is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License version 3, as + published by the Free Software Foundation. + + Rigs of Rods is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Rigs of Rods. If not, see . +*/ + +/// @file GUI_LoginBox.cpp +/// @author Rafael Galvan, 2024 + +#include "GUI_LoginBox.h" + +#include "Application.h" +#include "GameContext.h" +#include "GUIManager.h" +#include "GUIUtils.h" +#include "AppContext.h" +#include "Language.h" +#include "RoRVersion.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef USE_URL +# include +# include +#endif + +#if defined(_MSC_VER) && defined(GetObject) // This MS Windows macro from (Windows Kit 8.1) clashes with RapidJSON +# undef GetObject +#endif + +using namespace RoR; +using namespace GUI; + +#if defined(USE_CURL) + +static size_t CurlWriteFunc(void* ptr, size_t size, size_t nmemb, std::string* data) +{ + data->append((char*)ptr, size * nmemb); + return size * nmemb; +} + +void PostAuthWithTfa(std::string login, std::string passwd, std::string provider, std::string code) +{ + rapidjson::Document j_request_body; + j_request_body.SetObject(); + j_request_body.AddMember("login", rapidjson::StringRef(login.c_str()), j_request_body.GetAllocator()); + j_request_body.AddMember("password", rapidjson::StringRef(passwd.c_str()), j_request_body.GetAllocator()); + j_request_body.AddMember("limit_ip", rapidjson::StringRef("1.1.1.1"), j_request_body.GetAllocator()); + j_request_body.AddMember("tfa_provider", rapidjson::StringRef(provider.c_str()), j_request_body.GetAllocator()); + j_request_body.AddMember("code", rapidjson::StringRef(code.c_str()), j_request_body.GetAllocator()); + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + j_request_body.Accept(writer); + std::string request_body = buffer.GetString(); + + std::string user_agent = fmt::format("{}/{}", "Rigs of Rods Client", ROR_VERSION_STRING); + std::string url = App::remote_query_url->getStr() + "/auth"; + std::string response_payload; + std::string response_header; + long response_code = 0; + + struct curl_slist* slist; + slist = NULL; + slist = curl_slist_append(slist, "Content-Type: application/json"); + + + CURL* curl = curl_easy_init(); + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); // todo api url + endpoint + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request_body.c_str()); // post request body +#ifdef _WIN32 + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); +#endif // _WIN32 + curl_easy_setopt(curl, CURLOPT_POST, 1); + curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip"); + curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteFunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_payload); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_header); + + curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + + curl_easy_cleanup(curl); + curl = nullptr; + + if (response_code == 400) // a failure, bad tfa code + { + App::GetGameContext()->PushMessage( + Message(MSG_NET_USERAUTH_TFA_FAILURE, _LC("Login", "The two-step verification value could not be confirmed. Please retry.")) + ); + return; + } + else if (response_code != 200) // a net failure, restart from beginning + { + App::GetGameContext()->PushMessage( + Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "Connection error. Please check your connection and try again.")) + ); + return; + } + + // if tfa success, then sso success + App::GetGameContext()->PushMessage(Message(MSG_NET_USERAUTH_SUCCESS)); +} + +void PostAuthTriggerTfa(std::string login, std::string passwd, std::string provider) +{ + rapidjson::Document j_request_body; + j_request_body.SetObject(); + j_request_body.AddMember("login", rapidjson::StringRef(login.c_str()), j_request_body.GetAllocator()); + j_request_body.AddMember("password", rapidjson::StringRef(passwd.c_str()), j_request_body.GetAllocator()); + j_request_body.AddMember("tfa_provider", rapidjson::StringRef(provider.c_str()), j_request_body.GetAllocator()); + j_request_body.AddMember("tfa_trigger", rapidjson::StringRef("true"), j_request_body.GetAllocator()); + j_request_body.AddMember("limit_ip", rapidjson::StringRef("1.1.1.1"), j_request_body.GetAllocator()); + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + j_request_body.Accept(writer); + std::string request_body = buffer.GetString(); + + std::string user_agent = fmt::format("{}/{}", "Rigs of Rods Client", ROR_VERSION_STRING); + std::string url = App::remote_query_url->getStr() + "/auth"; + std::string response_payload; + std::string response_header; + long response_code = 0; + + struct curl_slist* slist; + slist = NULL; + slist = curl_slist_append(slist, "Content-Type: application/json"); + + CURL* curl = curl_easy_init(); + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request_body.c_str()); + curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); +#ifdef _WIN32 + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); +#endif // _WIN32 + curl_easy_setopt(curl, CURLOPT_POST, 1); + curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip"); + curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteFunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_payload); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_header); + + curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + + curl_easy_cleanup(curl); + curl = nullptr; + + if (response_code != 200) + { + App::GetGameContext()->PushMessage( + Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "Connection error. Please check your connection and try again.")) + ); + return; + } + + App::GetGameContext()->PushMessage(Message(MSG_NET_USERAUTH_TFA_TRIGGERED)); +} + +void PostAuth(std::string login, std::string passwd) +{ + rapidjson::Document j_request_body; + j_request_body.SetObject(); + j_request_body.AddMember("login", rapidjson::StringRef(login.c_str()), j_request_body.GetAllocator()); + j_request_body.AddMember("password", rapidjson::StringRef(passwd.c_str()), j_request_body.GetAllocator()); + j_request_body.AddMember("limit_ip", rapidjson::StringRef("1.1.1.1"), j_request_body.GetAllocator()); + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + j_request_body.Accept(writer); + std::string request_body = buffer.GetString(); + + std::string user_agent = fmt::format("{}/{}", "Rigs of Rods Client", ROR_VERSION_STRING); + std::string url = App::remote_query_url->getStr() + "/auth"; + std::string response_payload; + std::string response_header; + long response_code = 0; + + struct curl_slist* slist; + slist = NULL; + slist = curl_slist_append(slist, "Content-Type: application/json"); + + CURL* curl = curl_easy_init(); + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); +#ifdef _WIN32 + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); +#endif // _WIN32 + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request_body.c_str()); + curl_easy_setopt(curl, CURLOPT_POST, 1); + curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip"); + curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteFunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_payload); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_header); + + curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + + curl_easy_cleanup(curl); + curl = nullptr; + + if (response_code == 400) + { + App::GetGameContext()->PushMessage( + Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "You did not sign in correctly or your account is temporarily disabled. Please retry.")) + ); + return; + } + else if (response_code == 401) + { + App::GetGameContext()->PushMessage( + Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "Could not log you in. Your account has been suspended.")) + ); + return; + } + else if (response_code >= 300) + { + Ogre::LogManager::getSingleton().stream() + << "[RoR|User|Auth] Failed to sign user in; HTTP status code: " << response_code; + App::GetGameContext()->PushMessage( + Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "Connection error. Please check your connection and try again.")) + ); + return; + } + + rapidjson::Document j_response_body; + j_response_body.Parse(response_payload.c_str()); + if (j_response_body.HasParseError() || !j_response_body.IsObject()) + { + App::GetGameContext()->PushMessage( + Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "Received malformed data. Please retry.")) + ); + return; + } + + if (response_code == 202) + { + std::vector* tfa_providers_ptr = new std::vector(); + rapidjson::Value& j_tfa_providers = j_response_body["tfa_providers"]; + for (auto&& item : j_tfa_providers.GetArray()) + { + tfa_providers_ptr->push_back(item.GetString()); + } + App::GetGameContext()->PushMessage(Message(MSG_NET_USERAUTH_TFA_REQUESTED, static_cast(tfa_providers_ptr))); + return; + } + + App::GetGameContext()->PushMessage(Message(MSG_NET_USERAUTH_FAILURE)); +} + +#endif + +LoginBox::LoginBox() + : m_base_url(App::remote_query_url->getStr() + "/auth") +{} + +LoginBox::~LoginBox() +{} + +void LoginBox::Draw() +{ + // TODO do not load if the client is already signed in + + GUIManager::GuiTheme const& theme = App::GetGuiManager()->GetTheme(); + + ImGui::SetNextWindowSize(ImVec2(400.f, 250.f), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowPosCenter(ImGuiCond_Appearing); + ImGuiWindowFlags win_flags = + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoResize; + bool keep_open = true; + ImGui::Begin(_LC("Login", "Login"), &keep_open, win_flags); + + if (!m_loading) + { + if (!m_errors.empty()) + { + ImGui::TextColored(App::GetGuiManager()->GetTheme().error_text_color, "%s", m_errors.c_str()); + } + + if (m_needs_tfa) + { + // !! NEEDS REFACTORING !! + ImGui::BeginTabBar("TfaOptTab"); + if (std::find(m_tfa_providers.begin(), m_tfa_providers.end(), "totp") != m_tfa_providers.end()) + { + if (ImGui::BeginTabItem(_LC("Login", "Verification code via app"))) + { + m_tfa_provider = "totp"; + ImGui::TextWrapped(_LC("Login", "Please enter the verification code generated by the app on your phone.")); + ImGui::EndTabItem(); + } + } + if (std::find(m_tfa_providers.begin(), m_tfa_providers.end(), "email") != m_tfa_providers.end()) + { + if (ImGui::BeginTabItem(_LC("Login", "Email confirmation"))) + { + m_tfa_provider = "email"; + if (!m_tfa_trigger) + this->TriggerTfa(); // Without this, there will be an infinite loop. + ImGui::TextWrapped(_LC("Login", "An email has been sent with a single-use code. Please enter that code to continue.")); + ImGui::EndTabItem(); + } + } + ImGui::EndTabBar(); + ImGui::InputText("##2fa", m_tfa_code.GetBuffer(), m_tfa_code.GetCapacity()); + if (ImGui::Button("Confirm")) + { + this->ConfirmTfa(); + } + ImGui::Separator(); + ImGui::TextWrapped(_LC("Login", "A backup code can be used when you don't have access to an alternative verification method. To do so, you must login using a web browser.")); + } + else + { + ImGui::Text(_LC("Login", "Your name or email address")); + ImGui::InputText("##login", m_login.GetBuffer(), m_login.GetCapacity()); + ImGui::Text(_LC("Login", "Password")); + ImGui::InputText("##password", m_passwd.GetBuffer(), m_passwd.GetCapacity(), ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsNoBlank); + if (ImGui::Button("Login")) + { + this->Login(); + } + } + } + else + { + // !! REFACTOR THIS !! + float spinner_size = 27.f; + ImGui::SetCursorPosX((ImGui::GetWindowSize().x / 2.f) - spinner_size); + ImGui::SetCursorPosY((ImGui::GetWindowSize().y / 2.f) - spinner_size); + LoadingIndicatorCircle("spinner", spinner_size, theme.value_blue_text_color, theme.value_blue_text_color, 10, 10); + } + + ImGui::End(); + if (!keep_open) + { + this->SetVisible(false); + } +} + +void LoginBox::Login() +{ +#if defined(USE_CURL) + m_loading = true; + + if (m_login.IsEmpty() && m_passwd.IsEmpty()) + { + App::GetGameContext()->PushMessage( + Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "There must not be any empty fields.")) + ); + return; + } + + std::string login(m_login); + std::string passwd(m_passwd); + + std::packaged_task task(PostAuth); + std::thread(std::move(task), login, passwd).detach(); +#endif +} + +void LoginBox::ShowError(std::string const& msg) +{ + m_loading = false; + m_errors = msg; +} + +void LoginBox::ConfirmTfa() +{ +#if defined(USE_CURL) + m_loading = true; + + if (m_tfa_code.IsEmpty()) + { + App::GetGameContext()->PushMessage( + Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "There must not be any empty fields.")) + ); + return; + } + + std::string login(m_login); + std::string passwd(m_passwd); + std::string tfa_code(m_tfa_code); + + std::packaged_task task(PostAuthWithTfa); + std::thread(std::move(task), login, passwd, m_tfa_provider, tfa_code).detach(); +#endif +} + +void LoginBox::TriggerTfa() +{ +#if defined(USE_CURL) + m_loading = true; + m_tfa_trigger = true; + + std::string login(m_login); + std::string passwd(m_passwd); + + std::packaged_task task(PostAuthTriggerTfa); + std::thread(std::move(task), login, passwd, m_tfa_provider).detach(); +#endif +} + +void LoginBox::NeedsTfa(std::vector tfa_providers) +{ + m_loading = false; + m_needs_tfa = true; + m_tfa_providers = tfa_providers; +} + +void LoginBox::TfaTriggered() +{ + //m_tfa_trigger = false; + m_loading = false; +} + +void LoginBox::SetVisible(bool visible) +{ + m_is_visible = visible; + if (!visible && (App::app_state->getEnum() == AppState::MAIN_MENU)) + { + App::GetGuiManager()->GameMainMenu.SetVisible(true); + } +} \ No newline at end of file diff --git a/source/main/gui/panels/GUI_LoginBox.h b/source/main/gui/panels/GUI_LoginBox.h new file mode 100644 index 0000000000..3289e06696 --- /dev/null +++ b/source/main/gui/panels/GUI_LoginBox.h @@ -0,0 +1,81 @@ +/* + This source file is part of Rigs of Rods + Copyright 2005-2012 Pierre-Michel Ricordel + Copyright 2007-2012 Thomas Fischer + Copyright 2013-2024 Petr Ohlidal + + For more information, see http://www.rigsofrods.org/ + + Rigs of Rods is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License version 3, as + published by the Free Software Foundation. + + Rigs of Rods is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Rigs of Rods. If not, see . +*/ + +/// @file GUI_LoginBox.h +/// @author Rafael Galvan, 2024 + +#pragma once + +#include "Application.h" +#include "OgreImGui.h" + +#include +#include +#include +#include + +namespace RoR { +namespace GUI { + +struct UserLoginToken +{}; + +struct UserProfile +{ + std::string username; + std::string email; + std::string avatar_url; +}; + +const char* const ROUTE_LOGIN = "/login"; + +class LoginBox { +public: + LoginBox(); + ~LoginBox(); + + void SetVisible(bool visible); + bool IsVisible() const { return m_is_visible; } + void ShowError(std::string const& msg); + void ConfirmTfa(); + void TriggerTfa(); + void NeedsTfa(std::vector tfa_providers); + void TfaTriggered(); + void Login(); + void Draw(); + +private: + bool m_is_visible = false; + Str<1000> m_login; + Str<1000> m_passwd; + Str<1000> m_tfa_code; + bool m_remember = false; + std::string m_errors; + bool m_needs_tfa = false; + bool m_loading = false; + std::vector m_tfa_providers; + std::string m_tfa_provider; + bool m_tfa_trigger = false; + std::string m_base_url; +}; + +} +} \ No newline at end of file diff --git a/source/main/main.cpp b/source/main/main.cpp index adc108ec4a..af71d0ea0e 100644 --- a/source/main/main.cpp +++ b/source/main/main.cpp @@ -691,6 +691,37 @@ int main(int argc, char *argv[]) break; } + case MSG_NET_USERAUTH_SUCCESS: + { + // TODO assign cvar login token + break; + } + + case MSG_NET_USERAUTH_FAILURE: + { + App::GetGuiManager()->LoginBox.ShowError(m.description); + break; + } + + case MSG_NET_USERAUTH_TFA_REQUESTED: + { + std::vector* tfa_providers_ptr = reinterpret_cast*>(m.payload); + App::GetGuiManager()->LoginBox.NeedsTfa(*tfa_providers_ptr); + break; + } + + case MSG_NET_USERAUTH_TFA_FAILURE: + { + App::GetGuiManager()->LoginBox.ShowError(m.description); + break; + } + + case MSG_NET_USERAUTH_TFA_TRIGGERED: + { + App::GetGuiManager()->LoginBox.TfaTriggered(); + break; + } + case MSG_NET_REFRESH_SERVERLIST_SUCCESS: { GUI::MpServerInfoVec* data = static_cast(m.payload); diff --git a/source/main/system/CVar.cpp b/source/main/system/CVar.cpp index 99697ca057..8f4dc32a2f 100644 --- a/source/main/system/CVar.cpp +++ b/source/main/system/CVar.cpp @@ -76,6 +76,7 @@ void Console::cVarSetupBuiltins() App::mp_cyclethru_net_actors = this->cVarCreate("mp_cyclethru_net_actors", "", CVAR_ARCHIVE | CVAR_TYPE_BOOL, "false"); App::remote_query_url = this->cVarCreate("remote_query_url", "", CVAR_ARCHIVE, "https://v2.api.rigsofrods.org"); + App::remote_login_token = this->cVarCreate("remote_login_token", "", CVAR_ARCHIVE | CVAR_NO_LOG, ""); App::diag_auto_spawner_report= this->cVarCreate("diag_auto_spawner_report","AutoActorSpawnerReport", CVAR_ARCHIVE | CVAR_TYPE_BOOL, "false"); App::diag_camera = this->cVarCreate("diag_camera", "Camera Debug", CVAR_ARCHIVE | CVAR_TYPE_BOOL, "false");