From 51e39e5498188bca1f257f7e091d4ae697546d8d Mon Sep 17 00:00:00 2001 From: Bartosz Wieczorek Date: Tue, 30 Jan 2018 09:49:54 +0100 Subject: [PATCH] move all db dependent data into db subdir --- src/eedb/CMakeLists.txt | 1 + src/eedb/auth/PgUserAuth.cpp | 402 +++------------- src/eedb/auth/PgUserAuth.hpp | 6 +- ...IUserRepository.hpp => UserRepository.hpp} | 0 src/eedb/db/CMakeLists.txt | 11 +- src/eedb/{ => db}/data/PgCategory.cpp | 2 +- src/eedb/{ => db}/data/PgCategory.hpp | 0 src/eedb/{ => db}/data/PgUser.cpp | 2 +- src/eedb/{ => db}/data/PgUser.hpp | 0 src/eedb/db/data/PgUserAuth.cpp | 452 ++++++++++++++++++ src/eedb/db/data/PgUserAuth.hpp | 125 +++++ src/eedb/{ => db}/data/PgUsers.cpp | 4 +- src/eedb/{ => db}/data/PgUsers.hpp | 0 tests/unit/test_eedb_data_PgCategories.cpp | 2 +- tests/unit/test_eedb_data_PgCategory.cpp | 2 +- 15 files changed, 659 insertions(+), 350 deletions(-) rename src/eedb/data/{IUserRepository.hpp => UserRepository.hpp} (100%) rename src/eedb/{ => db}/data/PgCategory.cpp (93%) rename src/eedb/{ => db}/data/PgCategory.hpp (100%) rename src/eedb/{ => db}/data/PgUser.cpp (98%) rename src/eedb/{ => db}/data/PgUser.hpp (100%) create mode 100644 src/eedb/db/data/PgUserAuth.cpp create mode 100644 src/eedb/db/data/PgUserAuth.hpp rename src/eedb/{ => db}/data/PgUsers.cpp (88%) rename src/eedb/{ => db}/data/PgUsers.hpp (100%) diff --git a/src/eedb/CMakeLists.txt b/src/eedb/CMakeLists.txt index 030f982..37ead97 100644 --- a/src/eedb/CMakeLists.txt +++ b/src/eedb/CMakeLists.txt @@ -10,6 +10,7 @@ file(GLOB SOURCE ) include_directories( ${PostgreSQL_INCLUDE_DIRS} ) + add_subdirectory(db) add_library(auth STATIC ${SOURCE}) diff --git a/src/eedb/auth/PgUserAuth.cpp b/src/eedb/auth/PgUserAuth.cpp index c5fb6d8..3fff149 100644 --- a/src/eedb/auth/PgUserAuth.cpp +++ b/src/eedb/auth/PgUserAuth.cpp @@ -1,21 +1,7 @@ #include "PgUserAuth.hpp" +#include #include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include #include #include @@ -23,29 +9,9 @@ #include #include -#include using namespace Wt::Auth; -constexpr const char eedb::user_audit_::Data::_alias_t::_literal[]; -constexpr const char eedb::user_audit::_alias_t::_literal[]; - -// enum LoginActions { Login, Logout }; -// static std::array< std::string_view, 2 > UserActionNames = {{"login", "logout"}}; - -std::string RandomString(uint len) { - using namespace std; - srand(static_cast< unsigned int >(time(nullptr))); - experimental::string_view str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - string newstr; - std::size_t pos; - while(newstr.size() != len) { - pos = static_cast< std::size_t >((rand() % (static_cast< int >(str.size()) - 1))); - newstr += str.substr(pos, 1).to_string(); - } - return newstr; -} - template < typename Connection > struct TransactionGuard : public Wt::Auth::AbstractUserDatabase::Transaction { TransactionGuard(Connection & c, int & transaction_active) : _c(c), _transaction{transaction_active} { @@ -72,42 +38,15 @@ struct TransactionGuard : public Wt::Auth::AbstractUserDatabase::Transaction { namespace eedb::auth { -namespace { - constexpr eedb::user t_user; - constexpr eedb::user_audit t_user_history; - constexpr eedb::user_action t_user_action; - constexpr eedb::auth_identity t_identity; - constexpr eedb::auth_info t_info; - constexpr eedb::auth_token t_auth_token; - - static const auto auth_info_identity = t_info.join(t_identity).on(t_info.id == t_identity.auth_info_id); - static const auto user_auth_info = t_user.join(t_info).on(t_info.user_uid == t_user.uid); - static const auto auth_token_info = user_auth_info.join(t_auth_token).on(t_auth_token.auth_info_id == t_info.id); - - static const auto select_login_action_id = select(t_user_action.id) // - .from(t_user_action) // - .where(t_user_action.name == "login") // - .limit(1u); - static const auto select_logout_action_id = select(t_user_action.id) // - .from(t_user_action) // - .where(t_user_action.name == "logout") // - .limit(1u); -} // namespace - -PgUserAuth::PgUserAuth(eedb::db::PgConnection & _db, const Wt::WEnvironment & env) : db(_db), _env(env) { +PgUserAuth::PgUserAuth(eedb::db::PgConnection & _db, const Wt::WEnvironment & env) : db(_db), _env(env), _userAuth{_db} { this->setAuthService(eedb::auth::Services::authService()); } User PgUserAuth::findWithId(const std::string & id) const { - const auto uid_eq = t_user.uid == std::atoi(id.c_str()); + auto duser = _userAuth.findWithId(id); - if(db(select(exists(select(t_user.uid) // - .from(t_user) // - .where(uid_eq)))) - .front() - .exists) { - return User(id, *this); - } + if(duser) + return User(duser.moveid(), *this); return User(); } @@ -117,101 +56,53 @@ User PgUserAuth::findWithIdentity(const std::string & provider, const Wt::WStrin std::transform(_identity.begin(), _identity.end(), _identity.begin(), ::tolower); } - const auto identity_eq = t_identity.identity == _identity; - const auto provider_eq = t_identity.provider == provider; - - const auto result = db(select(t_info.user_uid) // - .from(auth_info_identity) // - .where(provider_eq and identity_eq)); - - if(!result.empty()) { - return User(std::to_string(result.front().user_uid), *this); + auto duser = _userAuth.findWithIdentity(provider, _identity); + if(duser) { + return User(duser.moveid(), *this); } return User(); } +User PgUserAuth::findWithEmailToken(const std::string & hash) const { + auto duser = _userAuth.findWithEmailToken(hash); + if(duser) + return {duser.moveid(), *this}; + return {}; +} + +User PgUserAuth::findWithEmail(const std::string & address) const { + auto duser = _userAuth.findWithEmail(address); + if(duser) + return {duser.moveid(), *this}; + return {}; +} + void PgUserAuth::addIdentity(const User & user, const std::string & provider, const Wt::WString & identity) { - const auto uid_eq = t_info.user_uid == std::atoi(user.id().c_str()); - const auto identity_eq = t_identity.identity == identity.toUTF8(); - const auto provider_eq = t_identity.provider == provider; - - auto res = db(select(t_info.user_uid, t_info.id) // - .from(auth_info_identity) - .where(identity_eq and provider_eq and uid_eq)); - - if(!res.empty()) { - Wt::log("error") << "cannot add identity " << provider << ":'" << identity << "': already exists"; - return; - } - - auto auth_info_id = db(select(t_info.id) - .from(t_info) // - .where(uid_eq)); - - if(auth_info_id.empty()) { - throw std::exception(); - } - - db(insert_into(t_identity) - .set(t_identity.identity = identity.toUTF8(), // - t_identity.provider = provider, // - t_identity.auth_info_id = auth_info_id.front().id)); - - Wt::log("info") << "created new identity for user"; + _userAuth.addIdentity({user.id()}, provider, identity.toUTF8()); } void PgUserAuth::setIdentity(const User & user, const std::string & provider, const Wt::WString & identity) { - const auto uid = std::atoi(user.id().c_str()); - const auto provider_eq = t_identity.provider == provider; - - db(update(t_identity) // - .set(t_identity.identity = identity.toUTF8()) // - .where(t_identity.auth_info_id == select(t_info.id).from(t_info).where(t_info.user_uid == uid) and provider_eq)); + _userAuth.setIdentity({user.id()}, provider, identity.toUTF8()); } Wt::WString PgUserAuth::identity(const User & user, const std::string & provider) const { - const int uid = std::atoi(user.id().c_str()); - const auto id_eq = t_info.user_uid == uid; - const auto provider_eq = t_identity.provider == provider; + auto id = _userAuth.identity({user.id()}, provider); - auto res = db(select(t_identity.identity) // - .from(auth_info_identity) // - .where(id_eq and provider_eq)); - - if(res.empty()) + if(id.empty()) return Wt::WString::Empty; - return {res.front().identity}; + return {id}; } void PgUserAuth::removeIdentity(const User & user, const std::string & provider) { - const auto id_eq = t_identity.id == std::atoi(user.id().c_str()); - const auto provider_eq = t_identity.provider == provider; - - db(remove_from(t_identity).where(id_eq and provider_eq)); + _userAuth.removeIdentity({user.id()}, provider); } User PgUserAuth::registerNew() { - auto user_id = db(sqlpp::postgresql::insert_into(t_user) // - .set( // - t_user.full_name = RandomString(256), // - t_user.status = -1) - .returning(t_user.uid)) - .front() - .uid; - - db(insert_into(t_info).set( // - t_info.password_hash = "NONE", // - t_info.password_method = "NONE", // - t_info.password_salt = "NONE", // - t_info.user_uid = user_id, // - t_info.email = RandomString(20) + "@NONE.org")); - - return User{std::to_string(user_id), *this}; + return User{_userAuth.registerNew().moveid(), *this}; } void PgUserAuth::deleteUser(const User & user) { - db(remove_from(t_info) // - .where(t_info.id == std::atoi(user.id().c_str()))); + _userAuth.deleteUser({user.id()}); } // User::Status PgUserAuth::status(const User & user) const { @@ -232,265 +123,98 @@ void PgUserAuth::deleteUser(const User & user) { //} void PgUserAuth::setPassword(const User & user, const PasswordHash & password) { - db(update(t_info) - .set( // - t_info.password_hash = password.value(), // - t_info.password_method = password.function(), // - t_info.password_salt = password.salt()) - .where(t_info.user_uid == std::atoi(user.id().c_str()))); + _userAuth.setPassword({user.id()}, {password.function(), password.salt(), password.value()}); } PasswordHash PgUserAuth::password(const User & user) const { - const auto id_eq = t_info.user_uid == std::atoi(user.id().c_str()); - - auto row = db(select(t_info.password_hash, t_info.password_method, t_info.password_salt) - .from(t_info) // - .where(id_eq)); - if(row.empty()) + auto pass = _userAuth.password({user.id()}); + if(!pass) return PasswordHash(); - auto & front = row.front(); - return {std::move(front.password_method), std::move(front.password_salt), std::move(front.password_hash)}; + return {pass->mfunction(), pass->msalt(), pass->mvalue()}; } bool PgUserAuth::setEmail(const User & user, const std::string & address) { - const auto uid_eq = t_info.user_uid == std::atoi(user.id().c_str()); - - db(update(t_info) // - .set( // - t_info.email = address, - t_info.email_verified = true) // - .where(uid_eq)); - return true; + return _userAuth.setEmail({user.id()}, address); } std::string PgUserAuth::email(const User & user) const { - auto ret = db(select(t_info.email) // - .from(t_info) // - .where(t_info.user_uid == std::atoi(user.id().c_str()) and t_info.email_verified == true)); - if(ret.empty()) - return {}; - return std::move(ret.front().email); + return _userAuth.email({user.id()}); } void PgUserAuth::setUnverifiedEmail(const User & user, const std::string & address) { - // orginal implementation of UserDatabase (provided by WT team) sets veryfied and unverified emails in - // different fields in database. So in order to verify email, they just copy the unverified_email to email and then - // set unverified_email to empty string (db don't allow empty strings as email*) - if(address.empty()) - db(update(t_info) // - .set(t_info.email_verified = true) // - .where(t_info.user_uid == std::atoi(user.id().c_str()))); - else - db(update(t_info) // - .set( // - t_info.email = address, // - t_info.email_verified = false) // - .where(t_info.user_uid == std::atoi(user.id().c_str()))); -} - -std::string PgUserAuth::unverifiedEmail(const User & user) const { - auto ret = db(select(t_info.email) // - .from(t_info) // - .where(t_info.user_uid == std::atoi(user.id().c_str()) and t_info.email_verified == false)); - if(ret.empty()) - return {}; - return std::move(ret.front().email); -} - -User PgUserAuth::findWithEmail(const std::string & address) const { - auto ret = db(select(t_info.user_uid) // - .from(t_info) // - .where(t_info.email == address and t_info.email_verified == true)); - - if(ret.empty()) - return {}; - return {std::to_string(ret.front().user_uid), *this}; + _userAuth.setUnverifiedEmail({user.id()}, address); } void PgUserAuth::setEmailToken(const User & user, const Token & token, EmailTokenRole role) { auto exp = ::date::floor<::std::chrono::milliseconds >(std::chrono::system_clock::from_time_t(token.expirationTime().toTime_t())); - db(update(t_info) // - .set( // - t_info.email_token = token.hash(), // - t_info.email_token_expires = exp, - t_info.email_token_role = static_cast< int >(role)) // - .where(t_info.user_uid == std::atoi(user.id().c_str()))); + _userAuth.setEmailToken({user.id()}, {token.hash(), exp}, static_cast< int >(role) ); +} + +std::string PgUserAuth::unverifiedEmail(const User & user) const { + _userAuth.unverifiedEmail({user.id()}); } Token PgUserAuth::emailToken(const User & user) const { - auto ret = db(select(t_info.email_token, t_info.email_token_expires) // - .from(t_info) // - .where(t_info.user_uid == std::atoi(user.id().c_str()))); - if(ret.empty()) + auto tok = _userAuth.emailToken({user.id()}); + if(!tok) return {}; auto exp = Wt::WDateTime(); - auto time = ret.front().email_token_expires.value(); + auto time = tok->expirationTime(); auto systime = std::chrono::system_clock::to_time_t(time); exp.setTime_t(systime); - return {ret.front().email_token, exp}; + return {tok->mhash(), exp}; } EmailTokenRole PgUserAuth::emailTokenRole(const User & user) const { - auto ret = db(select(t_info.email_token_role) // - .from(t_info) // - .where(t_info.user_uid == std::atoi(user.id().c_str()))); - if(ret.empty()) - throw std::exception(); - auto val = ret.front().email_token_role; - return static_cast< EmailTokenRole >(val.value()); -} - -User PgUserAuth::findWithEmailToken(const std::string & hash) const { - auto ret = db(select(t_user.uid) // - .from(user_auth_info) // - .where(t_info.email_token == hash)); - - if(ret.empty()) - return {}; - return {std::to_string(ret.front().uid), *this}; + auto val = _userAuth.emailTokenRole({user.id()}); + return static_cast< EmailTokenRole >(val); } void PgUserAuth::addAuthToken(const User & user, const Token & token) { - auto select_identity_id = select(t_info.id) // - .from(t_info) - .where(t_info.user_uid == std::atoi(user.id().c_str())) - .limit(1u); - db(insert_into(t_auth_token) // - .set( // - t_auth_token.auth_info_id = select_identity_id, // - t_auth_token.expires = std::chrono::system_clock::now() + ::sqlpp::chrono::days{14}, // - t_auth_token.value = token.hash())); + auto exp = std::chrono::time_point_cast< std::chrono::microseconds >(std::chrono::system_clock::now()) + ::sqlpp::chrono::days{14}; + auto tok = eedb::db::details::Token{token.hash(), exp}; + _userAuth.addAuthToken({user.id()}, std::move(tok)); } -void PgUserAuth::removeAuthToken(const User &, const std::string & hash) { - db(remove_from(t_auth_token).where(t_auth_token.value == hash)); +void PgUserAuth::removeAuthToken(const User &user, const std::string & hash) { + _userAuth.removeAuthToken({user.id()}, hash); } User PgUserAuth::findWithAuthToken(const std::string & hash) const { - auto fullUser = t_auth_token // - .join(t_info) - .on(t_info.id == t_auth_token.auth_info_id); - - auto ret = db(select(t_info.user_uid) // - .from(fullUser) // - .where(t_auth_token.value == hash and t_auth_token.expires > std::chrono::system_clock::now())); - - if(ret.empty()) + auto u = _userAuth.findWithAuthToken(hash); + if(!u) return {}; - return {std::to_string(ret.front().user_uid), *this}; + return {u->moveid(), *this}; } int PgUserAuth::updateAuthToken(const User & user, const std::string & oldhash, const std::string & newhash) { // method called only after successful login - using namespace std::chrono; - using namespace std::string_literals; - const auto identity_id = db(select(t_info.id) // - .from(t_info) // - .where(t_info.user_uid == std::atoi(user.id().c_str()))); - if(identity_id.empty()) - return 0; - - const auto expires = db(sqlpp::postgresql::update(t_auth_token) // - .set(t_auth_token.value = newhash) - .where(t_auth_token.value == oldhash) - .returning(t_auth_token.expires)); - - if(expires.empty()) - return 0; - - const nlohmann::json data = { - {"status"s, "success"}, // - {"method"s, "token"}, // - {"user_address"s, _env.clientAddress()}, // - {"user_agent"s, _env.userAgent()}, // - {"user_locale"s, _env.locale().name()}, // - {"referer", _env.referer()}, // - {"url_scheme", _env.urlScheme()} // - }; - - db(insert_into(t_user_history) - .set(t_user_history.user_id = std::atoi(user.id().c_str()), - t_user_history.action_id = select_login_action_id, // - t_user_history.data = data.dump())); - - const auto now = system_clock::now(); - const auto diff = expires.front().expires.value() - now; - return duration_cast< duration< int > >(diff).count(); + return _userAuth.updateAuthToken({user.id()}, oldhash, newhash); } void PgUserAuth::setFailedLoginAttempts(const User & user, int count) { - using namespace std::string_literals; - const auto getStatus = [count]() { return count ? "failed"s : "success"s; }; - - const nlohmann::json data = { - {"status"s, getStatus()}, // - {"method"s, "password"}, // - {"user_address"s, _env.clientAddress()}, // - {"user_agent"s, _env.userAgent()}, // - {"user_locale"s, _env.locale().name()}, // - {"referer", _env.referer()}, // - {"url_scheme", _env.urlScheme()} // - }; - - db(insert_into(t_user_history) - .set(t_user_history.user_id = std::atoi(user.id().c_str()), - t_user_history.action_id = select_login_action_id, // - t_user_history.data = data.dump())); -} - -template < typename T > -inline std::string name_of(const T &) { - static_assert(sqlpp::is_column_t< T >::value, "T should by a calumn"); - return std::string{T::_table::_alias_t::_literal} + '.' + T::_alias_t::_literal; + _userAuth.setFailedLoginAttempts({user.id()}, count); } int PgUserAuth::failedLoginAttempts(const User & user) const { - const auto res = - db(select(count(t_user_history.id)) // - .from(t_user_history) // - .where(t_user_history.user_id == std::atoi(user.id().c_str()) and // - t_user_history.action_id == select_login_action_id and // - sqlpp::verbatim< sqlpp::boolean >(name_of(t_user_history.data) + "->>'status' = 'failed'") and // - t_user_history.id > select(t_user_history.id) // - .from(t_user_history) // - .where(t_user_history.user_id == std::atoi(user.id().c_str()) and // - t_user_history.action_id == select_login_action_id and // - sqlpp::verbatim< sqlpp::boolean >(name_of(t_user_history.data) + "->>'status' = 'success'")) // - .limit(1u))); - return static_cast< int >(res.front().count); + return _userAuth.failedLoginAttempts({user.id()}); } void PgUserAuth::setLastLoginAttempt(const User &, const Wt::WDateTime &) {} Wt::WDateTime PgUserAuth::lastLoginAttempt(const User & user) const { - using namespace std::chrono; - const auto res = - db(select(t_user_history._when) // - .from(t_user_history) // - .where(t_user_history.action_id == select_login_action_id and t_user_history.user_id == std::atoi(user.id().c_str())) // - .order_by(t_user_history.id.desc()) // - .limit(1u)); - if(res.empty()) - return {}; - - const auto time = res.front()._when.value(); - const auto systime = system_clock::to_time_t(time); + const auto time = _userAuth.lastLoginAttempt({user.id()}); + const auto systime = std::chrono::system_clock::to_time_t(time); auto lastLogin = Wt::WDateTime(); lastLogin.setTime_t(systime); return lastLogin; } void PgUserAuth::logout(const User & user) { - using namespace std::string_literals; - const nlohmann::json data = {{"status"s, "success"}}; - - db(insert_into(t_user_history) - .set(t_user_history.user_id = std::atoi(user.id().c_str()), - t_user_history.action_id = select_logout_action_id, // - t_user_history.data = data.dump())); + _userAuth.logout({user.id()}); } AbstractUserDatabase::Transaction * PgUserAuth::startTransaction() { - return new TransactionGuard< decltype(db) >(db, _in_transaction); +// return new TransactionGuard< decltype(db) >(db, _in_transaction); } } // namespace eedb::auth diff --git a/src/eedb/auth/PgUserAuth.hpp b/src/eedb/auth/PgUserAuth.hpp index 0e877f3..383436e 100644 --- a/src/eedb/auth/PgUserAuth.hpp +++ b/src/eedb/auth/PgUserAuth.hpp @@ -2,12 +2,15 @@ #include +#include + namespace eedb::db { class PgConnection; +class PgUserAuth; } namespace Wt { -class WEnviroment; +class WEnvironment; } namespace Wt::Auth { @@ -65,6 +68,7 @@ class PgUserAuth : public Wt::Auth::AbstractUserDatabase { private: eedb::db::PgConnection & db; + eedb::db::PgUserAuth _userAuth; const Wt::WEnvironment & _env; const Wt::Auth::AuthService * _authService; int _in_transaction{0}; diff --git a/src/eedb/data/IUserRepository.hpp b/src/eedb/data/UserRepository.hpp similarity index 100% rename from src/eedb/data/IUserRepository.hpp rename to src/eedb/data/UserRepository.hpp diff --git a/src/eedb/db/CMakeLists.txt b/src/eedb/db/CMakeLists.txt index f378f64..736b3e0 100644 --- a/src/eedb/db/CMakeLists.txt +++ b/src/eedb/db/CMakeLists.txt @@ -1,9 +1,12 @@ set(SOURCE - connection.cpp - config.cpp -) + data/PgCategory.cpp + data/PgUser.cpp + data/PgUserAuth.cpp + data/PgUsers.cpp -INCLUDE_DIRECTORIES( ${PostgreSQL_INCLUDE_DIRS} ) + connection.cpp + config.cpp +) add_library(eedb_db ${SOURCE}) target_link_libraries( eedb_db wt sqlpp-postgresql ) diff --git a/src/eedb/data/PgCategory.cpp b/src/eedb/db/data/PgCategory.cpp similarity index 93% rename from src/eedb/data/PgCategory.cpp rename to src/eedb/db/data/PgCategory.cpp index 43a8079..9b98a5f 100644 --- a/src/eedb/data/PgCategory.cpp +++ b/src/eedb/db/data/PgCategory.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include diff --git a/src/eedb/data/PgCategory.hpp b/src/eedb/db/data/PgCategory.hpp similarity index 100% rename from src/eedb/data/PgCategory.hpp rename to src/eedb/db/data/PgCategory.hpp diff --git a/src/eedb/data/PgUser.cpp b/src/eedb/db/data/PgUser.cpp similarity index 98% rename from src/eedb/data/PgUser.cpp rename to src/eedb/db/data/PgUser.cpp index 9e42381..7a2500a 100644 --- a/src/eedb/data/PgUser.cpp +++ b/src/eedb/db/data/PgUser.cpp @@ -1,4 +1,4 @@ -#include +#include #include diff --git a/src/eedb/data/PgUser.hpp b/src/eedb/db/data/PgUser.hpp similarity index 100% rename from src/eedb/data/PgUser.hpp rename to src/eedb/db/data/PgUser.hpp diff --git a/src/eedb/db/data/PgUserAuth.cpp b/src/eedb/db/data/PgUserAuth.cpp new file mode 100644 index 0000000..ff9d55a --- /dev/null +++ b/src/eedb/db/data/PgUserAuth.cpp @@ -0,0 +1,452 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#include +#pragma GCC diagnostic pop + +#include +#include + +using User = eedb::db::details::User; + +constexpr const char eedb::user_audit_::Data::_alias_t::_literal[]; +constexpr const char eedb::user_audit::_alias_t::_literal[]; + +// enum LoginActions { Login, Logout }; +// static std::array< std::string_view, 2 > UserActionNames = {{"login", "logout"}}; + +std::string RandomString(uint len) { + using namespace std; + srand(static_cast< unsigned int >(time(nullptr))); + experimental::string_view str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + string newstr; + std::size_t pos; + while(newstr.size() != len) { + pos = static_cast< std::size_t >((rand() % (static_cast< int >(str.size()) - 1))); + newstr += str.substr(pos, 1).to_string(); + } + return newstr; +} + +namespace eedb::db { + +namespace { + constexpr eedb::user t_user; + constexpr eedb::user_audit t_user_history; + constexpr eedb::user_action t_user_action; + constexpr eedb::auth_identity t_identity; + constexpr eedb::auth_info t_info; + constexpr eedb::auth_token t_auth_token; + + static const auto auth_info_identity = t_info.join(t_identity).on(t_info.id == t_identity.auth_info_id); + static const auto user_auth_info = t_user.join(t_info).on(t_info.user_uid == t_user.uid); + static const auto auth_token_info = user_auth_info.join(t_auth_token).on(t_auth_token.auth_info_id == t_info.id); + + static const auto select_login_action_id = select(t_user_action.id) // + .from(t_user_action) // + .where(t_user_action.name == "login") // + .limit(1u); + static const auto select_logout_action_id = select(t_user_action.id) // + .from(t_user_action) // + .where(t_user_action.name == "logout") // + .limit(1u); +} // namespace + +PgUserAuth::PgUserAuth(PgConnection & _db) : db{_db} {} + +User PgUserAuth::findWithId(const std::string & id) const { + const auto uid_eq = t_user.uid == std::atoi(id.c_str()); + + if(db(select(exists(select(t_user.uid) // + .from(t_user) // + .where(uid_eq)))) + .front() + .exists) { + return User(id); + } + return User(); +} + +User PgUserAuth::findWithIdentity(const std::string & provider, const std::string & identity) const { + const auto identity_eq = t_identity.identity == identity; + const auto provider_eq = t_identity.provider == provider; + + const auto result = db(select(t_info.user_uid) // + .from(auth_info_identity) // + .where(provider_eq and identity_eq)); + + if(!result.empty()) { + return User(std::to_string(result.front().user_uid)); + } + return User(); +} + +User PgUserAuth::findWithEmailToken(const std::string & hash) const { + auto ret = db(select(t_user.uid) // + .from(user_auth_info) // + .where(t_info.email_token == hash)); + + if(ret.empty()) + return {}; + return {std::to_string(ret.front().uid)}; +} + +User PgUserAuth::findWithEmail(const std::string & address) const { + auto ret = db(select(t_info.user_uid) // + .from(t_info) // + .where(t_info.email == address and t_info.email_verified == true)); + + if(ret.empty()) + return {}; + return {std::to_string(ret.front().user_uid)}; +} + +void PgUserAuth::addIdentity(const User & user, const std::string & provider, const std::string & identity) { + const auto uid_eq = t_info.user_uid == std::atoi(user.id().c_str()); + const auto identity_eq = t_identity.identity == identity; + const auto provider_eq = t_identity.provider == provider; + + auto res = db(select(t_info.user_uid, t_info.id) // + .from(auth_info_identity) + .where(identity_eq and provider_eq and uid_eq)); + + if(!res.empty()) { +// Wt::log("error") << "cannot add identity " << provider << ":'" << identity << "': already exists"; + return; + } + + auto auth_info_id = db(select(t_info.id) + .from(t_info) // + .where(uid_eq)); + + if(auth_info_id.empty()) { + throw std::exception(); + } + + db(insert_into(t_identity) + .set(t_identity.identity = identity, // + t_identity.provider = provider, // + t_identity.auth_info_id = auth_info_id.front().id)); +} + +void PgUserAuth::setIdentity(const User & user, const std::string & provider, const std::string & identity) { + const auto uid = std::atoi(user.id().c_str()); + const auto provider_eq = t_identity.provider == provider; + + db(update(t_identity) // + .set(t_identity.identity = identity) // + .where(t_identity.auth_info_id == select(t_info.id).from(t_info).where(t_info.user_uid == uid) and provider_eq)); +} + +std::wstring PgUserAuth::identity(const User & user, const std::string & provider) const { + const int uid = std::atoi(user.id().c_str()); + const auto id_eq = t_info.user_uid == uid; + const auto provider_eq = t_identity.provider == provider; + + auto res = db(select(t_identity.identity) // + .from(auth_info_identity) // + .where(id_eq and provider_eq)); + + return boost::locale::conv::utf_to_utf< wchar_t >(res.front().identity.text); +} + +void PgUserAuth::removeIdentity(const User & user, const std::string & provider) { + const auto id_eq = t_identity.id == std::atoi(user.id().c_str()); + const auto provider_eq = t_identity.provider == provider; + + db(remove_from(t_identity).where(id_eq and provider_eq)); +} + +User PgUserAuth::registerNew() { + auto user_id = db(sqlpp::postgresql::insert_into(t_user) // + .set( // + t_user.full_name = RandomString(256), // + t_user.status = -1) + .returning(t_user.uid)) + .front() + .uid; + + db(insert_into(t_info).set( // + t_info.password_hash = "NONE", // + t_info.password_method = "NONE", // + t_info.password_salt = "NONE", // + t_info.user_uid = user_id, // + t_info.email = RandomString(20) + "@NONE.org")); + + return User{std::to_string(user_id)}; +} + +void PgUserAuth::deleteUser(const User & user) { + db(remove_from(t_info) // + .where(t_info.id == std::atoi(user.id().c_str()))); +} + +// User::Status PgUserAuth::status(const User & user) const { +// const auto id_eq = auth_identity.id == std::atoi(user.id().c_str()); + +// auto status = db(select(auth_info.status.as(user_status)) // +// .from(join) // +// .where(id_eq)) +// .front() +// .user_status; +// return status.value() == User::Status::Normal ? User::Status::Normal : User::Status::Disabled; +//} + +// void PgUserAuth::setStatus(const User & user, User::Status status) { +// db(update(auth_info) // +// .set(auth_info.status = static_cast< int >(status)) // +// .where(auth_info.id == std::atoi(user.id().c_str()))); +//} + +void PgUserAuth::setPassword(const User & user, details::PasswordHash password) { + db(update(t_info) + .set( // + t_info.password_hash = password.mvalue(), // + t_info.password_method = password.mfunction(), // + t_info.password_salt = password.msalt()) + .where(t_info.user_uid == std::atoi(user.id().c_str()))); +} + +std::optional PgUserAuth::password(const User & user) const { + const auto id_eq = t_info.user_uid == std::atoi(user.id().c_str()); + + auto row = db(select(t_info.password_hash, t_info.password_method, t_info.password_salt) + .from(t_info) // + .where(id_eq)); + if(row.empty()) + return {}; + auto & front = row.front(); + return details::PasswordHash{std::move(front.password_method), std::move(front.password_salt), std::move(front.password_hash)}; +} + +bool PgUserAuth::setEmail(const User & user, const std::string & address) { + const auto uid_eq = t_info.user_uid == std::atoi(user.id().c_str()); + + db(update(t_info) // + .set( // + t_info.email = address, + t_info.email_verified = true) // + .where(uid_eq)); + return true; +} + +std::string PgUserAuth::email(const User & user) const { + auto ret = db(select(t_info.email) // + .from(t_info) // + .where(t_info.user_uid == std::atoi(user.id().c_str()) and t_info.email_verified == true)); + if(ret.empty()) + return {}; + return std::move(ret.front().email); +} + +void PgUserAuth::setUnverifiedEmail(const User & user, const std::string & address) { + // orginal implementation of UserDatabase (provided by WT team) sets veryfied and unverified emails in + // different fields in database. So in order to verify email, they just copy the unverified_email to email and then + // set unverified_email to empty string (db don't allow empty strings as email*) + if(address.empty()) + db(update(t_info) // + .set(t_info.email_verified = true) // + .where(t_info.user_uid == std::atoi(user.id().c_str()))); + else + db(update(t_info) // + .set( // + t_info.email = address, // + t_info.email_verified = false) // + .where(t_info.user_uid == std::atoi(user.id().c_str()))); +} + +std::string PgUserAuth::unverifiedEmail(const User & user) const { + auto ret = db(select(t_info.email) // + .from(t_info) // + .where(t_info.user_uid == std::atoi(user.id().c_str()) and t_info.email_verified == false)); + if(ret.empty()) + return {}; + return std::move(ret.front().email); +} + +void PgUserAuth::setEmailToken(const User & user, details::Token token, int role) { + db(update(t_info) // + .set( // + t_info.email_token = token.mhash(), // + t_info.email_token_expires = token.expirationTime(), + t_info.email_token_role = static_cast< int >(role)) // + .where(t_info.user_uid == std::atoi(user.id().c_str()))); +} + +std::optional< details::Token > PgUserAuth::emailToken(const User & user) const { + auto ret = db(select(t_info.email_token, t_info.email_token_expires) // + .from(t_info) // + .where(t_info.user_uid == std::atoi(user.id().c_str()))); + if(ret.empty()) + return {}; + auto time = ret.front().email_token_expires.value(); + return details::Token{ret.front().email_token, time}; +} + +int PgUserAuth::emailTokenRole(const User & user) const { + auto ret = db(select(t_info.email_token_role) // + .from(t_info) // + .where(t_info.user_uid == std::atoi(user.id().c_str()))); + if(ret.empty()) + throw std::exception(); + auto val = ret.front().email_token_role; + return val.value(); +} + +void PgUserAuth::addAuthToken(const User & user, details::Token token) { + auto select_identity_id = select(t_info.id) // + .from(t_info) + .where(t_info.user_uid == std::atoi(user.id().c_str())) + .limit(1u); + db(insert_into(t_auth_token) // + .set( // + t_auth_token.auth_info_id = select_identity_id, // + t_auth_token.expires = std::chrono::system_clock::now() + ::sqlpp::chrono::days{14}, // + t_auth_token.value = token.mhash())); +} + +void PgUserAuth::removeAuthToken(const User &, const std::string & hash) { + db(remove_from(t_auth_token).where(t_auth_token.value == hash)); +} + +std::optional PgUserAuth::findWithAuthToken(const std::string & hash) const { + auto fullUser = t_auth_token // + .join(t_info) + .on(t_info.id == t_auth_token.auth_info_id); + + auto ret = db(select(t_info.user_uid) // + .from(fullUser) // + .where(t_auth_token.value == hash and t_auth_token.expires > std::chrono::system_clock::now())); + + if(ret.empty()) + return {}; + return details::User{std::to_string(ret.front().user_uid)}; +} + +int PgUserAuth::updateAuthToken(const User & user, const std::string & oldhash, const std::string & newhash) { + // method called only after successful login + using namespace std::chrono; + using namespace std::string_literals; + const auto identity_id = db(select(t_info.id) // + .from(t_info) // + .where(t_info.user_uid == std::atoi(user.id().c_str()))); + if(identity_id.empty()) + return 0; + + const auto expires = db(sqlpp::postgresql::update(t_auth_token) // + .set(t_auth_token.value = newhash) + .where(t_auth_token.value == oldhash) + .returning(t_auth_token.expires)); + + if(expires.empty()) + return 0; + + const nlohmann::json data = { + {"status"s, "success"}, // + {"method"s, "token"}, // +// {"user_address"s, _env.clientAddress()}, // +// {"user_agent"s, _env.userAgent()}, // +// {"user_locale"s, _env.locale().name()}, // +// {"referer", _env.referer()}, // +// {"url_scheme", _env.urlScheme()} // + }; + + db(insert_into(t_user_history) + .set(t_user_history.user_id = std::atoi(user.id().c_str()), + t_user_history.action_id = select_login_action_id, // + t_user_history.data = data.dump())); + + const auto now = system_clock::now(); + const auto diff = expires.front().expires.value() - now; + return duration_cast< duration< int > >(diff).count(); +} + +void PgUserAuth::setFailedLoginAttempts(const User & user, int count) { + using namespace std::string_literals; + const auto getStatus = [count]() { return count ? "failed"s : "success"s; }; + + const nlohmann::json data = { + {"status"s, getStatus()}, // + {"method"s, "password"}, // +// {"user_address"s, _env.clientAddress()}, // +// {"user_agent"s, _env.userAgent()}, // +// {"user_locale"s, _env.locale().name()}, // +// {"referer", _env.referer()}, // +// {"url_scheme", _env.urlScheme()} // + }; + + db(insert_into(t_user_history) + .set(t_user_history.user_id = std::atoi(user.id().c_str()), + t_user_history.action_id = select_login_action_id, // + t_user_history.data = data.dump())); +} + +template < typename T > +inline std::string name_of(const T &) { + static_assert(sqlpp::is_column_t< T >::value, "T should by a calumn"); + return std::string{T::_table::_alias_t::_literal} + '.' + T::_alias_t::_literal; +} + +int PgUserAuth::failedLoginAttempts(const User & user) const { + const auto res = + db(select(count(t_user_history.id)) // + .from(t_user_history) // + .where(t_user_history.user_id == std::atoi(user.id().c_str()) and // + t_user_history.action_id == select_login_action_id and // + sqlpp::verbatim< sqlpp::boolean >(name_of(t_user_history.data) + "->>'status' = 'failed'") and // + t_user_history.id > select(t_user_history.id) // + .from(t_user_history) // + .where(t_user_history.user_id == std::atoi(user.id().c_str()) and // + t_user_history.action_id == select_login_action_id and // + sqlpp::verbatim< sqlpp::boolean >(name_of(t_user_history.data) + "->>'status' = 'success'")) // + .limit(1u))); + return static_cast< int >(res.front().count); +} + +void PgUserAuth::setLastLoginAttempt(const User &, PgUserAuth::time_point) {} + +PgUserAuth::time_point PgUserAuth::lastLoginAttempt(const User & user) const { + using namespace std::chrono; + const auto res = + db(select(t_user_history._when) // + .from(t_user_history) // + .where(t_user_history.action_id == select_login_action_id and t_user_history.user_id == std::atoi(user.id().c_str())) // + .order_by(t_user_history.id.desc()) // + .limit(1u)); + if(res.empty()) + return {}; + + return res.front()._when.value(); +} + +void PgUserAuth::logout(const User & user) { + using namespace std::string_literals; + const nlohmann::json data = {{"status"s, "success"}}; + + db(insert_into(t_user_history) + .set(t_user_history.user_id = std::atoi(user.id().c_str()), + t_user_history.action_id = select_logout_action_id, // + t_user_history.data = data.dump())); +} + +//AbstractUserDatabase::Transaction * PgUserAuth::startTransaction() { +// return new TransactionGuard< decltype(db) >(db, _in_transaction); +//} +} // namespace eedb::db diff --git a/src/eedb/db/data/PgUserAuth.hpp b/src/eedb/db/data/PgUserAuth.hpp new file mode 100644 index 0000000..15aff7e --- /dev/null +++ b/src/eedb/db/data/PgUserAuth.hpp @@ -0,0 +1,125 @@ +#pragma once + +#include + +#include + +namespace eedb::db { +namespace details { + class User { + public: + User(std::string id = {}) : _id{std::move(id)} {} + const std::string & id() const { + return _id; + } + + std::string && moveid() { + return std::move(_id); + } + + operator bool() const { + return _id != ""; + } + + private: + std::string _id; + }; + + class PasswordHash { + public: + PasswordHash(std::string function, std::string salt, std::string value) + : function_{std::move(function)}, salt_{std::move(salt)}, value_{std::move(value)} {} + + bool empty() const { + return value_.empty(); + } + + std::string && mfunction() { + return std::move(function_); + } + + std::string && msalt() { + return std::move(salt_); + } + + std::string && mvalue() { + return std::move(value_); + } + + private: + std::string function_, salt_, value_; + }; + + class Token { + public: + using time_point = std::chrono::time_point< std::chrono::system_clock, std::chrono::microseconds >; + Token(std::string hash, time_point expirationTime) : hash_{std::move(hash)}, expirationTime_{expirationTime} {} + + bool empty() const { + return hash_.empty(); + } + + std::string && mhash() { + return std::move(hash_); + } + + time_point expirationTime() const { + return expirationTime_; + } + + private: + std::string hash_; + time_point expirationTime_; + }; +} // namespace details + +class PgUserAuth { + public: + using time_point = std::chrono::time_point< std::chrono::system_clock, std::chrono::microseconds >; + + PgUserAuth(PgConnection & db); + + details::User findWithId(const std::string & id) const; + details::User findWithIdentity(const std::string & provider, const std::string & identity) const; + details::User findWithEmailToken(const std::string & hash) const; + details::User findWithEmail(const std::string & address) const; + + void addIdentity(const details::User & user, const std::string & provider, const std::string & id); + void setIdentity(const details::User & user, const std::string & provider, const std::string & id); + std::wstring identity(const details::User & user, const std::string & provider) const; + void removeIdentity(const details::User & user, const std::string & provider); + + details::User registerNew(); + void deleteUser(const details::User & user); + + void setPassword(const details::User & user, details::PasswordHash password); + std::optional< details::PasswordHash > password(const details::User & user) const; + + bool setEmail(const details::User & user, const std::string & address); + std::string email(const details::User & user) const; + void setUnverifiedEmail(const details::User & user, const std::string & address); + std::string unverifiedEmail(const details::User & user) const; + + void setEmailToken(const details::User & user, details::Token token, int role); + std::optional emailToken(const details::User & user) const; + int emailTokenRole(const details::User & user) const; + + void addAuthToken(const details::User & user, details::Token token); + void removeAuthToken(const details::User &user, const std::string & hash); + std::optional findWithAuthToken(const std::string & hash) const; + int updateAuthToken(const details::User & user, const std::string & oldhash, const std::string & newhash); + + void setFailedLoginAttempts(const details::User & user, int count); + int failedLoginAttempts(const details::User & user) const; + void setLastLoginAttempt(const details::User & user, time_point t); + time_point lastLoginAttempt(const details::User & user) const; + + void logout(const details::User & user); + + private: + eedb::db::PgConnection & db; + // const Wt::WEnvironment & _env; + // const Wt::Auth::AuthService * _authService; + // int _in_transaction{0}; +}; +} // namespace eedb::db diff --git a/src/eedb/data/PgUsers.cpp b/src/eedb/db/data/PgUsers.cpp similarity index 88% rename from src/eedb/data/PgUsers.cpp rename to src/eedb/db/data/PgUsers.cpp index e4ec46b..199d94e 100644 --- a/src/eedb/data/PgUsers.cpp +++ b/src/eedb/db/data/PgUsers.cpp @@ -1,5 +1,5 @@ -#include -#include +#include +#include #include #include diff --git a/src/eedb/data/PgUsers.hpp b/src/eedb/db/data/PgUsers.hpp similarity index 100% rename from src/eedb/data/PgUsers.hpp rename to src/eedb/db/data/PgUsers.hpp diff --git a/tests/unit/test_eedb_data_PgCategories.cpp b/tests/unit/test_eedb_data_PgCategories.cpp index 38e56c1..8a0ac7a 100644 --- a/tests/unit/test_eedb_data_PgCategories.cpp +++ b/tests/unit/test_eedb_data_PgCategories.cpp @@ -1,6 +1,6 @@ #include -#include +#include class PgCategoriesTest : public testing::Test { public: diff --git a/tests/unit/test_eedb_data_PgCategory.cpp b/tests/unit/test_eedb_data_PgCategory.cpp index 8a392fc..46680f9 100644 --- a/tests/unit/test_eedb_data_PgCategory.cpp +++ b/tests/unit/test_eedb_data_PgCategory.cpp @@ -1,6 +1,6 @@ #include -#include +#include #include #include