summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNikita Kostovsky <nikita@kostovsky.me>2025-06-22 21:39:13 +0200
committerNikita Kostovsky <nikita@kostovsky.me>2025-06-22 21:39:13 +0200
commitc9fcceb74d861525b2defec8219374edb9c1455a (patch)
treefc10d7c2abcb08db76946e0bfd73f6b93a652422
parentf674e179d602d3ccb9818d28fe06f371059449dc (diff)
add User class
-rw-r--r--CMakeLists.txt6
-rw-r--r--src/atomchannel.cpp22
-rw-r--r--src/atomchannel.h5
-rw-r--r--src/atomitem.cpp12
-rw-r--r--src/atomitem.h4
-rw-r--r--src/main.cpp11
-rw-r--r--src/sql/db.sqlitebin36864 -> 110592 bytes
-rw-r--r--src/typedefs.cpp1
-rw-r--r--src/typedefs.h24
-rw-r--r--src/user.cpp123
-rw-r--r--src/user.h54
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
index c982736..aefa4d5 100644
--- a/src/sql/db.sqlite
+++ b/src/sql/db.sqlite
Binary files differ
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);
+};