diff options
| author | Nikita Kostovsky <nikita@kostovsky.me> | 2025-06-22 21:39:13 +0200 |
|---|---|---|
| committer | Nikita Kostovsky <nikita@kostovsky.me> | 2025-06-22 21:39:13 +0200 |
| commit | c9fcceb74d861525b2defec8219374edb9c1455a (patch) | |
| tree | fc10d7c2abcb08db76946e0bfd73f6b93a652422 | |
| parent | f674e179d602d3ccb9818d28fe06f371059449dc (diff) | |
add User class
| -rw-r--r-- | CMakeLists.txt | 6 | ||||
| -rw-r--r-- | src/atomchannel.cpp | 22 | ||||
| -rw-r--r-- | src/atomchannel.h | 5 | ||||
| -rw-r--r-- | src/atomitem.cpp | 12 | ||||
| -rw-r--r-- | src/atomitem.h | 4 | ||||
| -rw-r--r-- | src/main.cpp | 11 | ||||
| -rw-r--r-- | src/sql/db.sqlite | bin | 36864 -> 110592 bytes | |||
| -rw-r--r-- | src/typedefs.cpp | 1 | ||||
| -rw-r--r-- | src/typedefs.h | 24 | ||||
| -rw-r--r-- | src/user.cpp | 123 | ||||
| -rw-r--r-- | src/user.h | 54 |
11 files changed, 239 insertions, 23 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index f5ca22e..430e2d5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,8 +12,10 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Network Sql) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Network Sql) -file(GLOB_RECURSE SOURCES src/*.h src/*.cpp src/*.qrc) -add_executable(${PROJECT_NAME} ${SOURCES}) +file(GLOB_RECURSE SOURCES src/*.h src/*.cpp) +file(GLOB_RECURSE RESOURCES src/*.qrc) + +add_executable(${PROJECT_NAME} ${SOURCES} ${RESOURCES}) target_link_libraries(${PROJECT_NAME} Qt${QT_VERSION_MAJOR}::Core diff --git a/src/atomchannel.cpp b/src/atomchannel.cpp index 6982e8b..6a7c87c 100644 --- a/src/atomchannel.cpp +++ b/src/atomchannel.cpp @@ -78,10 +78,11 @@ AtomChannel::AtomChannel(QXmlStreamReader *xmlReader) qDebug() << "exit " << __func__; } +// TODO: move to a separate function, code is almost identical in all methods called `getDbId` int AtomChannel::getDbId() { - if (dbId != rsshit::db::IdNotFound) - return dbId; + if (id != rsshit::db::IdNotFound) + return id; const auto db = rsshit::db::open(); @@ -93,7 +94,7 @@ int AtomChannel::getDbId() if (!selectQ.exec()) { qWarning() << "cannot exec query" << selectQ.lastQuery() << ":" - << selectQ.lastError().text() << ":" << selectQ.executedQuery(); + << selectQ.lastError().text(); return rsshit::db::IdNotFound; } @@ -118,10 +119,11 @@ int AtomChannel::getDbId() return result; } +// TODO: move to a separate function, code is almost identical in all methods called `createInDb` int AtomChannel::createInDb() { - if (dbId != rsshit::db::IdNotFound) - return dbId; + if (id != rsshit::db::IdNotFound) + return id; const auto db = rsshit::db::open(); @@ -135,7 +137,7 @@ int AtomChannel::createInDb() if (!insertQ.exec()) { qWarning() << "cannot exec query" << insertQ.lastQuery() << ":" - << insertQ.lastError().text() << ":" << insertQ.executedQuery(); + << insertQ.lastError().text(); return rsshit::db::IdNotFound; } @@ -156,16 +158,16 @@ int AtomChannel::getOrInsertDbId() QList<int> AtomChannel::syncDbItems() { - if (dbId == rsshit::db::IdNotFound) - dbId = getOrInsertDbId(); + if (id == rsshit::db::IdNotFound) + id = getOrInsertDbId(); - if (dbId == rsshit::db::IdNotFound) + if (id == rsshit::db::IdNotFound) return {}; QList<int> result; for (auto &item : items) { - auto id = item.getOrInsertDbId(this->dbId); + auto id = item.getOrInsertDbId(this->id); if (id != rsshit::db::IdNotFound) result << id; diff --git a/src/atomchannel.h b/src/atomchannel.h index 0498a64..cde066b 100644 --- a/src/atomchannel.h +++ b/src/atomchannel.h @@ -20,6 +20,7 @@ public: explicit AtomChannel(QXmlStreamReader *xmlReader); public: + // TODO: update db data if differs. here and everywhere (item, user, etc) /*! * \brief getDbId - check if channel with corresponding `link` exists in db * and return db id if any @@ -48,9 +49,9 @@ public: public: /*! - * \brief dbId - cache db id + * \brief id - cache db id */ - int dbId{0}; + int id{0}; QString title; /*! diff --git a/src/atomitem.cpp b/src/atomitem.cpp index 5f099f6..191e04b 100644 --- a/src/atomitem.cpp +++ b/src/atomitem.cpp @@ -63,8 +63,8 @@ AtomItem::AtomItem(QXmlStreamReader *xmlReader) int AtomItem::getDbId() { - if (dbId != rsshit::db::IdNotFound) - return dbId; + if (id != rsshit::db::IdNotFound) + return id; const auto db = rsshit::db::open(); @@ -76,7 +76,7 @@ int AtomItem::getDbId() if (!selectQ.exec()) { qWarning() << "cannot exec query" << selectQ.lastQuery() << ":" - << selectQ.lastError().text() << ":" << selectQ.executedQuery(); + << selectQ.lastError().text(); return rsshit::db::IdNotFound; } @@ -103,8 +103,8 @@ int AtomItem::getDbId() int AtomItem::createInDb(const int feedId) { - if (dbId != rsshit::db::IdNotFound) - return dbId; + if (id != rsshit::db::IdNotFound) + return id; const auto db = rsshit::db::open(); @@ -124,7 +124,7 @@ int AtomItem::createInDb(const int feedId) if (!insertQ.exec()) { qWarning() << "cannot exec query" << insertQ.lastQuery() << ":" - << insertQ.lastError().text() << ":" << insertQ.executedQuery(); + << insertQ.lastError().text(); return rsshit::db::IdNotFound; } diff --git a/src/atomitem.h b/src/atomitem.h index 39917b5..7b36816 100644 --- a/src/atomitem.h +++ b/src/atomitem.h @@ -49,9 +49,9 @@ public: public: /*! - * \brief dbId - cache db id + * \brief id - cache db id */ - int dbId{0}; + int id{0}; QString title; QUrl link; diff --git a/src/main.cpp b/src/main.cpp index f9fab22..22029a9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,6 +8,7 @@ #include "constants.h" #include "playground.h" #include "rsshit_db.h" +#include "user.h" int main(int argc, char *argv[]) { @@ -35,7 +36,7 @@ int main(int argc, char *argv[]) const QString newDbFilepath{dataLocation + '/' + QFileInfo{dbSqliteResource}.fileName()}; - // #define RECREATE_DB_FILE +#define RECREATE_DB_FILE #ifdef RECREATE_DB_FILE if (!QFile::remove(newDbFilepath)) { @@ -60,6 +61,14 @@ int main(int argc, char *argv[]) return EXIT_FAILURE; } + User user{"admin", "123"}; + qDebug() << "verify password 123:" << user.verifyPassword("123"); + qDebug() << "verify password 321:" << user.verifyPassword("321"); + qDebug() << "user.getDbId()" << user.getDbId(); + qDebug() << "user.createInDb()" << user.createInDb(); + qDebug() << "user.createInDb()" << user.getOrInsertDbId(); + qDebug() << "user.createInDb()" << user.createInDb(); + Playground playground; QTimer::singleShot(0, [&]() { diff --git a/src/sql/db.sqlite b/src/sql/db.sqlite Binary files differindex c982736..aefa4d5 100644 --- a/src/sql/db.sqlite +++ b/src/sql/db.sqlite diff --git a/src/typedefs.cpp b/src/typedefs.cpp new file mode 100644 index 0000000..62128a1 --- /dev/null +++ b/src/typedefs.cpp @@ -0,0 +1 @@ +#include "typedefs.h" diff --git a/src/typedefs.h b/src/typedefs.h new file mode 100644 index 0000000..2175831 --- /dev/null +++ b/src/typedefs.h @@ -0,0 +1,24 @@ +#pragma once + +#include <QObject> + +namespace rsshit { +namespace db { +Q_NAMESPACE + +/*! + * \brief The Field enum - `item` fields to filter by + */ +enum class ItemField { + Title = 1, + Content = 2, + Link = 3, + Author = 4, + Tags = 5, + TitleOrContent = 6, + TitleOrContentOrTags = 7 +}; +Q_ENUM_NS(ItemField) + +} // namespace db +} // namespace rsshit diff --git a/src/user.cpp b/src/user.cpp new file mode 100644 index 0000000..4667ebf --- /dev/null +++ b/src/user.cpp @@ -0,0 +1,123 @@ +#include "user.h" + +#include <QCryptographicHash> +#include <QDebug> +#include <QRandomGenerator> +#include <QSqlError> +#include <QSqlQuery> + +#include "constants.h" +#include "rsshit_db.h" + +namespace rsshit { +QByteArray generateSalt() +{ + constexpr qsizetype hashSizeBytes{32}; + + QByteArray saltArray{hashSizeBytes, char{0}}; + + auto generator = QRandomGenerator::global(); + generator->fillRange((uint32_t *) saltArray.data(), hashSizeBytes); + + return saltArray; +} +} // namespace rsshit + +// FIXME: use better password hashing algo +User::User(const QString &login, const QString &password) + : login{login} + , salt{rsshit::generateSalt()} + , passwordHash{hashPassword(password)} +{ + qDebug() << __func__ << "login:" << login; + qDebug() << __func__ << "salt size:" << salt.size(); + qDebug() << __func__ << "passwordHash size:" << passwordHash.size(); +} + +int User::getDbId() +{ + if (id != rsshit::db::IdNotFound) + return id; + + const auto db = rsshit::db::open(); + + if (!db) + return rsshit::db::IdNotFound; + + QSqlQuery selectQ{"select id from users where login=?"}; + selectQ.addBindValue(login); + + if (!selectQ.exec()) { + qWarning() << "cannot exec query" << selectQ.lastQuery() << ":" + << selectQ.lastError().text(); + + return rsshit::db::IdNotFound; + } + + if (!selectQ.next()) + return rsshit::db::IdNotFound; + + const auto idVariant = selectQ.value(rsshit::db::idTag); + + if (!idVariant.isValid() || !idVariant.canConvert<int>()) + return rsshit::db::IdNotFound; + + bool ok{false}; + const auto result = idVariant.toInt(&ok); + + if (!ok) { + qWarning() << "got invalid id from db:" << idVariant; + + return rsshit::db::IdNotFound; + } + + return result; +} + +int User::createInDb() +{ + if (id != rsshit::db::IdNotFound) + return id; + + const auto db = rsshit::db::open(); + + if (!db) + return rsshit::db::IdNotFound; + + if (login.isEmpty() || salt.isEmpty() || passwordHash.isEmpty()) + return rsshit::db::IdNotFound; + + QSqlQuery insertQ{"insert into users(login, salt, password_hash) values(?, ?, ?)"}; + insertQ.addBindValue(login); + insertQ.addBindValue(salt); + insertQ.addBindValue(passwordHash); + + if (!insertQ.exec()) { + qWarning() << "cannot exec query" << insertQ.lastQuery() << ":" + << insertQ.lastError().text(); + + return rsshit::db::IdNotFound; + } + + return insertQ.lastInsertId().toInt(); +} + +int User::getOrInsertDbId() +{ + const auto id = getDbId(); + + if (id != rsshit::db::IdNotFound) + return id; + + return createInDb(); +} + +bool User::verifyPassword(const QString &password) +{ + return hashPassword(password) == passwordHash; +} + +QByteArray User::hashPassword(const QString &password) +{ + return QCryptographicHash::hash(salt + password.toUtf8(), QCryptographicHash::Sha256); +} diff --git a/src/user.h b/src/user.h new file mode 100644 index 0000000..6826ed4 --- /dev/null +++ b/src/user.h @@ -0,0 +1,54 @@ +#pragma once + +#include <QString> + +// TODO: tests +// - empty fields +// - too long fields (use arrays instead of QByteArray?) +// - try to break salt/hash fields + +// TODO: make all public fields private +class User +{ +public: + /*! + * \brief User - create user with given `login` and `password`. `salt` and + * `passwordHash` will be generated and stored instead of real `password`. + * Will *not* be committed to db automatically. + * \param login - max len is 32 + * \param password - max len is 32 + */ + explicit User(const QString &login, const QString &password); + +public: + /*! + * \brief getDbId - check if user with corresponding `link` exists in db + * and return db id if any + * \return id on success, 0 otherwise + */ + int getDbId(); + + /*! + * \brief createInDb - create user in db + * \return new user id on success, 0 otherwise + */ + int createInDb(); + + /*! + * \brief getOrInsertDbId - get existing user id or try to create a new + * user and get its id + * \return existing or new user id on success, 0 otherwise + */ + int getOrInsertDbId(); + + bool verifyPassword(const QString &password); + +public: + int id{0}; + QString login; + QByteArray salt; + QByteArray passwordHash; + +private: + QByteArray hashPassword(const QString &password); +}; |
