From 7a50bf12c936a72ba6c84af60c89b1203cc18162 Mon Sep 17 00:00:00 2001 From: talksik Date: Tue, 27 Jan 2026 11:18:08 -0800 Subject: [PATCH] feat: barebones auth implementation --- CMakeLists.txt | 8 ++ CMakeLists.txt.user | 63 ++++++------ assets/.DS_Store | Bin 0 -> 6148 bytes assets/FLOWY-4.png | Bin 0 -> 6234 bytes main.cpp | 16 +-- src/MainWindow.cpp | 81 ++++++++------- src/MainWindow.h | 5 + src/authdialog.cpp | 118 ++++++++++++++++++++++ src/authdialog.h | 37 +++++++ src/authdialog.ui | 116 ++++++++++++++++++++++ src/authmanager.cpp | 218 +++++++++++++++++++++++++++++++++++++++++ src/authmanager.h | 69 +++++++++++++ src/mainwindow.ui | 156 +++++++++-------------------- src/networkmanager.cpp | 78 +++++++++++++++ src/networkmanager.h | 53 ++++++++++ styles/remarkable.qss | 2 +- 16 files changed, 838 insertions(+), 182 deletions(-) create mode 100644 assets/.DS_Store create mode 100644 assets/FLOWY-4.png create mode 100644 src/authdialog.cpp create mode 100644 src/authdialog.h create mode 100644 src/authdialog.ui create mode 100644 src/authmanager.cpp create mode 100644 src/authmanager.h create mode 100644 src/networkmanager.cpp create mode 100644 src/networkmanager.h 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 0000000000000000000000000000000000000000..aa18b3c3e8bbff89d1c79d077ea338a261e2c674 GIT binary patch literal 6148 zcmeHKO-sW-5PhpX6ng2=WBz~!!Lx)CJrz9^6>p|(5HYsWCW5#8bl=RZVlaBtBD1jj zW@p~)$Aj!<0fb4jJO;)9hOB}?jGiLy-q2no3kDtI7Kb>&5w>{g8E6+>vi1?qF~ucr z@Ol0B&-HxTZ0ijfr9c=UoKpfDmg|@sUBqxcOMQjZjVF{K>wA2uv7{Su%Pa-di z*cw_og3iow;>_=lm!PxLpC}z6GxXLKa0R*w>|1v%_x~9`nchc!H^qCdfGhCN6bOTS zmQT2;c(;Dpp4_zw>m92U;k9D0(C+*Mup{Tlfd(y}h)rG=u{D%Y#INZ@{}E6@ym1A7 GL4h~%QZ*(3 literal 0 HcmV?d00001 diff --git a/assets/FLOWY-4.png b/assets/FLOWY-4.png new file mode 100644 index 0000000000000000000000000000000000000000..e39f5ecfd2147612a272fff1b2900fb2e2cc0fba GIT binary patch literal 6234 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 #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; }