#include "db.h" #ifdef VOICECAT_HAS_NET #include #include #include #include #include namespace voicecat::server { // ── Schema ──────────────────────────────────────────────────────────────────── static constexpr const char* kCreateSchema = R"sql( CREATE TABLE IF NOT EXISTS accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, pw_hash TEXT NOT NULL, is_admin INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, last_login INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS server_meta ( key TEXT PRIMARY KEY, value TEXT NOT NULL ); INSERT OR IGNORE INTO server_meta (key, value) VALUES ('schema_version', '1'); )sql"; // ── Database ────────────────────────────────────────────────────────────────── Database::Database(std::string path) : path_(std::move(path)) {} Database::~Database() { if (db_) { sqlite3_close(db_); db_ = nullptr; } } bool Database::open(std::string& error) { int rc = sqlite3_open(path_.c_str(), &db_); if (rc != SQLITE_OK) { error = sqlite3_errmsg(db_); sqlite3_close(db_); db_ = nullptr; return false; } sqlite3_busy_timeout(db_, 5000); // WAL mode for concurrency exec("PRAGMA journal_mode=WAL", error); exec("PRAGMA synchronous=NORMAL", error); error.clear(); if (!exec(kCreateSchema, error)) return false; return true; } bool Database::is_empty() { sqlite3_stmt* stmt = nullptr; sqlite3_prepare_v2(db_, "SELECT COUNT(*) FROM accounts", -1, &stmt, nullptr); int count = 0; if (sqlite3_step(stmt) == SQLITE_ROW) count = sqlite3_column_int(stmt, 0); sqlite3_finalize(stmt); return count == 0; } std::optional Database::create_account(const std::string& username, const std::string& password, bool is_admin, std::string& error) { if (username.empty() || password.empty()) { error = "username and password must not be empty"; return std::nullopt; } // Hash with Argon2id via libsodium char hash[crypto_pwhash_STRBYTES]; if (crypto_pwhash_str(hash, password.c_str(), password.size(), crypto_pwhash_OPSLIMIT_INTERACTIVE, crypto_pwhash_MEMLIMIT_INTERACTIVE) != 0) { error = "Argon2id hashing failed (OOM?)"; return std::nullopt; } int64_t now = now_unix(); sqlite3_stmt* stmt = nullptr; int rc = sqlite3_prepare_v2(db_, "INSERT INTO accounts (username, pw_hash, is_admin, created_at) VALUES (?,?,?,?)", -1, &stmt, nullptr); if (rc != SQLITE_OK) { error = sqlite3_errmsg(db_); return std::nullopt; } sqlite3_bind_text(stmt, 1, username.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, hash, -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, 3, is_admin ? 1 : 0); sqlite3_bind_int64(stmt, 4, now); rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { error = sqlite3_errmsg(db_); return std::nullopt; } Account acc; acc.id = sqlite3_last_insert_rowid(db_); acc.username = username; acc.is_admin = is_admin; acc.created_at = now; return acc; } bool Database::reset_password(const std::string& username, const std::string& new_password, std::string& error) { char hash[crypto_pwhash_STRBYTES]; if (crypto_pwhash_str(hash, new_password.c_str(), new_password.size(), crypto_pwhash_OPSLIMIT_INTERACTIVE, crypto_pwhash_MEMLIMIT_INTERACTIVE) != 0) { error = "Argon2id hashing failed"; return false; } sqlite3_stmt* stmt = nullptr; sqlite3_prepare_v2(db_, "UPDATE accounts SET pw_hash=? WHERE username=?", -1, &stmt, nullptr); sqlite3_bind_text(stmt, 1, hash, -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, username.c_str(), -1, SQLITE_TRANSIENT); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { error = sqlite3_errmsg(db_); return false; } if (sqlite3_changes(db_) == 0) { error = "user not found: " + username; return false; } return true; } bool Database::delete_account(const std::string& username, std::string& error) { sqlite3_stmt* stmt = nullptr; sqlite3_prepare_v2(db_, "DELETE FROM accounts WHERE username=?", -1, &stmt, nullptr); sqlite3_bind_text(stmt, 1, username.c_str(), -1, SQLITE_TRANSIENT); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { error = sqlite3_errmsg(db_); return false; } if (sqlite3_changes(db_) == 0) { error = "user not found: " + username; return false; } return true; } std::vector Database::list_accounts() { std::vector result; sqlite3_stmt* stmt = nullptr; sqlite3_prepare_v2(db_, "SELECT id, username, is_admin, created_at, last_login FROM accounts ORDER BY username", -1, &stmt, nullptr); while (sqlite3_step(stmt) == SQLITE_ROW) { Account acc; acc.id = sqlite3_column_int64(stmt, 0); acc.username = reinterpret_cast(sqlite3_column_text(stmt, 1)); acc.is_admin = sqlite3_column_int(stmt, 2) != 0; acc.created_at = sqlite3_column_int64(stmt, 3); acc.last_login = sqlite3_column_int64(stmt, 4); result.push_back(acc); } sqlite3_finalize(stmt); return result; } std::optional Database::authenticate(const std::string& username, const std::string& password) { sqlite3_stmt* stmt = nullptr; sqlite3_prepare_v2(db_, "SELECT id, pw_hash, is_admin, created_at, last_login FROM accounts WHERE username=?", -1, &stmt, nullptr); sqlite3_bind_text(stmt, 1, username.c_str(), -1, SQLITE_TRANSIENT); int rc = sqlite3_step(stmt); if (rc != SQLITE_ROW) { sqlite3_finalize(stmt); return std::nullopt; } int64_t id = sqlite3_column_int64(stmt, 0); std::string hash = reinterpret_cast(sqlite3_column_text(stmt, 1)); bool is_admin = sqlite3_column_int(stmt, 2) != 0; int64_t created = sqlite3_column_int64(stmt, 3); sqlite3_finalize(stmt); // Verify Argon2id — deliberately slow if (crypto_pwhash_str_verify(hash.c_str(), password.c_str(), password.size()) != 0) return std::nullopt; // Update last_login int64_t now = now_unix(); sqlite3_stmt* upd = nullptr; sqlite3_prepare_v2(db_, "UPDATE accounts SET last_login=? WHERE id=?", -1, &upd, nullptr); sqlite3_bind_int64(upd, 1, now); sqlite3_bind_int64(upd, 2, id); sqlite3_step(upd); sqlite3_finalize(upd); Account acc; acc.id = id; acc.username = username; acc.is_admin = is_admin; acc.created_at = created; acc.last_login = now; return acc; } std::string Database::generate_password(size_t length) { static const char kAlphabet[] = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789!@#$%^&*"; constexpr size_t kAlphaLen = sizeof(kAlphabet) - 1; std::string pw; pw.reserve(length); for (size_t i = 0; i < length; ++i) { uint8_t rnd[1]; randombytes_buf(rnd, 1); pw += kAlphabet[rnd[0] % kAlphaLen]; } return pw; } bool Database::exec(const std::string& sql, std::string& error) { char* errmsg = nullptr; int rc = sqlite3_exec(db_, sql.c_str(), nullptr, nullptr, &errmsg); if (rc != SQLITE_OK) { error = errmsg ? errmsg : "unknown error"; if (errmsg) sqlite3_free(errmsg); return false; } return true; } int64_t Database::now_unix() const { using namespace std::chrono; return duration_cast(system_clock::now().time_since_epoch()).count(); } } // namespace voicecat::server #endif // VOICECAT_HAS_NET