commit 07336a9284b182a23b3899d0b47350cc481e6430
parent b19a8d09d985c6d7eef29cd02b995e3f2c9e2e05
Author: Silas Brack <silasbrack@gmail.com>
Date: Fri, 26 Dec 2025 22:09:37 +0100
Idk
Diffstat:
7 files changed, 209 insertions(+), 2 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
@@ -15,6 +15,7 @@ endif()
# Add here any library you need
find_package(Qt5 COMPONENTS Widgets Qml Quick Network REQUIRED)
+find_package(Qt5Keychain REQUIRED)
# Add the resource file as it *must* be compiled (also possible with
# qmake or manually with rcc)
@@ -28,7 +29,7 @@ add_executable(demo
)
# Add here any library you need
-target_link_libraries(demo Qt5::Widgets Qt5::Qml Qt5::Quick Qt5::Network)
+target_link_libraries(demo Qt5::Widgets Qt5::Qml Qt5::Quick Qt5::Network qt5keychain)
# Install the binary
install(TARGETS demo DESTINATION bin)
diff --git a/derivation_demo.nix b/derivation_demo.nix
@@ -3,6 +3,7 @@
, cmake
, qt5
, wrapQtAppsHook
+, qtkeychain
}:
stdenv.mkDerivation rec {
pname = "Beegram";
@@ -16,6 +17,7 @@ stdenv.mkDerivation rec {
qt5.qtbase
qt5.qtdeclarative
qt5.qtquickcontrols2
+ qtkeychain
];
# macOS-specific: Create .app bundle
diff --git a/flake.nix b/flake.nix
@@ -34,6 +34,7 @@
libsForQt5.qt5.qtbase
libsForQt5.qt5.qtdeclarative
libsForQt5.qt5.qtquickcontrols2
+ qtkeychain
libsForQt5.wrapQtAppsHook
];
};
diff --git a/main.qml b/main.qml
@@ -14,6 +14,26 @@ Window {
property var navigationStack: []
property var stackView: stackViewItem // Expose for child views
+ Component.onCompleted: {
+ // Check for saved credentials on startup
+ NetworkManager.checkSavedCredentials()
+ }
+
+ // Handle auto-login result
+ Connections {
+ target: NetworkManager
+
+ function onAutoLoginSuccess() {
+ console.log("Auto-login successful, navigating to chat list")
+ stackViewItem.replace("qrc:/qml/views/ChatListView.qml")
+ }
+
+ function onAutoLoginFailed(reason) {
+ console.log("Auto-login failed:", reason)
+ // Stay on login view (already loaded by default)
+ }
+ }
+
Item {
id: stackViewItem
anchors.fill: parent
diff --git a/networkmanager.cpp b/networkmanager.cpp
@@ -5,6 +5,7 @@
#include <QJsonArray>
#include <QUrlQuery>
#include <QDebug>
+#include <qt5keychain/keychain.h>
NetworkManager::NetworkManager(QObject *parent)
: QObject(parent)
@@ -114,6 +115,10 @@ void NetworkManager::onLoginFinished()
m_authToken = authToken;
qDebug() << "Login successful, token stored";
+ // Save token to keychain
+ saveTokenToKeychain(authToken);
+
+ emit authenticationChanged();
emit loginSuccess(authToken);
// Clean up
@@ -844,3 +849,110 @@ void NetworkManager::resetReconnectBackoff()
m_reconnectAttempts = 0;
m_reconnectTimer->stop();
}
+
+// Keychain Methods
+
+void NetworkManager::saveTokenToKeychain(const QString& token)
+{
+ QKeychain::WritePasswordJob *job = new QKeychain::WritePasswordJob("Beegram", this);
+ job->setKey("auth_token");
+ job->setTextData(token);
+
+ connect(job, &QKeychain::WritePasswordJob::finished, this, [this, job]() {
+ onKeychainWriteFinished(job);
+ });
+
+ job->start();
+}
+
+void NetworkManager::loadTokenFromKeychain()
+{
+ QKeychain::ReadPasswordJob *job = new QKeychain::ReadPasswordJob("Beegram", this);
+ job->setKey("auth_token");
+
+ connect(job, &QKeychain::ReadPasswordJob::finished, this, [this, job]() {
+ onKeychainReadFinished(job);
+ });
+
+ job->start();
+}
+
+void NetworkManager::deleteTokenFromKeychain()
+{
+ QKeychain::DeletePasswordJob *job = new QKeychain::DeletePasswordJob("Beegram", this);
+ job->setKey("auth_token");
+
+ connect(job, &QKeychain::DeletePasswordJob::finished, this, [this, job]() {
+ onKeychainDeleteFinished(job);
+ });
+
+ job->start();
+}
+
+// Keychain Slots
+
+void NetworkManager::onKeychainReadFinished(QKeychain::ReadPasswordJob* job)
+{
+ if (job->error() == QKeychain::NoError) {
+ QString token = job->textData();
+ if (!token.isEmpty()) {
+ m_authToken = token;
+ qDebug() << "Token loaded from keychain";
+ emit authenticationChanged();
+ emit autoLoginSuccess();
+ } else {
+ qDebug() << "No token found in keychain";
+ emit autoLoginFailed("No saved credentials");
+ }
+ } else {
+ qDebug() << "Keychain read error:" << job->errorString();
+ emit autoLoginFailed("Failed to read saved credentials");
+ }
+
+ job->deleteLater();
+}
+
+void NetworkManager::onKeychainWriteFinished(QKeychain::WritePasswordJob* job)
+{
+ if (job->error() != QKeychain::NoError) {
+ qWarning() << "Failed to save token to keychain:" << job->errorString();
+ } else {
+ qDebug() << "Token saved to keychain successfully";
+ }
+
+ job->deleteLater();
+}
+
+void NetworkManager::onKeychainDeleteFinished(QKeychain::DeletePasswordJob* job)
+{
+ if (job->error() != QKeychain::NoError && job->error() != QKeychain::EntryNotFound) {
+ qWarning() << "Failed to delete token from keychain:" << job->errorString();
+ } else {
+ qDebug() << "Token deleted from keychain";
+ }
+
+ job->deleteLater();
+}
+
+// Public Methods
+
+void NetworkManager::logout()
+{
+ // Clear in-memory token
+ m_authToken.clear();
+
+ // Unsubscribe from any active SSE connections
+ unsubscribeFromChat();
+
+ // Delete token from keychain
+ deleteTokenFromKeychain();
+
+ emit authenticationChanged();
+ qDebug() << "User logged out";
+}
+
+void NetworkManager::checkSavedCredentials()
+{
+ qDebug() << "Checking for saved credentials...";
+ loadTokenFromKeychain();
+}
diff --git a/networkmanager.h b/networkmanager.h
@@ -9,18 +9,28 @@
#include <QSet>
#include <QSystemTrayIcon>
+namespace QKeychain {
+ class ReadPasswordJob;
+ class WritePasswordJob;
+ class DeletePasswordJob;
+}
+
class NetworkManager : public QObject
{
Q_OBJECT
Q_PROPERTY(QString currentUserId READ currentUserId CONSTANT)
+ Q_PROPERTY(bool isAuthenticated READ isAuthenticated NOTIFY authenticationChanged)
public:
explicit NetworkManager(QObject *parent = nullptr);
~NetworkManager();
QString currentUserId() const { return m_currentUserId; }
+ bool isAuthenticated() const { return !m_authToken.isEmpty(); }
Q_INVOKABLE void login(const QString& email, const QString& password);
+ Q_INVOKABLE void logout();
+ Q_INVOKABLE void checkSavedCredentials();
Q_INVOKABLE void postChat(const QString& name);
Q_INVOKABLE void fetchChats(const QString& cursor = "");
Q_INVOKABLE void postMessage(int chatId, const QString& content);
@@ -33,6 +43,9 @@ public:
signals:
void loginSuccess(QString authToken);
void loginFailed(QString error);
+ void authenticationChanged();
+ void autoLoginSuccess();
+ void autoLoginFailed(QString reason);
void chatCreated(QVariantMap chatData);
void chatsFetched(QVariantList chats, QString cursor);
void messageCreated(QVariantMap messageData);
@@ -47,6 +60,9 @@ signals:
private slots:
void onLoginFinished();
+ void onKeychainReadFinished(QKeychain::ReadPasswordJob* job);
+ void onKeychainWriteFinished(QKeychain::WritePasswordJob* job);
+ void onKeychainDeleteFinished(QKeychain::DeletePasswordJob* job);
void onPostChatFinished();
void onPostChatParticipationFinished();
void onFetchChatsFinished();
@@ -63,6 +79,11 @@ private:
// Helper methods
void createChatParticipation(int chatId, const QVariantMap& chatData);
+ // Keychain methods
+ void saveTokenToKeychain(const QString& token);
+ void loadTokenFromKeychain();
+ void deleteTokenFromKeychain();
+
// Helper methods for SSE
void parseSSEEvent(const QString& eventData);
bool isDuplicateMessage(int messageId);
diff --git a/qml/views/ProfileView.qml b/qml/views/ProfileView.qml
@@ -132,7 +132,57 @@ Item {
Layout.fillWidth: true
icon: "\ud83d\udd12"
label: "Privacy and Security"
- showDivider: false
+ }
+
+ // Spacer
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: TelegramStyle.standardMargin
+ color: "#f5f5f5"
+ }
+
+ // Logout button
+ Rectangle {
+ Layout.fillWidth: true
+ height: 50
+ color: logoutMouseArea.containsMouse ? "#ffebee" : "white"
+
+ Behavior on color {
+ ColorAnimation { duration: 100 }
+ }
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.margins: TelegramStyle.standardMargin
+ spacing: TelegramStyle.standardMargin
+
+ Text {
+ text: "\ud83d\udebb"
+ font.pixelSize: 24
+ }
+
+ Text {
+ Layout.fillWidth: true
+ text: "Logout"
+ font.pixelSize: TelegramStyle.fontSizeNormal
+ color: "#f44336"
+ }
+ }
+
+ MouseArea {
+ id: logoutMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ console.log("Logging out...")
+ NetworkManager.logout()
+ // Navigate back to login screen
+ if (typeof stackView !== 'undefined' && stackView) {
+ stackView.replace(Qt.resolvedUrl("LoginView.qml"))
+ }
+ }
+ }
}
}
}