Skip to content

Commit be80498

Browse files
authored
Merge pull request #421 from f-koehler/fk/multiple-accounts
2 parents d535030 + ba872da commit be80498

13 files changed

Lines changed: 470 additions & 13 deletions

src/CMakeLists.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ add_subdirectory(wrapper)
99
add_executable(
1010
ktailctl
1111
about.cpp
12+
account_data.cpp
13+
account_model.cpp
1214
app.cpp
1315
exit_node_model.cpp
1416
json.cpp
@@ -33,6 +35,7 @@ ecm_target_qml_sources(
3335
ktailctl
3436
SOURCES
3537
ui/About.qml
38+
ui/Accounts.qml
3639
ui/ExitNodes.qml
3740
ui/Main.qml
3841
ui/MullvadNodes.qml
@@ -42,6 +45,16 @@ ecm_target_qml_sources(
4245
ui/Settings.qml
4346
ui/Statistics.qml)
4447

48+
ecm_qt_declare_logging_category(
49+
ktailctl
50+
HEADER
51+
"logging_accounts_data.hpp"
52+
IDENTIFIER
53+
"Logging::AccountsData"
54+
CATEGORY_NAME
55+
"org.fkoehler.KTailctl.AccountsData"
56+
DEFAULT_SEVERITY
57+
Info)
4558
ecm_qt_declare_logging_category(
4659
ktailctl
4760
HEADER

src/account_data.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#include "account_data.hpp"
2+
#include "json.hpp"
3+
#include "logging_accounts_data.hpp"
4+
5+
void from_json(const nlohmann::json &j, AccountData &data)
6+
{
7+
try {
8+
j.at("ControlURL").get_to<QString>(data.controlUrl);
9+
j.at("ID").get_to<QString>(data.id);
10+
j.at("Key").get_to<QString>(data.key);
11+
j.at("Name").get_to<QString>(data.name);
12+
j.at("NetworkProfile").at("DomainName").get_to<QString>(data.domainName);
13+
j.at("NetworkProfile").at("MagicDNSName").get_to<QString>(data.magicDnsName);
14+
j.at("UserProfile").at("DisplayName").get_to<QString>(data.userName);
15+
j.at("UserProfile").at("ID").get_to<quint64>(data.userId);
16+
j.at("UserProfile").at("LoginName").get_to<QString>(data.loginName);
17+
} catch (json::exception &e) {
18+
qCCritical(Logging::AccountsData) << "Error parsing accounts data: " << e.what();
19+
}
20+
}

src/account_data.hpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#ifndef KTAICTL_DATA_ACCOUNTS_DATA_HPP
2+
#define KTAICTL_DATA_ACCOUNTS_DATA_HPP
3+
4+
#include <QString>
5+
#include <nlohmann/json.hpp>
6+
7+
struct AccountData {
8+
QString controlUrl;
9+
QString id;
10+
QString key;
11+
QString name;
12+
QString domainName;
13+
QString magicDnsName;
14+
QString userName;
15+
quint64 userId;
16+
QString loginName;
17+
bool isCurrent;
18+
};
19+
20+
void from_json(const nlohmann::json &j, AccountData &data);
21+
22+
#endif

src/account_model.cpp

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#include "account_model.hpp"
2+
#include "account_data.hpp"
3+
4+
AccountModel::AccountModel(QObject *parent)
5+
: QAbstractListModel(parent)
6+
{
7+
}
8+
9+
int AccountModel::rowCount(const QModelIndex &parent) const
10+
{
11+
Q_UNUSED(parent);
12+
13+
return mAccounts.size();
14+
}
15+
16+
QHash<int, QByteArray> AccountModel::roleNames() const
17+
{
18+
static const QHash<int, QByteArray> roles{
19+
{ControlUrlRole, "controlUrl"},
20+
{IdRole, "id"},
21+
{KeyRole, "key"},
22+
{NameRole, "name"},
23+
{DomainNameRole, "domainName"},
24+
{MagicDnsNameRole, "magicDnsName"},
25+
{UserIdRole, "userId"},
26+
{UserNameRole, "userName"},
27+
{LoginNameRole, "loginName"},
28+
{IsCurrentRole, "isCurrent"},
29+
};
30+
return roles;
31+
}
32+
33+
QVariant AccountModel::data(const QModelIndex &index, int role) const
34+
{
35+
if (!index.isValid()) {
36+
return QStringLiteral("Invalid inded");
37+
}
38+
switch (role) {
39+
case ControlUrlRole:
40+
return mAccounts.at(index.row()).controlUrl;
41+
case IdRole:
42+
return mAccounts.at(index.row()).id;
43+
case KeyRole:
44+
return mAccounts.at(index.row()).key;
45+
case NameRole:
46+
return mAccounts.at(index.row()).name;
47+
case DomainNameRole:
48+
return mAccounts.at(index.row()).domainName;
49+
case MagicDnsNameRole:
50+
return mAccounts.at(index.row()).magicDnsName;
51+
case UserIdRole:
52+
return mAccounts.at(index.row()).userId;
53+
case UserNameRole:
54+
return mAccounts.at(index.row()).userName;
55+
case LoginNameRole:
56+
return mAccounts.at(index.row()).loginName;
57+
case IsCurrentRole:
58+
return mAccounts.at(index.row()).isCurrent;
59+
default:
60+
return QStringLiteral("Invalid role");
61+
}
62+
}
63+
64+
const QVector<AccountData> &AccountModel::accounts() const
65+
{
66+
return mAccounts;
67+
}
68+
69+
void AccountModel::update(const QVector<AccountData> &accounts, const QString &currentID)
70+
{
71+
if (mAccounts.size() > accounts.size()) {
72+
beginRemoveRows(QModelIndex(), accounts.size(), mAccounts.size() - 1);
73+
mAccounts.erase(mAccounts.begin() + accounts.size(), mAccounts.end());
74+
endRemoveRows();
75+
}
76+
for (int i = 0; i < mAccounts.size(); ++i) {
77+
const QList<int> updatedRoles = updateRow(i, accounts[i], currentID);
78+
if (!updatedRoles.isEmpty()) {
79+
emit dataChanged(index(i), index(i), updatedRoles);
80+
}
81+
}
82+
if (mAccounts.size() < accounts.size()) {
83+
beginInsertRows(QModelIndex(), mAccounts.size(), accounts.size() - 1);
84+
for (int i = mAccounts.size(); i < accounts.size(); ++i) {
85+
mAccounts.append(accounts[i]);
86+
}
87+
endInsertRows();
88+
}
89+
}
90+
91+
QList<int> AccountModel::updateRow(int row, const AccountData &account, const QString &currentID)
92+
{
93+
QList<int> result;
94+
AccountData &current = mAccounts[row];
95+
if (current.controlUrl != account.controlUrl) {
96+
current.controlUrl = account.controlUrl;
97+
result.append(ControlUrlRole);
98+
}
99+
if (current.id != account.id) {
100+
current.id = account.id;
101+
result.append(IdRole);
102+
}
103+
if (current.key != account.key) {
104+
current.key = account.key;
105+
result.append(KeyRole);
106+
}
107+
if (current.name != account.name) {
108+
current.name = account.name;
109+
result.append(NameRole);
110+
}
111+
if (current.domainName != account.domainName) {
112+
current.domainName = account.domainName;
113+
result.append(DomainNameRole);
114+
}
115+
if (current.magicDnsName != account.magicDnsName) {
116+
current.magicDnsName = account.magicDnsName;
117+
result.append(MagicDnsNameRole);
118+
}
119+
if (current.userName != account.userName) {
120+
current.userName = account.userName;
121+
result.append(UserNameRole);
122+
}
123+
if (current.userId != account.userId) {
124+
current.userId = account.userId;
125+
result.append(UserIdRole);
126+
}
127+
if (current.loginName != account.loginName) {
128+
current.loginName = account.loginName;
129+
result.append(LoginNameRole);
130+
}
131+
const bool isCurrent = currentID == account.id;
132+
if (isCurrent != current.isCurrent) {
133+
current.isCurrent = isCurrent;
134+
result.append(IsCurrentRole);
135+
}
136+
return result;
137+
}

src/account_model.hpp

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#ifndef KTAILCTL_ACCOUNT_MODEL_HPP
2+
#define KTAILCTL_ACCOUNT_MODEL_HPP
3+
4+
#include "account_data.hpp"
5+
#include <QAbstractListModel>
6+
#include <QtCore/qnamespace.h>
7+
#include <QtCore/qobject.h>
8+
#include <QtCore/qstringview.h>
9+
10+
class AccountModel : public QAbstractListModel
11+
{
12+
Q_OBJECT
13+
14+
private:
15+
QVector<AccountData> mAccounts;
16+
17+
QList<int> updateRow(int row, const AccountData &account, const QString &currentID);
18+
19+
public:
20+
enum Roles : int {
21+
ControlUrlRole = Qt::UserRole + 1,
22+
IdRole,
23+
KeyRole,
24+
NameRole,
25+
DomainNameRole,
26+
MagicDnsNameRole,
27+
UserIdRole,
28+
UserNameRole,
29+
LoginNameRole,
30+
IsCurrentRole,
31+
};
32+
33+
explicit AccountModel(QObject *parent = nullptr);
34+
virtual ~AccountModel() = default;
35+
36+
int rowCount(const QModelIndex &parent) const override;
37+
QHash<int, QByteArray> roleNames() const override;
38+
QVariant data(const QModelIndex &index, int rolw) const override;
39+
40+
const QVector<AccountData> &accounts() const;
41+
42+
public slots:
43+
void update(const QVector<AccountData> &accounts, const QString &currentID);
44+
};
45+
46+
#endif

src/app.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@ App::App(QObject *parent)
2424
QObject::connect(Tailscale::instance(), &Tailscale::backendStateChanged, mTrayIcon, &TrayIcon::regenerate);
2525
QObject::connect(mTrayIcon, &TrayIcon::quitClicked, this, &App::quitApp);
2626

27-
QObject::connect(Tailscale::instance(), &Tailscale::refreshed, this, &App::refreshDetails);
27+
QObject::connect(Tailscale::instance(), &Tailscale::statusRefreshed, this, &App::refreshDetails);
2828

2929
mPeerModel->setSourceModel(Tailscale::instance()->peerModel());
3030
mPeerModel->setFilterRole(PeerModel::DnsNameRole);
3131
mMullvadNodesForCountryModel->setSourceModel(Tailscale::instance()->mullvadNodeModel());
3232
mMullvadNodesForCountryModel->setFilterRole(PeerModel::CountryCodeRole);
3333

3434
if (KTailctlConfig::peerFilter() == "UNINITIALIZED") {
35-
Tailscale::instance()->refresh();
35+
Tailscale::instance()->refreshStatus();
3636
const QString domain = Tailscale::instance()->self().mDnsName.section('.', 1);
3737
mPeerModel->setFilterRegularExpression(domain);
3838
KTailctlConfig::setPeerFilter(domain);
@@ -121,4 +121,4 @@ void App::refreshDetails()
121121
mPeerDetails = Tailscale::instance()->peerModel()->peers().front();
122122
}
123123
emit peerDetailsChanged(mPeerDetails);
124-
}
124+
}

src/tailscale.cpp

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
#include "tailscale.hpp"
2+
#include "account_data.hpp"
3+
#include "account_model.hpp"
24
#include "ktailctlconfig.h"
35
#include "libktailctl_wrapper.h"
6+
#include <algorithm>
47

58
Tailscale::Tailscale(QObject *parent)
69
: QObject(parent)
710
, mPeerModel(new PeerModel(this))
11+
, mAccountModel(new AccountModel(this))
812
, mExitNodeModel(new ExitNodeModel(this))
913
, mMullvadNodeModel(new MullvadNodeModel(this))
1014
, mMullvadCountryModel(new MullvadCountryModel(this))
@@ -19,6 +23,10 @@ Tailscale *Tailscale::instance()
1923
return &instance;
2024
}
2125

26+
AccountModel *Tailscale::accountModel() const
27+
{
28+
return mAccountModel;
29+
}
2230
PeerModel *Tailscale::peerModel() const
2331
{
2432
return mPeerModel;
@@ -39,6 +47,10 @@ bool Tailscale::success() const
3947
{
4048
return mSuccess;
4149
}
50+
bool Tailscale::accountsSuccess() const
51+
{
52+
return mAccountsSuccess;
53+
}
4254
const QString &Tailscale::version() const
4355
{
4456
return mVersion;
@@ -88,7 +100,7 @@ void Tailscale::toggle()
88100
up();
89101
}
90102
}
91-
void Tailscale::refresh()
103+
void Tailscale::refreshStatus()
92104
{
93105
StatusData data;
94106
{
@@ -182,7 +194,36 @@ void Tailscale::refresh()
182194

183195
mMullvadCountryModel->update(countries);
184196

185-
emit refreshed();
197+
emit statusRefreshed();
198+
}
199+
200+
void Tailscale::refreshAccounts()
201+
{
202+
QVector<AccountData> data;
203+
QString currentID;
204+
{
205+
std::unique_ptr<char, decltype(&free)> jsonStr(tailscale_accounts(), free);
206+
if (jsonStr == nullptr) {
207+
if (mAccountsSuccess) {
208+
mAccountsSuccess = false;
209+
emit successChanged(false);
210+
}
211+
return;
212+
}
213+
if (!mAccountsSuccess) {
214+
mAccountsSuccess = true;
215+
emit successChanged(true);
216+
}
217+
auto parsed = json::parse(jsonStr.get());
218+
parsed.at("accounts").get_to(data);
219+
parsed.at("currentID").get_to(currentID);
220+
}
221+
std::sort(data.begin(), data.end(), [](const AccountData &a, const AccountData &b) {
222+
return a.id < b.id;
223+
});
224+
mAccountModel->update(data, currentID);
225+
226+
emit accountsRefreshed();
186227
}
187228

188229
void Tailscale::setExitNode(const QString &dnsName)
@@ -211,8 +252,15 @@ void Tailscale::setExitNode(const QString &dnsName)
211252
GoString tmp{targetBytes.data(), targetBytes.size()};
212253
tailscale_set_exit_node(&tmp);
213254
}
255+
256+
void Tailscale::switchAccount(const QString &account)
257+
{
258+
QByteArray accountBytes = account.toUtf8();
259+
GoString tmp{accountBytes.data(), accountBytes.size()};
260+
tailscale_switch_account(&tmp);
261+
}
214262
void Tailscale::unsetExitNode()
215263
{
216264
GoString tmp{nullptr, 0};
217265
tailscale_set_exit_node(&tmp);
218-
}
266+
}

0 commit comments

Comments
 (0)