#include "store/pairings.hpp" #include #include #include "util/crypto.hpp" namespace velox::daemon::store { namespace { std::string uuid4() { std::random_device rd; std::uniform_int_distribution d; std::uint32_t a = d(rd), b = d(rd), c = d(rd), e = d(rd); b = (b & 0xFFFF0FFFu) | 0x00004000u; c = (c & 0x3FFFFFFFu) | 0x80000000u; char buf[37]; std::snprintf(buf, sizeof(buf), "%08x-%04x-%04x-%04x-%04x%08x", a, (b >> 16), (b & 0xFFFF), (c >> 16), (c & 0xFFFF), e); return std::string(buf); } Pairing read_row(Stmt& s) { Pairing p; p.pairing_id = s.column_text(0); p.origin = s.column_text(1); p.label = s.column_text(2); p.created_at = s.column_text(3); if (!s.column_is_null(4)) p.last_seen_at = s.column_text(4); if (!s.column_is_null(5)) p.revoked_at = s.column_text(5); return p; } constexpr std::string_view kCols = "pairing_id, origin, label, created_at, last_seen_at, revoked_at"; } // namespace DbResult Pairings::create(std::string_view origin, std::string_view label, std::string_view now_iso) { Created out{uuid4(), velox::daemon::crypto::random_token(32)}; const std::string hash = velox::daemon::crypto::sha256_hex(out.token); auto st = db_.prepare( "INSERT INTO pairings(pairing_id, token_sha256, origin, label, created_at) " "VALUES(?1, ?2, ?3, ?4, ?5)"); if (!st) return std::unexpected(st.error()); if (auto r = st->bind(1, out.pairing_id); !r) return std::unexpected(r.error()); if (auto r = st->bind(2, std::string_view(hash)); !r) return std::unexpected(r.error()); if (auto r = st->bind(3, origin); !r) return std::unexpected(r.error()); if (auto r = st->bind(4, label); !r) return std::unexpected(r.error()); if (auto r = st->bind(5, now_iso); !r) return std::unexpected(r.error()); if (auto r = st->step(); !r) return std::unexpected(r.error()); return out; } DbResult> Pairings::find_active_by_token(std::string_view token) { const std::string hash = velox::daemon::crypto::sha256_hex(token); auto st = db_.prepare(std::string("SELECT ").append(kCols).append( " FROM pairings WHERE token_sha256 = ?1 AND revoked_at IS NULL")); if (!st) return std::unexpected(st.error()); if (auto r = st->bind(1, std::string_view(hash)); !r) return std::unexpected(r.error()); auto row = st->step(); if (!row) return std::unexpected(row.error()); if (!*row) return std::optional{}; return std::optional{read_row(*st)}; } DbResult Pairings::touch(std::string_view pairing_id, std::string_view now_iso) { auto st = db_.prepare("UPDATE pairings SET last_seen_at = ?2 WHERE pairing_id = ?1"); if (!st) return std::unexpected(st.error()); if (auto r = st->bind(1, pairing_id); !r) return std::unexpected(r.error()); if (auto r = st->bind(2, now_iso); !r) return std::unexpected(r.error()); if (auto r = st->step(); !r) return std::unexpected(r.error()); return {}; } DbResult Pairings::revoke(std::string_view pairing_id, std::string_view now_iso) { auto st = db_.prepare( "UPDATE pairings SET revoked_at = ?2 WHERE pairing_id = ?1 AND revoked_at IS NULL"); if (!st) return std::unexpected(st.error()); if (auto r = st->bind(1, pairing_id); !r) return std::unexpected(r.error()); if (auto r = st->bind(2, now_iso); !r) return std::unexpected(r.error()); if (auto r = st->step(); !r) return std::unexpected(r.error()); return sqlite3_changes(db_.raw()) > 0; } DbResult> Pairings::list_active() { auto st = db_.prepare(std::string("SELECT ").append(kCols).append( " FROM pairings WHERE revoked_at IS NULL ORDER BY created_at")); if (!st) return std::unexpected(st.error()); std::vector out; for (;;) { auto row = st->step(); if (!row) return std::unexpected(row.error()); if (!*row) break; out.push_back(read_row(*st)); } return out; } } // namespace velox::daemon::store