feat: barebones auth implementation

This commit is contained in:
talksik
2026-01-27 11:18:08 -08:00
parent 63f7dd3a06
commit 7a50bf12c9
16 changed files with 838 additions and 182 deletions
+8
View File
@@ -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)
+34 -29
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE QtCreatorProject>
<!-- Written by QtCreator 16.0.2, 2026-01-24T08:42:11. -->
<!-- Written by QtCreator 16.0.2, 2026-01-27T10:36:09. -->
<qtcreator>
<data>
<variable>EnvironmentId</variable>
@@ -86,6 +86,9 @@
<valuelist type="QVariantList" key="ClangTools.SuppressedDiagnostics"/>
<value type="bool" key="ClangTools.UseGlobalSettings">true</value>
</valuemap>
<valuemap type="QVariantMap" key="CppEditor.QuickFix">
<value type="bool" key="UseGlobalSettings">true</value>
</valuemap>
</valuemap>
</data>
<data>
@@ -103,15 +106,15 @@
<value type="int" key="CMake.Configure.BaseEnvironment">2</value>
<value type="bool" key="CMake.Configure.ClearSystemEnvironment">false</value>
<valuelist type="QVariantList" key="CMake.Configure.UserEnvironmentChanges"/>
<value type="QString" key="CMake.Initial.Parameters">-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake
<value type="QString" key="CMake.Initial.Parameters">-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}</value>
-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake
-DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON</value>
<value type="int" key="EnableQmlDebugging">0</value>
<value type="QString" key="ProjectExplorer.BuildConfiguration.BuildDirectory">/Users/talksik/Documents/code/llink/build/Qt_6_8_6_for_macOS-Debug</value>
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
@@ -161,15 +164,15 @@
<value type="int" key="CMake.Configure.BaseEnvironment">2</value>
<value type="bool" key="CMake.Configure.ClearSystemEnvironment">false</value>
<valuelist type="QVariantList" key="CMake.Configure.UserEnvironmentChanges"/>
<value type="QString" key="CMake.Initial.Parameters">-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake
<value type="QString" key="CMake.Initial.Parameters">-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}</value>
-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake
-DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON</value>
<value type="QString" key="ProjectExplorer.BuildConfiguration.BuildDirectory">/Users/talksik/Documents/code/llink/build/Qt_6_8_6_for_macOS-Release</value>
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
@@ -216,15 +219,15 @@
<value type="int" key="CMake.Configure.BaseEnvironment">2</value>
<value type="bool" key="CMake.Configure.ClearSystemEnvironment">false</value>
<valuelist type="QVariantList" key="CMake.Configure.UserEnvironmentChanges"/>
<value type="QString" key="CMake.Initial.Parameters">-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake
<value type="QString" key="CMake.Initial.Parameters">-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}</value>
-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake
-DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON</value>
<value type="QString" key="ProjectExplorer.BuildConfiguration.BuildDirectory">/Users/talksik/Documents/code/llink/build/Qt_6_8_6_for_macOS-RelWithDebInfo</value>
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
@@ -271,15 +274,15 @@
<value type="int" key="CMake.Configure.BaseEnvironment">2</value>
<value type="bool" key="CMake.Configure.ClearSystemEnvironment">false</value>
<valuelist type="QVariantList" key="CMake.Configure.UserEnvironmentChanges"/>
<value type="QString" key="CMake.Initial.Parameters">-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake
<value type="QString" key="CMake.Initial.Parameters">-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}</value>
-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake
-DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON</value>
<value type="int" key="EnableQmlDebugging">0</value>
<value type="QString" key="ProjectExplorer.BuildConfiguration.BuildDirectory">/Users/talksik/Documents/code/llink/build/Qt_6_8_6_for_macOS-Profile</value>
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
@@ -327,15 +330,15 @@
<value type="int" key="CMake.Configure.BaseEnvironment">2</value>
<value type="bool" key="CMake.Configure.ClearSystemEnvironment">false</value>
<valuelist type="QVariantList" key="CMake.Configure.UserEnvironmentChanges"/>
<value type="QString" key="CMake.Initial.Parameters">-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake
<value type="QString" key="CMake.Initial.Parameters">-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}</value>
-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake
-DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON</value>
<value type="QString" key="ProjectExplorer.BuildConfiguration.BuildDirectory">/Users/talksik/Documents/code/llink/build/Qt_6_8_6_for_macOS-MinSizeRel</value>
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
@@ -435,12 +438,14 @@
<valuelist type="QVariantList" key="PE.EnvironmentAspect.Changes"/>
<value type="bool" key="PE.EnvironmentAspect.PrintOnRun">false</value>
<value type="QString" key="PerfRecordArgsId">-e cpu-cycles --call-graph dwarf,4096 -F 250</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName"></value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.CustomExecutableRunConfiguration</value>
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey"></value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">llink</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.CMakeRunConfiguration.</value>
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey">llink</value>
<value type="bool" key="ProjectExplorer.RunConfiguration.Customized">false</value>
<value type="bool" key="RunConfiguration.UseCppDebuggerAuto">true</value>
<value type="bool" key="RunConfiguration.UseLibrarySearchPath">true</value>
<value type="bool" key="RunConfiguration.UseQmlDebuggerAuto">true</value>
<value type="QString" key="RunConfiguration.WorkingDirectory.default">/Users/talksik/Documents/code/llink/build/Qt_6_8_6_for_macOS-Debug/llink.app/Contents/MacOS</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.Target.RunConfigurationCount">1</value>
</valuemap>
BIN
View File
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

+10 -6
View File
@@ -1,18 +1,22 @@
#include <QApplication>
#include <QFile>
#include "src/MainWindow.h"
#include <QCoreApplication>
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();
+47 -34
View File
@@ -3,55 +3,68 @@
//
#include "MainWindow.h"
#include <QSystemTrayIcon>
#include <QMenu>
#include <QApplication>
#include "ui_mainwindow.h"
#include <QApplication>
#include <QLabel>
#include <QMenu>
#include <QSystemTrayIcon>
#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;
}
+5
View File
@@ -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
+118
View File
@@ -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;
}
+37
View File
@@ -0,0 +1,37 @@
#ifndef AUTHDIALOG_H
#define AUTHDIALOG_H
#include <QObject>
#include <QWidget>
#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
+116
View File
@@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<widget class="QGroupBox" name="groupBox">
<property name="geometry">
<rect>
<x>80</x>
<y>30</y>
<width>231</width>
<height>183</height>
</rect>
</property>
<property name="title">
<string>Sign in</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="emailLabel">
<property name="text">
<string>Email</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="emailLineEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="cursor">
<cursorShape>IBeamCursor</cursorShape>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="codeLabel">
<property name="text">
<string>Code</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="codeLineEdit"/>
</item>
<item>
<widget class="QLabel" name="errorMessage">
<property name="text">
<string>Error</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QPushButton" name="sendCodeButton">
<property name="geometry">
<rect>
<x>230</x>
<y>250</y>
<width>101</width>
<height>32</height>
</rect>
</property>
<property name="text">
<string>Send code</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
<widget class="QPushButton" name="signInButton">
<property name="geometry">
<rect>
<x>230</x>
<y>250</y>
<width>101</width>
<height>32</height>
</rect>
</property>
<property name="text">
<string>Sign in</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
<widget class="QLabel" name="successLabel">
<property name="geometry">
<rect>
<x>120</x>
<y>120</y>
<width>121</width>
<height>16</height>
</rect>
</property>
<property name="text">
<string>Successful</string>
</property>
</widget>
</widget>
<resources/>
<connections/>
</ui>
+218
View File
@@ -0,0 +1,218 @@
#include "authmanager.h"
#include "networkmanager.h"
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QSettings>
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);
}
+69
View File
@@ -0,0 +1,69 @@
#ifndef AUTHMANAGER_H
#define AUTHMANAGER_H
#include <QObject>
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
+44 -112
View File
@@ -14,122 +14,52 @@
<string>Flowy.llink</string>
</property>
<widget class="QWidget" name="centralwidget">
<widget class="QTabWidget" name="tabWidget">
<widget class="QLabel" name="label_3">
<property name="enabled">
<bool>true</bool>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>801</width>
<height>541</height>
<x>360</x>
<y>20</y>
<width>61</width>
<height>16</height>
</rect>
</property>
<property name="currentIndex">
<number>0</number>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="../build/Qt_6_8_6_for_macOS-Debug/.qt/rcc/PREFIX_PATH.qrc">:/assets/FLOWY-4.png</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>300</x>
<y>250</y>
<width>181</width>
<height>41</height>
</rect>
</property>
<property name="text">
<string>Welcome to Flowy.llink</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
<widget class="QWidget" name="tab_1">
<attribute name="title">
<string>Sign in</string>
</attribute>
<widget class="QFrame" name="frame">
<property name="geometry">
<rect>
<x>250</x>
<y>150</y>
<width>291</width>
<height>151</height>
</rect>
</property>
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<widget class="QLineEdit" name="lineEdit">
<property name="geometry">
<rect>
<x>20</x>
<y>30</y>
<width>251</width>
<height>21</height>
</rect>
</property>
</widget>
<widget class="QLineEdit" name="lineEdit_2">
<property name="geometry">
<rect>
<x>20</x>
<y>80</y>
<width>71</width>
<height>21</height>
</rect>
</property>
</widget>
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>20</x>
<y>10</y>
<width>58</width>
<height>16</height>
</rect>
</property>
<property name="text">
<string>Email</string>
</property>
</widget>
<widget class="QLabel" name="label_2">
<property name="geometry">
<rect>
<x>20</x>
<y>60</y>
<width>58</width>
<height>16</height>
</rect>
</property>
<property name="text">
<string>Code</string>
</property>
</widget>
<widget class="QPushButton" name="pushButton">
<property name="geometry">
<rect>
<x>20</x>
<y>110</y>
<width>101</width>
<height>32</height>
</rect>
</property>
<property name="text">
<string>Continue</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
<property name="default">
<bool>true</bool>
</property>
<property name="flat">
<bool>false</bool>
</property>
</widget>
</widget>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>App</string>
</attribute>
<widget class="QCalendarWidget" name="calendarWidget">
<property name="geometry">
<rect>
<x>110</x>
<y>40</y>
<width>551</width>
<height>401</height>
</rect>
</property>
</widget>
</widget>
</widget>
</widget>
<widget class="QStatusBar" name="statusbar">
@@ -141,6 +71,8 @@
</property>
</widget>
</widget>
<resources/>
<resources>
<include location="../build/Qt_6_8_6_for_macOS-Debug/.qt/rcc/PREFIX_PATH.qrc"/>
</resources>
<connections/>
</ui>
+78
View File
@@ -0,0 +1,78 @@
#include "networkmanager.h"
#include <QJsonDocument>
#include <QNetworkReply>
#include <QNetworkRequest>
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;
}
+53
View File
@@ -0,0 +1,53 @@
#ifndef NETWORKMANAGER_H
#define NETWORKMANAGER_H
#include <QObject>
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
+1 -1
View File
@@ -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;
}