From ae36d56a72dceed014b12658032d401409905436 Mon Sep 17 00:00:00 2001 From: ParticleG Date: Wed, 7 Aug 2024 04:04:50 +0800 Subject: [PATCH] - Update PlayerManage - Update config structure --- plugins/PlayerManager.cc | 403 ++++++++++++++++++++------------------- plugins/PlayerManager.h | 54 +++--- 2 files changed, 239 insertions(+), 218 deletions(-) diff --git a/plugins/PlayerManager.cc b/plugins/PlayerManager.cc index 36fb441..7e0a84f 100644 --- a/plugins/PlayerManager.cc +++ b/plugins/PlayerManager.cc @@ -2,11 +2,12 @@ // Created by particleg on 2022/8/29. // +#include + #include #include #include #include -#include #include #include #include @@ -14,10 +15,6 @@ #include #include -using ranges::to; -namespace view = ranges::view; -namespace views = ranges::views; - using namespace drogon; using namespace magic_enum; using namespace std; @@ -31,11 +28,11 @@ using Player = drogon_model::studio26f::Player; PlayerManager::PlayerManager() : RedisHelper(CMAKE_PROJECT_NAME), _playerMapper(app().getDbClient()) {} -void PlayerManager::initAndStart(const Json::Value &config) { +void PlayerManager::initAndStart(const Json::Value& config) { if (!( - config["expirations"]["access"].isUInt64() && - config["expirations"]["refresh"].isUInt64() && - config["expirations"]["email"].isUInt64() + config["expirations"]["access"].isUInt64() && + config["expirations"]["refresh"].isUInt64() && + config["expirations"]["email"].isUInt64() )) { LOG_ERROR << R"("Invalid expiration config")"; abort(); @@ -45,12 +42,12 @@ void PlayerManager::initAndStart(const Json::Value &config) { _emailExpiration = chrono::minutes(config["expirations"]["email"].asUInt64()); if (!( - config["tokenBuckets"]["ip"]["interval"].isUInt64() && - config["tokenBuckets"]["ip"]["maxCount"].isUInt64() && - config["tokenBuckets"]["login"]["interval"].isUInt64() && - config["tokenBuckets"]["login"]["maxCount"].isUInt64() && - config["tokenBuckets"]["verify"]["interval"].isUInt64() && - config["tokenBuckets"]["verify"]["maxCount"].isUInt64() + config["tokenBuckets"]["ip"]["interval"].isUInt64() && + config["tokenBuckets"]["ip"]["maxCount"].isUInt64() && + config["tokenBuckets"]["login"]["interval"].isUInt64() && + config["tokenBuckets"]["login"]["maxCount"].isUInt64() && + config["tokenBuckets"]["verify"]["interval"].isUInt64() && + config["tokenBuckets"]["verify"]["maxCount"].isUInt64() )) { LOG_ERROR << R"(Invalid tokenBucket config)"; abort(); @@ -63,29 +60,32 @@ void PlayerManager::initAndStart(const Json::Value &config) { _verifyMaxCount = config["tokenBuckets"]["verify"]["maxCount"].asUInt64(); if (!( - config["oauth"]["secret"].isString() && - config["oauth"]["products"]["quatrack"]["host"].isString() && - config["oauth"]["products"]["quatrack"]["path"].isString() && - config["oauth"]["products"]["techmino"]["host"].isString() && - config["oauth"]["products"]["techmino"]["path"].isString() && - config["oauth"]["products"]["techminoGalaxy"]["host"].isString() && - config["oauth"]["products"]["techminoGalaxy"]["path"].isString() + config["oauth"]["secret"].isString() && + config["oauth"]["products"]["quatrack"]["host"].isString() && + config["oauth"]["products"]["quatrack"]["path"].isString() && + config["oauth"]["products"]["techmino"]["host"].isString() && + config["oauth"]["products"]["techmino"]["path"].isString() && + config["oauth"]["products"]["techminoGalaxy"]["host"].isString() && + config["oauth"]["products"]["techminoGalaxy"]["path"].isString() )) { LOG_ERROR << R"(Invalid recaptcha config)"; abort(); } _recaptchaSecret = config["oauth"]["secret"].asString(); _productAddressMap[Products::quatrack] = { - config["oauth"]["products"]["quatrack"]["host"].asString(), - config["oauth"]["products"]["quatrack"]["path"].asString() + config["oauth"]["products"]["quatrack"]["host"].asString(), + config["oauth"]["products"]["quatrack"]["path"].asString(), + config["oauth"]["products"]["quatrack"]["secret"].asString(), }; _productAddressMap[Products::techmino] = { - config["oauth"]["products"]["techmino"]["host"].asString(), - config["oauth"]["products"]["techmino"]["path"].asString() + config["oauth"]["products"]["techmino"]["host"].asString(), + config["oauth"]["products"]["techmino"]["path"].asString(), + config["oauth"]["products"]["techmino"]["secret"].asString(), }; _productAddressMap[Products::techminoGalaxy] = { - config["oauth"]["products"]["techminoGalaxy"]["host"].asString(), - config["oauth"]["products"]["techminoGalaxy"]["path"].asString() + config["oauth"]["products"]["techminoGalaxy"]["host"].asString(), + config["oauth"]["products"]["techminoGalaxy"]["path"].asString(), + config["oauth"]["products"]["techminoGalaxy"]["secret"].asString(), }; LOG_INFO << "PlayerManager loaded."; } @@ -95,111 +95,109 @@ void PlayerManager::shutdown() { } string PlayerManager::oauth( - const string &recaptcha, - const trantor::InetAddress &address, - const string &product, - int64_t playerId + const string& recaptcha, + const trantor::InetAddress& address, + const string& product, + int64_t playerId ) { const auto productOptional = enum_cast(product); if (!productOptional.has_value()) { throw ResponseException( - i18n("invalidArguments"), - ResultCode::InvalidArguments, - k400BadRequest + i18n("invalidArguments"), + ResultCode::InvalidArguments, + k400BadRequest ); - } - { + } { setClient("https://www.recaptcha.net"); const auto response = request( - Post, - "/recaptcha/api/siteverify", - { - {"secret", _recaptchaSecret}, - {"response", recaptcha}, - {"remoteip", address.toIp()}, - }, - { - {"success", JsonValue::Bool} - } + Post, + "/recaptcha/api/siteverify", + { + {"secret", _recaptchaSecret}, + {"response", recaptcha}, + {"remoteip", address.toIp()}, + }, + { + {"success", JsonValue::Bool} + } ); if (response["score"].isDouble() && response["score"].asDouble() < 0.7) { throw ResponseException( - i18n("areYouARobot"), - internal::BaseException(to_string(response["score"].asDouble())), - ResultCode::NotAcceptable, - k406NotAcceptable + i18n("areYouARobot"), + internal::BaseException(to_string(response["score"].asDouble())), + ResultCode::NotAcceptable, + k406NotAcceptable ); } if (!response["success"].asBool()) { throw ResponseException( - i18n("recaptchaFailed"), - internal::BaseException(response["error-codes"][0].asString()), - ResultCode::NotAcceptable, - k406NotAcceptable + i18n("recaptchaFailed"), + internal::BaseException(response["error-codes"][0].asString()), + ResultCode::NotAcceptable, + k406NotAcceptable ); } - } - { - const auto [host, path] = _productAddressMap[productOptional.value()]; + } { + const auto [host, path, secret] = _productAddressMap[productOptional.value()]; setClient(host); Json::Value body; body["playerId"] = playerId; const auto response = request( - Post, - path, - {}, - { - {"code", JsonValue::UInt64} - }, - {}, - body, - false + Post, + format("{}/auth/oauth/{}", path, secret), + {}, + { + {"code", JsonValue::UInt64} + }, + {}, + body, + false ); if (response.notEqual("code", enum_integer(ResultCode::Completed))) { throw ResponseException( - response["message"].isString() ? response["message"].asString() : i18n("invalidArguments"), - enum_cast(response["code"].asUInt64()).value_or(ResultCode::Unknown), - k403Forbidden + response["message"].isString() ? response["message"].asString() : i18n("invalidArguments"), + enum_cast(response["code"].asUInt64()).value_or(ResultCode::Unknown), + k403Forbidden ); } if (!response.check("data", JsonValue::String)) { throw ResponseException( - i18n("networkError"), - ResultCode::NetworkError, - drogon::k503ServiceUnavailable + i18n("networkError"), + ResultCode::NetworkError, + drogon::k503ServiceUnavailable ); } return response["data"].asString(); } } -int64_t PlayerManager::getPlayerIdByAccessToken(const string &accessToken) { +int64_t PlayerManager::getPlayerIdByAccessToken(const string& accessToken) { try { return stoll(get(data::join({"auth", "access-id", accessToken}, ':'))); - } catch (const redis_exception::KeyNotFound &e) { + } catch (const redis_exception::KeyNotFound& e) { throw ResponseException( - i18n("invalidAccessToken"), - e, - ResultCode::NotAcceptable, - k401Unauthorized + i18n("invalidAccessToken"), + e, + ResultCode::NotAcceptable, + k401Unauthorized ); } } -bool PlayerManager::tryRefresh(string &accessToken) { +bool PlayerManager::tryRefresh(string& accessToken) { const auto ttl = chrono::milliseconds(pTtl(data::join({"auth", "access-id", accessToken}, ':'))); if (ttl.count() == -2) { throw ResponseException( - i18n("invalidAccessToken"), - ResultCode::NotAcceptable, - k401Unauthorized + i18n("invalidAccessToken"), + ResultCode::NotAcceptable, + k401Unauthorized ); } if (ttl < _refreshExpiration) { const auto playerId = getPlayerIdByAccessToken(accessToken); NO_EXCEPTION( - del(data::join({"auth", "access-id", accessToken}, ':')); + del(data::join({"auth", "access-id", accessToken}, ':')); ) accessToken = _generateAccessToken(to_string(playerId)); return true; @@ -207,38 +205,38 @@ bool PlayerManager::tryRefresh(string &accessToken) { return false; } -void PlayerManager::verifyEmail(const string &email) { +void PlayerManager::verifyEmail(const string& email) { const auto code = data::randomString(8); _setEmailCode(email, code); // TODO: Make I18N emails auto mailContent = io::getFileContent("./verifyEmail.html"); drogon::utils::replaceAll( - mailContent, - "{{VERIFY_CODE}}", - code + mailContent, + "{{VERIFY_CODE}}", + code ); mailContent = regex_replace(mailContent, regex{R"((\s*[\r\n]+\s*|\s+))"}, " "); app().getPlugin()->smtp( - email, - "[26F Studio] Verification Code/验证码", - mailContent + email, + "[26F Studio] Verification Code/验证码", + mailContent ); } -string PlayerManager::seedEmail(const string &email) { +string PlayerManager::seedEmail(const string& email) { try { const auto player = _playerMapper.findOne(orm::Criteria( - Player::Cols::_email, - orm::CompareOperator::EQ, - email + Player::Cols::_email, + orm::CompareOperator::EQ, + email )); return crypto::blake2B(to_string(player.getValueOfId())); - } catch (const orm::UnexpectedRows &) { + } catch (const orm::UnexpectedRows&) { return crypto::blake2B(to_string(-1)); } } -tuple PlayerManager::loginEmailCode(const string &email, const string &code) { +tuple PlayerManager::loginEmailCode(const string& email, const string& code) { _checkEmailCode(email, code); Player player; @@ -246,16 +244,16 @@ tuple PlayerManager::loginEmailCode(const string &email, const str Player::Cols::_email, orm::CompareOperator::EQ, email - )) == 0) { + )) == 0) { player.setEmail(email); _playerMapper.insert(player); _setEmailCode(email, code); } else { player = _playerMapper.findOne(orm::Criteria( - Player::Cols::_email, - orm::CompareOperator::EQ, - email + Player::Cols::_email, + orm::CompareOperator::EQ, + email )); if (player.getPasswordHash() == nullptr) { @@ -264,24 +262,24 @@ tuple PlayerManager::loginEmailCode(const string &email, const str } return { - _generateAccessToken(to_string(player.getValueOfId())), - player.getPasswordHash() == nullptr + _generateAccessToken(to_string(player.getValueOfId())), + player.getPasswordHash() == nullptr }; } -string PlayerManager::loginEmailPassword(const string &email, const string &password) { +string PlayerManager::loginEmailPassword(const string& email, const string& password) { try { auto player = _playerMapper.findOne(orm::Criteria( - Player::Cols::_email, - orm::CompareOperator::EQ, - email + Player::Cols::_email, + orm::CompareOperator::EQ, + email )); if (player.getPasswordHash() == nullptr) { throw ResponseException( - i18n("noPassword"), - ResultCode::NullValue, - k403Forbidden + i18n("noPassword"), + ResultCode::NullValue, + k403Forbidden ); } @@ -292,47 +290,47 @@ string PlayerManager::loginEmailPassword(const string &email, const string &pass } return _generateAccessToken(id); - } catch (const orm::UnexpectedRows &) { + } catch (const orm::UnexpectedRows&) { LOG_DEBUG << "invalidEmailPass: " << email; throw ResponseException( - i18n("invalidEmailPass"), - ResultCode::NotAcceptable, - k403Forbidden + i18n("invalidEmailPass"), + ResultCode::NotAcceptable, + k403Forbidden ); } } void PlayerManager::resetEmail( - const string &email, - const string &code, - const string &newPassword + const string& email, + const string& code, + const string& newPassword ) { _checkEmailCode(email, code); try { auto player = _playerMapper.findOne(orm::Criteria( - Player::Cols::_email, - orm::CompareOperator::EQ, - email + Player::Cols::_email, + orm::CompareOperator::EQ, + email )); player.setPasswordHash(crypto::blake2B( - newPassword + crypto::blake2B(to_string(player.getValueOfId())) + newPassword + crypto::blake2B(to_string(player.getValueOfId())) )); _playerMapper.update(player); - } catch (const orm::UnexpectedRows &) { + } catch (const orm::UnexpectedRows&) { LOG_DEBUG << "playerNotFound: " << email; throw ResponseException( - i18n("playerNotFound"), - ResultCode::NotFound, - k404NotFound + i18n("playerNotFound"), + ResultCode::NotFound, + k404NotFound ); } } void PlayerManager::migrateEmail( - const int64_t playerId, - const string &newEmail, - const string &code + const int64_t playerId, + const string& newEmail, + const string& code ) { _checkEmailCode(newEmail, code); @@ -342,31 +340,31 @@ void PlayerManager::migrateEmail( return; } if (_playerMapper.count(orm::Criteria( - Player::Cols::_email, - orm::CompareOperator::EQ, - newEmail + Player::Cols::_email, + orm::CompareOperator::EQ, + newEmail ))) { throw ResponseException( - i18n("emailExists"), - ResultCode::Conflict, - k409Conflict + i18n("emailExists"), + ResultCode::Conflict, + k409Conflict ); } player.setEmail(newEmail); _playerMapper.update(player); - } catch (const redis_exception::KeyNotFound &e) { + } catch (const redis_exception::KeyNotFound& e) { throw ResponseException( - i18n("invalidAccessToken"), - e, - ResultCode::NotAcceptable, - k401Unauthorized + i18n("invalidAccessToken"), + e, + ResultCode::NotAcceptable, + k401Unauthorized ); } } void PlayerManager::deactivateEmail( - const int64_t playerId, - const string &code + const int64_t playerId, + const string& code ) { try { auto player = _playerMapper.findByPrimaryKey(playerId); @@ -374,48 +372,71 @@ void PlayerManager::deactivateEmail( player.setPermission(string{enum_name(Permission::Banned)}); _playerMapper.update(player); - } catch (const redis_exception::KeyNotFound &e) { + } catch (const redis_exception::KeyNotFound& e) { throw ResponseException( - i18n("invalidAccessToken"), - e, - ResultCode::NotAcceptable, - k401Unauthorized + i18n("invalidAccessToken"), + e, + ResultCode::NotAcceptable, + k401Unauthorized ); + } { + for (const auto& [host, path, secret]: _productAddressMap | views::values) { + setClient(host); + Json::Value body; + body["playerId"] = playerId; + if (const auto response = request( + Post, + format("{}/auth/deactivate/{}", path, secret), + {}, + { + {"code", JsonValue::UInt64} + }, + {}, + body, + false + ); response.notEqual("code", enum_integer(ResultCode::Completed))) { + throw ResponseException( + response["message"].isString() ? response["message"].asString() : i18n("invalidArguments"), + enum_cast(response["code"].asUInt64()).value_or(ResultCode::Unknown), + k403Forbidden + ); + } + } } } -string PlayerManager::getAvatar(const string &accessToken, int64_t playerId) { +string PlayerManager::getAvatar(const string& accessToken, int64_t playerId) { int64_t targetId = playerId; NO_EXCEPTION( - targetId = getPlayerIdByAccessToken(accessToken); + targetId = getPlayerIdByAccessToken(accessToken); ) try { auto player = _playerMapper.findOne(orm::Criteria( - Player::Cols::_id, - orm::CompareOperator::EQ, - targetId + Player::Cols::_id, + orm::CompareOperator::EQ, + targetId )); return player.getValueOfAvatar(); - } catch (const orm::UnexpectedRows &) { + } catch (const orm::UnexpectedRows&) { LOG_DEBUG << "playerNotFound: " << targetId; throw ResponseException( - i18n("playerNotFound"), - ResultCode::NotFound, - k404NotFound + i18n("playerNotFound"), + ResultCode::NotFound, + k404NotFound ); } } -Json::Value PlayerManager::getPlayerInfo(const string &accessToken, int64_t playerId) { +Json::Value PlayerManager::getPlayerInfo(const string& accessToken, int64_t playerId) { int64_t targetId = playerId; NO_EXCEPTION( - targetId = getPlayerIdByAccessToken(accessToken); + targetId = getPlayerIdByAccessToken(accessToken); ) try { auto player = _playerMapper.findOne(orm::Criteria( - Player::Cols::_id, - orm::CompareOperator::EQ, - targetId + Player::Cols::_id, + orm::CompareOperator::EQ, + targetId )).toJson(); player.removeMember("password_hash"); player.removeMember("avatar"); @@ -424,11 +445,11 @@ Json::Value PlayerManager::getPlayerInfo(const string &accessToken, int64_t play player.removeMember("phone"); } return player; - } catch (const orm::UnexpectedRows &) { + } catch (const orm::UnexpectedRows&) { throw ResponseException( - i18n("playerNotFound"), - ResultCode::NotFound, - k404NotFound + i18n("playerNotFound"), + ResultCode::NotFound, + k404NotFound ); } } @@ -442,64 +463,64 @@ void PlayerManager::updatePlayerInfo(int64_t playerId, JsonHelper request) { _playerMapper.update(player); } -bool PlayerManager::ipLimit(const string &ip) { +bool PlayerManager::ipLimit(const string& ip) { return tokenBucket( - data::join({"ip", ip}, ':'), - _ipInterval, - _ipMaxCount + data::join({"ip", ip}, ':'), + _ipInterval, + _ipMaxCount ); } -bool PlayerManager::loginLimit(const string &type, const string &key) { +bool PlayerManager::loginLimit(const string& type, const string& key) { return tokenBucket( - data::join({"code", type, key}, ':'), - _loginInterval, - _loginMaxCount + data::join({"code", type, key}, ':'), + _loginInterval, + _loginMaxCount ); } -bool PlayerManager::verifyLimit(const string &type, const string &key) { +bool PlayerManager::verifyLimit(const string& type, const string& key) { return tokenBucket( - data::join({"verify", type, key}, ':'), - _verifyInterval, - _verifyMaxCount + data::join({"verify", type, key}, ':'), + _verifyInterval, + _verifyMaxCount ); } -void PlayerManager::_checkEmailCode(const string &email, const string &code) { +void PlayerManager::_checkEmailCode(const string& email, const string& code) { const auto key = data::join({"auth", "code", "email", email}, ':'); if (exists({key})) { if (get(key) != code) { throw ResponseException( - i18n("invalidCode"), - ResultCode::NotAcceptable, - k403Forbidden + i18n("invalidCode"), + ResultCode::NotAcceptable, + k403Forbidden ); } del({data::join({"auth", "code", "email", email}, ':')}); } else { throw ResponseException( - i18n("invalidEmail"), - ResultCode::NotFound, - k404NotFound + i18n("invalidEmail"), + ResultCode::NotFound, + k404NotFound ); } } -void PlayerManager::_setEmailCode(const string &email, const string &code) { +void PlayerManager::_setEmailCode(const string& email, const string& code) { setPx( - data::join({"auth", "code", "email", email}, ':'), - code, - _emailExpiration + data::join({"auth", "code", "email", email}, ':'), + code, + _emailExpiration ); } -string PlayerManager::_generateAccessToken(const string &playerId) { +string PlayerManager::_generateAccessToken(const string& playerId) { auto accessToken = crypto::blake2B(drogon::utils::getUuid()); setPx( - data::join({"auth", "access-id", accessToken}, ':'), - playerId, - _accessExpiration + _refreshExpiration + data::join({"auth", "access-id", accessToken}, ':'), + playerId, + _accessExpiration + _refreshExpiration ); return accessToken; } diff --git a/plugins/PlayerManager.h b/plugins/PlayerManager.h index 00918d1..a6b5393 100644 --- a/plugins/PlayerManager.h +++ b/plugins/PlayerManager.h @@ -21,54 +21,54 @@ namespace studio26f::plugins { public: PlayerManager(); - void initAndStart(const Json::Value &config) override; + void initAndStart(const Json::Value& config) override; void shutdown() override; std::string oauth( - const std::string &recaptcha, - const trantor::InetAddress &address, - const std::string &product, - int64_t playerId + const std::string& recaptcha, + const trantor::InetAddress& address, + const std::string& product, + int64_t playerId ); - int64_t getPlayerIdByAccessToken(const std::string &accessToken); + int64_t getPlayerIdByAccessToken(const std::string& accessToken); - bool tryRefresh(std::string &accessToken); + bool tryRefresh(std::string& accessToken); - void verifyEmail(const std::string &email); + void verifyEmail(const std::string& email); - std::string seedEmail(const std::string &email); + std::string seedEmail(const std::string& email); - std::tuple loginEmailCode(const std::string &email, const std::string &code); + std::tuple loginEmailCode(const std::string& email, const std::string& code); - std::string loginEmailPassword(const std::string &email, const std::string &password); + std::string loginEmailPassword(const std::string& email, const std::string& password); void resetEmail( - const std::string &email, - const std::string &code, - const std::string &newPassword + const std::string& email, + const std::string& code, + const std::string& newPassword ); void migrateEmail( - int64_t playerId, - const std::string &newEmail, - const std::string &code + int64_t playerId, + const std::string& newEmail, + const std::string& code ); - void deactivateEmail(int64_t playerId, const std::string &code); + void deactivateEmail(int64_t playerId, const std::string& code); - std::string getAvatar(const std::string &accessToken, int64_t playerId); + std::string getAvatar(const std::string& accessToken, int64_t playerId); - Json::Value getPlayerInfo(const std::string &accessToken, int64_t playerId); + Json::Value getPlayerInfo(const std::string& accessToken, int64_t playerId); void updatePlayerInfo(int64_t playerId, helpers::JsonHelper request); - bool ipLimit(const std::string &ip); + bool ipLimit(const std::string& ip); - bool loginLimit(const std::string &type, const std::string &key); + bool loginLimit(const std::string& type, const std::string& key); - bool verifyLimit(const std::string &type, const std::string &key); + bool verifyLimit(const std::string& type, const std::string& key); private: std::string _recaptchaSecret; @@ -76,14 +76,14 @@ namespace studio26f::plugins { _accessExpiration{}, _refreshExpiration{}, _emailExpiration{}; uint64_t _ipMaxCount{}, _verifyMaxCount{}, _loginMaxCount{}; - std::unordered_map> _productAddressMap; + std::unordered_map> _productAddressMap; drogon::orm::Mapper _playerMapper; - void _checkEmailCode(const std::string &email, const std::string &code); + void _checkEmailCode(const std::string& email, const std::string& code); - void _setEmailCode(const std::string &email, const std::string &code); + void _setEmailCode(const std::string& email, const std::string& code); - std::string _generateAccessToken(const std::string &playerId); + std::string _generateAccessToken(const std::string& playerId); }; }