From 0643682cda5f47690693bf04e892c7683f0093ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20Gren=C3=A4ngen?= Date: Fri, 12 Jul 2024 11:51:26 +0200 Subject: [PATCH] Login & Logout flows now working * Login will now trigger the proper actions and grab the URL and open it for the user * Logout will properly trigger the proper actions and set the correct UI states --- AccountsTabUiManager.cpp | 29 ++++++++++++---- AccountsTabUiManager.h | 5 ++- MainWindow.cpp | 17 +++++++++- MainWindow.h | 3 ++ MainWindow.ui | 4 +-- README.md | 4 +-- TailRunner.cpp | 72 ++++++++++++++++++++++++++++++++++++---- TailRunner.h | 6 ++++ TrayMenuManager.cpp | 46 ++++++++++++++++--------- TrayMenuManager.h | 1 + 10 files changed, 154 insertions(+), 33 deletions(-) diff --git a/AccountsTabUiManager.cpp b/AccountsTabUiManager.cpp index f396b5a..0c47151 100644 --- a/AccountsTabUiManager.cpp +++ b/AccountsTabUiManager.cpp @@ -9,27 +9,43 @@ #include #include "./ui_MainWindow.h" +#include "MainWindow.h" #include "models.h" -AccountsTabUiManager::AccountsTabUiManager(Ui::MainWindow* u, QObject* parent) +AccountsTabUiManager::AccountsTabUiManager(Ui::MainWindow* u, TailRunner* runner, QObject* parent) : QObject(parent) , ui(u) + , pTailRunner(runner) { - - connect(ui->btnAdminConsole, &QPushButton::clicked, - this, [this]() { + connect(ui->btnAdminConsole, &QPushButton::clicked, this, [this]() { QDesktopServices::openUrl(QUrl("https://login.tailscale.com/admin")); } ); + + connect(ui->btnLogout, &QPushButton::clicked, this, [this]() { + pTailRunner->logout(); + auto* wnd = dynamic_cast(this->parent()); + wnd->userLoggedOut(); + wnd->hide(); + } + ); } AccountsTabUiManager::~AccountsTabUiManager() { } void AccountsTabUiManager::onTailStatusChanged(TailStatus* pTailStatus) { - if (pTailStatus->user->id <= 0) - return; // Not logged in + if (pTailStatus->user->id <= 0) { + // Not logged in + + // Hide account details view, nothing to show + ui->accountDetailsContainer->setVisible(false); + ui->lstAccounts->clear(); + return; + } + // Show account details view + ui->accountDetailsContainer->setVisible(true); ui->lstAccounts->clear(); auto* pCurrent = new QListWidgetItem(pTailStatus->user->displayName + "\n" + pTailStatus->user->loginName); @@ -42,6 +58,7 @@ void AccountsTabUiManager::onTailStatusChanged(TailStatus* pTailStatus) { ui->lblTailnetName->setText(pTailStatus->user->loginName); ui->lblEmail->setText(pTailStatus->user->loginName); ui->lblStatus->setText(pTailStatus->backendState); + ui->lblKeyExpiry->setText(""); // Show the key expiry date in a more human readable format const auto now = QDateTime::currentDateTime(); diff --git a/AccountsTabUiManager.h b/AccountsTabUiManager.h index 59829e9..ce9757d 100644 --- a/AccountsTabUiManager.h +++ b/AccountsTabUiManager.h @@ -7,6 +7,8 @@ #include +#include "TailRunner.h" + QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; @@ -19,13 +21,14 @@ class AccountsTabUiManager : public QObject { Q_OBJECT public: - explicit AccountsTabUiManager(Ui::MainWindow* ui, QObject* parent = nullptr); + explicit AccountsTabUiManager(Ui::MainWindow* ui, TailRunner* runner, QObject* parent = nullptr); virtual ~AccountsTabUiManager(); void onTailStatusChanged(TailStatus* pTailStatus); private: Ui::MainWindow* ui; + TailRunner* pTailRunner; }; diff --git a/MainWindow.cpp b/MainWindow.cpp index d48d7ec..e75e72d 100644 --- a/MainWindow.cpp +++ b/MainWindow.cpp @@ -15,11 +15,12 @@ MainWindow::MainWindow(QWidget* parent) , pTailStatus(nullptr) { ui->setupUi(this); - accountsTabUi = new AccountsTabUiManager(ui, this); pCurrentExecution = new TailRunner(settings, this); connect(pCurrentExecution, &TailRunner::statusUpdated, this, &MainWindow::onTailStatusChanged); + connect(pCurrentExecution, &TailRunner::loginFlowCompleted, this, &MainWindow::loginFlowCompleted); + accountsTabUi = new AccountsTabUiManager(ui, pCurrentExecution, this); pTrayManager = new TrayMenuManager(settings, pCurrentExecution, this); changeToState(TailState::NotLoggedIn); @@ -63,12 +64,26 @@ void MainWindow::settingsClosed() { hide(); } +void MainWindow::loginFlowCompleted() { + pCurrentExecution->start(); +} + TailState MainWindow::changeToState(TailState newState) { auto retVal = eCurrentState; eCurrentState = newState; + if (eCurrentState == TailState::NotLoggedIn) + { + // Clear the status + delete pTailStatus; + pTailStatus = new TailStatus(); + pTailStatus->self = new TailDeviceInfo(); + pTailStatus->user = new TailUser(); + } + pTrayManager->stateChangedTo(newState, pTailStatus); + accountsTabUi->onTailStatusChanged(pTailStatus); return retVal; } diff --git a/MainWindow.h b/MainWindow.h index 7a2e47c..38e5b25 100644 --- a/MainWindow.h +++ b/MainWindow.h @@ -31,6 +31,8 @@ class MainWindow : public QMainWindow void syncSettingsToUi(); void syncSettingsFromUi(); + void userLoggedOut() { changeToState(TailState::NotLoggedIn); } + private: Ui::MainWindow* ui; AccountsTabUiManager* accountsTabUi; @@ -44,6 +46,7 @@ class MainWindow : public QMainWindow private slots: void settingsClosed(); + void loginFlowCompleted(); private: // Switch to the new state and return the prev (old) state back to caller diff --git a/MainWindow.ui b/MainWindow.ui index f308c38..9d104e4 100644 --- a/MainWindow.ui +++ b/MainWindow.ui @@ -52,7 +52,7 @@ QTabWidget::TabPosition::North - 1 + 0 @@ -139,7 +139,7 @@ - + 325 diff --git a/README.md b/README.md index bfc0d0b..09a772b 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,8 @@ To do that, plese see the Getting started section below. 9. It will now be installed to `/usr/local/bin/tail-tray` and can be started by running `tail-tray` in a terminal or by clicking the Tail Tray icon in the launcher. ### Participating & Filing bugs -* If you would like to participate in the development of this project, please feel free to fork the repo and submit a pull request. -* If you find any bugs, please file an issue in the issues tab. +* If you would like to participate in the development of this project, feel free to fork the repo and submit a pull request. +* Bugs, we all get them... Please file an issue in the issues tab and we'll sort it out together. ### Screenshots ![Screenshot](screenshots/launcher.png) diff --git a/TailRunner.cpp b/TailRunner.cpp index e90576a..26cb936 100644 --- a/TailRunner.cpp +++ b/TailRunner.cpp @@ -4,11 +4,13 @@ #include #include #include +#include #include #include +#include TailRunner::TailRunner(const TailSettings& s, QObject* parent) - : QObject(parent) +: QObject(parent) , settings(s) , pProcess(nullptr) , eCommand(Command::Status) @@ -23,6 +25,20 @@ void TailRunner::checkStatus() { runCommand("status", QStringList(), true); } +void TailRunner::login() { + eCommand = Command::Login; + QStringList args; + args << "--operator" << qEnvironmentVariable("USER"); + + runCommand("login", args, false, true); +} + +void TailRunner::logout() { + eCommand = Command::Logout; + QStringList args; + runCommand("logout", args, false, true); +} + void TailRunner::start(bool usePkExec) { eCommand = Command::Connect; QStringList args; @@ -77,7 +93,7 @@ void TailRunner::stop() { void TailRunner::runCommand(QString cmd, QStringList args, bool jsonResult, bool usePkExec) { if (pProcess != nullptr) { if (pProcess->state() == QProcess::Running) { - assert(!"Process already running!"); + qDebug() << "Process already running!" << "Will skip running " << cmd << args << "command"; return; } @@ -107,7 +123,7 @@ void TailRunner::runCommand(QString cmd, QStringList args, bool jsonResult, bool } else { // After we've invoked a command not status command we check for new status update - if (eCommand != Command::Status) { + if (eCommand != Command::Status && eCommand != Command::Logout) { checkStatus(); } } @@ -116,11 +132,46 @@ void TailRunner::runCommand(QString cmd, QStringList args, bool jsonResult, bool connect(pProcess, &QProcess::readyReadStandardOutput, this, &TailRunner::onProcessCanReadStdOut); + // TODO: @grenis This needs some refactoring connect(pProcess, &QProcess::readyReadStandardError, this, [this]() { - QString errorInfo(pProcess->readAllStandardError()); - if (!errorInfo.isEmpty()) { - qDebug() << errorInfo; + // NOTE! For whatever reason, the login command output is not captured by the readyReadStandardOutput signal + // and arrives as a error output, so we need to check for that here. + if (eCommand == Command::Login) { + QString message(pProcess->readAllStandardError()); + if (!message.isEmpty()) { + qDebug() << message; + if (message.startsWith("Success", Qt::CaseSensitivity::CaseInsensitive)) { + // Login was successfull + // Wait for a bit before triggering flow completed + // Flow completed will call start etc + QTimer::singleShot(1000, this, [this]() { + emit loginFlowCompleted(); + }); + } + else { + QRegularExpression regex(R"(https:\/\/login\.tailscale\.com\/a\/[a-zA-Z0-9]+)"); + QRegularExpressionMatch match = regex.match(message); + if (match.hasMatch()) { + QString url = match.captured(0); + auto res = QMessageBox::information(nullptr, "Login", "To login you will have to visit " + url + "\n\nPress OK to open the URL", + QMessageBox::Ok, QMessageBox::Ok); + if (res == QMessageBox::Ok) + QDesktopServices::openUrl(QUrl(url)); + } + else { + // Failure + QMessageBox::warning(nullptr, "Login failure", "Login failed. Message: \n" + message, + QMessageBox::Ok, QMessageBox::Ok); + } + } + } + } + else { + QString errorInfo(pProcess->readAllStandardError()); + if (!errorInfo.isEmpty()) { + qDebug() << errorInfo; + } } }); @@ -135,6 +186,12 @@ void TailRunner::runCommand(QString cmd, QStringList args, bool jsonResult, bool else { pProcess->start("tailscale", args); } + + // We need to handle the login by reading as soon as ther is output available since + // the process will be running until the login flow have completed + if (eCommand == Command::Login) { + pProcess->waitForReadyRead(); + } } void TailRunner::onProcessCanReadStdOut() { @@ -174,6 +231,9 @@ void TailRunner::onProcessCanReadStdOut() { } break; } + case Command::Login: { + break; + } default: // Just echo out to console for now qDebug() << QString(data); diff --git a/TailRunner.h b/TailRunner.h index 41dcb21..446c5d3 100644 --- a/TailRunner.h +++ b/TailRunner.h @@ -18,6 +18,9 @@ class TailRunner : public QObject void checkStatus(); + void login(); + void logout(); + void start(bool usePkExec = false); void stop(); @@ -25,6 +28,8 @@ class TailRunner : public QObject const TailSettings& settings; QProcess* pProcess; enum class Command { + Login, + Logout, Connect, Disconnect, SettingsChange, @@ -35,6 +40,7 @@ class TailRunner : public QObject signals: void statusUpdated(TailStatus* newStatus); + void loginFlowCompleted(); private: void runCommand(QString cmd, QStringList args, bool jsonResult = false, bool usePkExec = false); diff --git a/TrayMenuManager.cpp b/TrayMenuManager.cpp index 4672c7f..b4b6ffb 100644 --- a/TrayMenuManager.cpp +++ b/TrayMenuManager.cpp @@ -23,6 +23,7 @@ TrayMenuManager::TrayMenuManager(TailSettings& s, TailRunner* runner, QObject* p , pLogoutAction(nullptr) , pPreferences(nullptr) , pAbout(nullptr) + , pThisDevice(nullptr) { pTrayMenu = new QMenu("Tailscale"); @@ -48,11 +49,10 @@ TrayMenuManager::TrayMenuManager(TailSettings& s, TailRunner* runner, QObject* p pConnected->setEnabled(false); pConnect = new QAction("Connect"); pDisconnect = new QAction("Disconnect"); + pThisDevice = new QAction("This device"); setupWellKnownActions(); - stateChangedTo(TailState::NotLoggedIn, nullptr); - // Periodic status check pStatusCheckTimer = new QTimer(this); connect(pStatusCheckTimer, &QTimer::timeout, this, [this]() { @@ -60,6 +60,8 @@ TrayMenuManager::TrayMenuManager(TailSettings& s, TailRunner* runner, QObject* p }); pStatusCheckTimer->setSingleShot(false); pStatusCheckTimer->start(1000 * 30); // 30sec interval + + stateChangedTo(TailState::NotLoggedIn, nullptr); } TrayMenuManager::~TrayMenuManager() @@ -77,25 +79,32 @@ TrayMenuManager::~TrayMenuManager() delete pLogoutAction; delete pPreferences; delete pAbout; + delete pThisDevice; } void TrayMenuManager::stateChangedTo(TailState newState, TailStatus const* pTailStatus) { switch (newState) { case TailState::Connected: - case TailState::LoggedIn: - buildConnectedMenu(pTailStatus); + case TailState::LoggedIn: { + buildConnectedMenu(pTailStatus); + pStatusCheckTimer->start(); break; + } case TailState::NoAccount: - case TailState::NotLoggedIn: + case TailState::NotLoggedIn: { buildNotLoggedInMenu(); - break; - case TailState::NotConnected: + pStatusCheckTimer->stop(); + break; + } + case TailState::NotConnected: { buildNotConnectedMenu(pTailStatus); - break; + pStatusCheckTimer->stop(); + break; + } case TailState::ConnectedWithExitNode: buildConnectedExitNodeMenu(pTailStatus); - break; + break; default: assert(!"Unhandled TailState status!"); } @@ -119,7 +128,8 @@ void TrayMenuManager::buildNotConnectedMenu(TailStatus const* pTailStatus) pTrayMenu->clear(); pTrayMenu->addAction(pConnect); pTrayMenu->addSeparator(); - pTrayMenu->addAction(pTailStatus->user->loginName); + pThisDevice->setText(pTailStatus->user->loginName); + pTrayMenu->addAction(pThisDevice); pTrayMenu->addSeparator(); pTrayMenu->addAction(pPreferences); pTrayMenu->addAction(pAbout); @@ -135,11 +145,8 @@ void TrayMenuManager::buildConnectedMenu(TailStatus const* pTailStatus) pTrayMenu->addAction(pConnected); pTrayMenu->addAction(pDisconnect); pTrayMenu->addSeparator(); - const QAction* self = pTrayMenu->addAction(pTailStatus->user->loginName); - connect(self, &QAction::triggered, this, [this](bool) { - auto* wnd = dynamic_cast(this->parent()); - wnd->showAccountsTab(); - }); + pThisDevice->setText(pTailStatus->user->loginName); + pTrayMenu->addAction(pThisDevice); pTrayMenu->addSeparator(); pTrayMenu->addAction("This device: " + pTailStatus->self->hostName); @@ -210,6 +217,10 @@ void TrayMenuManager::buildConnectedExitNodeMenu(TailStatus const* pTailStatus) } void TrayMenuManager::setupWellKnownActions() { + connect(pLoginAction, &QAction::triggered, this, [this](bool) { + pTailRunner->login(); + }); + connect(pConnect, &QAction::triggered, this, [this](bool) { pTailRunner->start(); }); @@ -228,6 +239,11 @@ void TrayMenuManager::setupWellKnownActions() { wnd->showAboutTab(); }); + connect(pThisDevice, &QAction::triggered, this, [this](bool) { + auto* wnd = dynamic_cast(this->parent()); + wnd->showAccountsTab(); + }); + connect(pQuitAction, &QAction::triggered, qApp, &QApplication::quit); connect(pSysTray, &QSystemTrayIcon::activated, diff --git a/TrayMenuManager.h b/TrayMenuManager.h index bcb9147..45c2381 100644 --- a/TrayMenuManager.h +++ b/TrayMenuManager.h @@ -38,6 +38,7 @@ class TrayMenuManager : public QObject QAction* pDisconnect; QAction* pPreferences; QAction* pAbout; + QAction* pThisDevice; private: void buildNotLoggedInMenu();