move all db dependent data into db subdir

This commit is contained in:
Bartosz Wieczorek 2018-01-30 09:49:54 +01:00
parent 207e50a880
commit 51e39e5498
15 changed files with 659 additions and 350 deletions

View File

@ -10,6 +10,7 @@ file(GLOB SOURCE
) )
include_directories( ${PostgreSQL_INCLUDE_DIRS} ) include_directories( ${PostgreSQL_INCLUDE_DIRS} )
add_subdirectory(db) add_subdirectory(db)
add_library(auth STATIC ${SOURCE}) add_library(auth STATIC ${SOURCE})

View File

@ -1,21 +1,7 @@
#include "PgUserAuth.hpp" #include "PgUserAuth.hpp"
#include <eedb/db/data/PgUserAuth.hpp>
#include <eedb/auth/Services.hpp> #include <eedb/auth/Services.hpp>
#include <eedb/db/connection.hpp>
#include <eedb/db/model/auth_identity.h>
#include <eedb/db/model/auth_info.h>
#include <eedb/db/model/auth_token.h>
#include <eedb/db/model/user.h>
#include <eedb/db/model/user_action.h>
#include <eedb/db/model/user_audit.h>
#include <algorithm>
#include <cctype>
#include <experimental/string_view>
#include <functional>
#include <iostream>
#include <random>
#include <string>
#include <Wt/Auth/AuthService.h> #include <Wt/Auth/AuthService.h>
#include <Wt/Auth/Dbo/AuthInfo.h> #include <Wt/Auth/Dbo/AuthInfo.h>
@ -23,29 +9,9 @@
#include <Wt/WLogger.h> #include <Wt/WLogger.h>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <sqlpp11/sqlpp11.h>
using namespace Wt::Auth; 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 > template < typename Connection >
struct TransactionGuard : public Wt::Auth::AbstractUserDatabase::Transaction { struct TransactionGuard : public Wt::Auth::AbstractUserDatabase::Transaction {
TransactionGuard(Connection & c, int & transaction_active) : _c(c), _transaction{transaction_active} { 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 eedb::auth {
namespace { PgUserAuth::PgUserAuth(eedb::db::PgConnection & _db, const Wt::WEnvironment & env) : db(_db), _env(env), _userAuth{_db} {
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) {
this->setAuthService(eedb::auth::Services::authService()); this->setAuthService(eedb::auth::Services::authService());
} }
User PgUserAuth::findWithId(const std::string & id) const { 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) // if(duser)
.from(t_user) // return User(duser.moveid(), *this);
.where(uid_eq))))
.front()
.exists) {
return User(id, *this);
}
return User(); 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); std::transform(_identity.begin(), _identity.end(), _identity.begin(), ::tolower);
} }
const auto identity_eq = t_identity.identity == _identity; auto duser = _userAuth.findWithIdentity(provider, _identity);
const auto provider_eq = t_identity.provider == provider; if(duser) {
return User(duser.moveid(), *this);
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);
} }
return User(); 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) { 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()); _userAuth.addIdentity({user.id()}, provider, identity.toUTF8());
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";
} }
void PgUserAuth::setIdentity(const User & user, const std::string & provider, const Wt::WString & identity) { void PgUserAuth::setIdentity(const User & user, const std::string & provider, const Wt::WString & identity) {
const auto uid = std::atoi(user.id().c_str()); _userAuth.setIdentity({user.id()}, provider, identity.toUTF8());
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));
} }
Wt::WString PgUserAuth::identity(const User & user, const std::string & provider) const { Wt::WString PgUserAuth::identity(const User & user, const std::string & provider) const {
const int uid = std::atoi(user.id().c_str()); auto id = _userAuth.identity({user.id()}, provider);
const auto id_eq = t_info.user_uid == uid;
const auto provider_eq = t_identity.provider == provider;
auto res = db(select(t_identity.identity) // if(id.empty())
.from(auth_info_identity) //
.where(id_eq and provider_eq));
if(res.empty())
return Wt::WString::Empty; return Wt::WString::Empty;
return {res.front().identity}; return {id};
} }
void PgUserAuth::removeIdentity(const User & user, const std::string & provider) { void PgUserAuth::removeIdentity(const User & user, const std::string & provider) {
const auto id_eq = t_identity.id == std::atoi(user.id().c_str()); _userAuth.removeIdentity({user.id()}, provider);
const auto provider_eq = t_identity.provider == provider;
db(remove_from(t_identity).where(id_eq and provider_eq));
} }
User PgUserAuth::registerNew() { User PgUserAuth::registerNew() {
auto user_id = db(sqlpp::postgresql::insert_into(t_user) // return User{_userAuth.registerNew().moveid(), *this};
.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};
} }
void PgUserAuth::deleteUser(const User & user) { void PgUserAuth::deleteUser(const User & user) {
db(remove_from(t_info) // _userAuth.deleteUser({user.id()});
.where(t_info.id == std::atoi(user.id().c_str())));
} }
// User::Status PgUserAuth::status(const User & user) const { // 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) { void PgUserAuth::setPassword(const User & user, const PasswordHash & password) {
db(update(t_info) _userAuth.setPassword({user.id()}, {password.function(), password.salt(), password.value()});
.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())));
} }
PasswordHash PgUserAuth::password(const User & user) const { PasswordHash PgUserAuth::password(const User & user) const {
const auto id_eq = t_info.user_uid == std::atoi(user.id().c_str()); auto pass = _userAuth.password({user.id()});
if(!pass)
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 PasswordHash(); return PasswordHash();
auto & front = row.front(); return {pass->mfunction(), pass->msalt(), pass->mvalue()};
return {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) { bool PgUserAuth::setEmail(const User & user, const std::string & address) {
const auto uid_eq = t_info.user_uid == std::atoi(user.id().c_str()); return _userAuth.setEmail({user.id()}, address);
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 { std::string PgUserAuth::email(const User & user) const {
auto ret = db(select(t_info.email) // return _userAuth.email({user.id()});
.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) { void PgUserAuth::setUnverifiedEmail(const User & user, const std::string & address) {
// orginal implementation of UserDatabase (provided by WT team) sets veryfied and unverified emails in _userAuth.setUnverifiedEmail({user.id()}, address);
// 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};
} }
void PgUserAuth::setEmailToken(const User & user, const Token & token, EmailTokenRole role) { 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())); auto exp = ::date::floor<::std::chrono::milliseconds >(std::chrono::system_clock::from_time_t(token.expirationTime().toTime_t()));
db(update(t_info) // _userAuth.setEmailToken({user.id()}, {token.hash(), exp}, static_cast< int >(role) );
.set( // }
t_info.email_token = token.hash(), //
t_info.email_token_expires = exp, std::string PgUserAuth::unverifiedEmail(const User & user) const {
t_info.email_token_role = static_cast< int >(role)) // _userAuth.unverifiedEmail({user.id()});
.where(t_info.user_uid == std::atoi(user.id().c_str())));
} }
Token PgUserAuth::emailToken(const User & user) const { Token PgUserAuth::emailToken(const User & user) const {
auto ret = db(select(t_info.email_token, t_info.email_token_expires) // auto tok = _userAuth.emailToken({user.id()});
.from(t_info) // if(!tok)
.where(t_info.user_uid == std::atoi(user.id().c_str())));
if(ret.empty())
return {}; return {};
auto exp = Wt::WDateTime(); 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); auto systime = std::chrono::system_clock::to_time_t(time);
exp.setTime_t(systime); exp.setTime_t(systime);
return {ret.front().email_token, exp}; return {tok->mhash(), exp};
} }
EmailTokenRole PgUserAuth::emailTokenRole(const User & user) const { EmailTokenRole PgUserAuth::emailTokenRole(const User & user) const {
auto ret = db(select(t_info.email_token_role) // auto val = _userAuth.emailTokenRole({user.id()});
.from(t_info) // return static_cast< EmailTokenRole >(val);
.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};
} }
void PgUserAuth::addAuthToken(const User & user, const Token & token) { void PgUserAuth::addAuthToken(const User & user, const Token & token) {
auto select_identity_id = select(t_info.id) // auto exp = std::chrono::time_point_cast< std::chrono::microseconds >(std::chrono::system_clock::now()) + ::sqlpp::chrono::days{14};
.from(t_info) auto tok = eedb::db::details::Token{token.hash(), exp};
.where(t_info.user_uid == std::atoi(user.id().c_str())) _userAuth.addAuthToken({user.id()}, std::move(tok));
.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()));
} }
void PgUserAuth::removeAuthToken(const User &, const std::string & hash) { void PgUserAuth::removeAuthToken(const User &user, const std::string & hash) {
db(remove_from(t_auth_token).where(t_auth_token.value == hash)); _userAuth.removeAuthToken({user.id()}, hash);
} }
User PgUserAuth::findWithAuthToken(const std::string & hash) const { User PgUserAuth::findWithAuthToken(const std::string & hash) const {
auto fullUser = t_auth_token // auto u = _userAuth.findWithAuthToken(hash);
.join(t_info) if(!u)
.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 {};
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) { int PgUserAuth::updateAuthToken(const User & user, const std::string & oldhash, const std::string & newhash) {
// method called only after successful login // method called only after successful login
using namespace std::chrono; return _userAuth.updateAuthToken({user.id()}, oldhash, newhash);
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) { void PgUserAuth::setFailedLoginAttempts(const User & user, int count) {
using namespace std::string_literals; _userAuth.setFailedLoginAttempts({user.id()}, count);
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 { int PgUserAuth::failedLoginAttempts(const User & user) const {
const auto res = return _userAuth.failedLoginAttempts({user.id()});
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 &, const Wt::WDateTime &) {} void PgUserAuth::setLastLoginAttempt(const User &, const Wt::WDateTime &) {}
Wt::WDateTime PgUserAuth::lastLoginAttempt(const User & user) const { Wt::WDateTime PgUserAuth::lastLoginAttempt(const User & user) const {
using namespace std::chrono; const auto time = _userAuth.lastLoginAttempt({user.id()});
const auto res = const auto systime = std::chrono::system_clock::to_time_t(time);
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);
auto lastLogin = Wt::WDateTime(); auto lastLogin = Wt::WDateTime();
lastLogin.setTime_t(systime); lastLogin.setTime_t(systime);
return lastLogin; return lastLogin;
} }
void PgUserAuth::logout(const User & user) { void PgUserAuth::logout(const User & user) {
using namespace std::string_literals; _userAuth.logout({user.id()});
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() { AbstractUserDatabase::Transaction * PgUserAuth::startTransaction() {
return new TransactionGuard< decltype(db) >(db, _in_transaction); // return new TransactionGuard< decltype(db) >(db, _in_transaction);
} }
} // namespace eedb::auth } // namespace eedb::auth

View File

@ -2,12 +2,15 @@
#include <Wt/Auth/AbstractUserDatabase.h> #include <Wt/Auth/AbstractUserDatabase.h>
#include <eedb/db/data/PgUserAuth.hpp>
namespace eedb::db { namespace eedb::db {
class PgConnection; class PgConnection;
class PgUserAuth;
} }
namespace Wt { namespace Wt {
class WEnviroment; class WEnvironment;
} }
namespace Wt::Auth { namespace Wt::Auth {
@ -65,6 +68,7 @@ class PgUserAuth : public Wt::Auth::AbstractUserDatabase {
private: private:
eedb::db::PgConnection & db; eedb::db::PgConnection & db;
eedb::db::PgUserAuth _userAuth;
const Wt::WEnvironment & _env; const Wt::WEnvironment & _env;
const Wt::Auth::AuthService * _authService; const Wt::Auth::AuthService * _authService;
int _in_transaction{0}; int _in_transaction{0};

View File

@ -1,9 +1,12 @@
set(SOURCE set(SOURCE
data/PgCategory.cpp
data/PgUser.cpp
data/PgUserAuth.cpp
data/PgUsers.cpp
connection.cpp connection.cpp
config.cpp config.cpp
) )
INCLUDE_DIRECTORIES( ${PostgreSQL_INCLUDE_DIRS} )
add_library(eedb_db ${SOURCE}) add_library(eedb_db ${SOURCE})
target_link_libraries( eedb_db wt sqlpp-postgresql ) target_link_libraries( eedb_db wt sqlpp-postgresql )

View File

@ -1,4 +1,4 @@
#include <eedb/data/PgCategory.hpp> #include <eedb/db/data/PgCategory.hpp>
#include <eedb/db/model/category.h> #include <eedb/db/model/category.h>
#include <eedb/db/model/stat.h> #include <eedb/db/model/stat.h>

View File

@ -1,4 +1,4 @@
#include <eedb/data/PgUser.hpp> #include <eedb/db/data/PgUser.hpp>
#include <eedb/db/connection.hpp> #include <eedb/db/connection.hpp>

View File

@ -0,0 +1,452 @@
#include <eedb/db/data/PgUserAuth.hpp>
#include <eedb/db/connection.hpp>
#include <eedb/db/model/auth_identity.h>
#include <eedb/db/model/auth_info.h>
#include <eedb/db/model/auth_token.h>
#include <eedb/db/model/user.h>
#include <eedb/db/model/user_action.h>
#include <eedb/db/model/user_audit.h>
#include <algorithm>
#include <cctype>
#include <experimental/string_view>
#include <functional>
#include <iostream>
#include <random>
#include <string>
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#include <boost/locale.hpp>
#pragma GCC diagnostic pop
#include <nlohmann/json.hpp>
#include <sqlpp11/sqlpp11.h>
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<details::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())
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<details::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())
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

View File

@ -0,0 +1,125 @@
#pragma once
#include <eedb/db/connection.hpp>
#include <string>
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<details::Token> 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<details::User> 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

View File

@ -1,5 +1,5 @@
#include <eedb/data/PgUser.hpp> #include <eedb/db/data/PgUser.hpp>
#include <eedb/data/PgUsers.hpp> #include <eedb/db/data/PgUsers.hpp>
#include <eedb/db/model/auth_identity.h> #include <eedb/db/model/auth_identity.h>
#include <eedb/db/model/auth_info.h> #include <eedb/db/model/auth_info.h>

View File

@ -1,6 +1,6 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <eedb/data/PgCategory.hpp> #include <eedb/db/data/PgCategory.hpp>
class PgCategoriesTest : public testing::Test { class PgCategoriesTest : public testing::Test {
public: public:

View File

@ -1,6 +1,6 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <eedb/data/PgCategory.hpp> #include <eedb/db/data/PgCategory.hpp>
#include <eedb/db/config.hpp> #include <eedb/db/config.hpp>
#include <eedb/db/connection.hpp> #include <eedb/db/connection.hpp>