Huge changes, it works properly now

This commit is contained in:
zneix 2021-07-25 03:59:58 +02:00
parent e06618be1e
commit f50b50736a
No known key found for this signature in database
GPG key ID: 911916E0523B22F6
8 changed files with 384 additions and 379 deletions

View file

@ -288,6 +288,7 @@ SOURCES += \
src/widgets/helper/DebugPopup.cpp \ src/widgets/helper/DebugPopup.cpp \
src/widgets/helper/EditableModelView.cpp \ src/widgets/helper/EditableModelView.cpp \
src/widgets/helper/EffectLabel.cpp \ src/widgets/helper/EffectLabel.cpp \
src/widgets/helper/LoginServer.cpp \
src/widgets/helper/NotebookButton.cpp \ src/widgets/helper/NotebookButton.cpp \
src/widgets/helper/NotebookTab.cpp \ src/widgets/helper/NotebookTab.cpp \
src/widgets/helper/QColorPicker.cpp \ src/widgets/helper/QColorPicker.cpp \
@ -551,6 +552,7 @@ HEADERS += \
src/widgets/helper/EditableModelView.hpp \ src/widgets/helper/EditableModelView.hpp \
src/widgets/helper/EffectLabel.hpp \ src/widgets/helper/EffectLabel.hpp \
src/widgets/helper/Line.hpp \ src/widgets/helper/Line.hpp \
src/widgets/helper/LoginServer.hpp \
src/widgets/helper/NotebookButton.hpp \ src/widgets/helper/NotebookButton.hpp \
src/widgets/helper/NotebookTab.hpp \ src/widgets/helper/NotebookTab.hpp \
src/widgets/helper/QColorPicker.hpp \ src/widgets/helper/QColorPicker.hpp \

View file

@ -378,6 +378,8 @@ set(SOURCE_FILES
widgets/helper/EditableModelView.hpp widgets/helper/EditableModelView.hpp
widgets/helper/EffectLabel.cpp widgets/helper/EffectLabel.cpp
widgets/helper/EffectLabel.hpp widgets/helper/EffectLabel.hpp
widgets/helper/LoginServer.cpp
widgets/helper/LoginServer.hpp
widgets/helper/NotebookButton.cpp widgets/helper/NotebookButton.cpp
widgets/helper/NotebookButton.hpp widgets/helper/NotebookButton.hpp
widgets/helper/NotebookTab.cpp widgets/helper/NotebookTab.cpp

View file

@ -15,9 +15,16 @@ namespace chatterino {
static const char *ANONYMOUS_USERNAME ATTR_UNUSED = "justinfan64537"; 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<QColor> TWITCH_USERNAME_COLORS = { static const std::vector<QColor> TWITCH_USERNAME_COLORS = {
@ -38,4 +45,28 @@ static const std::vector<QColor> TWITCH_USERNAME_COLORS = {
{0, 255, 127}, // SpringGreen {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 } // namespace chatterino

View file

@ -5,8 +5,10 @@
#include "common/NetworkRequest.hpp" #include "common/NetworkRequest.hpp"
#include "common/QLogging.hpp" #include "common/QLogging.hpp"
#include "controllers/accounts/AccountController.hpp" #include "controllers/accounts/AccountController.hpp"
#include "providers/twitch/TwitchCommon.hpp"
#include "util/Clipboard.hpp" #include "util/Clipboard.hpp"
#include "util/Helpers.hpp" #include "util/Helpers.hpp"
#include "widgets/helper/LoginServer.hpp"
#ifdef USEWINSDK #ifdef USEWINSDK
# include <Windows.h> # include <Windows.h>
@ -15,307 +17,23 @@
#include <QClipboard> #include <QClipboard>
#include <QDebug> #include <QDebug>
#include <QDesktopServices> #include <QDesktopServices>
#include <QFile>
#include <QMessageBox>
#include <QUrl> #include <QUrl>
#include <QtHttpServer/QHttpServer>
#include <QtHttpServer/QHttpServerResponder>
#include <pajlada/settings/setting.hpp> #include <pajlada/settings/setting.hpp>
namespace chatterino { namespace chatterino {
namespace { LoginDialog::LoginDialog(QWidget *parent)
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("<br>"));
messageBox.setStandardButtons(QMessageBox::Ok);
messageBox.exec();
return;
}
std::string basePath = "/accounts/uid" + userID.toStdString();
pajlada::Settings::Setting<QString>::set(basePath + "/username",
username);
pajlada::Settings::Setting<QString>::set(basePath + "/userID", userID);
pajlada::Settings::Setting<QString>::set(basePath + "/clientID",
clientID);
pajlada::Settings::Setting<QString>::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 <a href=\"%1\">the "
"log in link (%1)</a> - 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 &param : 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)
: QDialog(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 #ifdef USEWINSDK
::SetWindowPos(HWND(this->winId()), HWND_TOPMOST, 0, 0, 0, 0, ::SetWindowPos(HWND(this->winId()), HWND_TOPMOST, 0, 0, 0, 0,
@ -323,27 +41,155 @@ LoginWidget::LoginWidget(QWidget *parent)
#endif #endif
this->setWindowTitle("Chatterino - add new account"); this->setWindowTitle("Chatterino - add new account");
this->setLayout(&this->ui_.layout);
this->setLayout(&this->ui_.mainLayout); // Label with explanation of what does user have to do here
this->ui_.mainLayout.addWidget(&this->ui_.tabWidget); this->ui_.helpLabel.setText(
QString("Click on the \"Log in\" button to open <a href=\"%1\">Twitch "
"login page</a> in browser.<br>If it doesn't open "
"automatically, right click the link and open it yourself."
"<br><br>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"); // Label that ensures this is babyproof
this->ui_.tabWidget.addTab(&this->ui_.advanced, "Advanced"); 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); this->ui_.buttonBox.setStandardButtons(QDialogButtonBox::Close);
QObject::connect(&this->ui_.buttonBox, &QDialogButtonBox::rejected, // Add everything to layout
[this]() { this->ui_.layout.addWidget(&this->ui_.helpLabel);
this->ui_.layout.addWidget(&this->ui_.warningLabel);
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(); this->close();
}); });
this->ui_.mainLayout.addWidget(&this->ui_.buttonBox); 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<void()> successCallback,
std::function<void()> failureCallback,
std::function<void()> 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<QString>::set(basePath + "/username",
validation.login);
pajlada::Settings::Setting<QString>::set(basePath + "/userID",
validation.userId);
pajlada::Settings::Setting<QString>::set(basePath + "/clientID",
validation.clientId);
pajlada::Settings::Setting<QString>::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 // Make the port free
this->ui_.basic.closeHttpServer(); this->loginServer_->close();
// Restore login button
this->activateLoginButton();
} }
} // namespace chatterino } // namespace chatterino

View file

@ -1,97 +1,81 @@
#pragma once #pragma once
#include "common/NetworkRequest.hpp"
#include "common/Outcome.hpp"
#include "widgets/BaseWidget.hpp" #include "widgets/BaseWidget.hpp"
#include "widgets/helper/LoginServer.hpp"
#include <QAction>
#include <QApplication> #include <QApplication>
#include <QButtonGroup>
#include <QDialog> #include <QDialog>
#include <QDialogButtonBox> #include <QDialogButtonBox>
#include <QFormLayout>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QHeaderView> #include <QJsonArray>
#include <QJsonObject>
#include <QLabel> #include <QLabel>
#include <QLineEdit>
#include <QPushButton> #include <QPushButton>
#include <QTabWidget>
#include <QTcpServer>
#include <QTcpSocket>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QtCore/QVariant> #include <QtCore/QVariant>
#include <QtHttpServer/QHttpServer>
#define LOGIN_BUTTON_START "Click to Log in"
#define TOKEN_BUTTON_START "Paste token from clipboard"
#define VALIDATING_TOKEN "Validating your token..."
namespace chatterino { namespace chatterino {
class BasicLoginWidget : public QWidget class LoginServer;
struct TokenValidationResponse {
QString clientId;
QString login;
QString userId;
std::vector<QString> 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: public:
BasicLoginWidget(); LoginDialog(QWidget *parent);
private:
struct { struct {
QVBoxLayout layout; QVBoxLayout layout;
QHBoxLayout horizontalLayout;
QLabel helpLabel;
QLabel warningLabel;
QHBoxLayout buttons;
QPushButton loginButton; QPushButton loginButton;
QPushButton pasteCodeButton; QPushButton pasteTokenButton;
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;
QDialogButtonBox buttonBox; QDialogButtonBox buttonBox;
BasicLoginWidget basic;
AdvancedLoginWidget advanced;
} ui_; } ui_;
// Local server listening to login data returned from Twitch
LoginServer *loginServer_;
QUrl loginLink;
void deactivateLoginButton();
void activateLoginButton();
void logInWithToken(QString token, std::function<void()> successCallback,
std::function<void()> failureCallback,
std::function<void()> finallyCallback);
void hideEvent(QHideEvent *e) override; void hideEvent(QHideEvent *e) override;
friend class LoginServer;
}; };
} // namespace chatterino } // namespace chatterino

View file

@ -0,0 +1,104 @@
#include "widgets/helper/LoginServer.hpp"
#include "common/QLogging.hpp"
#include "widgets/dialogs/LoginDialog.hpp"
#include <QFile>
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<QHttpServerResponder>(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

View file

@ -0,0 +1,36 @@
#pragma once
#include "widgets/dialogs/LoginDialog.hpp"
#include <QHttpServer>
#include <QTcpServer>
#include <QtHttpServer/QHttpServer>
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

View file

@ -33,10 +33,10 @@ AccountsPage::AccountsPage()
view->getTableView()->horizontalHeader()->setStretchLastSection(true); view->getTableView()->horizontalHeader()->setStretchLastSection(true);
view->addButtonPressed.connect([this] { view->addButtonPressed.connect([this] {
static auto loginWidget = new LoginWidget(this); static auto loginDialog = new LoginDialog(this);
loginWidget->show(); loginDialog->show();
loginWidget->raise(); loginDialog->raise();
}); });
view->getTableView()->setStyleSheet("background: #333"); view->getTableView()->setStyleSheet("background: #333");