mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-21 22:24:07 +01:00
Huge changes, it works properly now
This commit is contained in:
parent
e06618be1e
commit
f50b50736a
8 changed files with 384 additions and 379 deletions
|
@ -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 \
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<QColor> TWITCH_USERNAME_COLORS = {
|
||||
|
@ -38,4 +45,28 @@ static const std::vector<QColor> 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
|
||||
|
|
|
@ -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 <Windows.h>
|
||||
|
@ -15,307 +17,23 @@
|
|||
#include <QClipboard>
|
||||
#include <QDebug>
|
||||
#include <QDesktopServices>
|
||||
#include <QFile>
|
||||
#include <QMessageBox>
|
||||
#include <QUrl>
|
||||
#include <QtHttpServer/QHttpServer>
|
||||
#include <QtHttpServer/QHttpServerResponder>
|
||||
#include <pajlada/settings/setting.hpp>
|
||||
|
||||
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("<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 ¶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 <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");
|
||||
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]() {
|
||||
// Add everything to layout
|
||||
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->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();
|
||||
}
|
||||
|
||||
void LoginWidget::hideEvent(QHideEvent *event)
|
||||
// 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 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
|
||||
this->ui_.basic.closeHttpServer();
|
||||
this->loginServer_->close();
|
||||
// Restore login button
|
||||
this->activateLoginButton();
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -1,97 +1,81 @@
|
|||
#pragma once
|
||||
|
||||
#include "common/NetworkRequest.hpp"
|
||||
#include "common/Outcome.hpp"
|
||||
#include "widgets/BaseWidget.hpp"
|
||||
#include "widgets/helper/LoginServer.hpp"
|
||||
|
||||
#include <QAction>
|
||||
#include <QApplication>
|
||||
#include <QButtonGroup>
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QTabWidget>
|
||||
#include <QTcpServer>
|
||||
#include <QTcpSocket>
|
||||
#include <QVBoxLayout>
|
||||
#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 {
|
||||
|
||||
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:
|
||||
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<void()> successCallback,
|
||||
std::function<void()> failureCallback,
|
||||
std::function<void()> finallyCallback);
|
||||
|
||||
void hideEvent(QHideEvent *e) override;
|
||||
|
||||
friend class LoginServer;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
104
src/widgets/helper/LoginServer.cpp
Normal file
104
src/widgets/helper/LoginServer.cpp
Normal 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
|
36
src/widgets/helper/LoginServer.hpp
Normal file
36
src/widgets/helper/LoginServer.hpp
Normal 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
|
|
@ -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");
|
||||
|
|
Loading…
Reference in a new issue