-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathgithubauth.cpp
More file actions
187 lines (156 loc) · 7.18 KB
/
Copy pathgithubauth.cpp
File metadata and controls
187 lines (156 loc) · 7.18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
#include "githubauth.h"
#include <QNetworkRequest>
#include <QUrlQuery>
#include <QJsonDocument>
#include <QJsonObject>
#include <QDebug>
#include <keychain.h>
GithubAuth::GithubAuth(QObject *parent) : QObject(parent) {
m_nam = new QNetworkAccessManager(this);
m_pollTimer = new QTimer(this);
connect(m_pollTimer, &QTimer::timeout, this, &GithubAuth::pollAccessToken);
// Application-level Client ID for SyntaxFlow desktop app.
// NOTE: This currently uses the GitHub CLI public Client ID so the auth flow
// works immediately for testing. For your final production build, you will
// want to substitute this with your own SyntaxFlow Client ID.
m_clientId = "178c6fc778ccc68e1d6a";
// Attempt to load existing token from secure OS keyring
auto *job = new QKeychain::ReadPasswordJob("SyntaxFlow", this);
job->setKey("github_access_token");
connect(job, &QKeychain::Job::finished, this, [this, job]() {
if (job->error()) {
qWarning() << "Reading GitHub credential...";
qWarning() << "QKeychain read failed for Service:" << job->service() << "Key:" << job->key();
qWarning() << "Error:" << job->errorString() << "(Code:" << job->error() << ")";
} else {
qDebug() << "Reading GitHub credential...";
qDebug() << "QKeychain read completed: success for Service:" << job->service() << "Key:" << job->key();
QString savedToken = job->textData();
emit authenticated(savedToken);
fetchUserProfile(savedToken);
}
});
job->start();
}
void GithubAuth::initiateAuth() {
QNetworkRequest request(QUrl("https://github.com/login/device/code"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
request.setRawHeader("Accept", "application/json");
QUrlQuery params;
params.addQueryItem("client_id", m_clientId);
params.addQueryItem("scope", "read:user");
QNetworkReply *reply = m_nam->post(request, params.toString(QUrl::FullyEncoded).toUtf8());
connect(reply, &QNetworkReply::finished, this, &GithubAuth::onDeviceCodeReply);
}
void GithubAuth::onDeviceCodeReply() {
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
if (!reply) return;
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
emit errorOccurred("Failed to initiate GitHub login: " + reply->errorString());
return;
}
QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
QJsonObject obj = doc.object();
if (obj.contains("error")) {
emit errorOccurred(obj["error_description"].toString());
return;
}
m_deviceCode = obj["device_code"].toString();
QString userCode = obj["user_code"].toString();
QString verificationUri = obj["verification_uri"].toString();
int interval = obj["interval"].toInt(5);
emit userCodeReceived(userCode, verificationUri);
m_pollTimer->start(interval * 1000);
}
void GithubAuth::pollAccessToken() {
QNetworkRequest request(QUrl("https://github.com/login/oauth/access_token"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
request.setRawHeader("Accept", "application/json");
QUrlQuery params;
params.addQueryItem("client_id", m_clientId);
params.addQueryItem("device_code", m_deviceCode);
params.addQueryItem("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
QNetworkReply *reply = m_nam->post(request, params.toString(QUrl::FullyEncoded).toUtf8());
connect(reply, &QNetworkReply::finished, this, &GithubAuth::onAccessTokenReply);
}
void GithubAuth::onAccessTokenReply() {
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
if (!reply) return;
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
// Ignore network errors during polling
return;
}
QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
QJsonObject obj = doc.object();
if (obj.contains("error")) {
QString errorType = obj["error"].toString();
if (errorType == "authorization_pending") {
// keep polling
} else if (errorType == "slow_down") {
m_pollTimer->setInterval(m_pollTimer->interval() + 5000);
} else {
m_pollTimer->stop();
emit errorOccurred("GitHub login failed: " + obj["error_description"].toString());
}
return;
}
if (obj.contains("access_token")) {
m_pollTimer->stop();
QString token = obj["access_token"].toString();
// Save the token securely to the OS keyring
auto *job = new QKeychain::WritePasswordJob("SyntaxFlow", this);
job->setKey("github_access_token");
job->setTextData(token);
connect(job, &QKeychain::Job::finished, this, [job]() {
if (job->error()) {
qWarning() << "Writing GitHub credential...";
qWarning() << "Failed to save token to keyring for Service:" << job->service() << "Key:" << job->key();
qWarning() << "Error:" << job->errorString() << "(Code:" << job->error() << ")";
} else {
qDebug() << "Writing GitHub credential...";
qDebug() << "QKeychain write completed: success for Service:" << job->service() << "Key:" << job->key();
}
});
job->start();
emit authenticated(token);
fetchUserProfile(token);
}
}
void GithubAuth::fetchUserProfile(const QString &token) {
QNetworkRequest request(QUrl("https://api.github.com/user"));
request.setRawHeader("Authorization", ("Bearer " + token).toUtf8());
request.setRawHeader("Accept", "application/vnd.github.v3+json");
QNetworkReply *reply = m_nam->get(request);
connect(reply, &QNetworkReply::finished, this, &GithubAuth::onProfileReply);
}
void GithubAuth::onProfileReply() {
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
if (!reply) return;
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
// Token is likely invalid or revoked. Delete it from keyring securely.
auto *job = new QKeychain::DeletePasswordJob("SyntaxFlow", this);
job->setKey("github_access_token");
connect(job, &QKeychain::Job::finished, this, [job]() {
if (job->error()) {
qWarning() << "Deleting GitHub credential...";
qWarning() << "QKeychain delete failed:" << job->errorString() << "(Error code:" << job->error() << ")";
} else {
qDebug() << "Deleting GitHub credential...";
qDebug() << "QKeychain delete completed: success";
}
});
job->start();
emit errorOccurred("Failed to fetch profile (or token invalid): " + reply->errorString());
return;
}
QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
QJsonObject obj = doc.object();
QString avatarUrl = obj["avatar_url"].toString();
QString login = obj["login"].toString();
QString name = obj["name"].toString();
if (name.isEmpty()) name = login;
emit profileDataReceived(avatarUrl, login, name);
}