diff --git a/chatterino.pro b/chatterino.pro index 67c2b6271..3eb7d382d 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -288,6 +288,7 @@ SOURCES += \ src/widgets/helper/DebugPopup.cpp \ src/widgets/helper/EditableModelView.cpp \ src/widgets/helper/EffectLabel.cpp \ + src/widgets/helper/LoginServer.cpp \ src/widgets/helper/NotebookButton.cpp \ src/widgets/helper/NotebookTab.cpp \ src/widgets/helper/QColorPicker.cpp \ @@ -551,6 +552,7 @@ HEADERS += \ src/widgets/helper/EditableModelView.hpp \ src/widgets/helper/EffectLabel.hpp \ src/widgets/helper/Line.hpp \ + src/widgets/helper/LoginServer.hpp \ src/widgets/helper/NotebookButton.hpp \ src/widgets/helper/NotebookTab.hpp \ src/widgets/helper/QColorPicker.hpp \ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 11cfe7c6d..8190e576e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -378,6 +378,8 @@ set(SOURCE_FILES widgets/helper/EditableModelView.hpp widgets/helper/EffectLabel.cpp widgets/helper/EffectLabel.hpp + widgets/helper/LoginServer.cpp + widgets/helper/LoginServer.hpp widgets/helper/NotebookButton.cpp widgets/helper/NotebookButton.hpp widgets/helper/NotebookTab.cpp diff --git a/src/providers/twitch/TwitchCommon.hpp b/src/providers/twitch/TwitchCommon.hpp index 6c41dbdc4..dae4fb594 100644 --- a/src/providers/twitch/TwitchCommon.hpp +++ b/src/providers/twitch/TwitchCommon.hpp @@ -15,9 +15,16 @@ namespace chatterino { static const char *ANONYMOUS_USERNAME ATTR_UNUSED = "justinfan64537"; -inline QByteArray getDefaultClientID() +inline QString getDefaultClientID() { - return QByteArray("7ue61iz46fz11y3cugd0l3tawb4taal"); + // return "g5zg0400k4vhrx2g6xi4hgveruamlv"; // official one, used on chatterino.com/client_login + return "t9xehoda0otdcfkzs24afh5h14wss6"; // zneix's testing instance +} + +// Redirect URI used +inline QString getRedirectURI() +{ + return "http://localhost:52107/redirect"; } static const std::vector TWITCH_USERNAME_COLORS = { @@ -38,4 +45,28 @@ static const std::vector TWITCH_USERNAME_COLORS = { {0, 255, 127}, // SpringGreen }; +static const QStringList loginScopes{ + // clang-format off + "user_subscriptions", // + "user_blocks_edit", // [DEPRECATED] replaced with "user:manage:blocked_users" + "user_blocks_read", // [DEPRECATED] replaced with "user:read:blocked_users" + "user_follows_edit", // [DEPRECATED] soon to be removed later since we now use "user:edit:follows" + "channel_editor", // [DEPRECATED] for /raid + "channel:moderate", // + "channel:read:redemptions", // + "chat:edit", // + "chat:read", // + "whispers:read", // + "whispers:edit", // + "channel_commercial", // [DEPRECATED] for /commercial + "channel:edit:commercial", // in case twitch upgrades things in the future (and this scope is required) + "user:edit:follows", // for (un)following + "clips:edit", // for clip creation + "channel:manage:broadcast", // for creating stream markers with /marker command, and for the /settitle and /setgame commands + "user:read:blocked_users", // [DEPRECATED] for getting list of blocked users + "user:manage:blocked_users", // [DEPRECATED] for blocking/unblocking other users + "moderator:manage:automod", // for approving/denying automod messages + // clang-format on +}; + } // namespace chatterino diff --git a/src/widgets/dialogs/LoginDialog.cpp b/src/widgets/dialogs/LoginDialog.cpp index b912e7bd9..c72002666 100644 --- a/src/widgets/dialogs/LoginDialog.cpp +++ b/src/widgets/dialogs/LoginDialog.cpp @@ -5,8 +5,10 @@ #include "common/NetworkRequest.hpp" #include "common/QLogging.hpp" #include "controllers/accounts/AccountController.hpp" +#include "providers/twitch/TwitchCommon.hpp" #include "util/Clipboard.hpp" #include "util/Helpers.hpp" +#include "widgets/helper/LoginServer.hpp" #ifdef USEWINSDK # include @@ -15,307 +17,23 @@ #include #include #include -#include -#include #include -#include -#include #include namespace chatterino { -namespace { - - void logInWithCredentials(const QString &userID, const QString &username, - const QString &clientID, - const QString &oauthToken) - { - QStringList errors; - - if (userID.isEmpty()) - { - errors.append("Missing user ID"); - } - if (username.isEmpty()) - { - errors.append("Missing username"); - } - if (clientID.isEmpty()) - { - errors.append("Missing Client ID"); - } - if (oauthToken.isEmpty()) - { - errors.append("Missing OAuth Token"); - } - - if (errors.length() > 0) - { - QMessageBox messageBox; -// Set error window on top -#ifdef USEWINSDK - ::SetWindowPos(HWND(messageBox.winId()), HWND_TOPMOST, 0, 0, 0, 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); - -#endif - messageBox.setWindowTitle( - "Chatterino - invalid account credentials"); - messageBox.setIcon(QMessageBox::Critical); - messageBox.setText(errors.join("
")); - messageBox.setStandardButtons(QMessageBox::Ok); - messageBox.exec(); - return; - } - - std::string basePath = "/accounts/uid" + userID.toStdString(); - pajlada::Settings::Setting::set(basePath + "/username", - username); - pajlada::Settings::Setting::set(basePath + "/userID", userID); - pajlada::Settings::Setting::set(basePath + "/clientID", - clientID); - pajlada::Settings::Setting::set(basePath + "/oauthToken", - oauthToken); - - getApp()->accounts->twitch.reloadUsers(); - getApp()->accounts->twitch.currentUsername = username; - } - -} // namespace - -BasicLoginWidget::BasicLoginWidget() -{ - // Initialize HTTP server and its routes - qCDebug(chatterinoWidget) << "Creating new HTTP server"; - this->httpServer_ = new QHttpServer(this); - this->tcpServer_ = new QTcpServer(this->httpServer_); - this->httpServer_->bind(this->tcpServer_); - - qCDebug(chatterinoWidget) << "Initializing HTTP server's routes"; - this->httpServer_->route( - "/redirect", QHttpServerRequest::Method::GET, - [](const QHttpServerRequest &req, QHttpServerResponder &&resp) { - QFile redirectHTML(":/auth.html"); - redirectHTML.open(QIODevice::ReadOnly); - - resp.write(redirectHTML.readAll(), - {{"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "GET, PUT"}, - {"Access-Control-Allow-Headers", "X-Access-Token"}}, - QHttpServerResponder::StatusCode::Ok); - }); - this->httpServer_->route( - ".*", QHttpServerRequest::Method::OPTIONS, - [](const QHttpServerRequest &req, QHttpServerResponder &&resp) { - qDebug() << "options called!"; - resp.write({{"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "GET, PUT"}, - {"Access-Control-Allow-Headers", "X-Access-Token"}}, - QHttpServerResponder::StatusCode::Ok); - }); - this->httpServer_->route( - "/token", QHttpServerRequest::Method::PUT, - [](const QHttpServerRequest &req, QHttpServerResponder &&resp) { - if (!req.headers().contains("X-Access-Token")) - { - resp.write(QHttpServerResponder::StatusCode::BadRequest); - return; - } - - // Handle token - const auto token = req.headers().value("X-Access-Token").toString(); - qDebug() << token; - resp.write({{"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "GET, PUT"}, - {"Access-Control-Allow-Headers", "X-Access-Token"}}, - QHttpServerResponder::StatusCode::Ok); - }); - - const QString loginLink = "http://localhost:1234"; - this->setLayout(&this->ui_.layout); - - this->ui_.loginButton.setText("Log in (Opens in browser)"); - this->ui_.pasteCodeButton.setText("Paste login info"); - this->ui_.unableToOpenBrowserHelper.setWindowTitle( - "Chatterino - unable to open in browser"); - this->ui_.unableToOpenBrowserHelper.setWordWrap(true); - this->ui_.unableToOpenBrowserHelper.hide(); - this->ui_.unableToOpenBrowserHelper.setText( - QString("An error occurred while attempting to open the " - "log in link (%1) - open it manually in your browser and " - "proceed from there.") - .arg(loginLink)); - this->ui_.unableToOpenBrowserHelper.setOpenExternalLinks(true); - - this->ui_.horizontalLayout.addWidget(&this->ui_.loginButton); - this->ui_.horizontalLayout.addWidget(&this->ui_.pasteCodeButton); - - this->ui_.layout.addLayout(&this->ui_.horizontalLayout); - this->ui_.layout.addWidget(&this->ui_.unableToOpenBrowserHelper); - - connect(&this->ui_.loginButton, &QPushButton::clicked, [this, loginLink]() { - // Start listening for credentials - if (!this->tcpServer_->listen(serverAddress, serverPort)) - { - qCWarning(chatterinoWidget) << "Failed to start HTTP server"; - } - else - { - qInfo(chatterinoWidget) << QString("HTTP server Listening on %1:%2") - .arg(serverAddress.toString()) - .arg(serverPort); - this->ui_.loginButton.setText("Listening..."); - this->ui_.loginButton.setDisabled(true); - } - - // Open login page - if (!QDesktopServices::openUrl(QUrl(loginLink))) - { - this->ui_.unableToOpenBrowserHelper.show(); - return; - } - }); - - connect(&this->ui_.pasteCodeButton, &QPushButton::clicked, [this]() { - QStringList parameters = getClipboardText().split(";"); - QString oauthToken, clientID, username, userID; - - for (const auto ¶m : parameters) - { - QStringList kvParameters = param.split('='); - if (kvParameters.size() != 2) - { - continue; - } - QString key = kvParameters[0]; - QString value = kvParameters[1]; - - if (key == "oauth_token") - { - oauthToken = value; - } - else if (key == "client_id") - { - clientID = value; - } - else if (key == "username") - { - username = value; - } - else if (key == "user_id") - { - userID = value; - } - else - { - qCWarning(chatterinoWidget) << "Unknown key in code: " << key; - } - } - - logInWithCredentials(userID, username, clientID, oauthToken); - - // Removing clipboard content to prevent accidental paste of credentials into somewhere - crossPlatformCopy(""); - this->window()->close(); - }); -} - -void BasicLoginWidget::closeHttpServer() -{ - // Revert login button - this->ui_.loginButton.setText("Log in (Opens in browser)"); - this->ui_.loginButton.setEnabled(true); - - qCDebug(chatterinoWidget) << "Closing TCP servers bind to HTTP server"; - for (const auto &server : this->httpServer_->servers()) - { - server->close(); - } -} - -AdvancedLoginWidget::AdvancedLoginWidget() -{ - this->setLayout(&this->ui_.layout); - - this->ui_.instructionsLabel.setText("1. Fill in your username" - "\n2. Fill in your user ID" - "\n3. Fill in your client ID" - "\n4. Fill in your OAuth token" - "\n5. Press Add user"); - this->ui_.instructionsLabel.setWordWrap(true); - - this->ui_.layout.addWidget(&this->ui_.instructionsLabel); - this->ui_.layout.addLayout(&this->ui_.formLayout); - this->ui_.layout.addLayout(&this->ui_.buttonUpperRow.layout); - - this->refreshButtons(); - - /// Form - this->ui_.formLayout.addRow("Username", &this->ui_.usernameInput); - this->ui_.formLayout.addRow("User ID", &this->ui_.userIDInput); - this->ui_.formLayout.addRow("Client ID", &this->ui_.clientIDInput); - this->ui_.formLayout.addRow("OAuth token", &this->ui_.oauthTokenInput); - - this->ui_.oauthTokenInput.setEchoMode(QLineEdit::Password); - - connect(&this->ui_.userIDInput, &QLineEdit::textChanged, [=]() { - this->refreshButtons(); - }); - connect(&this->ui_.usernameInput, &QLineEdit::textChanged, [=]() { - this->refreshButtons(); - }); - connect(&this->ui_.clientIDInput, &QLineEdit::textChanged, [=]() { - this->refreshButtons(); - }); - connect(&this->ui_.oauthTokenInput, &QLineEdit::textChanged, [=]() { - this->refreshButtons(); - }); - - /// Upper button row - - this->ui_.buttonUpperRow.addUserButton.setText("Add user"); - this->ui_.buttonUpperRow.clearFieldsButton.setText("Clear fields"); - - this->ui_.buttonUpperRow.layout.addWidget( - &this->ui_.buttonUpperRow.addUserButton); - this->ui_.buttonUpperRow.layout.addWidget( - &this->ui_.buttonUpperRow.clearFieldsButton); - - connect(&this->ui_.buttonUpperRow.clearFieldsButton, &QPushButton::clicked, - [=]() { - this->ui_.userIDInput.clear(); - this->ui_.usernameInput.clear(); - this->ui_.clientIDInput.clear(); - this->ui_.oauthTokenInput.clear(); - }); - - connect(&this->ui_.buttonUpperRow.addUserButton, &QPushButton::clicked, - [=]() { - QString userID = this->ui_.userIDInput.text(); - QString username = this->ui_.usernameInput.text(); - QString clientID = this->ui_.clientIDInput.text(); - QString oauthToken = this->ui_.oauthTokenInput.text(); - - logInWithCredentials(userID, username, clientID, oauthToken); - }); -} - -void AdvancedLoginWidget::refreshButtons() -{ - if (this->ui_.userIDInput.text().isEmpty() || - this->ui_.usernameInput.text().isEmpty() || - this->ui_.clientIDInput.text().isEmpty() || - this->ui_.oauthTokenInput.text().isEmpty()) - { - this->ui_.buttonUpperRow.addUserButton.setEnabled(false); - } - else - { - this->ui_.buttonUpperRow.addUserButton.setEnabled(true); - } -} - -LoginWidget::LoginWidget(QWidget *parent) +LoginDialog::LoginDialog(QWidget *parent) : QDialog(parent) + , loginServer_(new LoginServer(this)) + // By default, QUrl constructor urlencodes our string so we don't have to do this ourselves + , loginLink(QString("https://id.twitch.tv/oauth2/authorize" + "?client_id=%1" + "&redirect_uri=%2" + "&response_type=%3" + "&scope=%4" + "&force_verify=%5") + .arg(getDefaultClientID(), getRedirectURI(), "token", + loginScopes.join(" "), QVariant(true).toString())) { #ifdef USEWINSDK ::SetWindowPos(HWND(this->winId()), HWND_TOPMOST, 0, 0, 0, 0, @@ -323,27 +41,155 @@ LoginWidget::LoginWidget(QWidget *parent) #endif this->setWindowTitle("Chatterino - add new account"); + this->setLayout(&this->ui_.layout); - this->setLayout(&this->ui_.mainLayout); - this->ui_.mainLayout.addWidget(&this->ui_.tabWidget); + // Label with explanation of what does user have to do here + this->ui_.helpLabel.setText( + QString("Click on the \"Log in\" button to open Twitch " + "login page in browser.
If it doesn't open " + "automatically, right click the link and open it yourself." + "

You can also paste your token from clipboard.") + .arg(this->loginLink.toString())); + this->ui_.helpLabel.setOpenExternalLinks(true); - this->ui_.tabWidget.addTab(&this->ui_.basic, "Basic"); - this->ui_.tabWidget.addTab(&this->ui_.advanced, "Advanced"); + // Label that ensures this is babyproof + this->ui_.warningLabel.setText("DO NOT SHOW THIS ON STREAM!!!"); + this->ui_.warningLabel.setStyleSheet( + "QLabel { color: red; font-weight: 900;" + "font-size: 3em; text-align: center; }"); + // Login buttons + this->ui_.loginButton.setText(LOGIN_BUTTON_START); + this->ui_.pasteTokenButton.setText(TOKEN_BUTTON_START); + + // Separate box with Close button this->ui_.buttonBox.setStandardButtons(QDialogButtonBox::Close); - QObject::connect(&this->ui_.buttonBox, &QDialogButtonBox::rejected, - [this]() { - this->close(); - }); + // Add everything to layout + this->ui_.layout.addWidget(&this->ui_.helpLabel); + this->ui_.layout.addWidget(&this->ui_.warningLabel); - this->ui_.mainLayout.addWidget(&this->ui_.buttonBox); + this->ui_.buttons.addWidget(&this->ui_.loginButton); + this->ui_.buttons.addWidget(&this->ui_.pasteTokenButton); + this->ui_.layout.addLayout(&this->ui_.buttons); + + this->ui_.layout.addWidget(&this->ui_.buttonBox); + + // Connect to button events + QObject::connect(&this->ui_.buttonBox, &QDialogButtonBox::rejected, [this] { + this->close(); + }); + + QObject::connect(&this->ui_.loginButton, &QPushButton::clicked, [this] { + // Start listening for credentials + if (!this->loginServer_->listen()) + { + qCWarning(chatterinoWidget) << "Failed to start HTTP server"; + } + else + { + qInfo(chatterinoWidget) << "HTTP server Listening on " + + this->loginServer_->getAddress(); + } + + // Open login page + QDesktopServices::openUrl(this->loginLink); + this->deactivateLoginButton(); + }); + + QObject::connect(&this->ui_.pasteTokenButton, &QPushButton::clicked, [this] { + this->ui_.pasteTokenButton.setText(VALIDATING_TOKEN); + this->ui_.pasteTokenButton.setDisabled(true); + this->logInWithToken( + getClipboardText(), + [] { + // success + // Removing clipboard content to prevent accidental paste of credentials into somewhere + crossPlatformCopy(QString()); + }, + [this] { + // failure + this->ui_.pasteTokenButton.setText("Invalid token pasted!"); + }, + [this] { + // finally + // Add some sort of "rate-limit" to not spam logInWithToken + QTimer::singleShot(3000, [this] { + this->ui_.pasteTokenButton.setText(TOKEN_BUTTON_START); + this->ui_.pasteTokenButton.setEnabled(true); + }); + }); + }); } -void LoginWidget::hideEvent(QHideEvent *event) +void LoginDialog::deactivateLoginButton() +{ + this->ui_.loginButton.setText("Continue in browser..."); + this->ui_.loginButton.setDisabled(true); +} + +void LoginDialog::activateLoginButton() +{ + this->ui_.loginButton.setText(LOGIN_BUTTON_START); + this->ui_.loginButton.setEnabled(true); +} + +void LoginDialog::logInWithToken(QString token, + std::function successCallback, + std::function failureCallback, + std::function finallyCallback) +{ + NetworkRequest("https://id.twitch.tv/oauth2/validate") + .timeout(5 * 1000) + .header("Accept", "application/json") + .header("Authorization", "OAuth " + token) + .onSuccess([successCallback, failureCallback, token, + this](NetworkResult result) -> Outcome { + auto root = result.parseJson(); + TokenValidationResponse validation(root); + + if (validation.clientId.isEmpty() || validation.login.isEmpty() || + validation.userId.isEmpty()) + { + failureCallback(); + return Failure; + } + + // Update account settings and call it a success + std::string basePath = + "/accounts/uid" + validation.userId.toStdString(); + pajlada::Settings::Setting::set(basePath + "/username", + validation.login); + pajlada::Settings::Setting::set(basePath + "/userID", + validation.userId); + pajlada::Settings::Setting::set(basePath + "/clientID", + validation.clientId); + pajlada::Settings::Setting::set(basePath + "/oauthToken", + token); + + getApp()->accounts->twitch.reloadUsers(); + getApp()->accounts->twitch.currentUsername = validation.login; + // Closing the window will emit hideEvent which will close the server + // however, any already extablished connections will not terminate immidiatelly, + // so it's fine to close the window already + this->window()->close(); + + successCallback(); + return Success; + }) + .onError([failureCallback](auto result) { + failureCallback(); + }) + .finally(finallyCallback) + .execute(); +} + +void LoginDialog::hideEvent(QHideEvent *event) { // Make the port free - this->ui_.basic.closeHttpServer(); + this->loginServer_->close(); + // Restore login button + this->activateLoginButton(); } } // namespace chatterino diff --git a/src/widgets/dialogs/LoginDialog.hpp b/src/widgets/dialogs/LoginDialog.hpp index e02bebb7a..d1117305b 100644 --- a/src/widgets/dialogs/LoginDialog.hpp +++ b/src/widgets/dialogs/LoginDialog.hpp @@ -1,97 +1,81 @@ #pragma once +#include "common/NetworkRequest.hpp" +#include "common/Outcome.hpp" #include "widgets/BaseWidget.hpp" +#include "widgets/helper/LoginServer.hpp" -#include #include -#include #include #include -#include #include -#include +#include +#include #include -#include #include -#include -#include -#include #include #include -#include + +#define LOGIN_BUTTON_START "Click to Log in" +#define TOKEN_BUTTON_START "Paste token from clipboard" +#define VALIDATING_TOKEN "Validating your token..." namespace chatterino { -class BasicLoginWidget : public QWidget +class LoginServer; + +struct TokenValidationResponse { + QString clientId; + QString login; + QString userId; + std::vector scopes; + int expiresIn; + + explicit TokenValidationResponse(QJsonObject root) + : clientId(root.value("client_id").toString()) + , login(root.value("login").toString()) + , userId(root.value("user_id").toString()) + , expiresIn(root.value("expires_in").toInt()) + { + for (const auto &scope : root.value("scopes").toArray()) + { + this->scopes.emplace_back(scope.toString()); + } + } +}; + +class LoginDialog : public QDialog { public: - BasicLoginWidget(); + LoginDialog(QWidget *parent); +private: struct { QVBoxLayout layout; - QHBoxLayout horizontalLayout; + + QLabel helpLabel; + QLabel warningLabel; + + QHBoxLayout buttons; QPushButton loginButton; - QPushButton pasteCodeButton; - QLabel unableToOpenBrowserHelper; - } ui_; - - void closeHttpServer(); - -private: - // Local server listening to login data - const QHostAddress serverAddress{QHostAddress::LocalHost}; - static const int serverPort = 52107; - QHttpServer *httpServer_; - QTcpServer *tcpServer_; -}; - -class AdvancedLoginWidget : public QWidget -{ -public: - AdvancedLoginWidget(); - - void refreshButtons(); - - struct { - QVBoxLayout layout; - - QLabel instructionsLabel; - - QFormLayout formLayout; - - QLineEdit userIDInput; - QLineEdit usernameInput; - QLineEdit clientIDInput; - QLineEdit oauthTokenInput; - - struct { - QHBoxLayout layout; - - QPushButton addUserButton; - QPushButton clearFieldsButton; - } buttonUpperRow; - } ui_; -}; - -class LoginWidget : public QDialog -{ -public: - LoginWidget(QWidget *parent); - -private: - struct { - QVBoxLayout mainLayout; - - QTabWidget tabWidget; + QPushButton pasteTokenButton; QDialogButtonBox buttonBox; - - BasicLoginWidget basic; - - AdvancedLoginWidget advanced; } ui_; + // Local server listening to login data returned from Twitch + LoginServer *loginServer_; + QUrl loginLink; + + void deactivateLoginButton(); + void activateLoginButton(); + void logInWithToken(QString token, std::function successCallback, + std::function failureCallback, + std::function finallyCallback); + void hideEvent(QHideEvent *e) override; + + friend class LoginServer; }; } // namespace chatterino diff --git a/src/widgets/helper/LoginServer.cpp b/src/widgets/helper/LoginServer.cpp new file mode 100644 index 000000000..5dc2f44a8 --- /dev/null +++ b/src/widgets/helper/LoginServer.cpp @@ -0,0 +1,104 @@ +#include "widgets/helper/LoginServer.hpp" + +#include "common/QLogging.hpp" +#include "widgets/dialogs/LoginDialog.hpp" + +#include + +namespace chatterino { + +LoginServer::LoginServer(LoginDialog *parent) + : parent_(parent) + , http_(new QHttpServer()) + , tcp_(new QTcpServer(this->http_)) +{ + qCDebug(chatterinoWidget) << "Creating new HTTP server"; + this->http_->bind(this->tcp_); + this->initializeRoutes(); +} + +QString LoginServer::getAddress() +{ + return QString("%1:%2") + .arg(this->bind_.ip.toString()) + .arg(this->bind_.port); +} + +bool LoginServer::listen() +{ + return this->tcp_->listen(this->bind_.ip, this->bind_.port); +} + +void LoginServer::close() +{ + // There's no way to close QHttpServer, so we have to work with our QTcpServer instead + qCDebug(chatterinoWidget) << "Closing TCP server bound to HTTP server"; + this->tcp_->close(); +} + +void LoginServer::initializeRoutes() +{ + // Redirect page containing JS script that takes token from URL fragment and calls /token + this->http_->route( + "/redirect", QHttpServerRequest::Method::GET, + [](QHttpServerResponder &&resp) { + QFile redirectPage(":/auth.html"); + redirectPage.open(QIODevice::ReadOnly); + + resp.write(redirectPage.readAll(), + {{"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "GET, PUT"}, + {"Access-Control-Allow-Headers", "X-Access-Token"}}, + QHttpServerResponder::StatusCode::Ok); + }); + // For CORS, letting the browser know that it's fine to make a cross-origin request + this->http_->route( + ".*", QHttpServerRequest::Method::OPTIONS, + [](QHttpServerResponder &&resp) { + resp.write({{"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "GET, PUT"}, + {"Access-Control-Allow-Headers", "X-Access-Token"}}, + QHttpServerResponder::StatusCode::Ok); + }); + // Endpoint called from /redirect that processes token passed in headers + // Returns no content, but different headers indicating token's validity + this->http_->route( + "/token", QHttpServerRequest::Method::PUT, + [this](const QHttpServerRequest &req, QHttpServerResponder &&resp) { + // Access token wasn't specified + if (!req.headers().contains("X-Access-Token")) + { + resp.write(QHttpServerResponder::StatusCode::BadRequest); + return; + } + + // Validate token + const auto token = req.headers().value("X-Access-Token").toString(); + + auto respPtr = + std::make_shared(std::move(resp)); + this->parent_->ui_.loginButton.setText(VALIDATING_TOKEN); + this->parent_->logInWithToken( + token, + [respPtr] { + respPtr->write( + {{"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "GET, PUT"}, + {"Access-Control-Allow-Headers", "X-Access-Token"}}, + QHttpServerResponder::StatusCode::Ok); + }, + [respPtr] { + respPtr->write( + {{"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "GET, PUT"}, + {"Access-Control-Allow-Headers", "X-Access-Token"}}, + QHttpServerResponder::StatusCode::BadRequest); + }, + [this] { + this->close(); + this->parent_->activateLoginButton(); + }); + }); +} + +} // namespace chatterino diff --git a/src/widgets/helper/LoginServer.hpp b/src/widgets/helper/LoginServer.hpp new file mode 100644 index 000000000..1f71e777a --- /dev/null +++ b/src/widgets/helper/LoginServer.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include "widgets/dialogs/LoginDialog.hpp" + +#include +#include +#include + +namespace chatterino { + +class LoginDialog; + +class LoginServer +{ +public: + LoginServer(LoginDialog *parent); + + QString getAddress(); + bool listen(); + void close(); + +private: + LoginDialog *parent_; + + const struct { + QHostAddress ip = QHostAddress::LocalHost; + int port = 52107; + } bind_; + + QHttpServer *http_; + QTcpServer *tcp_; + + void initializeRoutes(); +}; + +} // namespace chatterino diff --git a/src/widgets/settingspages/AccountsPage.cpp b/src/widgets/settingspages/AccountsPage.cpp index 1c53becbf..0fc335721 100644 --- a/src/widgets/settingspages/AccountsPage.cpp +++ b/src/widgets/settingspages/AccountsPage.cpp @@ -33,10 +33,10 @@ AccountsPage::AccountsPage() view->getTableView()->horizontalHeader()->setStretchLastSection(true); view->addButtonPressed.connect([this] { - static auto loginWidget = new LoginWidget(this); + static auto loginDialog = new LoginDialog(this); - loginWidget->show(); - loginWidget->raise(); + loginDialog->show(); + loginDialog->raise(); }); view->getTableView()->setStyleSheet("background: #333");