commit
eabc9727d8
Binary file not shown.
After Width: | Height: | Size: 607 B |
Binary file not shown.
After Width: | Height: | Size: 517 B |
Binary file not shown.
After Width: | Height: | Size: 588 B |
Binary file not shown.
After Width: | Height: | Size: 526 B |
Binary file not shown.
After Width: | Height: | Size: 708 B |
@ -0,0 +1 @@
|
||||
Subproject commit 6e27aa4c8671e183f11e327a2e1f556c64fdc4a9
|
@ -0,0 +1,113 @@
|
||||
// Copyright 2018 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <QStandardItem>
|
||||
#include <QStandardItemModel>
|
||||
#include "citra_qt/multiplayer/moderation_dialog.h"
|
||||
#include "network/network.h"
|
||||
#include "network/room_member.h"
|
||||
#include "ui_moderation_dialog.h"
|
||||
|
||||
namespace Column {
|
||||
enum {
|
||||
SUBJECT,
|
||||
TYPE,
|
||||
COUNT,
|
||||
};
|
||||
}
|
||||
|
||||
ModerationDialog::ModerationDialog(QWidget* parent)
|
||||
: QDialog(parent), ui(std::make_unique<Ui::ModerationDialog>()) {
|
||||
ui->setupUi(this);
|
||||
|
||||
qRegisterMetaType<Network::Room::BanList>();
|
||||
|
||||
if (auto member = Network::GetRoomMember().lock()) {
|
||||
callback_handle_status_message = member->BindOnStatusMessageReceived(
|
||||
[this](const Network::StatusMessageEntry& status_message) {
|
||||
emit StatusMessageReceived(status_message);
|
||||
});
|
||||
connect(this, &ModerationDialog::StatusMessageReceived, this,
|
||||
&ModerationDialog::OnStatusMessageReceived);
|
||||
callback_handle_ban_list = member->BindOnBanListReceived(
|
||||
[this](const Network::Room::BanList& ban_list) { emit BanListReceived(ban_list); });
|
||||
connect(this, &ModerationDialog::BanListReceived, this, &ModerationDialog::PopulateBanList);
|
||||
}
|
||||
|
||||
// Initialize the UI
|
||||
model = new QStandardItemModel(ui->ban_list_view);
|
||||
model->insertColumns(0, Column::COUNT);
|
||||
model->setHeaderData(Column::SUBJECT, Qt::Horizontal, tr("Subject"));
|
||||
model->setHeaderData(Column::TYPE, Qt::Horizontal, tr("Type"));
|
||||
|
||||
ui->ban_list_view->setModel(model);
|
||||
|
||||
// Load the ban list in background
|
||||
LoadBanList();
|
||||
|
||||
connect(ui->refresh, &QPushButton::clicked, this, [this] { LoadBanList(); });
|
||||
connect(ui->unban, &QPushButton::clicked, this, [this] {
|
||||
auto index = ui->ban_list_view->currentIndex();
|
||||
SendUnbanRequest(model->item(index.row(), 0)->text());
|
||||
});
|
||||
connect(ui->ban_list_view, &QTreeView::clicked, [this] { ui->unban->setEnabled(true); });
|
||||
}
|
||||
|
||||
ModerationDialog::~ModerationDialog() {
|
||||
if (callback_handle_status_message) {
|
||||
if (auto room = Network::GetRoomMember().lock()) {
|
||||
room->Unbind(callback_handle_status_message);
|
||||
}
|
||||
}
|
||||
|
||||
if (callback_handle_ban_list) {
|
||||
if (auto room = Network::GetRoomMember().lock()) {
|
||||
room->Unbind(callback_handle_ban_list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ModerationDialog::LoadBanList() {
|
||||
if (auto room = Network::GetRoomMember().lock()) {
|
||||
ui->refresh->setEnabled(false);
|
||||
ui->refresh->setText(tr("Refreshing"));
|
||||
ui->unban->setEnabled(false);
|
||||
room->RequestBanList();
|
||||
}
|
||||
}
|
||||
|
||||
void ModerationDialog::PopulateBanList(const Network::Room::BanList& ban_list) {
|
||||
model->removeRows(0, model->rowCount());
|
||||
for (const auto& username : ban_list.first) {
|
||||
QStandardItem* subject_item = new QStandardItem(QString::fromStdString(username));
|
||||
QStandardItem* type_item = new QStandardItem(tr("Forum Username"));
|
||||
model->invisibleRootItem()->appendRow({subject_item, type_item});
|
||||
}
|
||||
for (const auto& ip : ban_list.second) {
|
||||
QStandardItem* subject_item = new QStandardItem(QString::fromStdString(ip));
|
||||
QStandardItem* type_item = new QStandardItem(tr("IP Address"));
|
||||
model->invisibleRootItem()->appendRow({subject_item, type_item});
|
||||
}
|
||||
for (int i = 0; i < Column::COUNT - 1; ++i) {
|
||||
ui->ban_list_view->resizeColumnToContents(i);
|
||||
}
|
||||
ui->refresh->setEnabled(true);
|
||||
ui->refresh->setText(tr("Refresh"));
|
||||
ui->unban->setEnabled(false);
|
||||
}
|
||||
|
||||
void ModerationDialog::SendUnbanRequest(const QString& subject) {
|
||||
if (auto room = Network::GetRoomMember().lock()) {
|
||||
room->SendModerationRequest(Network::IdModUnban, subject.toStdString());
|
||||
}
|
||||
}
|
||||
|
||||
void ModerationDialog::OnStatusMessageReceived(const Network::StatusMessageEntry& status_message) {
|
||||
if (status_message.type != Network::IdMemberBanned &&
|
||||
status_message.type != Network::IdAddressUnbanned)
|
||||
return;
|
||||
|
||||
// Update the ban list for ban/unban
|
||||
LoadBanList();
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
// Copyright 2018 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <QDialog>
|
||||
#include "network/room.h"
|
||||
#include "network/room_member.h"
|
||||
|
||||
namespace Ui {
|
||||
class ModerationDialog;
|
||||
}
|
||||
|
||||
class QStandardItemModel;
|
||||
|
||||
class ModerationDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ModerationDialog(QWidget* parent = nullptr);
|
||||
~ModerationDialog();
|
||||
|
||||
signals:
|
||||
void StatusMessageReceived(const Network::StatusMessageEntry&);
|
||||
void BanListReceived(const Network::Room::BanList&);
|
||||
|
||||
private:
|
||||
void LoadBanList();
|
||||
void PopulateBanList(const Network::Room::BanList& ban_list);
|
||||
void SendUnbanRequest(const QString& subject);
|
||||
void OnStatusMessageReceived(const Network::StatusMessageEntry& status_message);
|
||||
|
||||
std::unique_ptr<Ui::ModerationDialog> ui;
|
||||
QStandardItemModel* model;
|
||||
Network::RoomMember::CallbackHandle<Network::StatusMessageEntry> callback_handle_status_message;
|
||||
Network::RoomMember::CallbackHandle<Network::Room::BanList> callback_handle_ban_list;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(Network::Room::BanList);
|
@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ModerationDialog</class>
|
||||
<widget class="QDialog" name="ModerationDialog">
|
||||
<property name="windowTitle">
|
||||
<string>Moderation</string>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>500</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="ban_list_group_box">
|
||||
<property name="title">
|
||||
<string>Ban List</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="refresh">
|
||||
<property name="text">
|
||||
<string>Refreshing</string>
|
||||
</property>
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="unban">
|
||||
<property name="text">
|
||||
<string>Unban</string>
|
||||
</property>
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTreeView" name="ban_list_view"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>ModerationDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
</connection>
|
||||
</connections>
|
||||
<resources/>
|
||||
</ui>
|
@ -0,0 +1,18 @@
|
||||
// Copyright 2018 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include "network/verify_user.h"
|
||||
|
||||
namespace Network::VerifyUser {
|
||||
|
||||
Backend::~Backend() = default;
|
||||
|
||||
NullBackend::~NullBackend() = default;
|
||||
|
||||
UserData NullBackend::LoadUserData([[maybe_unused]] const std::string& verify_UID,
|
||||
[[maybe_unused]] const std::string& token) {
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace Network::VerifyUser
|
@ -0,0 +1,46 @@
|
||||
// Copyright 2018 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include "common/logging/log.h"
|
||||
|
||||
namespace Network::VerifyUser {
|
||||
|
||||
struct UserData {
|
||||
std::string username;
|
||||
std::string display_name;
|
||||
std::string avatar_url;
|
||||
bool moderator = false; ///< Whether the user is a Citra Moderator.
|
||||
};
|
||||
|
||||
/**
|
||||
* A backend used for verifying users and loading user data.
|
||||
*/
|
||||
class Backend {
|
||||
public:
|
||||
virtual ~Backend();
|
||||
|
||||
/**
|
||||
* Verifies the given token and loads the information into a UserData struct.
|
||||
* @param verify_UID A GUID that may be used for verification.
|
||||
* @param token A token that contains user data and verification data. The format and content is
|
||||
* decided by backends.
|
||||
*/
|
||||
virtual UserData LoadUserData(const std::string& verify_UID, const std::string& token) = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* A null backend where the token is ignored.
|
||||
* No verification is performed here and the function returns an empty UserData.
|
||||
*/
|
||||
class NullBackend final : public Backend {
|
||||
public:
|
||||
~NullBackend();
|
||||
|
||||
UserData LoadUserData(const std::string& verify_UID, const std::string& token) override;
|
||||
};
|
||||
|
||||
} // namespace Network::VerifyUser
|
@ -0,0 +1,60 @@
|
||||
// Copyright 2018 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <system_error>
|
||||
#include <jwt/jwt.hpp>
|
||||
#include "common/logging/log.h"
|
||||
#include "common/web_result.h"
|
||||
#include "web_service/verify_user_jwt.h"
|
||||
#include "web_service/web_backend.h"
|
||||
|
||||
namespace WebService {
|
||||
|
||||
static std::string public_key;
|
||||
std::string GetPublicKey(const std::string& host) {
|
||||
if (public_key.empty()) {
|
||||
Client client(host, "", ""); // no need for credentials here
|
||||
public_key = client.GetPlain("/jwt/external/key.pem", true).returned_data;
|
||||
if (public_key.empty()) {
|
||||
LOG_ERROR(WebService, "Could not fetch external JWT public key, verification may fail");
|
||||
} else {
|
||||
LOG_INFO(WebService, "Fetched external JWT public key (size={})", public_key.size());
|
||||
}
|
||||
}
|
||||
return public_key;
|
||||
}
|
||||
|
||||
VerifyUserJWT::VerifyUserJWT(const std::string& host) : pub_key(GetPublicKey(host)) {}
|
||||
|
||||
Network::VerifyUser::UserData VerifyUserJWT::LoadUserData(const std::string& verify_UID,
|
||||
const std::string& token) {
|
||||
const std::string audience = fmt::format("external-{}", verify_UID);
|
||||
using namespace jwt::params;
|
||||
std::error_code error;
|
||||
auto decoded =
|
||||
jwt::decode(token, algorithms({"rs256"}), error, secret(pub_key), issuer("citra-core"),
|
||||
aud(audience), validate_iat(true), validate_jti(true));
|
||||
if (error) {
|
||||
LOG_INFO(WebService, "Verification failed: category={}, code={}, message={}",
|
||||
error.category().name(), error.value(), error.message());
|
||||
return {};
|
||||
}
|
||||
Network::VerifyUser::UserData user_data{};
|
||||
if (decoded.payload().has_claim("username")) {
|
||||
user_data.username = decoded.payload().get_claim_value<std::string>("username");
|
||||
}
|
||||
if (decoded.payload().has_claim("displayName")) {
|
||||
user_data.display_name = decoded.payload().get_claim_value<std::string>("displayName");
|
||||
}
|
||||
if (decoded.payload().has_claim("avatarUrl")) {
|
||||
user_data.avatar_url = decoded.payload().get_claim_value<std::string>("avatarUrl");
|
||||
}
|
||||
if (decoded.payload().has_claim("roles")) {
|
||||
auto roles = decoded.payload().get_claim_value<std::vector<std::string>>("roles");
|
||||
user_data.moderator = std::find(roles.begin(), roles.end(), "moderator") != roles.end();
|
||||
}
|
||||
return user_data;
|
||||
}
|
||||
|
||||
} // namespace WebService
|
@ -0,0 +1,25 @@
|
||||
// Copyright 2018 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include "network/verify_user.h"
|
||||
#include "web_service/web_backend.h"
|
||||
|
||||
namespace WebService {
|
||||
|
||||
class VerifyUserJWT final : public Network::VerifyUser::Backend {
|
||||
public:
|
||||
VerifyUserJWT(const std::string& host);
|
||||
~VerifyUserJWT() = default;
|
||||
|
||||
Network::VerifyUser::UserData LoadUserData(const std::string& verify_UID,
|
||||
const std::string& token) override;
|
||||
|
||||
private:
|
||||
std::string pub_key;
|
||||
};
|
||||
|
||||
} // namespace WebService
|
Loading…
Reference in New Issue