diff --git a/CMakeLists.txt b/CMakeLists.txt index 79a4abc..2ee7098 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,11 +3,13 @@ cmake_minimum_required(VERSION 3.16) project(llink VERSION 0.1 LANGUAGES CXX) set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets + Network ) qt_standard_project_setup(REQUIRES 6.8) @@ -19,6 +21,10 @@ qt_add_executable(llink src/VerticalLabel.cpp src/VerticalLabel.h src/mainwindow.ui + src/authmanager.cpp + src/networkmanager.cpp + src/authdialog.ui + src/authdialog.cpp ) qt_add_resources(llink @@ -26,6 +32,7 @@ qt_add_resources(llink FILES assets/icons/mono/basic/menu.svg assets/icons/mono/basic/plus.svg + assets/FLOWY-4.png assets/logo.png styles/remarkable.qss ) @@ -46,6 +53,7 @@ target_link_libraries(llink Qt6::Core Qt6::Gui Qt6::Widgets + Qt6::Network ) include(GNUInstallDirs) diff --git a/CMakeLists.txt.user b/CMakeLists.txt.user index f2a6217..ca8dec4 100644 --- a/CMakeLists.txt.user +++ b/CMakeLists.txt.user @@ -1,6 +1,6 @@ - + EnvironmentId @@ -86,6 +86,9 @@ true + + true + @@ -103,15 +106,15 @@ 2 false - -DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake + -DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} +-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} +-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} -DCMAKE_BUILD_TYPE:STRING=Debug -DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX} -DCMAKE_GENERATOR:STRING=Ninja --DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON --DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} -DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx} --DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} --DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} +-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake +-DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON 0 /Users/talksik/Documents/code/llink/build/Qt_6_8_6_for_macOS-Debug @@ -161,15 +164,15 @@ 2 false - -DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake + -DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} +-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} +-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX} -DCMAKE_GENERATOR:STRING=Ninja --DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON --DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} -DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx} --DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} --DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} +-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake +-DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON /Users/talksik/Documents/code/llink/build/Qt_6_8_6_for_macOS-Release @@ -216,15 +219,15 @@ 2 false - -DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake + -DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} +-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} +-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} -DCMAKE_BUILD_TYPE:STRING=RelWithDebInfo -DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX} -DCMAKE_GENERATOR:STRING=Ninja --DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON --DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} -DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx} --DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} --DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} +-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake +-DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON /Users/talksik/Documents/code/llink/build/Qt_6_8_6_for_macOS-RelWithDebInfo @@ -271,15 +274,15 @@ 2 false - -DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake + -DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} +-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} +-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} -DCMAKE_BUILD_TYPE:STRING=RelWithDebInfo -DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX} -DCMAKE_GENERATOR:STRING=Ninja --DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON --DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} -DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx} --DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} --DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} +-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake +-DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON 0 /Users/talksik/Documents/code/llink/build/Qt_6_8_6_for_macOS-Profile @@ -327,15 +330,15 @@ 2 false - -DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake + -DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} +-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} +-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} -DCMAKE_BUILD_TYPE:STRING=MinSizeRel -DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX} -DCMAKE_GENERATOR:STRING=Ninja --DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON --DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} -DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx} --DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} --DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} +-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake +-DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON /Users/talksik/Documents/code/llink/build/Qt_6_8_6_for_macOS-MinSizeRel @@ -435,12 +438,14 @@ false -e cpu-cycles --call-graph dwarf,4096 -F 250 - - ProjectExplorer.CustomExecutableRunConfiguration - + llink + CMakeProjectManager.CMakeRunConfiguration. + llink false true + true true + /Users/talksik/Documents/code/llink/build/Qt_6_8_6_for_macOS-Debug/llink.app/Contents/MacOS 1 diff --git a/assets/.DS_Store b/assets/.DS_Store new file mode 100644 index 0000000..aa18b3c Binary files /dev/null and b/assets/.DS_Store differ diff --git a/assets/FLOWY-4.png b/assets/FLOWY-4.png new file mode 100644 index 0000000..e39f5ec Binary files /dev/null and b/assets/FLOWY-4.png differ diff --git a/main.cpp b/main.cpp index f7a480c..73a656c 100644 --- a/main.cpp +++ b/main.cpp @@ -1,18 +1,22 @@ #include #include #include "src/MainWindow.h" +#include int main(int argc, char *argv[]) { - qDebug() << "hello world"; + QCoreApplication::setOrganizationName("Flowy Labs, Inc"); + QCoreApplication::setOrganizationDomain("flowylabs.ai"); + QCoreApplication::setApplicationName("llink"); + QApplication a(argc, argv); // Load reMarkable e-ink stylesheet - QFile styleFile(":/styles/remarkable.qss"); - if (styleFile.open(QFile::ReadOnly | QFile::Text)) { - a.setStyleSheet(styleFile.readAll()); - styleFile.close(); - } + // QFile styleFile(":/styles/remarkable.qss"); + // if (styleFile.open(QFile::ReadOnly | QFile::Text)) { + // a.setStyleSheet(styleFile.readAll()); + // styleFile.close(); + // } MainWindow w; w.show(); diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index a40420c..6d8ab3a 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -3,55 +3,68 @@ // #include "MainWindow.h" -#include -#include -#include #include "ui_mainwindow.h" +#include #include +#include +#include +#include "authmanager.h" +#include "authdialog.h" -MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), m_trayIcon(nullptr) +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent), m_trayIcon(nullptr), m_authManager(new AuthManager(this)), m_authDialog(nullptr) { - ui = new Ui::MainWindow; - ui->setupUi(this); - QWidget *widget = new QWidget(this); - ui->tabWidget->addTab(widget, "Flowy Labs, Inc"); + m_authDialog = new AuthDialog(m_authManager, this); - connect(ui->pushButton, &QPushButton::clicked, this, [this]() { - qDebug() << "Button clicked for signing on"; - // TODO: this would make some other calls, and then - // we listen to another signal from some service/underlying model - // when auth state changes - // And then we can hide the UI or change the stackview - ui->tabWidget->setCurrentIndex(1); + ui = new Ui::MainWindow; + ui->setupUi(this); - // we can call some other method to initialize some data whenever we are done logging in - // the complexity of this flow in qml is similar but just less imperative, more declarative - // managing the ui is technically more work, but not once we get used to it, perhaps. - }); - setupTrayIcon(); + // Show auth dialog initially (user sees this while init() checks for session) + m_authDialog->setModal(true); + m_authDialog->reset(); + m_authDialog->show(); + + // Connect signal so dialog hides if valid session found, shows and resets on sign out + connect(m_authManager, &AuthManager::isSignedInChanged, this, [this]() { + if (m_authManager->isSignedIn()) + { + m_authDialog->hide(); + } + else + { + m_authDialog->reset(); + m_authDialog->show(); + } + }); + + // Check for cached session (async) + m_authManager->init(); + + setupTrayIcon(); } void MainWindow::setupTrayIcon() { - m_trayIcon = new QSystemTrayIcon(this); - m_trayIcon->setIcon(QIcon(":/assets/logo.png")); - m_trayIcon->setToolTip("llink"); + m_trayIcon = new QSystemTrayIcon(this); + m_trayIcon->setIcon(QIcon(":/assets/logo.png")); + m_trayIcon->setToolTip("llink"); - QMenu *trayMenu = new QMenu(this); - trayMenu->addAction("Show", this, &MainWindow::show); - trayMenu->addAction("Quit", qApp, &QApplication::quit); - m_trayIcon->setContextMenu(trayMenu); + QMenu *trayMenu = new QMenu(this); + trayMenu->addAction("Show", this, &MainWindow::show); + trayMenu->addAction("Quit", qApp, &QApplication::quit); + m_trayIcon->setContextMenu(trayMenu); - connect(m_trayIcon, &QSystemTrayIcon::activated, this, [this](QSystemTrayIcon::ActivationReason reason) { - if (reason == QSystemTrayIcon::Trigger) { - isVisible() ? hide() : show(); - } - }); + connect(m_trayIcon, &QSystemTrayIcon::activated, this, [this](QSystemTrayIcon::ActivationReason reason) { + if (reason == QSystemTrayIcon::Trigger) + { + isVisible() ? hide() : show(); + } + }); - m_trayIcon->show(); + m_trayIcon->show(); } MainWindow::~MainWindow() { - delete ui; + delete ui; } diff --git a/src/MainWindow.h b/src/MainWindow.h index d6097f2..21a9ad6 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -9,6 +9,8 @@ QT_BEGIN_NAMESPACE class QSystemTrayIcon; +class AuthManager; +class AuthDialog; QT_END_NAMESPACE namespace Ui { @@ -29,6 +31,9 @@ private: QSystemTrayIcon *m_trayIcon; Ui::MainWindow *ui; + + AuthManager *m_authManager; + AuthDialog *m_authDialog; }; #endif //LLINK_MAINWINDOW_H diff --git a/src/authdialog.cpp b/src/authdialog.cpp new file mode 100644 index 0000000..b5724ec --- /dev/null +++ b/src/authdialog.cpp @@ -0,0 +1,118 @@ +#include "authdialog.h" +#include "authmanager.h" + +AuthDialog::AuthDialog(AuthManager *authManager, QWidget *parent) + : QDialog{parent}, m_authManager(authManager), m_state(ENTER_EMAIL) +{ + ui = new Ui::Dialog; + ui->setupUi(this); + + // initialize form based on current state + updateForm(); + + connect(ui->sendCodeButton, &QPushButton::clicked, this, [this]() { + QString emailInput = ui->emailLineEdit->text(); + if (emailInput.isEmpty()) + { + ui->errorMessage->setText("Must provide a valid email"); + return; + } + + ui->errorMessage->setText(""); + + m_authManager->requestSignInCode(emailInput); + }); + + connect(ui->signInButton, &QPushButton::clicked, this, [this]() { + QString codeInput = ui->codeLineEdit->text(); + if (codeInput.isEmpty()) + { + ui->errorMessage->setText("Must provide a valid code"); + return; + } + + ui->errorMessage->setText(""); + + m_authManager->signIn(codeInput); + }); + + connect(m_authManager, &AuthManager::codeSentToEmail, this, [this](const QString &email) { + m_state = ENTER_CODE; + updateForm(); + }); + + connect(m_authManager, &AuthManager::isSignedInChanged, this, [this]() { + if (m_authManager->isSignedIn()) + { + m_state = SIGNED_IN; + } + else + { + m_state = ENTER_EMAIL; + } + + updateForm(); + }); + + connect(m_authManager, &AuthManager::errorOccurred, this, [this](const QString &message) { + ui->errorMessage->setText(message); + ui->errorMessage->show(); + }); +} + +void AuthDialog::reset() +{ + m_state = ENTER_EMAIL; + ui->emailLineEdit->clear(); + ui->codeLineEdit->clear(); + updateForm(); +} + +void AuthDialog::updateForm() +{ + ui->errorMessage->setText(""); + + switch (m_state) + { + case ENTER_EMAIL: + ui->successLabel->hide(); + + ui->sendCodeButton->show(); + ui->emailLineEdit->show(); + ui->emailLabel->show(); + + ui->signInButton->hide(); + ui->codeLabel->hide(); + ui->codeLineEdit->hide(); + break; + case ENTER_CODE: + ui->successLabel->hide(); + + ui->sendCodeButton->hide(); + ui->emailLineEdit->hide(); + ui->emailLabel->hide(); + + ui->signInButton->show(); + ui->codeLabel->show(); + ui->codeLineEdit->show(); + + break; + case SIGNED_IN: + ui->successLabel->show(); + + ui->sendCodeButton->hide(); + ui->emailLineEdit->hide(); + ui->emailLabel->hide(); + ui->signInButton->hide(); + ui->codeLabel->hide(); + ui->codeLineEdit->hide(); + break; + default: + break; + } +} + +AuthDialog::~AuthDialog() +{ + delete ui; +} diff --git a/src/authdialog.h b/src/authdialog.h new file mode 100644 index 0000000..23fc7f7 --- /dev/null +++ b/src/authdialog.h @@ -0,0 +1,37 @@ +#ifndef AUTHDIALOG_H +#define AUTHDIALOG_H + +#include +#include + +#include "ui_authdialog.h" + +class AuthManager; + +class AuthDialog : public QDialog +{ + Q_OBJECT +public: + explicit AuthDialog(AuthManager *, QWidget *parent = nullptr); + ~AuthDialog(); + + enum State + { + ENTER_EMAIL, + ENTER_CODE, + SIGNED_IN + }; + + void reset(); + +signals: + +private: + Ui::Dialog *ui; + AuthManager *m_authManager; + State m_state; + + void updateForm(); +}; + +#endif // AUTHDIALOG_H diff --git a/src/authdialog.ui b/src/authdialog.ui new file mode 100644 index 0000000..54981ce --- /dev/null +++ b/src/authdialog.ui @@ -0,0 +1,116 @@ + + + Dialog + + + + 0 + 0 + 400 + 300 + + + + Dialog + + + + + 80 + 30 + 231 + 183 + + + + Sign in + + + + + + Email + + + + + + + + 0 + 0 + + + + IBeamCursor + + + + + + + Code + + + + + + + + + + Error + + + + + + + + + 230 + 250 + 101 + 32 + + + + Send code + + + true + + + + + + 230 + 250 + 101 + 32 + + + + Sign in + + + true + + + + + + 120 + 120 + 121 + 16 + + + + Successful + + + + + + diff --git a/src/authmanager.cpp b/src/authmanager.cpp new file mode 100644 index 0000000..b43764d --- /dev/null +++ b/src/authmanager.cpp @@ -0,0 +1,218 @@ +#include "authmanager.h" +#include "networkmanager.h" +#include +#include +#include +#include +#include + +namespace +{ +constexpr char key[] = "network/session_token"; +} + +AuthManager::AuthManager(QObject *parent) + : QObject{parent}, m_sessionData(nullptr), m_settings(new QSettings(this)), m_isSignedIn(false), m_lastEmail(""), + m_sessionToken("") +{ + connect(&NetworkManager::instance(), &NetworkManager::unauthorizedDetected, this, [this]() { + if (!m_isSignedIn) + { + return; + } + + setIsSignedIn(false); + if (m_sessionData) + { + delete m_sessionData; + m_sessionData = nullptr; + } + setSessionToken(""); + }); +} + +void AuthManager::init() +{ + QString storageSessionToken = m_settings->value(key).toString(); + if (storageSessionToken.isEmpty()) + { + return; + } + qDebug() << "Storage session token is: " << storageSessionToken; + + // set it here, and unset if it fails...network manager needs to inject into the request + setSessionToken(storageSessionToken); + + QNetworkReply *reply = NetworkManager::instance().get("/auth/me"); + connect(reply, &QNetworkReply::finished, this, [=]() { + reply->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) + { + qDebug() << "Saved session token is invalid"; + setSessionToken(""); + return; + } + + qDebug() << "Saved session token is valid"; + + QByteArray responseData = reply->readAll(); + QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData); + if (jsonDoc.isNull()) + { + qWarning() << "Failed to create a JSON doc."; + setSessionToken(""); + return; + } + + QJsonObject jsonObj = jsonDoc.object(); + + if (!m_sessionData) + { + m_sessionData = new SessionData{}; + } + m_sessionData->id = jsonObj["id"].toString(); + m_sessionData->email = jsonObj["email"].toString(); + m_sessionData->emailPrefix = jsonObj["email_prefix"].toString(); + + setSessionToken(storageSessionToken); + + setIsSignedIn(true); + }); +} + +void AuthManager::setIsSignedIn(bool newValue) +{ + if (m_isSignedIn == newValue) + { + return; + } + + m_isSignedIn = newValue; + emit isSignedInChanged(); +} + +bool AuthManager::isSignedIn() const +{ + return m_isSignedIn; +} + +SessionData *AuthManager::sessionData() const +{ + return m_sessionData; +} + +void AuthManager::requestSignInCode(const QString &email) +{ + if (isSignedIn()) + { + qWarning() << "Must sign out first"; + return; + } + + QJsonDocument body; + QJsonObject object; + object["email"] = email; + body.setObject(object); + QNetworkReply *reply = NetworkManager::instance().post("/auth/request-code", body); + + connect(reply, &QNetworkReply::finished, this, [=]() { + reply->deleteLater(); + if (reply->error() != QNetworkReply::NoError) + { + emit errorOccurred("Unable to send code. Please try again."); + qDebug() << "Error in emailing auth code: " << reply->errorString(); + return; + } + + qDebug() << "Code successfully sent"; + m_lastEmail = email; + emit codeSentToEmail(email); + }); +} + +void AuthManager::signIn(const QString &code) +{ + if (isSignedIn()) + { + qWarning() << "Must sign out first"; + return; + } + assert(!m_lastEmail.isEmpty()); + + QJsonDocument body; + QJsonObject object; + object["email"] = m_lastEmail; + object["code"] = code; + body.setObject(object); + QNetworkReply *reply = NetworkManager::instance().post("/auth/sign-in", body); + + connect(reply, &QNetworkReply::finished, this, [=]() { + reply->deleteLater(); + if (reply->error() != QNetworkReply::NoError) + { + emit errorOccurred("Unable to verify code. Please try again."); + qDebug() << "Error in verify code: " << reply->errorString(); + return; + } + + qDebug() << "Session successfully created"; + QByteArray responseData = reply->readAll(); + QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData); + if (jsonDoc.isNull()) + { + qWarning() << "Failed to create a JSON doc."; + return; + } + + QJsonObject jsonObj = jsonDoc.object(); + + if (!m_sessionData) + { + m_sessionData = new SessionData{}; + } + QJsonObject human = jsonObj["human"].toObject(); + m_sessionData->id = human["id"].toString(); + m_sessionData->email = human["email"].toString(); + m_sessionData->emailPrefix = human["email_prefix"].toString(); + + setSessionToken(jsonObj["token"].toString()); + + setIsSignedIn(true); + }); +} + +void AuthManager::signOut() +{ + if (!isSignedIn()) + { + qWarning() << "Not signed in, skipping sign out"; + return; + } + + setIsSignedIn(false); + if (m_sessionData) + { + delete m_sessionData; + m_sessionData = nullptr; + } + setSessionToken(""); + + QNetworkReply *reply = NetworkManager::instance().post("/auth/sign-out", QJsonDocument()); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); +} + +void AuthManager::setSessionToken(const QString &newValue) +{ + if (newValue.isEmpty()) + { + m_settings->remove(key); + } + else + { + m_settings->setValue(key, newValue); + } + + m_sessionToken = newValue; + NetworkManager::instance().setSessionToken(newValue); +} diff --git a/src/authmanager.h b/src/authmanager.h new file mode 100644 index 0000000..1d74998 --- /dev/null +++ b/src/authmanager.h @@ -0,0 +1,69 @@ +#ifndef AUTHMANAGER_H +#define AUTHMANAGER_H + +#include + +QT_BEGIN_NAMESPACE +class QSettings; +QT_END_NAMESPACE + +struct SessionData +{ + QString id; + QString email; + QString emailPrefix; +}; + +class AuthManager : public QObject +{ + Q_OBJECT +public: + explicit AuthManager(QObject *parent = nullptr); + + bool isSignedIn() const; + + /// @brief If isSignedIn() this will contain the human's session data. + /// @returns Session data for the authed human. nullptr if not signed in. + SessionData *sessionData() const; + +public slots: + /// @brief Sends a code to the requested email. + /// + /// You must verify the code in the signIn method to receive a session. + /// + /// @params email to send code to. + void requestSignInCode(const QString &email); + + /// @brief Creates a session if authenticated. + /// + /// @params code that was sent to the email in the previous step + void signIn(const QString &code); + + void signOut(); + + /// @brief checks if there is a cached session that is valid + void init(); + +signals: + void isSignedInChanged(); + void codeSentToEmail(const QString &email); + + void errorOccurred(const QString &message); + +private: + QString m_lastEmail; + + bool m_isSignedIn; + void setIsSignedIn(bool); + + SessionData *m_sessionData; + + QSettings *m_settings; + QString m_sessionToken; + + /// Sets session token internally to class, in storage, and sets networkmanager appropriately + /// Pass "" if you want to remove it. + void setSessionToken(const QString &); +}; + +#endif // AUTHMANAGER_H diff --git a/src/mainwindow.ui b/src/mainwindow.ui index 3544510..e2d14d9 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -14,122 +14,52 @@ Flowy.llink - + + + true + - 0 - 0 - 801 - 541 + 360 + 20 + 61 + 16 - - 0 + + + 0 + 0 + + + + + + + :/assets/FLOWY-4.png + + + true + + + Qt::AlignmentFlag::AlignCenter + + + + + + 300 + 250 + 181 + 41 + + + + Welcome to Flowy.llink + + + Qt::AlignmentFlag::AlignCenter - - - Sign in - - - - - 250 - 150 - 291 - 151 - - - - QFrame::Shape::StyledPanel - - - QFrame::Shadow::Raised - - - - - 20 - 30 - 251 - 21 - - - - - - - 20 - 80 - 71 - 21 - - - - - - - 20 - 10 - 58 - 16 - - - - Email - - - - - - 20 - 60 - 58 - 16 - - - - Code - - - - - - 20 - 110 - 101 - 32 - - - - Continue - - - false - - - true - - - false - - - - - - - App - - - - - 110 - 40 - 551 - 401 - - - - @@ -141,6 +71,8 @@ - + + + diff --git a/src/networkmanager.cpp b/src/networkmanager.cpp new file mode 100644 index 0000000..96116f5 --- /dev/null +++ b/src/networkmanager.cpp @@ -0,0 +1,78 @@ +#include "networkmanager.h" +#include +#include +#include + +namespace +{ +constexpr char API_URL[] = "https://orion.dev.flowy.live"; +} + +NetworkManager::NetworkManager() +{ + m_qnam = new QNetworkAccessManager(this); + connect(m_qnam, &QNetworkAccessManager::finished, this, [this](QNetworkReply *reply) { + // In case particular instances of QNetworkReply are not cleaned up. + // It's safe to be redundant with deleteLater + // Calling deleteLater here also doesn't disrupt QNetworkReply::finished slots, as this both here + // and in those slots are fired in tandem and deleteLater waits for return to event loop to perform deleteLater + reply->deleteLater(); + int statusCode = reply->attribute(QNetworkRequest::Attribute::HttpStatusCodeAttribute).toInt(); + qDebug() << "Network request status code: " << statusCode; + if (statusCode == 401) + { + qWarning() << "Unauthorized request: " << reply->errorString(); + emit unauthorizedDetected(); + } + }); +} + +NetworkManager::~NetworkManager() +{ +} + +NetworkManager &NetworkManager::instance() +{ + static NetworkManager instance; + return instance; +} + +void NetworkManager::setAuthHeader(QNetworkRequest &request) +{ + if (m_sessionToken.isEmpty()) + { + return; + } + QString headerValue = QString("Bearer %1").arg(m_sessionToken); + request.setRawHeader("Authorization", headerValue.toUtf8()); +} + +QString NetworkManager::getFullUrl(const QString &url) +{ + return QString("%1%2").arg(API_URL, url); +} + +QNetworkReply *NetworkManager::get(const QString &path) +{ + QNetworkRequest req = QNetworkRequest(getFullUrl(path)); + setAuthHeader(req); + return m_qnam->get(req); +} + +QNetworkReply *NetworkManager::post(const QString &path, const QJsonDocument &doc) +{ + QNetworkRequest request = QNetworkRequest(getFullUrl(path)); + setAuthHeader(request); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + return m_qnam->post(request, doc.toJson()); +} + +QString NetworkManager::sessionToken() const +{ + return m_sessionToken; +} + +void NetworkManager::setSessionToken(const QString &newToken) +{ + m_sessionToken = newToken; +} diff --git a/src/networkmanager.h b/src/networkmanager.h new file mode 100644 index 0000000..fcf9aaf --- /dev/null +++ b/src/networkmanager.h @@ -0,0 +1,53 @@ +#ifndef NETWORKMANAGER_H +#define NETWORKMANAGER_H + +#include + +QT_BEGIN_NAMESPACE +class QNetworkReply; +class QNetworkRequest; +class QNetworkAccessManager; +class QJsonDocument; +QT_END_NAMESPACE + +class NetworkManager : public QObject +{ + Q_OBJECT +public: + static NetworkManager &instance(); + + // Delete copy constructor and assignment + NetworkManager(const NetworkManager &) = delete; + void operator=(const NetworkManager &) = delete; + + QString sessionToken() const; + +public slots: + // Convenience methods that inject session token. + // Network manager discard replies internally, in cases where a consumer callsite prefers to not manage QNetworkReply. + QNetworkReply *get(const QString &path); + QNetworkReply *post(const QString &path, const QJsonDocument &data); + // QNetworkReply *put(QNetworkRequest &request); + // QNetworkReply *del(QNetworkRequest &request); + + /// @brief sets session token which will is injected into every request's headers + void setSessionToken(const QString &token); + +signals: + /// @brief Emitted when a request is made and fails required authentication. + void unauthorizedDetected(); + +private: + NetworkManager(); + ~NetworkManager(); + + QString m_sessionToken; + QNetworkAccessManager *m_qnam; + + void setAuthHeader(QNetworkRequest &request); + /// @brief Combines the api url with given path + /// @param path should be like "/auth/me" + QString getFullUrl(const QString &path); +}; + +#endif // NETWORKMANAGER_H diff --git a/styles/remarkable.qss b/styles/remarkable.qss index 5694863..87cbd5c 100644 --- a/styles/remarkable.qss +++ b/styles/remarkable.qss @@ -90,7 +90,7 @@ QLineEdit { border: 1px solid #D4D4D4; border-bottom: 1px solid #1A1A1A; border-radius: 0px; - padding: 8px; + selection-background-color: #1A1A1A; selection-color: #FAFAF8; }