feat: barebones auth implementation

This commit is contained in:
talksik
2026-01-27 11:29:07 -08:00
parent 63f7dd3a06
commit 7a50bf12c9
16 changed files with 838 additions and 182 deletions
+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