/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright 2013 - 2020, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. * This project including source code and documentation is protected by * copyright law, and remains the property of nymea GmbH. All rights, including * reproduction, publication, editing and translation, are reserved. The use of * this project is subject to the terms of a license agreement to be concluded * with nymea GmbH in accordance with the terms of use of nymea GmbH, available * under https://nymea.io/license * * GNU Lesser General Public License Usage * Alternatively, this project may be redistributed and/or modified under the * terms of the GNU Lesser General Public License as published by the Free * Software Foundation; version 3. This project 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this project. If not, see . * * For any further details and any questions please contact us under * contact@nymea.io or see our FAQ/Licensing Information on * https://nymea.io/license/faq * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include #include #include "tempo.h" #include "extern-plugininfo.h" Tempo::Tempo(NetworkAccessManager *networkmanager, const QByteArray &clientId, const QByteArray &clientSecret, QObject *parent) : QObject(parent), m_clientId(clientId), m_clientSecret(clientSecret), m_networkManager(networkmanager) { m_tokenRefreshTimer = new QTimer(this); m_tokenRefreshTimer->setSingleShot(true); connect(m_tokenRefreshTimer, &QTimer::timeout, this, &Tempo::onRefreshTimer); } QByteArray Tempo::accessToken() { return m_accessToken; } QByteArray Tempo::refreshToken() { return m_refreshToken; } QUrl Tempo::getLoginUrl(const QUrl &redirectUrl, const QString &jiraCloudInstanceName) { if (m_clientId.isEmpty()) { qWarning(dcTempo) << "Client Id not defined!"; return QUrl(""); } if (redirectUrl.isEmpty()){ qWarning(dcTempo) << "No redirect uri defined!"; } m_redirectUri = QUrl::toPercentEncoding(redirectUrl.toString()); QUrl url; url.setScheme("https"); url.setHost(jiraCloudInstanceName+".atlassian.net"); url.setPath("/plugins/servlet/ac/io.tempo.jira/oauth-authorize/"); QUrlQuery query; query.addQueryItem("client_id", m_clientId); query.addQueryItem("redirect_uri", m_redirectUri); query.addQueryItem("access_type", "tenant_user"); url.setQuery(query); return url; } void Tempo::getAccessTokenFromRefreshToken(const QByteArray &refreshToken) { if (refreshToken.isEmpty()) { qWarning(dcTempo) << "No refresh token given!"; setAuthenticated(false); return; } QUrl url(m_baseTokenUrl); QUrlQuery query; query.clear(); query.addQueryItem("grant_type", "refresh_token"); query.addQueryItem("refresh_token", refreshToken); query.addQueryItem("client_secret", m_clientSecret); QNetworkRequest request(url); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); QNetworkReply *reply = m_networkManager->post(request, query.toString().toUtf8()); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [this, reply](){ QByteArray rawData = reply->readAll(); if (!checkStatusCode(reply, rawData)) { return; } QJsonDocument data = QJsonDocument::fromJson(rawData); if(!data.toVariant().toMap().contains("access_token")) { setAuthenticated(false); return; } m_accessToken = data.toVariant().toMap().value("access_token").toByteArray(); emit receivedAccessToken(m_accessToken); if (data.toVariant().toMap().contains("expires_in")) { int expireTime = data.toVariant().toMap().value("expires_in").toInt(); qCDebug(dcTempo) << "Access token expires int" << expireTime << "s, at" << QDateTime::currentDateTime().addSecs(expireTime).toString(); if (!m_tokenRefreshTimer) { qWarning(dcTempo()) << "Access token refresh timer not initialized"; return; } if (expireTime < 20) { qCWarning(dcTempo()) << "Expire time too short"; return; } m_tokenRefreshTimer->start((expireTime - 20) * 1000); } }); } void Tempo::getAccessTokenFromAuthorizationCode(const QByteArray &authorizationCode) { // Obtaining access token if(authorizationCode.isEmpty()) qWarning(dcTempo()) << "No authorization code given!"; if(m_clientId.isEmpty()) qWarning(dcTempo()) << "Client key not set!"; if(m_clientSecret.isEmpty()) qWarning(dcTempo()) << "Client secret not set!"; QUrl url = QUrl(m_baseTokenUrl); QUrlQuery query; url.setQuery(query); query.clear(); query.addQueryItem("client_id", m_clientId); query.addQueryItem("client_secret", m_clientSecret); query.addQueryItem("redirect_uri", m_redirectUri); query.addQueryItem("grant_type", "authorization_code"); query.addQueryItem("code", authorizationCode); // query.addQueryItem("code_verifier", m_codeChallenge); QNetworkRequest request(url); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); QNetworkReply *reply = m_networkManager->post(request, query.toString().toUtf8()); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [this, reply](){ QByteArray rawData = reply->readAll(); if (!checkStatusCode(reply, rawData)) { return; } QJsonDocument jsonDoc = QJsonDocument::fromJson(rawData); if(!jsonDoc.toVariant().toMap().contains("access_token") || !jsonDoc.toVariant().toMap().contains("refresh_token") ) { setAuthenticated(false); return; } m_accessToken = jsonDoc.toVariant().toMap().value("access_token").toByteArray(); receivedAccessToken(m_accessToken); m_refreshToken = jsonDoc.toVariant().toMap().value("refresh_token").toByteArray(); receivedRefreshToken(m_refreshToken); if (jsonDoc.toVariant().toMap().contains("expires_in")) { int expireTime = jsonDoc.toVariant().toMap().value("expires_in").toInt(); qCDebug(dcTempo()) << "Token expires in" << expireTime << "s, at" << QDateTime::currentDateTime().addSecs(expireTime).toString(); if (!m_tokenRefreshTimer) { qWarning(dcTempo()) << "Token refresh timer not initialized"; setAuthenticated(false); return; } if (expireTime < 20) { qCWarning(dcTempo()) << "Expire time too short"; return; } m_tokenRefreshTimer->start((expireTime - 20) * 1000); } }); } void Tempo::getAccounts() { QUrl url = QUrl(m_baseControlUrl+"/accounts"); QNetworkRequest request(url); request.setRawHeader("Authorization", "Bearer "+m_accessToken); QNetworkReply *reply = m_networkManager->get(request); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [this, reply]{ QByteArray rawData = reply->readAll(); if (!checkStatusCode(reply, rawData)) { return; } QVariantMap dataMap = QJsonDocument::fromJson(rawData).toVariant().toMap(); QVariantList accountList = dataMap.value("results").toList(); QList accounts; Q_FOREACH(QVariant var, accountList) { QVariantMap map = var.toMap(); Account account; account.self = map["self"].toString(); account.key = map["key"].toString(); account.id = map["id"].toInt(); account.name = map["name"].toString(); if (map["status"] == "OPEN") { account.status = Status::Open; } else if (map["status"] == "CLOSED") { account.status = Status::Closed; } else if (map["status"] == "ARCHIVED") { account.status = Status::Archived; } account.global = map["global"].toBool(); account.monthlyBudget = map["monthlyBudget"].toInt(); QVariantMap lead = map["lead"].toMap(); account.lead.self = lead["self"].toString(); account.lead.accountId = lead["accountId"].toString(); account.lead.displayName = lead["displayName"].toString(); //TODO Customer QVariantMap customer = map["customer"].toMap(); account.customer.self = lead["self"].toString(); //TODO Category QVariantMap category = map["category"].toMap(); account.category.self = lead["self"].toString(); //TODO Contact QVariantMap contact = map["contact"].toMap(); account.contact.self = lead["self"].toString(); accounts.append(account); } if (!accounts.isEmpty()) { } }); } void Tempo::getWorkloadByAccount(const QString &accountKey, QDate from, QDate to) { QUrl url = QUrl(m_baseControlUrl+"/worklogs/account/"+accountKey); QUrlQuery query; query.addQueryItem("from", from.toString(Qt::DateFormat::ISODate)); query.addQueryItem("to", to.toString(Qt::DateFormat::ISODate)); url.setQuery(query); QNetworkRequest request(url); request.setRawHeader("Authorization", "Bearer "+m_accessToken); QNetworkReply *reply = m_networkManager->get(request); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); connect(reply, &QNetworkReply::finished, this, [this, accountKey, reply]{ QByteArray rawData = reply->readAll(); if (!checkStatusCode(reply, rawData)) { return; } QVariantMap dataMap = QJsonDocument::fromJson(rawData).toVariant().toMap(); QVariantList worklogList = dataMap.value("results").toList(); QList worklogs; Q_FOREACH(QVariant var, worklogList) { QVariantMap map = var.toMap(); Worklog worklog; worklog.self = map["self"].toString(); worklog.tempoWorklogId = map["tempoWorklogId"].toInt(); worklog.jiraWorklogId = map["jiraWorklogId"].toInt(); worklog.issue = map["issue"].toMap().value("key").toString(); worklog.timeSpentSeconds = map["timeSpentSeconds"].toInt(); //TODO startDate: required (date-only) //TODO startTime: required (time-only) worklog.description = map["description"].toString(); //TODO createdAt: required (datetime) //TODO updatedAt: required (datetime) worklog.authorAccountId = map["author"].toMap().value("accountId").toString(); worklog.authorDisplayName = map["author"].toMap().value("displayName").toString(); worklogs.append(worklog); } if (!worklogs.isEmpty()) emit accountWorklogsReceived(accountKey, worklogs); }); } void Tempo::onRefreshTimer() { qCDebug(dcTempo()) << "Refresh authentication token"; getAccessTokenFromRefreshToken(m_refreshToken); } void Tempo::setAuthenticated(bool state) { if (state != m_authenticated) { m_authenticated = state; emit authenticationStatusChanged(state); } } void Tempo::setConnected(bool state) { if (state != m_connected) { m_connected = state; emit connectionChanged(state); } } bool Tempo::checkStatusCode(QNetworkReply *reply, const QByteArray &rawData) { // Check for the internet connection if (reply->error() == QNetworkReply::NetworkError::HostNotFoundError || reply->error() == QNetworkReply::NetworkError::UnknownNetworkError || reply->error() == QNetworkReply::NetworkError::TemporaryNetworkFailureError) { qCWarning(dcTempo()) << "Connection error" << reply->errorString(); setConnected(false); setAuthenticated(false); return false; } else { setConnected(true); } int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(rawData, &error); switch (status){ case 200: //The request was successful. Typically returned for successful GET requests. case 204: //The request was successful. Typically returned for successful PUT/DELETE requests with no payload. break; case 400: //Error occurred (e.g. validation error - value is out of range) if(!jsonDoc.toVariant().toMap().contains("error")) { if(jsonDoc.toVariant().toMap().value("error").toString() == "invalid_client") { qWarning(dcTempo()) << "Client token provided doesn’t correspond to client that generated auth code."; } if(jsonDoc.toVariant().toMap().value("error").toString() == "invalid_redirect_uri") { qWarning(dcTempo()) << "Missing redirect_uri parameter."; } if(jsonDoc.toVariant().toMap().value("error").toString() == "invalid_code") { qWarning(dcTempo()) << "Expired authorization code."; } } setAuthenticated(false); return false; case 401: qWarning(dcTempo()) << "Client does not have permission to use this API."; setAuthenticated(false); return false; case 403: qCWarning(dcTempo()) << "Forbidden, Scope has not been granted or home appliance is not assigned to HC account"; setAuthenticated(false); return false; case 404: qCWarning(dcTempo()) << "Not Found. This resource is not available (e.g. no images on washing machine)"; return false; case 405: qWarning(dcTempo()) << "Wrong HTTP method used."; setAuthenticated(false); return false; case 408: qCWarning(dcTempo())<< "Request Timeout, API Server failed to produce an answer or has no connection to backend service"; return false; case 409: qCWarning(dcTempo()) << "Conflict - Command/Query cannot be executed for the home appliance, the error response contains the error details"; qCWarning(dcTempo()) << "Error" << jsonDoc; return false; case 415: qCWarning(dcTempo())<< "Unsupported Media Type. The request's Content-Type is not supported"; return false; case 429: qCWarning(dcTempo())<< "Too Many Requests, the number of requests for a specific endpoint exceeded the quota of the client"; return false; case 500: qCWarning(dcTempo())<< "Internal Server Error, in case of a server configuration error or any errors in resource files"; return false; case 503: qCWarning(dcTempo())<< "Service Unavailable,if a required backend service is not available"; return false; default: break; } if (error.error != QJsonParseError::NoError) { qCWarning(dcTempo()) << "Received invalide JSON object" << rawData; qCWarning(dcTempo()) << "Status" << status; setAuthenticated(false); return false; } setAuthenticated(true); return true; }