diff --git a/CMakeLists.txt b/CMakeLists.txt index 3746276..94796a7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -158,4 +158,14 @@ target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ) +if(EXISTS ${CMAKE_SOURCE_DIR}/.env) + add_custom_command( + TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_SOURCE_DIR}/.env + $/.env + COMMENT "Copying .env to build directory" + ) +endif() + add_subdirectory(tests) \ No newline at end of file diff --git a/forms/login_window.ui b/forms/login_window.ui index 872344f..1117caf 100644 --- a/forms/login_window.ui +++ b/forms/login_window.ui @@ -10,6 +10,12 @@ 400 + + + 0 + 0 + + 🦀 Login @@ -31,7 +37,7 @@ Storage Crab - Qt::AlignmentFlag::AlignCenter + Qt::AlignCenter @@ -46,14 +52,17 @@ 🦀 - Qt::AlignmentFlag::AlignCenter + Qt::AlignCenter - Qt::Orientation::Vertical + Qt::Vertical + + + QSizePolicy::Expanding @@ -72,7 +81,7 @@ - Qt::AlignmentFlag::AlignCenter + Qt::AlignCenter @@ -85,7 +94,10 @@ - Qt::AlignmentFlag::AlignCenter + Qt::AlignCenter + + + Email @@ -98,22 +110,25 @@ - QLineEdit::EchoMode::Password + QLineEdit::Password - Qt::AlignmentFlag::AlignCenter + Qt::AlignCenter + + + Password - Qt::Orientation::Vertical + Qt::Vertical - 20 - 40 + 40 + 20 @@ -129,6 +144,7 @@ min-width: 150px; min-height: 40px; padding: 0 10px; + text-align: center; } QPushButton:hover { background-color: #2276E4; @@ -143,13 +159,26 @@ QPushButton:pressed { - + + + QPushButton { + background: transparent; + border: none; + padding: 0; + margin: 0; + text-align: center; + color: palette(windowText); + } + QPushButton:hover { + color: palette(windowText); + } + QPushButton:pressed { + color: palette(windowText); + } + Don't have an account yet? - - Qt::AlignmentFlag::AlignCenter - diff --git a/forms/register_window.ui b/forms/register_window.ui new file mode 100644 index 0000000..0ebaaf9 --- /dev/null +++ b/forms/register_window.ui @@ -0,0 +1,191 @@ + + + RegisterWindow + + + + 0 + 0 + 400 + 400 + + + + 🦀 Login + + + + + 14 + + + + + + + + 56 + + + + Storage Crab + + + Qt::AlignCenter + + + + + + + + 80 + + + + 🦀 + + + Qt::AlignCenter + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + color: red; + + + + + + Qt::AlignCenter + + + + + + + min-height: 1.25em; + + + + + + Qt::AlignCenter + + + Email + + + + + + + min-height: 1.25em; + + + Qt::AlignCenter + + + Username + + + + + + + min-height: 1.25em; + + + + + + QLineEdit::Password + + + Qt::AlignCenter + + + Password + + + + + + + + 0 + 22 + + + + min-height: 1.25em; + + + QLineEdit::Password + + + Qt::AlignCenter + + + Repeat password + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + QPushButton { + color: white; + font-size: 24pt; + background-color: #2684FF; + border: 0; + min-width: 150px; + min-height: 40px; + padding: 0 10px; +} +QPushButton:hover { + background-color: #2276E4; +} +QPushButton:pressed { + background-color: #2065BF; +} + + + Register + + + + + + + + + diff --git a/include/api/api_dispatcher.hpp b/include/api/api_dispatcher.hpp index 29c64ca..106e592 100644 --- a/include/api/api_dispatcher.hpp +++ b/include/api/api_dispatcher.hpp @@ -27,6 +27,8 @@ Q_OBJECT RequestResultFuture login(const std::string& email, const std::string& password_hash) const; + RequestResultFuture register_user(const std::string& email, const std::string& username, const std::string& password_hash) const; + RequestResultFuture me(); RequestResultFuture shareFile(const size_t fileID); diff --git a/include/api/requests.hpp b/include/api/requests.hpp index 8317a4f..af53330 100644 --- a/include/api/requests.hpp +++ b/include/api/requests.hpp @@ -12,8 +12,6 @@ #include "token_pair.h" -// TODO: refactor, move basic configurations to a helper function - namespace API::Requests { inline RequestResult POST( @@ -31,16 +29,20 @@ inline RequestResult POST( // String stream for retrieving std::ostringstream responseStream; + std::list headers; // Informing that we are using JSON - request.setOpt(cURLpp::options::HttpHeader({"Content-Type: application/json"})); + headers.push_back("Content-Type: application/json"); // Add authorization field if access token is provided if (!access_token.empty()) - request.setOpt(cURLpp::options::HttpHeader({"Authorization: Bearer " + access_token})); + headers.push_back("Authorization: Bearer " + access_token); + request.setOpt(cURLpp::options::HttpHeader(headers)); + // Adding the body and its size to request - request.setOpt(curlpp::options::PostFields(body.dump())); - request.setOpt(curlpp::options::PostFieldSize(static_cast(body.dump().length()))); + const std::string bodyStr = body.dump(); + request.setOpt(curlpp::options::PostFields(bodyStr)); + request.setOpt(curlpp::options::PostFieldSize(static_cast(bodyStr.length()))); request.setOpt(curlpp::options::WriteStream(&responseStream)); // Timeout @@ -64,6 +66,8 @@ inline RequestResult POST( return RequestResult::error_msg("Request timed out"); } catch (cURLpp::LogicError& e) { return RequestResult::error_msg("Logic error: " + std::string(e.what())); + } catch (std::exception& e) { + return RequestResult::error_msg("Other error: " + std::string(e.what())); } } @@ -85,11 +89,15 @@ inline RequestResult POST_UPLOAD( std::ostringstream responseStream; request.setOpt(cURLpp::options::WriteStream(&responseStream)); - // Forming header - request.setOpt(cURLpp::options::HttpHeader({"Content-Type: multipart/form-data"})); + std::list headers; + // Informing that we are using JSON + headers.push_back("Content-Type: multipart/form-data"); + // Add authorization field if access token is provided if (!access_token.empty()) - request.setOpt(cURLpp::options::HttpHeader({"Authorization: Bearer " + access_token})); + headers.push_back("Authorization: Bearer " + access_token); + + request.setOpt(cURLpp::options::HttpHeader(headers)); // Forming request cURLpp::Forms formParts; @@ -200,12 +208,15 @@ inline RequestResult GET( // String stream for retrieving std::ostringstream responseStream; + std::list headers; // Informing that we are using JSON - request.setOpt(cURLpp::options::HttpHeader({"Content-Type: application/json"})); + headers.push_back("Content-Type: multipart/form-data"); // Add authorization field if access token is provided if (!access_token.empty()) - request.setOpt(cURLpp::options::HttpHeader({"Authorization: Bearer " + access_token})); + headers.push_back("Authorization: Bearer " + access_token); + + request.setOpt(cURLpp::options::HttpHeader(headers)); request.setOpt(cURLpp::options::WriteStream(&responseStream)); diff --git a/include/windows/login_window.h b/include/windows/login_window.h index b69eb86..2033e6c 100644 --- a/include/windows/login_window.h +++ b/include/windows/login_window.h @@ -1,5 +1,4 @@ -#ifndef MAIN_WINDOW_H -#define MAIN_WINDOW_H +#pragma once #include #include @@ -20,6 +19,8 @@ Q_OBJECT private slots: void onLoginButtonClicked(); + void onRegisterButtonClicked(); + private: void onLoginSuccessfull(const API::RequestResult& response); @@ -30,6 +31,3 @@ private slots: QMovie *loadingAnimation; }; - - -#endif //MAIN_WINDOW_H diff --git a/include/windows/register_window.h b/include/windows/register_window.h new file mode 100644 index 0000000..ada2f0a --- /dev/null +++ b/include/windows/register_window.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include "api/request_result.hpp" + +QT_BEGIN_NAMESPACE +namespace Ui { class RegisterWindow; } +QT_END_NAMESPACE + +class RegisterWindow final : public QMainWindow { +Q_OBJECT + +public: + explicit RegisterWindow(QWidget *parent = nullptr); + ~RegisterWindow() override; + +private slots: + void onRegisterButtonClicked(); + +private: + void onRegisterSuccessfull(const API::RequestResult& response); + + void resetRegisterButton(); + + std::unique_ptr ui; + + QMovie *loadingAnimation; + +}; diff --git a/src/api/api_dispatcher.cpp b/src/api/api_dispatcher.cpp index d330834..07ba9b5 100644 --- a/src/api/api_dispatcher.cpp +++ b/src/api/api_dispatcher.cpp @@ -34,6 +34,19 @@ RequestResultFuture ApiDispatcher::login(const std::string& email, const std::st }); } +RequestResultFuture ApiDispatcher::register_user(const std::string& email, const std::string& username, const std::string& password_hash) const { + return dispatch([email, password_hash, username] { + return Requests::POST( + std::getenv("REGISTER_URL"), + { + {"email", email}, + {"username", username}, + {"password_hash", password_hash}, + } + ); + }); +} + RequestResultFuture ApiDispatcher::me() { return dispatch([this] { return Requests::GET(std::getenv("ME_URL"), this->tokenPair.getAccess()); diff --git a/src/windows/login_window.cpp b/src/windows/login_window.cpp index 135b79f..61c2f7a 100644 --- a/src/windows/login_window.cpp +++ b/src/windows/login_window.cpp @@ -9,6 +9,7 @@ #include "api/api_dispatcher.hpp" #include "utils/watch_future.hpp" #include "windows/user_dashboard.h" +#include "windows/register_window.h" LoginWindow::LoginWindow(QWidget *parent) : QMainWindow(parent), ui(std::make_unique()) @@ -25,6 +26,7 @@ LoginWindow::LoginWindow(QWidget *parent) loadingAnimation->setFileName(QString(ASSETS_PATH) + "/loading.gif"); connect(ui->loginButton, &QPushButton::clicked, this, &LoginWindow::onLoginButtonClicked); + connect(ui->registerButton, &QPushButton::clicked, this, &LoginWindow::onRegisterButtonClicked); } LoginWindow::~LoginWindow() = default; @@ -44,9 +46,9 @@ void LoginWindow::onLoginButtonClicked() { loadingAnimation->start(); const std::string email = ui->emailLineEdit->text().toStdString(); - const std::string pass_hash = ui->passwordLineEdit->text().toStdString(); + const std::size_t pass_hash = std::hash{}(ui->passwordLineEdit->text().toStdString()); watchFuture( - this, ApiDispatcher::instance().login(email, pass_hash), + this, ApiDispatcher::instance().login(email, std::to_string(pass_hash)), [this](const API::RequestResult& response) { this->onLoginSuccessfull(response); }, [this](const API::RequestResult& response) { ui->errorLabel->setText(QString::fromStdString(response.extractErrorDetails())); @@ -55,6 +57,14 @@ void LoginWindow::onLoginButtonClicked() { ); } +void LoginWindow::onRegisterButtonClicked() { + this->close(); // Close current window + + auto *register_window = new RegisterWindow; + register_window->setAttribute(Qt::WA_DeleteOnClose); + register_window->show(); +} + void LoginWindow::onLoginSuccessfull(const API::RequestResult& response) { ApiDispatcher::instance().storeTokens(response.body.at("access_token"), response.body.at("refresh_token")); @@ -64,10 +74,9 @@ void LoginWindow::onLoginSuccessfull(const API::RequestResult& response) { [this](const API::RequestResult& response) { this->close(); // Close current window - // Proceed to player's personal shelter - auto *shelter = new UserDashboard(response.body.at("username").get()); - shelter->setAttribute(Qt::WA_DeleteOnClose); // Automatically frees memory allocated for this window - shelter->show(); + auto *dashboard = new UserDashboard(response.body.at("username").get()); + dashboard->setAttribute(Qt::WA_DeleteOnClose); + dashboard->show(); }, [this](const API::RequestResult& response) { QMessageBox::critical(this, "Error", QString::fromStdString(response.extractErrorDetails())); diff --git a/src/windows/register_window.cpp b/src/windows/register_window.cpp new file mode 100644 index 0000000..94fa02a --- /dev/null +++ b/src/windows/register_window.cpp @@ -0,0 +1,69 @@ +#include "windows/register_window.h" +#include "ui_register_window.h" + +#include + +#include "api/api_dispatcher.hpp" +#include "windows/login_window.h" +#include "utils/watch_future.hpp" + +RegisterWindow::RegisterWindow(QWidget* parent) + : QMainWindow(parent), ui(std::make_unique()) +{ + ui->setupUi(this); + + loadingAnimation = new QMovie(this); + loadingAnimation->setFileName(QString(ASSETS_PATH) + "/loading.gif"); + + connect(ui->registerButton, &QPushButton::clicked, this, &RegisterWindow::onRegisterButtonClicked); +} + +RegisterWindow::~RegisterWindow() = default; + +void RegisterWindow::onRegisterButtonClicked() { + if (ui->emailLineEdit->text().isEmpty() || ui->passwordLineEdit->text().isEmpty() + || ui->passwordRepeatLineEdit->text().isEmpty() || ui->usernameLineEdit->text().isEmpty()) { + ui->errorLabel->setText("Please fill the fields below"); + return; + } + + if (ui->passwordLineEdit->text() != ui->passwordRepeatLineEdit->text()) { + ui->errorLabel->setText("Passwords don't match"); + return; + } + + ui->registerButton->setText(""); + connect(loadingAnimation, &QMovie::frameChanged, ui->registerButton, [this] { + ui->registerButton->setIcon(loadingAnimation->currentPixmap()); + }); + loadingAnimation->start(); + + const std::string email = ui->emailLineEdit->text().toStdString(); + const std::string username = ui->usernameLineEdit->text().toStdString(); + const std::size_t pass_hash = std::hash{}(ui->passwordLineEdit->text().toStdString()); + watchFuture( + this, ApiDispatcher::instance().register_user(email, username, std::to_string(pass_hash)), + [this](const API::RequestResult& response) { this->onRegisterSuccessfull(response); }, + [this](const API::RequestResult& response) { + ui->errorLabel->setText(QString::fromStdString(response.extractErrorDetails())); + this->resetRegisterButton(); + } + ); +} + +void RegisterWindow::onRegisterSuccessfull(const API::RequestResult& response) { + std::string welcome_msg = "Welcome aboard, " + response.body.at("username").get(); + QMessageBox::information(this, "Success", welcome_msg.c_str()); + + // Return to login page + this->close(); + auto *login_window = new LoginWindow; + login_window->setAttribute(Qt::WA_DeleteOnClose); + login_window->show(); +} + +void RegisterWindow::resetRegisterButton() { + loadingAnimation->stop(); + ui->registerButton->setText("Register"); + ui->registerButton->setIcon(QIcon()); +}