mirror of
https://github.com/libretro/scummvm.git
synced 2025-03-06 02:10:28 +00:00
CLOUD: Update storages to refresh token via cloud.scummvm.org
This commit is contained in:
parent
4a427faf9c
commit
60504dce75
@ -41,7 +41,7 @@ void BaseStorage::getAccessToken(Common::String code) {
|
||||
Networking::JsonCallback callback = new Common::Callback<BaseStorage, Networking::JsonResponse>(this, &BaseStorage::codeFlowComplete);
|
||||
Networking::ErrorCallback errorCallback = new Common::Callback<BaseStorage, Networking::ErrorResponse>(this, &BaseStorage::codeFlowFailed);
|
||||
|
||||
Common::String url = Common::String::format("https://cloud.scummvm.org/%s/%s", cloudProvider().c_str(), code.c_str());
|
||||
Common::String url = Common::String::format("https://cloud.scummvm.org/%s/token/%s", cloudProvider().c_str(), code.c_str());
|
||||
Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(callback, errorCallback, url);
|
||||
|
||||
addRequest(request);
|
||||
@ -82,17 +82,33 @@ void BaseStorage::codeFlowComplete(Networking::JsonResponse response) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Networking::CurlJsonRequest::jsonContainsString(result, "access_token", "BaseStorage::codeFlowComplete")) {
|
||||
warning("BaseStorage: bad response, no 'access_token' attribute passed");
|
||||
if (!Networking::CurlJsonRequest::jsonContainsObject(result, "oauth", "BaseStorage::codeFlowComplete")) {
|
||||
warning("BaseStorage: bad response, no 'oauth' attribute passed");
|
||||
debug(9, "%s", json->stringify(true).c_str());
|
||||
CloudMan.removeStorage(this);
|
||||
} else {
|
||||
debug(9, "%s", json->stringify(true).c_str()); // TODO: remove before commit
|
||||
_token = result.getVal("access_token")->asString();
|
||||
CloudMan.replaceStorage(this, storageIndex());
|
||||
ConfMan.flushToDisk();
|
||||
delete json;
|
||||
return;
|
||||
}
|
||||
|
||||
Common::JSONObject oauth = result.getVal("oauth")->asObject();
|
||||
bool requiresRefreshToken = needsRefreshToken();
|
||||
if (!Networking::CurlJsonRequest::jsonContainsString(oauth, "access_token", "BaseStorage::codeFlowComplete") ||
|
||||
!Networking::CurlJsonRequest::jsonContainsString(oauth, "refresh_token", "BaseStorage::codeFlowComplete", !requiresRefreshToken)) {
|
||||
warning("BaseStorage: bad response, no 'access_token' or 'refresh_token' attribute passed");
|
||||
debug(9, "%s", json->stringify(true).c_str());
|
||||
CloudMan.removeStorage(this);
|
||||
delete json;
|
||||
return;
|
||||
}
|
||||
|
||||
debug(9, "%s", json->stringify(true).c_str()); // TODO: remove before commit
|
||||
_token = oauth.getVal("access_token")->asString();
|
||||
if (requiresRefreshToken) {
|
||||
_refreshToken = oauth.getVal("refresh_token")->asString();
|
||||
}
|
||||
CloudMan.replaceStorage(this, storageIndex());
|
||||
ConfMan.flushToDisk();
|
||||
|
||||
delete json;
|
||||
}
|
||||
|
||||
@ -102,4 +118,97 @@ void BaseStorage::codeFlowFailed(Networking::ErrorResponse error) {
|
||||
CloudMan.removeStorage(this);
|
||||
}
|
||||
|
||||
void BaseStorage::refreshAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback) {
|
||||
if (_refreshToken == "") {
|
||||
warning("BaseStorage: no refresh token available to get new access token.");
|
||||
if (callback) (*callback)(BoolResponse(nullptr, false));
|
||||
return;
|
||||
}
|
||||
|
||||
Networking::JsonCallback innerCallback = new Common::CallbackBridge<BaseStorage, BoolResponse, Networking::JsonResponse>(this, &BaseStorage::tokenRefreshed, callback);
|
||||
if (errorCallback == nullptr)
|
||||
errorCallback = getErrorPrintingCallback();
|
||||
|
||||
Common::String url = Common::String::format("https://cloud.scummvm.org/%s/refresh?code=%s", cloudProvider().c_str(), _refreshToken.c_str());
|
||||
Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, url);
|
||||
addRequest(request);
|
||||
}
|
||||
|
||||
void BaseStorage::tokenRefreshed(BoolCallback callback, Networking::JsonResponse response) {
|
||||
Common::JSONValue *json = response.value;
|
||||
if (json == nullptr) {
|
||||
debug(9, "BaseStorage::tokenRefreshed: got NULL instead of JSON!");
|
||||
if (callback)
|
||||
(*callback)(BoolResponse(nullptr, false));
|
||||
delete callback;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json->isObject()) {
|
||||
debug(9, "BaseStorage::tokenRefreshed: passed JSON is not an object!");
|
||||
if (callback)
|
||||
(*callback)(BoolResponse(nullptr, false));
|
||||
delete json;
|
||||
delete callback;
|
||||
return;
|
||||
}
|
||||
|
||||
Common::JSONObject result = json->asObject();
|
||||
if (!Networking::CurlJsonRequest::jsonContainsAttribute(result, "error", "BaseStorage::tokenRefreshed")) {
|
||||
warning("BaseStorage: bad response, no 'error' attribute passed");
|
||||
debug(9, "%s", json->stringify(true).c_str());
|
||||
if (callback)
|
||||
(*callback)(BoolResponse(nullptr, false));
|
||||
delete json;
|
||||
delete callback;
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.getVal("error")->asBool()) {
|
||||
Common::String errorMessage = "{error: true}, message is missing";
|
||||
if (Networking::CurlJsonRequest::jsonContainsString(result, "message", "BaseStorage::tokenRefreshed")) {
|
||||
errorMessage = result.getVal("message")->asString();
|
||||
}
|
||||
warning("BaseStorage: response says error occurred: %s", errorMessage.c_str());
|
||||
if (callback)
|
||||
(*callback)(BoolResponse(nullptr, false));
|
||||
delete json;
|
||||
delete callback;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Networking::CurlJsonRequest::jsonContainsObject(result, "oauth", "BaseStorage::tokenRefreshed")) {
|
||||
warning("BaseStorage: bad response, no 'oauth' attribute passed");
|
||||
debug(9, "%s", json->stringify(true).c_str());
|
||||
if (callback)
|
||||
(*callback)(BoolResponse(nullptr, false));
|
||||
delete json;
|
||||
delete callback;
|
||||
return;
|
||||
}
|
||||
|
||||
Common::JSONObject oauth = result.getVal("oauth")->asObject();
|
||||
bool requiresRefreshToken = needsRefreshToken(); // TODO: it seems Google Drive might not send new refresh token, and still accept old one
|
||||
if (!Networking::CurlJsonRequest::jsonContainsString(oauth, "access_token", "BaseStorage::tokenRefreshed") ||
|
||||
!Networking::CurlJsonRequest::jsonContainsString(oauth, "refresh_token", "BaseStorage::tokenRefreshed", !requiresRefreshToken)) {
|
||||
warning("BaseStorage: bad response, no 'access_token' or 'refresh_token' attribute passed");
|
||||
debug(9, "%s", json->stringify(true).c_str());
|
||||
if (callback)
|
||||
(*callback)(BoolResponse(nullptr, false));
|
||||
delete json;
|
||||
delete callback;
|
||||
return;
|
||||
}
|
||||
|
||||
_token = oauth.getVal("access_token")->asString();
|
||||
if (requiresRefreshToken) {
|
||||
_refreshToken = oauth.getVal("refresh_token")->asString();
|
||||
}
|
||||
CloudMan.save(); //ask CloudManager to save our new access_token and refresh_token
|
||||
if (callback)
|
||||
(*callback)(BoolResponse(nullptr, true));
|
||||
delete json;
|
||||
delete callback;
|
||||
}
|
||||
|
||||
} // End of namespace Cloud
|
||||
|
@ -64,10 +64,24 @@ protected:
|
||||
*/
|
||||
virtual uint32 storageIndex() = 0;
|
||||
|
||||
/**
|
||||
* Return whether storage needs refresh_token to work.
|
||||
*/
|
||||
virtual bool needsRefreshToken() = 0;
|
||||
|
||||
private:
|
||||
void tokenRefreshed(BoolCallback callback, Networking::JsonResponse response);
|
||||
|
||||
public:
|
||||
BaseStorage();
|
||||
BaseStorage(Common::String token, Common::String refreshToken);
|
||||
virtual ~BaseStorage();
|
||||
|
||||
/**
|
||||
* Gets new access_token. Pass a callback, so you could
|
||||
* continue your work when new token is available.
|
||||
*/
|
||||
virtual void refreshAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback = nullptr);
|
||||
};
|
||||
|
||||
} // End of namespace Cloud
|
||||
|
@ -55,57 +55,7 @@ Common::String BoxStorage::cloudProvider() { return "box"; }
|
||||
|
||||
uint32 BoxStorage::storageIndex() { return kStorageBoxId; }
|
||||
|
||||
void BoxStorage::refreshAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback) {
|
||||
if (_refreshToken == "") {
|
||||
warning("BoxStorage: no refresh token available to get new access token.");
|
||||
if (callback) (*callback)(BoolResponse(nullptr, false));
|
||||
return;
|
||||
}
|
||||
|
||||
Networking::JsonCallback innerCallback = new Common::CallbackBridge<BoxStorage, BoolResponse, Networking::JsonResponse>(this, &BoxStorage::tokenRefreshed, callback);
|
||||
if (errorCallback == nullptr)
|
||||
errorCallback = getErrorPrintingCallback();
|
||||
|
||||
Common::String url = "https://cloud.scummvm.org/box/refresh/" + _refreshToken; // TODO: subject to change
|
||||
Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, url);
|
||||
addRequest(request);
|
||||
}
|
||||
|
||||
void BoxStorage::tokenRefreshed(BoolCallback callback, Networking::JsonResponse response) {
|
||||
Common::JSONValue *json = response.value;
|
||||
if (!json) {
|
||||
warning("BoxStorage: got NULL instead of JSON");
|
||||
if (callback)
|
||||
(*callback)(BoolResponse(nullptr, false));
|
||||
delete callback;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Networking::CurlJsonRequest::jsonIsObject(json, "BoxStorage")) {
|
||||
if (callback)
|
||||
(*callback)(BoolResponse(nullptr, false));
|
||||
delete json;
|
||||
delete callback;
|
||||
return;
|
||||
}
|
||||
|
||||
Common::JSONObject result = json->asObject();
|
||||
if (!Networking::CurlJsonRequest::jsonContainsString(result, "access_token", "BoxStorage") ||
|
||||
!Networking::CurlJsonRequest::jsonContainsString(result, "refresh_token", "BoxStorage")) {
|
||||
warning("BoxStorage: bad response, no token passed");
|
||||
debug(9, "%s", json->stringify().c_str());
|
||||
if (callback)
|
||||
(*callback)(BoolResponse(nullptr, false));
|
||||
} else {
|
||||
_token = result.getVal("access_token")->asString();
|
||||
_refreshToken = result.getVal("refresh_token")->asString();
|
||||
CloudMan.save(); //ask CloudManager to save our new refreshToken
|
||||
if (callback)
|
||||
(*callback)(BoolResponse(nullptr, true));
|
||||
}
|
||||
delete json;
|
||||
delete callback;
|
||||
}
|
||||
bool BoxStorage::needsRefreshToken() { return true; }
|
||||
|
||||
void BoxStorage::saveConfig(Common::String keyPrefix) {
|
||||
ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain);
|
||||
|
@ -33,8 +33,6 @@ class BoxStorage: public Id::IdStorage {
|
||||
/** This private constructor is called from loadFromConfig(). */
|
||||
BoxStorage(Common::String token, Common::String refreshToken);
|
||||
|
||||
void tokenRefreshed(BoolCallback callback, Networking::JsonResponse response);
|
||||
|
||||
/** Constructs StorageInfo based on JSON response from cloud. */
|
||||
void infoInnerCallback(StorageInfoCallback outerCallback, Networking::JsonResponse json);
|
||||
|
||||
@ -51,6 +49,8 @@ protected:
|
||||
*/
|
||||
virtual uint32 storageIndex();
|
||||
|
||||
virtual bool needsRefreshToken();
|
||||
|
||||
public:
|
||||
/** This constructor uses OAuth code flow to get tokens. */
|
||||
BoxStorage(Common::String code);
|
||||
@ -104,12 +104,6 @@ public:
|
||||
|
||||
virtual Common::String getRootDirectoryId();
|
||||
|
||||
/**
|
||||
* Gets new access_token. Pass a callback, so you could
|
||||
* continue your work when new token is available.
|
||||
*/
|
||||
void refreshAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback = nullptr);
|
||||
|
||||
Common::String accessToken() const { return _token; }
|
||||
};
|
||||
|
||||
|
@ -52,6 +52,8 @@ Common::String DropboxStorage::cloudProvider() { return "dropbox"; }
|
||||
|
||||
uint32 DropboxStorage::storageIndex() { return kStorageDropboxId; }
|
||||
|
||||
bool DropboxStorage::needsRefreshToken() { return false; }
|
||||
|
||||
void DropboxStorage::saveConfig(Common::String keyPrefix) {
|
||||
ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain);
|
||||
}
|
||||
|
@ -45,6 +45,8 @@ protected:
|
||||
*/
|
||||
virtual uint32 storageIndex();
|
||||
|
||||
virtual bool needsRefreshToken();
|
||||
|
||||
public:
|
||||
/** This constructor uses OAuth code flow to get tokens. */
|
||||
DropboxStorage(Common::String code);
|
||||
|
@ -56,60 +56,7 @@ Common::String GoogleDriveStorage::cloudProvider() { return "gdrive"; }
|
||||
|
||||
uint32 GoogleDriveStorage::storageIndex() { return kStorageGoogleDriveId; }
|
||||
|
||||
void GoogleDriveStorage::refreshAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback) {
|
||||
if (_refreshToken == "") {
|
||||
warning("GoogleDriveStorage: no refresh token available to get new access token.");
|
||||
if (callback)
|
||||
(*callback)(BoolResponse(nullptr, false));
|
||||
return;
|
||||
}
|
||||
|
||||
Networking::JsonCallback innerCallback = new Common::CallbackBridge<GoogleDriveStorage, BoolResponse, Networking::JsonResponse>(this, &GoogleDriveStorage::tokenRefreshed, callback);
|
||||
if (errorCallback == nullptr)
|
||||
errorCallback = getErrorPrintingCallback();
|
||||
|
||||
Common::String url = "https://cloud.scummvm.org/gdrive/refresh/" + _refreshToken; // TODO: subject to change
|
||||
Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, url);
|
||||
addRequest(request);
|
||||
}
|
||||
|
||||
void GoogleDriveStorage::tokenRefreshed(BoolCallback callback, Networking::JsonResponse response) {
|
||||
Common::JSONValue *json = response.value;
|
||||
if (!json) {
|
||||
warning("GoogleDriveStorage: got NULL instead of JSON");
|
||||
if (callback)
|
||||
(*callback)(BoolResponse(nullptr, false));
|
||||
delete callback;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Networking::CurlJsonRequest::jsonIsObject(json, "GoogleDriveStorage")) {
|
||||
if (callback)
|
||||
(*callback)(BoolResponse(nullptr, false));
|
||||
delete json;
|
||||
delete callback;
|
||||
return;
|
||||
}
|
||||
|
||||
Common::JSONObject result = json->asObject();
|
||||
if (!Networking::CurlJsonRequest::jsonContainsString(result, "access_token", "GoogleDriveStorage")) {
|
||||
warning("GoogleDriveStorage: bad response, no token passed");
|
||||
debug(9, "%s", json->stringify().c_str());
|
||||
if (callback)
|
||||
(*callback)(BoolResponse(nullptr, false));
|
||||
} else {
|
||||
_token = result.getVal("access_token")->asString();
|
||||
if (!Networking::CurlJsonRequest::jsonContainsString(result, "refresh_token", "GoogleDriveStorage"))
|
||||
warning("GoogleDriveStorage: no refresh_token passed");
|
||||
else
|
||||
_refreshToken = result.getVal("refresh_token")->asString();
|
||||
CloudMan.save(); //ask CloudManager to save our new refreshToken
|
||||
if (callback)
|
||||
(*callback)(BoolResponse(nullptr, true));
|
||||
}
|
||||
delete json;
|
||||
delete callback;
|
||||
}
|
||||
bool GoogleDriveStorage::needsRefreshToken() { return true; }
|
||||
|
||||
void GoogleDriveStorage::saveConfig(Common::String keyPrefix) {
|
||||
ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain);
|
||||
|
@ -33,8 +33,6 @@ class GoogleDriveStorage: public Id::IdStorage {
|
||||
/** This private constructor is called from loadFromConfig(). */
|
||||
GoogleDriveStorage(Common::String token, Common::String refreshToken);
|
||||
|
||||
void tokenRefreshed(BoolCallback callback, Networking::JsonResponse response);
|
||||
|
||||
/** Constructs StorageInfo based on JSON response from cloud. */
|
||||
void infoInnerCallback(StorageInfoCallback outerCallback, Networking::JsonResponse json);
|
||||
|
||||
@ -54,6 +52,8 @@ protected:
|
||||
*/
|
||||
virtual uint32 storageIndex();
|
||||
|
||||
virtual bool needsRefreshToken();
|
||||
|
||||
public:
|
||||
/** This constructor uses OAuth code flow to get tokens. */
|
||||
GoogleDriveStorage(Common::String code);
|
||||
@ -106,12 +106,6 @@ public:
|
||||
|
||||
virtual Common::String getRootDirectoryId();
|
||||
|
||||
/**
|
||||
* Gets new access_token. Pass a callback, so you could
|
||||
* continue your work when new token is available.
|
||||
*/
|
||||
void refreshAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback = nullptr);
|
||||
|
||||
Common::String accessToken() const { return _token; }
|
||||
};
|
||||
|
||||
|
@ -42,7 +42,7 @@ namespace OneDrive {
|
||||
#define ONEDRIVE_API_SPECIAL_APPROOT_ID "https://api.onedrive.com/v1.0/drive/special/approot:/"
|
||||
#define ONEDRIVE_API_SPECIAL_APPROOT "https://api.onedrive.com/v1.0/drive/special/approot"
|
||||
|
||||
OneDriveStorage::OneDriveStorage(Common::String token, Common::String refreshToken):
|
||||
OneDriveStorage::OneDriveStorage(Common::String token, Common::String refreshToken):
|
||||
BaseStorage(token, refreshToken) {}
|
||||
|
||||
OneDriveStorage::OneDriveStorage(Common::String code) {
|
||||
@ -55,59 +55,7 @@ Common::String OneDriveStorage::cloudProvider() { return "onedrive"; }
|
||||
|
||||
uint32 OneDriveStorage::storageIndex() { return kStorageOneDriveId; }
|
||||
|
||||
void OneDriveStorage::refreshAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback) {
|
||||
if (_refreshToken == "") {
|
||||
warning("OneDriveStorage: no refresh token available to get new access token.");
|
||||
if (callback)
|
||||
(*callback)(BoolResponse(nullptr, false));
|
||||
return;
|
||||
}
|
||||
|
||||
Networking::JsonCallback innerCallback = new Common::CallbackBridge<OneDriveStorage, BoolResponse, Networking::JsonResponse>(this, &OneDriveStorage::tokenRefreshed, callback);
|
||||
if (errorCallback == nullptr)
|
||||
errorCallback = getErrorPrintingCallback();
|
||||
|
||||
Common::String url = "https://cloud.scummvm.org/onedrive/refresh/" + _refreshToken; // TODO: subject to change
|
||||
Networking::CurlJsonRequest *request = new Networking::CurlJsonRequest(innerCallback, errorCallback, url);
|
||||
addRequest(request);
|
||||
}
|
||||
|
||||
void OneDriveStorage::tokenRefreshed(BoolCallback callback, Networking::JsonResponse response) {
|
||||
Common::JSONValue *json = response.value;
|
||||
if (!json) {
|
||||
warning("OneDriveStorage: got NULL instead of JSON");
|
||||
if (callback)
|
||||
(*callback)(BoolResponse(nullptr, false));
|
||||
delete callback;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Networking::CurlJsonRequest::jsonIsObject(json, "OneDriveStorage")) {
|
||||
if (callback)
|
||||
(*callback)(BoolResponse(nullptr, false));
|
||||
delete json;
|
||||
delete callback;
|
||||
return;
|
||||
}
|
||||
|
||||
Common::JSONObject result = json->asObject();
|
||||
if (!Networking::CurlJsonRequest::jsonContainsString(result, "access_token", "OneDriveStorage") ||
|
||||
!Networking::CurlJsonRequest::jsonContainsString(result, "user_id", "OneDriveStorage") ||
|
||||
!Networking::CurlJsonRequest::jsonContainsString(result, "refresh_token", "OneDriveStorage")) {
|
||||
warning("OneDriveStorage: bad response, no token or user_id passed");
|
||||
debug(9, "%s", json->stringify().c_str());
|
||||
if (callback)
|
||||
(*callback)(BoolResponse(nullptr, false));
|
||||
} else {
|
||||
_token = result.getVal("access_token")->asString();
|
||||
_refreshToken = result.getVal("refresh_token")->asString();
|
||||
CloudMan.save(); //ask CloudManager to save our new refreshToken
|
||||
if (callback)
|
||||
(*callback)(BoolResponse(nullptr, true));
|
||||
}
|
||||
delete json;
|
||||
delete callback;
|
||||
}
|
||||
bool OneDriveStorage::needsRefreshToken() { return true; }
|
||||
|
||||
void OneDriveStorage::saveConfig(Common::String keyPrefix) {
|
||||
ConfMan.set(keyPrefix + "access_token", _token, ConfMan.kCloudDomain);
|
||||
|
@ -33,8 +33,6 @@ class OneDriveStorage: public Cloud::BaseStorage {
|
||||
/** This private constructor is called from loadFromConfig(). */
|
||||
OneDriveStorage(Common::String token, Common::String refreshToken);
|
||||
|
||||
void tokenRefreshed(BoolCallback callback, Networking::JsonResponse response);
|
||||
|
||||
/** Constructs StorageInfo based on JSON response from cloud. */
|
||||
void infoInnerCallback(StorageInfoCallback outerCallback, Networking::JsonResponse json);
|
||||
|
||||
@ -51,6 +49,8 @@ protected:
|
||||
*/
|
||||
virtual uint32 storageIndex();
|
||||
|
||||
virtual bool needsRefreshToken();
|
||||
|
||||
public:
|
||||
/** This constructor uses OAuth code flow to get tokens. */
|
||||
OneDriveStorage(Common::String code);
|
||||
@ -101,12 +101,6 @@ public:
|
||||
*/
|
||||
static OneDriveStorage *loadFromConfig(Common::String keyPrefix);
|
||||
|
||||
/**
|
||||
* Gets new access_token. Pass a callback, so you could
|
||||
* continue your work when new token is available.
|
||||
*/
|
||||
void refreshAccessToken(BoolCallback callback, Networking::ErrorCallback errorCallback = nullptr);
|
||||
|
||||
Common::String accessToken() const { return _token; }
|
||||
};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user