Skip to content
Snippets Groups Projects
Select Git revision
  • f21092e597a471a2b914dfa67ac0e2c5e91cbf97
  • master default protected
  • release/202005
  • release/202001
  • release/201912
  • release/201911
  • release/releaseWindowsTestOne
  • release/windowsReleaseTest
  • release/releaseTest
  • release/releaseWindowsTest
  • release/201910
  • release/qt/201910
  • release/windows-test/201910
  • release/201908
  • release/201906
  • release/201905
  • release/201904
  • release/201903
  • release/201902
  • release/201901
  • release/201812
  • 4.0.0
  • 2.2.0
  • 2.1.0
  • 2.0.1
  • 2.0.0
  • 1.4.1
  • 1.4.0
  • 1.3.0
  • 1.2.0
  • 1.1.0
31 results

managerimpl.cpp

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    server_account_manager.cpp 31.68 KiB
    /*
     *  Copyright (C) 2004-2025 Savoir-faire Linux Inc.
     *
     *  This program is free software: you can redistribute it and/or modify
     *  it under the terms of the GNU General Public License as published by
     *  the Free Software Foundation, either version 3 of the License, or
     *  (at your option) any later version.
     *
     *  This program is distributed in the hope that it will be useful,
     *  but WITHOUT ANY WARRANTY; without even the implied warranty of
     *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
     *  GNU General Public License for more details.
     *
     *  You should have received a copy of the GNU General Public License
     *  along with this program. If not, see <https://www.gnu.org/licenses/>.
     */
    #include "server_account_manager.h"
    #include "base64.h"
    #include "jami/account_const.h"
    #include "fileutils.h"
    
    #include <opendht/http.h>
    #include <opendht/log.h>
    #include <opendht/thread_pool.h>
    
    #include "conversation_module.h"
    #include "jamiaccount.h"
    #include "manager.h"
    
    #include <algorithm>
    #include <string_view>
    
    using namespace std::literals;
    
    namespace jami {
    
    using Request = dht::http::Request;
    
    #define JAMI_PATH_LOGIN "/api/login"
    #define JAMI_PATH_AUTH  "/api/auth"
    constexpr std::string_view PATH_DEVICE = JAMI_PATH_AUTH "/device";
    constexpr std::string_view PATH_DEVICES = JAMI_PATH_AUTH "/devices";
    constexpr std::string_view PATH_SEARCH = JAMI_PATH_AUTH "/directory/search";
    constexpr std::string_view PATH_CONTACTS = JAMI_PATH_AUTH "/contacts";
    constexpr std::string_view PATH_CONVERSATIONS = JAMI_PATH_AUTH "/conversations";
    constexpr std::string_view PATH_CONVERSATIONS_REQUESTS = JAMI_PATH_AUTH "/conversationRequests";
    constexpr std::string_view PATH_BLUEPRINT = JAMI_PATH_AUTH "/policyData";
    
    ServerAccountManager::ServerAccountManager(const std::string& accountId,
                                               const std::filesystem::path& path,
                                               const std::string& managerHostname,
                                               const std::string& nameServer)
        : AccountManager(accountId, path, nameServer)
        , managerHostname_(managerHostname)
        , logger_(Logger::dhtLogger()) {}
    
    void
    ServerAccountManager::setAuthHeaderFields(Request& request) const
    {
        request.set_header_field(restinio::http_field_t::authorization, "Bearer " + token_);
    }
    
    void
    ServerAccountManager::initAuthentication(PrivateKey key,
                                             std::string deviceName,
                                             std::unique_ptr<AccountCredentials> credentials,
                                             AuthSuccessCallback onSuccess,
                                             AuthFailureCallback onFailure,
                                             const OnChangeCallback& onChange)
    {
        auto ctx = std::make_shared<AuthContext>();
        ctx->accountId = accountId_;
        ctx->key = key;
        ctx->request = buildRequest(key);
        ctx->deviceName = std::move(deviceName);
        ctx->credentials = dynamic_unique_cast<ServerAccountCredentials>(std::move(credentials));
        ctx->onSuccess = std::move(onSuccess);
        ctx->onFailure = std::move(onFailure);
        if (not ctx->credentials or ctx->credentials->username.empty()) {
            ctx->onFailure(AuthError::INVALID_ARGUMENTS, "invalid credentials");
            return;
        }
    
        onChange_ = std::move(onChange);
        const std::string url = managerHostname_ + PATH_DEVICE;
        JAMI_WARNING("[Account {}] [Auth] Authentication with: {} to {}", accountId_, ctx->credentials->username, url);
    
        dht::ThreadPool::computation().run([ctx, url, w=weak_from_this()] {
            auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
            if (not this_) return;
            Json::Value body;
            {
                auto csr = ctx->request.get()->toString();
                body["csr"] = csr;
                body["deviceName"] = ctx->deviceName;
            }
            auto request = std::make_shared<Request>(
                *Manager::instance().ioContext(),
                url,
                body,
                [ctx, w](Json::Value json, const dht::http::Response& response) {
                    auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
                    JAMI_DEBUG("[Auth] Got request callback with status code={}",
                                response.status_code);
                    if (response.status_code == 0 || this_ == nullptr)
                        ctx->onFailure(AuthError::SERVER_ERROR, "Unable to connect to server");
                    else if (response.status_code >= 400 && response.status_code < 500)
                        ctx->onFailure(AuthError::INVALID_ARGUMENTS, "Invalid credentials provided!");
                    else if (response.status_code < 200 || response.status_code > 299)
                        ctx->onFailure(AuthError::INVALID_ARGUMENTS, "");
                    else {
                        do {
                            try {
                                JAMI_WARNING("[Account {}] [Auth] Got server response: {} bytes", this_->accountId_, response.body.size());
                                auto cert = std::make_shared<dht::crypto::Certificate>(
                                    json["certificateChain"].asString());
                                auto accountCert = cert->issuer;
                                if (not accountCert) {
                                    JAMI_ERR("[Auth] Unable to parse certificate: no issuer");
                                    ctx->onFailure(AuthError::SERVER_ERROR,
                                                    "Invalid certificate from server");
                                    break;
                                }
                                auto receipt = json["deviceReceipt"].asString();
                                Json::Value receiptJson;
                                std::string err;
                                auto receiptReader = std::unique_ptr<Json::CharReader>(
                                    Json::CharReaderBuilder {}.newCharReader());
                                if (!receiptReader->parse(receipt.data(),
                                                            receipt.data() + receipt.size(),
                                                            &receiptJson,
                                                            &err)) {
                                    JAMI_ERR("[Auth] Unable to parse receipt from server: %s",
                                                err.c_str());
                                    ctx->onFailure(AuthError::SERVER_ERROR,
                                                    "Unable to parse receipt from server");
                                    break;
                                }
                                auto receiptSignature = base64::decode(
                                    json["receiptSignature"].asString());
    
                                auto info = std::make_unique<AccountInfo>();
                                info->identity.first = ctx->key.get();
                                info->identity.second = cert;
                                info->devicePk = cert->getSharedPublicKey();
                                info->deviceId = info->devicePk->getLongId().toString();
                                info->accountId = accountCert->getId().toString();
                                info->contacts = std::make_unique<ContactList>(ctx->accountId,
                                                                                accountCert,
                                                                                this_->path_,
                                                                                this_->onChange_);
                                // info->contacts->setContacts(a.contacts);
                                if (ctx->deviceName.empty())
                                    ctx->deviceName = info->deviceId.substr(8);
                                info->contacts->foundAccountDevice(cert,
                                                                    ctx->deviceName,
                                                                    clock::now());
                                info->ethAccount = receiptJson["eth"].asString();
                                info->announce
                                    = parseAnnounce(receiptJson["announce"].asString(),
                                                    info->accountId,
                                                    info->devicePk->getId().toString(),
                                                    info->devicePk->getLongId().toString());
                                if (not info->announce) {
                                    ctx->onFailure(AuthError::SERVER_ERROR,
                                                    "Unable to parse announce from server");
                                    return;
                                }
                                info->username = ctx->credentials->username;
    
                                this_->creds_ = std::move(ctx->credentials);
                                this_->info_ = std::move(info);
                                std::map<std::string, std::string> config;
                                for (auto itr = json.begin(); itr != json.end(); ++itr) {
                                    const auto& name = itr.name();
                                    if (name == "nameServer"sv) {
                                        auto nameServer = json["nameServer"].asString();
                                        if (!nameServer.empty() && nameServer[0] == '/')
                                            nameServer = this_->managerHostname_ + nameServer;
                                        this_->nameDir_ = NameDirectory::instance(nameServer);
                                        config
                                            .emplace(libjami::Account::ConfProperties::RingNS::URI,
                                                        std::move(nameServer));
                                    } else if (name == "userPhoto"sv) {
                                        this_->info_->photo = json["userPhoto"].asString();
                                    } else {
                                        config.emplace(name, itr->asString());
                                    }
                                }
    
                                ctx->onSuccess(*this_->info_,
                                                std::move(config),
                                                std::move(receipt),
                                                std::move(receiptSignature));
                            } catch (const std::exception& e) {
                                JAMI_ERROR("[Account {}] [Auth] Error when loading account: {}", this_->accountId_, e.what());
                                ctx->onFailure(AuthError::NETWORK, "");
                            }
                        } while (false);
                    }
    
                    if (auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock()))
                        this_->clearRequest(response.request);
                },
                this_->logger_);
            request->set_auth(ctx->credentials->username, ctx->credentials->password);
            this_->sendRequest(request);
        });
    }
    
    void
    ServerAccountManager::onAuthEnded(const Json::Value& json,
                                      const dht::http::Response& response,
                                      TokenScope expectedScope)
    {
        if (response.status_code >= 200 && response.status_code < 300) {
            auto scopeStr = json["scope"].asString();
            auto scope = scopeStr == "DEVICE"sv
                             ? TokenScope::Device
                             : (scopeStr == "USER"sv ? TokenScope::User : TokenScope::None);
            auto expires_in = json["expires_in"].asLargestUInt();
            auto expiration = std::chrono::steady_clock::now() + std::chrono::seconds(expires_in);
            JAMI_WARNING("[Account {}] [Auth] Got server response: {} ({} bytes)", accountId_, response.status_code, response.body.size());
            setToken(json["access_token"].asString(), scope, expiration);
        } else {
            JAMI_WARNING("[Account {}] [Auth] Got server response: {} ({} bytes)", accountId_, response.status_code, response.body.size());
            authFailed(expectedScope, response.status_code);
        }
        clearRequest(response.request);
    }
    
    void
    ServerAccountManager::authenticateDevice()
    {
        if (not info_) {
            authFailed(TokenScope::Device, 0);
        }
        const std::string url = managerHostname_ + JAMI_PATH_LOGIN;
        JAMI_WARNING("[Account {}] [Auth] Getting a device token: {}", accountId_, url);
        auto request = std::make_shared<Request>(
            *Manager::instance().ioContext(),
            url,
            Json::Value {Json::objectValue},
            [w=weak_from_this()](Json::Value json, const dht::http::Response& response) {
                if (auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock()))
                    this_->onAuthEnded(json, response, TokenScope::Device);
            },
            logger_);
        request->set_identity(info_->identity);
        // request->set_certificate_authority(info_->identity.second->issuer->issuer);
        sendRequest(request);
    }
    
    void
    ServerAccountManager::authenticateAccount(const std::string& username, const std::string& password)
    {
        const std::string url = managerHostname_ + JAMI_PATH_LOGIN;
        JAMI_WARNING("[Account {}] [Auth] Getting a user token: {}", accountId_, url);
        auto request = std::make_shared<Request>(
            *Manager::instance().ioContext(),
            url,
            Json::Value {Json::objectValue},
            [w=weak_from_this()](Json::Value json, const dht::http::Response& response) {
                if (auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock()))
                    this_->onAuthEnded(json, response, TokenScope::User);
            },
            logger_);
        request->set_auth(username, password);
        sendRequest(request);
    }
    
    void
    ServerAccountManager::sendRequest(const std::shared_ptr<dht::http::Request>& request)
    {
        request->set_header_field(restinio::http_field_t::user_agent, userAgent());
        {
            std::lock_guard lock(requestLock_);
            requests_.emplace(request);
        }
        request->send();
    }
    
    void
    ServerAccountManager::clearRequest(const std::weak_ptr<dht::http::Request>& request)
    {
        Manager::instance().ioContext()->post([w=weak_from_this(), request] {
            if (auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock())) {
                if (auto req = request.lock()) {
                    std::lock_guard lock(this_->requestLock_);
                    this_->requests_.erase(req);
                }
            }
        });
    }
    
    void
    ServerAccountManager::authFailed(TokenScope scope, int code)
    {
        RequestQueue requests;
        {
            std::lock_guard lock(tokenLock_);
            requests = std::move(getRequestQueue(scope));
        }
        JAMI_DEBUG("[Auth] Failed auth with scope {}, ending {} pending requests",
                 (int) scope,
                 requests.size());
        if (code == 401) {
            // NOTE: we do not login every time to the server but retrieve a device token to use the account
            // If authentificate device fails with 401
            // it means that the device is revoked
            if (onNeedsMigration_)
                onNeedsMigration_();
        }
        while (not requests.empty()) {
            auto req = std::move(requests.front());
            requests.pop();
            req->terminate(code == 0 ? asio::error::not_connected : asio::error::access_denied);
        }
    }
    
    void
    ServerAccountManager::authError(TokenScope scope)
    {
        {
            std::lock_guard lock(tokenLock_);
            if (scope <= tokenScope_) {
                token_ = {};
                tokenScope_ = TokenScope::None;
            }
        }
        if (scope == TokenScope::Device)
            authenticateDevice();
    }
    
    void
    ServerAccountManager::setToken(std::string token,
                                   TokenScope scope,
                                   std::chrono::steady_clock::time_point expiration)
    {
        std::lock_guard lock(tokenLock_);
        token_ = std::move(token);
        tokenScope_ = scope;
        tokenExpire_ = expiration;
    
        nameDir_.get().setToken(token_);
        if (not token_.empty() and scope != TokenScope::None) {
            auto& reqQueue = getRequestQueue(scope);
            JAMI_DEBUG("[Account {}] [Auth] Got token with scope {}, handling {} pending requests",
                     accountId_,
                     (int) scope,
                     reqQueue.size());
            while (not reqQueue.empty()) {
                auto req = std::move(reqQueue.front());
                reqQueue.pop();
                setAuthHeaderFields(*req);
                sendRequest(req);
            }
        }
    }
    
    void
    ServerAccountManager::sendDeviceRequest(const std::shared_ptr<dht::http::Request>& req)
    {
        std::lock_guard lock(tokenLock_);
        if (hasAuthorization(TokenScope::Device)) {
            setAuthHeaderFields(*req);
            sendRequest(req);
        } else {
            auto& rQueue = getRequestQueue(TokenScope::Device);
            if (rQueue.empty())
                authenticateDevice();
            rQueue.emplace(req);
        }
    }
    
    void
    ServerAccountManager::sendAccountRequest(const std::shared_ptr<dht::http::Request>& req,
                                             const std::string& pwd)
    {
        std::lock_guard lock(tokenLock_);
        if (hasAuthorization(TokenScope::User)) {
            setAuthHeaderFields(*req);
            sendRequest(req);
        } else {
            auto& rQueue = getRequestQueue(TokenScope::User);
            if (rQueue.empty())
                authenticateAccount(info_->username, pwd);
            rQueue.emplace(req);
        }
    }
    
    void
    ServerAccountManager::syncDevices()
    {
        const std::string urlDevices = managerHostname_ + PATH_DEVICES;
        const std::string urlContacts = managerHostname_ + PATH_CONTACTS;
        const std::string urlConversations = managerHostname_ + PATH_CONVERSATIONS;
        const std::string urlConversationsRequests = managerHostname_ + PATH_CONVERSATIONS_REQUESTS;
    
        JAMI_WARNING("[Account {}] [Auth] Sync conversations {}", accountId_, urlConversations);
        Json::Value jsonConversations(Json::arrayValue);
        for (const auto& [key, convInfo] : ConversationModule::convInfos(accountId_)) {
            jsonConversations.append(convInfo.toJson());
        }
        sendDeviceRequest(std::make_shared<Request>(
            *Manager::instance().ioContext(),
            urlConversations,
            jsonConversations,
            [w=weak_from_this()](Json::Value json, const dht::http::Response& response) {
                auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
                if (!this_) return;
                JAMI_DEBUG("[Account {}] [Auth] Got conversation sync request callback with status code={}",
                             this_->accountId_,
                             response.status_code);
                if (response.status_code >= 200 && response.status_code < 300) {
                    try {
                        JAMI_WARNING("[Account {}] [Auth] Got server response: {} bytes", this_->accountId_, response.body.size());
                        if (not json.isArray()) {
                            JAMI_ERROR("[Account {}] [Auth] Unable to parse server response: not an array", this_->accountId_);
                        } else {
                            SyncMsg convs;
                            for (unsigned i = 0, n = json.size(); i < n; i++) {
                                const auto& e = json[i];
                                auto ci = ConvInfo(e);
                                convs.c[ci.id] = std::move(ci);
                            }
                            dht::ThreadPool::io().run([accountId=this_->accountId_, convs] {
                                if (auto acc = Manager::instance().getAccount<JamiAccount>(accountId)) {
                                    acc->convModule()->onSyncData(convs, "", "");
                                }
                            });
                        }
                    } catch (const std::exception& e) {
                        JAMI_ERROR("[Account {}] Error when iterating conversation list: {}", this_->accountId_, e.what());
                    }
                } else if (response.status_code == 401)
                    this_->authError(TokenScope::Device);
    
                this_->clearRequest(response.request);
            },
            logger_));
    
        JAMI_WARNING("[Account {}] [Auth] Sync conversations requests {}", accountId_, urlConversationsRequests);
        Json::Value jsonConversationsRequests(Json::arrayValue);
        for (const auto& [key, convRequest] : ConversationModule::convRequests(accountId_)) {
            auto jsonConversation = convRequest.toJson();
            jsonConversationsRequests.append(std::move(jsonConversation));
        }
        sendDeviceRequest(std::make_shared<Request>(
            *Manager::instance().ioContext(),
            urlConversationsRequests,
            jsonConversationsRequests,
            [w=weak_from_this()](Json::Value json, const dht::http::Response& response) {
                auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
                if (!this_) return;
                JAMI_DEBUG("[Account {}] [Auth] Got conversations requests sync request callback with status code={}",
                                this_->accountId_, response.status_code);
                if (response.status_code >= 200 && response.status_code < 300) {
                    try {
                        JAMI_WARNING("[Account {}] [Auth] Got server response: {}", this_->accountId_, response.body);
                        if (not json.isArray()) {
                            JAMI_ERROR("[Account {}] [Auth] Unable to parse server response: not an array", this_->accountId_);
                        } else {
                            SyncMsg convReqs;
                            for (unsigned i = 0, n = json.size(); i < n; i++) {
                                const auto& e = json[i];
                                auto cr = ConversationRequest(e);
                                convReqs.cr[cr.conversationId] = std::move(cr);
                            }
                            dht::ThreadPool::io().run([accountId=this_->accountId_, convReqs] {
                                if (auto acc = Manager::instance().getAccount<JamiAccount>(accountId)) {
                                    acc->convModule()->onSyncData(convReqs, "", "");
                                }
                            });
                        }
                    } catch (const std::exception& e) {
                        JAMI_ERROR("[Account {}] Error when iterating conversations requests list: {}", this_->accountId_, e.what());
                    }
                } else if (response.status_code == 401)
                    this_->authError(TokenScope::Device);
    
                this_->clearRequest(response.request);
            },
            logger_));
    
        JAMI_WARNING("[Account {}] [Auth] syncContacts {}", accountId_, urlContacts);
        Json::Value jsonContacts(Json::arrayValue);
        for (const auto& contact : info_->contacts->getContacts()) {
            auto jsonContact = contact.second.toJson();
            jsonContact["uri"] = contact.first.toString();
            jsonContacts.append(std::move(jsonContact));
        }
        sendDeviceRequest(std::make_shared<Request>(
            *Manager::instance().ioContext(),
            urlContacts,
            jsonContacts,
            [w=weak_from_this()](Json::Value json, const dht::http::Response& response) {
                auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
                JAMI_DEBUG("[Account {}] [Auth] Got contact sync request callback with status code={}",
                    this_->accountId_, response.status_code);
                if (!this_) return;
                if (response.status_code >= 200 && response.status_code < 300) {
                    try {
                        JAMI_WARNING("[Account {}] [Auth] Got server response: {} bytes", this_->accountId_, response.body.size());
                        if (not json.isArray()) {
                            JAMI_ERROR("[Auth] Unable to parse server response: not an array");
                        } else {
                            for (unsigned i = 0, n = json.size(); i < n; i++) {
                                const auto& e = json[i];
                                Contact contact(e);
                                this_->info_->contacts
                                    ->updateContact(dht::InfoHash {e["uri"].asString()}, contact);
                            }
                            this_->info_->contacts->saveContacts();
                        }
                    } catch (const std::exception& e) {
                        JAMI_ERROR("Error when iterating contact list: {}", e.what());
                    }
                } else if (response.status_code == 401)
                    this_->authError(TokenScope::Device);
    
                this_->clearRequest(response.request);
            },
            logger_));
    
        JAMI_WARNING("[Account {}] [Auth] syncDevices {}", accountId_, urlDevices);
        sendDeviceRequest(std::make_shared<Request>(
            *Manager::instance().ioContext(),
            urlDevices,
            [w=weak_from_this()](Json::Value json, const dht::http::Response& response) {
                auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
                if (!this_) return;
                JAMI_DEBUG("[Account {}] [Auth] Got request callback with status code={}", this_->accountId_, response.status_code);
                if (response.status_code >= 200 && response.status_code < 300) {
                    try {
                        JAMI_LOG("[Account {}] [Auth] Got server response: {} bytes", this_->accountId_, response.body.size());
                        if (not json.isArray()) {
                            JAMI_ERROR("[Auth] Unable to parse server response: not an array");
                        } else {
                            for (unsigned i = 0, n = json.size(); i < n; i++) {
                                const auto& e = json[i];
                                const bool revoked = e["revoked"].asBool();
                                dht::PkId deviceId(e["deviceId"].asString());
                                if(!deviceId){
                                    continue;
                                }
                                if (!revoked) {
                                    this_->info_->contacts->foundAccountDevice(deviceId,
                                                                                e["alias"].asString(),
                                                                                clock::now());
                                }
                                else {
                                    this_->info_->contacts->removeAccountDevice(deviceId);
                                }
                            }
                        }
                    } catch (const std::exception& e) {
                        JAMI_ERROR("Error when iterating device list: {}", e.what());
                    }
                } else if (response.status_code == 401)
                    this_->authError(TokenScope::Device);
    
                this_->clearRequest(response.request);
            },
            logger_));
    }
    
    void
    ServerAccountManager::syncBlueprintConfig(SyncBlueprintCallback onSuccess)
    {
        auto syncBlueprintCallback = std::make_shared<SyncBlueprintCallback>(onSuccess);
        const std::string urlBlueprints = managerHostname_ + PATH_BLUEPRINT;
        JAMI_DEBUG("[Auth] Synchronize blueprint configuration {}", urlBlueprints);
        sendDeviceRequest(std::make_shared<Request>(
            *Manager::instance().ioContext(),
            urlBlueprints,
            [syncBlueprintCallback, w=weak_from_this()](Json::Value json, const dht::http::Response& response) {
                JAMI_DEBUG("[Auth] Got sync request callback with status code={}", response.status_code);
                auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
                if (!this_) return;
                if (response.status_code >= 200 && response.status_code < 300) {
                    try {
                        std::map<std::string, std::string> config;
                        for (auto itr = json.begin(); itr != json.end(); ++itr) {
                            const auto& name = itr.name();
                            config.emplace(name, itr->asString());
                        }
                        (*syncBlueprintCallback)(config);
                    } catch (const std::exception& e) {
                        JAMI_ERROR("Error when iterating blueprint config json: {}", e.what());
                    }
                } else if (response.status_code == 401)
                    this_->authError(TokenScope::Device);
                this_->clearRequest(response.request);
            },
            logger_));
    }
    
    bool
    ServerAccountManager::revokeDevice(const std::string& device,
                                       std::string_view scheme, const std::string& password,
                                       RevokeDeviceCallback cb)
    {
        if (not info_ || scheme != fileutils::ARCHIVE_AUTH_SCHEME_PASSWORD) {
            if (cb)
                cb(RevokeDeviceResult::ERROR_CREDENTIALS);
            return false;
        }
        const std::string url = managerHostname_ + PATH_DEVICE + "/" + device;
        JAMI_WARNING("[Revoke] Revoking device of {} at {}", info_->username, url);
        auto request = std::make_shared<Request>(
            *Manager::instance().ioContext(),
            url,
            [cb, w=weak_from_this()](Json::Value json, const dht::http::Response& response) {
                JAMI_DEBUG("[Revoke] Got request callback with status code={}", response.status_code);
                auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
                if (!this_) return;
                if (response.status_code >= 200 && response.status_code < 300) {
                    try {
                        JAMI_WARNING("[Revoke] Got server response");
                        if (json["errorDetails"].empty()) {
                            if (cb)
                                cb(RevokeDeviceResult::SUCCESS);
                            this_->syncDevices(); // this will remove the devices from the known devices
                        }
                    } catch (const std::exception& e) {
                        JAMI_ERROR("Error when loading device list: {}", e.what());
                    }
                } else if (cb)
                    cb(RevokeDeviceResult::ERROR_NETWORK);
                this_->clearRequest(response.request);
            },
            logger_);
        request->set_method(restinio::http_method_delete());
        sendAccountRequest(request, password);
        return true;
    }
    
    void
    ServerAccountManager::registerName(const std::string& name, std::string_view scheme, const std::string&, RegistrationCallback cb)
    {
        cb(NameDirectory::RegistrationResponse::unsupported, name);
    }
    
    bool
    ServerAccountManager::searchUser(const std::string& query, SearchCallback cb)
    {
        const std::string url = managerHostname_ + PATH_SEARCH + "?queryString=" + query;
        JAMI_WARNING("[Search] Searching user {} at {}", query, url);
        sendDeviceRequest(std::make_shared<Request>(
            *Manager::instance().ioContext(),
            url,
            [cb, w=weak_from_this()](Json::Value json, const dht::http::Response& response) {
                JAMI_DEBUG("[Search] Got request callback with status code={}", response.status_code);
                auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
                if (!this_) return;
                if (response.status_code >= 200 && response.status_code < 300) {
                    try {
                        const auto& profiles = json["profiles"];
                        Json::Value::ArrayIndex rcount = profiles.size();
                        std::vector<std::map<std::string, std::string>> results;
                        results.reserve(rcount);
                        JAMI_WARNING("[Search] Got server response: {} bytes", response.body.size());
                        for (Json::Value::ArrayIndex i = 0; i < rcount; i++) {
                            const auto& ruser = profiles[i];
                            std::map<std::string, std::string> user;
                            for (const auto& member : ruser.getMemberNames()) {
                                const auto& rmember = ruser[member];
                                if (rmember.isString())
                                    user[member] = rmember.asString();
                            }
                            results.emplace_back(std::move(user));
                        }
                        if (cb)
                            cb(results, SearchResponse::found);
                    } catch (const std::exception& e) {
                        JAMI_ERROR("[Search] Error during search: {}", e.what());
                    }
                } else {
                    if (response.status_code == 401)
                        this_->authError(TokenScope::Device);
                    if (cb)
                        cb({}, SearchResponse::error);
                }
                this_->clearRequest(response.request);
            },
            logger_));
        return true;
    }
    
    } // namespace jami