Changed AccountManager to only rely on a proper OAuth response.

This involved making another request after the initial OAuth authorization.
Because of this now-two-step process, some methods and signals were renamed
to make their purpose more clear. Additionally, a _hasProfile member variable
was added to DataServerAccountInfo in order to allow for knowledge of whether
the profile elements had been fetched when being deserialized from disk.
Error handling for the whole process is still nonexistant.
This commit is contained in:
John Grosen 2014-06-12 23:28:43 -07:00
parent e8ba3c8bb1
commit 14f56310f6
6 changed files with 172 additions and 97 deletions

View file

@ -127,7 +127,7 @@ Menu::Menu() :
toggleLoginMenuItem();
// connect to the appropriate slots of the AccountManager so that we can change the Login/Logout menu item
connect(&accountManager, &AccountManager::accessTokenChanged, this, &Menu::toggleLoginMenuItem);
connect(&accountManager, &AccountManager::profileChanged, this, &Menu::toggleLoginMenuItem);
connect(&accountManager, &AccountManager::logoutComplete, this, &Menu::toggleLoginMenuItem);
addDisabledActionAndSeparator(fileMenu, "Scripts");
@ -326,7 +326,7 @@ Menu::Menu() :
shadowGroup->addAction(addCheckableActionToQMenuAndActionHash(shadowMenu, "None", 0, true));
shadowGroup->addAction(addCheckableActionToQMenuAndActionHash(shadowMenu, MenuOption::SimpleShadows, 0, false));
shadowGroup->addAction(addCheckableActionToQMenuAndActionHash(shadowMenu, MenuOption::CascadedShadows, 0, false));
addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Metavoxels, 0, true);
addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::BuckyBalls, 0, false);
addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Particles, 0, true);
@ -407,7 +407,7 @@ Menu::Menu() :
addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::DisableNackPackets, 0, false);
addDisabledActionAndSeparator(developerMenu, "Testing");
QMenu* timingMenu = developerMenu->addMenu("Timing and Statistics Tools");
QMenu* perfTimerMenu = timingMenu->addMenu("Performance Timer");
addCheckableActionToQMenuAndActionHash(perfTimerMenu, MenuOption::DisplayTimingDetails, 0, true);
@ -462,7 +462,7 @@ Menu::Menu() :
false,
appInstance->getAudio(),
SLOT(toggleToneInjection()));
addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::AudioScope,
addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::AudioScope,
Qt::CTRL | Qt::Key_P, false,
appInstance->getAudio(),
SLOT(toggleScope()));
@ -1775,4 +1775,3 @@ QString Menu::getSnapshotsLocation() const {
}
return _snapshotsLocation;
}

View file

@ -23,7 +23,7 @@ XmppClient::XmppClient() :
_xmppMUCManager()
{
AccountManager& accountManager = AccountManager::getInstance();
connect(&accountManager, SIGNAL(accessTokenChanged()), this, SLOT(connectToServer()));
connect(&accountManager, SIGNAL(profileChanged()), this, SLOT(connectToServer()));
connect(&accountManager, SIGNAL(logoutComplete()), this, SLOT(disconnectFromServer()));
}

View file

@ -55,13 +55,13 @@ AccountManager::AccountManager() :
{
qRegisterMetaType<OAuthAccessToken>("OAuthAccessToken");
qRegisterMetaTypeStreamOperators<OAuthAccessToken>("OAuthAccessToken");
qRegisterMetaType<DataServerAccountInfo>("DataServerAccountInfo");
qRegisterMetaTypeStreamOperators<DataServerAccountInfo>("DataServerAccountInfo");
qRegisterMetaType<QNetworkAccessManager::Operation>("QNetworkAccessManager::Operation");
qRegisterMetaType<JSONCallbackParameters>("JSONCallbackParameters");
connect(&_accountInfo, &DataServerAccountInfo::balanceChanged, this, &AccountManager::accountInfoBalanceChanged);
}
@ -70,18 +70,18 @@ const QString DOUBLE_SLASH_SUBSTITUTE = "slashslash";
void AccountManager::logout() {
// a logout means we want to delete the DataServerAccountInfo we currently have for this URL, in-memory and in file
_accountInfo = DataServerAccountInfo();
emit balanceChanged(0);
connect(&_accountInfo, &DataServerAccountInfo::balanceChanged, this, &AccountManager::accountInfoBalanceChanged);
QSettings settings;
settings.beginGroup(ACCOUNTS_GROUP);
QString keyURLString(_authURL.toString().replace("//", DOUBLE_SLASH_SUBSTITUTE));
settings.remove(keyURLString);
qDebug() << "Removed account info for" << _authURL << "from in-memory accounts and .ini file";
emit logoutComplete();
// the username has changed to blank
emit usernameChanged(QString());
@ -93,7 +93,7 @@ void AccountManager::updateBalance() {
JSONCallbackParameters callbackParameters;
callbackParameters.jsonCallbackReceiver = &_accountInfo;
callbackParameters.jsonCallbackMethod = "setBalanceFromJSON";
authenticatedRequest("/api/v1/wallets/mine", QNetworkAccessManager::GetOperation, callbackParameters);
}
}
@ -105,28 +105,33 @@ void AccountManager::accountInfoBalanceChanged(qint64 newBalance) {
void AccountManager::setAuthURL(const QUrl& authURL) {
if (_authURL != authURL) {
_authURL = authURL;
qDebug() << "URL for node authentication has been changed to" << qPrintable(_authURL.toString());
qDebug() << "Re-setting authentication flow.";
// check if there are existing access tokens to load from settings
QSettings settings;
settings.beginGroup(ACCOUNTS_GROUP);
foreach(const QString& key, settings.allKeys()) {
// take a key copy to perform the double slash replacement
QString keyCopy(key);
QUrl keyURL(keyCopy.replace("slashslash", "//"));
if (keyURL == _authURL) {
// pull out the stored access token and store it in memory
_accountInfo = settings.value(key).value<DataServerAccountInfo>();
qDebug() << "Found a data-server access token for" << qPrintable(keyURL.toString());
emit accessTokenChanged();
// profile info isn't guaranteed to be saved too
if (_accountInfo.hasProfile()) {
emit profileChanged();
} else {
requestProfile();
}
}
}
// tell listeners that the auth endpoint has changed
emit authEndpointChanged();
}
@ -147,36 +152,36 @@ void AccountManager::authenticatedRequest(const QString& path, QNetworkAccessMan
void AccountManager::invokedRequest(const QString& path, QNetworkAccessManager::Operation operation,
const JSONCallbackParameters& callbackParams,
const QByteArray& dataByteArray, QHttpMultiPart* dataMultiPart) {
if (!_networkAccessManager) {
_networkAccessManager = new QNetworkAccessManager(this);
}
if (hasValidAccessToken()) {
QNetworkRequest authenticatedRequest;
QUrl requestURL = _authURL;
if (path.startsWith("/")) {
requestURL.setPath(path);
} else {
requestURL.setPath("/" + path);
}
requestURL.setQuery("access_token=" + _accountInfo.getAccessToken().token);
authenticatedRequest.setUrl(requestURL);
if (VERBOSE_HTTP_REQUEST_DEBUGGING) {
qDebug() << "Making an authenticated request to" << qPrintable(requestURL.toString());
if (!dataByteArray.isEmpty()) {
qDebug() << "The POST/PUT body -" << QString(dataByteArray);
}
}
QNetworkReply* networkReply = NULL;
switch (operation) {
case QNetworkAccessManager::GetOperation:
networkReply = _networkAccessManager->get(authenticatedRequest);
@ -198,24 +203,24 @@ void AccountManager::invokedRequest(const QString& path, QNetworkAccessManager::
networkReply = _networkAccessManager->put(authenticatedRequest, dataByteArray);
}
}
break;
default:
// other methods not yet handled
break;
}
if (networkReply) {
if (!callbackParams.isEmpty()) {
// if we have information for a callback, insert the callbackParams into our local map
_pendingCallbackMap.insert(networkReply, callbackParams);
if (callbackParams.updateReciever && !callbackParams.updateSlot.isEmpty()) {
callbackParams.updateReciever->connect(networkReply, SIGNAL(uploadProgress(qint64, qint64)),
callbackParams.updateSlot.toStdString().c_str());
}
}
// if we ended up firing of a request, hook up to it now
connect(networkReply, SIGNAL(finished()), SLOT(processReply()));
}
@ -224,7 +229,7 @@ void AccountManager::invokedRequest(const QString& path, QNetworkAccessManager::
void AccountManager::processReply() {
QNetworkReply* requestReply = reinterpret_cast<QNetworkReply*>(sender());
if (requestReply->error() == QNetworkReply::NoError) {
passSuccessToCallback(requestReply);
} else {
@ -235,17 +240,17 @@ void AccountManager::processReply() {
void AccountManager::passSuccessToCallback(QNetworkReply* requestReply) {
QJsonDocument jsonResponse = QJsonDocument::fromJson(requestReply->readAll());
JSONCallbackParameters callbackParams = _pendingCallbackMap.value(requestReply);
if (callbackParams.jsonCallbackReceiver) {
// invoke the right method on the callback receiver
QMetaObject::invokeMethod(callbackParams.jsonCallbackReceiver, qPrintable(callbackParams.jsonCallbackMethod),
Q_ARG(const QJsonObject&, jsonResponse.object()));
// remove the related reply-callback group from the map
_pendingCallbackMap.remove(requestReply);
} else {
if (VERBOSE_HTTP_REQUEST_DEBUGGING) {
qDebug() << "Received JSON response from data-server that has no matching callback.";
@ -256,13 +261,13 @@ void AccountManager::passSuccessToCallback(QNetworkReply* requestReply) {
void AccountManager::passErrorToCallback(QNetworkReply* requestReply) {
JSONCallbackParameters callbackParams = _pendingCallbackMap.value(requestReply);
if (callbackParams.errorCallbackReceiver) {
// invoke the right method on the callback receiver
QMetaObject::invokeMethod(callbackParams.errorCallbackReceiver, qPrintable(callbackParams.errorCallbackMethod),
Q_ARG(QNetworkReply::NetworkError, requestReply->error()),
Q_ARG(const QString&, requestReply->errorString()));
// remove the related reply-callback group from the map
_pendingCallbackMap.remove(requestReply);
} else {
@ -274,12 +279,12 @@ void AccountManager::passErrorToCallback(QNetworkReply* requestReply) {
}
bool AccountManager::hasValidAccessToken() {
if (_accountInfo.getAccessToken().token.isEmpty() || _accountInfo.getAccessToken().isExpired()) {
if (VERBOSE_HTTP_REQUEST_DEBUGGING) {
qDebug() << "An access token is required for requests to" << qPrintable(_authURL.toString());
}
return false;
} else {
return true;
@ -288,12 +293,12 @@ bool AccountManager::hasValidAccessToken() {
bool AccountManager::checkAndSignalForAccessToken() {
bool hasToken = hasValidAccessToken();
if (!hasToken) {
// emit a signal so somebody can call back to us and request an access token given a username and password
emit authRequired();
}
return hasToken;
}
@ -304,36 +309,36 @@ void AccountManager::requestAccessToken(const QString& login, const QString& pas
}
QNetworkRequest request;
QUrl grantURL = _authURL;
grantURL.setPath("/oauth/token");
const QString ACCOUNT_MANAGER_REQUESTED_SCOPE = "owner";
QByteArray postData;
postData.append("grant_type=password&");
postData.append("username=" + login + "&");
postData.append("password=" + QUrl::toPercentEncoding(password) + "&");
postData.append("scope=" + ACCOUNT_MANAGER_REQUESTED_SCOPE);
request.setUrl(grantURL);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
QNetworkReply* requestReply = _networkAccessManager->post(request, postData);
connect(requestReply, &QNetworkReply::finished, this, &AccountManager::requestFinished);
connect(requestReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestError(QNetworkReply::NetworkError)));
connect(requestReply, &QNetworkReply::finished, this, &AccountManager::requestAccessTokenFinished);
connect(requestReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestAccessTokenError(QNetworkReply::NetworkError)));
}
void AccountManager::requestFinished() {
void AccountManager::requestAccessTokenFinished() {
QNetworkReply* requestReply = reinterpret_cast<QNetworkReply*>(sender());
QJsonDocument jsonResponse = QJsonDocument::fromJson(requestReply->readAll());
const QJsonObject& rootObject = jsonResponse.object();
if (!rootObject.contains("error")) {
// construct an OAuthAccessToken from the json object
if (!rootObject.contains("access_token") || !rootObject.contains("expires_in")
|| !rootObject.contains("token_type")) {
// TODO: error handling - malformed token response
@ -342,23 +347,21 @@ void AccountManager::requestFinished() {
// clear the path from the response URL so we have the right root URL for this access token
QUrl rootURL = requestReply->url();
rootURL.setPath("");
qDebug() << "Storing an account with access-token for" << qPrintable(rootURL.toString());
_accountInfo = DataServerAccountInfo(rootObject);
_accountInfo = DataServerAccountInfo();
_accountInfo.setAccessTokenFromJSON(rootObject);
emit loginComplete(rootURL);
// the username has changed to whatever came back
emit usernameChanged(_accountInfo.getUsername());
// we have found or requested an access token
emit accessTokenChanged();
// store this access token into the local settings
QSettings localSettings;
localSettings.beginGroup(ACCOUNTS_GROUP);
localSettings.setValue(rootURL.toString().replace("//", DOUBLE_SLASH_SUBSTITUTE),
QVariant::fromValue(_accountInfo));
requestProfile();
}
} else {
// TODO: error handling
@ -367,7 +370,53 @@ void AccountManager::requestFinished() {
}
}
void AccountManager::requestError(QNetworkReply::NetworkError error) {
void AccountManager::requestAccessTokenError(QNetworkReply::NetworkError error) {
// TODO: error handling
qDebug() << "AccountManager requestError - " << error;
}
void AccountManager::requestProfile() {
if (!_networkAccessManager) {
_networkAccessManager = new QNetworkAccessManager(this);
}
QUrl profileURL = _authURL;
profileURL.setPath("/api/v1/users/profile");
profileURL.setQuery("access_token=" + _accountInfo.getAccessToken().token);
QNetworkReply* profileReply = _networkAccessManager->get(QNetworkRequest(profileURL));
connect(profileReply, &QNetworkReply::finished, this, &AccountManager::requestProfileFinished);
connect(profileReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestProfileError(QNetworkReply::NetworkError)));
}
void AccountManager::requestProfileFinished() {
QNetworkReply* profileReply = reinterpret_cast<QNetworkReply*>(sender());
QJsonDocument jsonResponse = QJsonDocument::fromJson(profileReply->readAll());
const QJsonObject& rootObject = jsonResponse.object();
if (rootObject.contains("status") && rootObject["status"].toString() == "success") {
_accountInfo.setProfileInfoFromJSON(rootObject);
emit profileChanged();
// the username has changed to whatever came back
emit usernameChanged(_accountInfo.getUsername());
// store the whole profile into the local settings
QUrl rootURL = profileReply->url();
rootURL.setPath("");
QSettings localSettings;
localSettings.beginGroup(ACCOUNTS_GROUP);
localSettings.setValue(rootURL.toString().replace("//", DOUBLE_SLASH_SUBSTITUTE),
QVariant::fromValue(_accountInfo));
} else {
// TODO: error handling
qDebug() << "Error in response for profile";
}
}
void AccountManager::requestProfileError(QNetworkReply::NetworkError error) {
// TODO: error handling
qDebug() << "AccountManager requestProfileError - " << error;
}

View file

@ -23,9 +23,9 @@
class JSONCallbackParameters {
public:
JSONCallbackParameters();
bool isEmpty() const { return !jsonCallbackReceiver && !errorCallbackReceiver; }
QObject* jsonCallbackReceiver;
QString jsonCallbackMethod;
QObject* errorCallbackReceiver;
@ -38,30 +38,33 @@ class AccountManager : public QObject {
Q_OBJECT
public:
static AccountManager& getInstance();
void authenticatedRequest(const QString& path,
QNetworkAccessManager::Operation operation = QNetworkAccessManager::GetOperation,
const JSONCallbackParameters& callbackParams = JSONCallbackParameters(),
const QByteArray& dataByteArray = QByteArray(),
QHttpMultiPart* dataMultiPart = NULL);
const QUrl& getAuthURL() const { return _authURL; }
void setAuthURL(const QUrl& authURL);
bool hasAuthEndpoint() { return !_authURL.isEmpty(); }
bool isLoggedIn() { return !_authURL.isEmpty() && hasValidAccessToken(); }
bool hasValidAccessToken();
Q_INVOKABLE bool checkAndSignalForAccessToken();
void requestAccessToken(const QString& login, const QString& password);
void requestProfile();
const DataServerAccountInfo& getAccountInfo() const { return _accountInfo; }
void destroy() { delete _networkAccessManager; }
public slots:
void requestFinished();
void requestError(QNetworkReply::NetworkError error);
void requestAccessTokenFinished();
void requestProfileFinished();
void requestAccessTokenError(QNetworkReply::NetworkError error);
void requestProfileError(QNetworkReply::NetworkError error);
void logout();
void updateBalance();
void accountInfoBalanceChanged(qint64 newBalance);
@ -69,7 +72,7 @@ signals:
void authRequired();
void authEndpointChanged();
void usernameChanged(const QString& username);
void accessTokenChanged();
void profileChanged();
void loginComplete(const QUrl& authURL);
void loginFailed();
void logoutComplete();
@ -80,19 +83,19 @@ private:
AccountManager();
AccountManager(AccountManager const& other); // not implemented
void operator=(AccountManager const& other); // not implemented
void passSuccessToCallback(QNetworkReply* reply);
void passErrorToCallback(QNetworkReply* reply);
Q_INVOKABLE void invokedRequest(const QString& path, QNetworkAccessManager::Operation operation,
const JSONCallbackParameters& callbackParams,
const QByteArray& dataByteArray,
QHttpMultiPart* dataMultiPart);
QUrl _authURL;
QNetworkAccessManager* _networkAccessManager;
QMap<QNetworkReply*, JSONCallbackParameters> _pendingCallbackMap;
DataServerAccountInfo _accountInfo;
};

View file

@ -19,11 +19,13 @@ DataServerAccountInfo::DataServerAccountInfo() :
_xmppPassword(),
_discourseApiKey(),
_balance(0),
_hasBalance(false)
_hasBalance(false),
_hasProfile(false)
{
}
/*
DataServerAccountInfo::DataServerAccountInfo(const QJsonObject& jsonObject) :
_accessToken(jsonObject),
_username(),
@ -36,6 +38,7 @@ DataServerAccountInfo::DataServerAccountInfo(const QJsonObject& jsonObject) :
setXMPPPassword(userJSONObject["xmpp_password"].toString());
setDiscourseApiKey(userJSONObject["discourse_api_key"].toString());
}
*/
DataServerAccountInfo::DataServerAccountInfo(const DataServerAccountInfo& otherInfo) {
_accessToken = otherInfo._accessToken;
@ -44,6 +47,7 @@ DataServerAccountInfo::DataServerAccountInfo(const DataServerAccountInfo& otherI
_discourseApiKey = otherInfo._discourseApiKey;
_balance = otherInfo._balance;
_hasBalance = otherInfo._hasBalance;
_hasProfile = otherInfo._hasProfile;
}
DataServerAccountInfo& DataServerAccountInfo::operator=(const DataServerAccountInfo& otherInfo) {
@ -54,19 +58,24 @@ DataServerAccountInfo& DataServerAccountInfo::operator=(const DataServerAccountI
void DataServerAccountInfo::swap(DataServerAccountInfo& otherInfo) {
using std::swap;
swap(_accessToken, otherInfo._accessToken);
swap(_username, otherInfo._username);
swap(_xmppPassword, otherInfo._xmppPassword);
swap(_discourseApiKey, otherInfo._discourseApiKey);
swap(_balance, otherInfo._balance);
swap(_hasBalance, otherInfo._hasBalance);
swap(_hasProfile, otherInfo._hasProfile);
}
void DataServerAccountInfo::setAccessTokenFromJSON(const QJsonObject& jsonObject) {
_accessToken = OAuthAccessToken(jsonObject);
}
void DataServerAccountInfo::setUsername(const QString& username) {
if (_username != username) {
_username = username;
qDebug() << "Username changed to" << username;
}
}
@ -87,7 +96,7 @@ void DataServerAccountInfo::setBalance(qint64 balance) {
if (!_hasBalance || _balance != balance) {
_balance = balance;
_hasBalance = true;
emit balanceChanged(_balance);
}
}
@ -99,12 +108,20 @@ void DataServerAccountInfo::setBalanceFromJSON(const QJsonObject& jsonObject) {
}
}
void DataServerAccountInfo::setProfileInfoFromJSON(const QJsonObject& jsonObject) {
QJsonObject user = jsonObject["data"].toObject()["user"].toObject();
setUsername(user["username"].toString());
setXMPPPassword(user["xmpp_password"].toString());
setDiscourseApiKey(user["discourse_api_key"].toString());
setHasProfile(true);
}
QDataStream& operator<<(QDataStream &out, const DataServerAccountInfo& info) {
out << info._accessToken << info._username << info._xmppPassword << info._discourseApiKey;
out << info._accessToken << info._username << info._xmppPassword << info._discourseApiKey << info._hasProfile;
return out;
}
QDataStream& operator>>(QDataStream &in, DataServerAccountInfo& info) {
in >> info._accessToken >> info._username >> info._xmppPassword >> info._discourseApiKey;
in >> info._accessToken >> info._username >> info._xmppPassword >> info._discourseApiKey >> info._hasProfile;
return in;
}

View file

@ -22,12 +22,13 @@ class DataServerAccountInfo : public QObject {
Q_OBJECT
public:
DataServerAccountInfo();
DataServerAccountInfo(const QJsonObject& jsonObject);
// DataServerAccountInfo(const QJsonObject& jsonObject);
DataServerAccountInfo(const DataServerAccountInfo& otherInfo);
DataServerAccountInfo& operator=(const DataServerAccountInfo& otherInfo);
const OAuthAccessToken& getAccessToken() const { return _accessToken; }
void setAccessTokenFromJSON(const QJsonObject& jsonObject);
const QString& getUsername() const { return _username; }
void setUsername(const QString& username);
@ -36,26 +37,32 @@ public:
const QString& getDiscourseApiKey() const { return _discourseApiKey; }
void setDiscourseApiKey(const QString& discourseApiKey);
qint64 getBalance() const { return _balance; }
void setBalance(qint64 balance);
bool hasBalance() const { return _hasBalance; }
void setHasBalance(bool hasBalance) { _hasBalance = hasBalance; }
Q_INVOKABLE void setBalanceFromJSON(const QJsonObject& jsonObject);
bool hasProfile() const { return _hasProfile; }
void setHasProfile(bool hasProfile) { _hasProfile = hasProfile; }
void setProfileInfoFromJSON(const QJsonObject& jsonObject);
friend QDataStream& operator<<(QDataStream &out, const DataServerAccountInfo& info);
friend QDataStream& operator>>(QDataStream &in, DataServerAccountInfo& info);
signals:
qint64 balanceChanged(qint64 newBalance);
private:
void swap(DataServerAccountInfo& otherInfo);
OAuthAccessToken _accessToken;
QString _username;
QString _xmppPassword;
QString _discourseApiKey;
qint64 _balance;
bool _hasBalance;
bool _hasProfile;
};
#endif // hifi_DataServerAccountInfo_h