|
|
@@ -2,6 +2,7 @@
|
|
|
|
|
|
#include <spdlog/spdlog.h>
|
|
|
#include <nlohmann/json.hpp>
|
|
|
+#include <openssl/rand.h>
|
|
|
|
|
|
#include <algorithm>
|
|
|
#include <fstream>
|
|
|
@@ -35,7 +36,26 @@ bool MigrationRunner::runMigrations() {
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
- spdlog::info("Found {} migration files, checking for pending...", files.size());
|
|
|
+ // Snapshot "fresh install" state before applying anything — operations
|
|
|
+ // that need the distinction (e.g. upsert_setting only_on_fresh_install)
|
|
|
+ // must see a stable value across the whole batch.
|
|
|
+ //
|
|
|
+ // We use the `settings` collection rather than `_migrations` because
|
|
|
+ // `_migrations` entries are written by this runner *before* the
|
|
|
+ // PersistenceManager has started, so they are not captured in the WAL
|
|
|
+ // and do not survive across DB restarts. The `settings` collection is
|
|
|
+ // populated by actual user/runtime writes that go through the normal
|
|
|
+ // persistence path, so its presence is a reliable "this DB has been
|
|
|
+ // used before" signal.
|
|
|
+ {
|
|
|
+ Query q;
|
|
|
+ q.limit = 1;
|
|
|
+ auto res = store_.find("settings", q);
|
|
|
+ fresh_install_at_start_ = res.documents.empty();
|
|
|
+ }
|
|
|
+
|
|
|
+ spdlog::info("Found {} migration files, checking for pending... (fresh_install={})",
|
|
|
+ files.size(), fresh_install_at_start_);
|
|
|
|
|
|
int applied = 0;
|
|
|
int skipped = 0;
|
|
|
@@ -425,6 +445,91 @@ bool MigrationRunner::applyOperation(const nlohmann::json& operation) {
|
|
|
return true; // Success even if collection didn't exist
|
|
|
}
|
|
|
|
|
|
+ if (type == "upsert_setting") {
|
|
|
+ // Settings-collection shortcut: writes {value: ...} to
|
|
|
+ // {collection}/{key}. If `only_on_fresh_install` is set AND we
|
|
|
+ // started this run on an already-migrated database AND the key
|
|
|
+ // does not yet exist, use that value instead of `value`. If the
|
|
|
+ // key already exists on an upgrade, leave it alone. On a fresh
|
|
|
+ // install `value` always wins.
|
|
|
+ std::string collection = operation.value("collection", "");
|
|
|
+ std::string key = operation.value("key", "");
|
|
|
+ if (collection.empty() || key.empty() || !operation.contains("value")) {
|
|
|
+ spdlog::error("upsert_setting: missing collection, key, or value");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ const bool exists = store_.exists(collection, key);
|
|
|
+ std::string value_to_write;
|
|
|
+
|
|
|
+ if (fresh_install_at_start_) {
|
|
|
+ // Fresh install → always use `value`.
|
|
|
+ value_to_write = operation["value"].get<std::string>();
|
|
|
+ } else if (!exists && operation.contains("only_on_fresh_install")) {
|
|
|
+ // Upgrade with missing key and an explicit upgrade-default.
|
|
|
+ value_to_write = operation["only_on_fresh_install"].get<std::string>();
|
|
|
+ } else if (!exists) {
|
|
|
+ // Upgrade with missing key, no upgrade-default supplied →
|
|
|
+ // fall back to `value` so the migration still seeds the key.
|
|
|
+ value_to_write = operation["value"].get<std::string>();
|
|
|
+ } else {
|
|
|
+ // Upgrade, key already set → preserve operator's choice.
|
|
|
+ spdlog::debug("upsert_setting '{}/{}': already set, preserving", collection, key);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ Document doc;
|
|
|
+ doc.id = key;
|
|
|
+ doc.data = nlohmann::json{{"value", value_to_write}};
|
|
|
+ store_.upsert(collection, doc);
|
|
|
+ spdlog::debug("upsert_setting '{}/{}' = '{}' (fresh_install={})",
|
|
|
+ collection, key, value_to_write, fresh_install_at_start_);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (type == "generate_secret") {
|
|
|
+ // Cryptographic random hex string, upserted once. Never rotated
|
|
|
+ // by a re-run: if the key already exists, we keep the current
|
|
|
+ // value. This lets the same migration ID remain in the history
|
|
|
+ // without worrying about secret churn.
|
|
|
+ std::string collection = operation.value("collection", "");
|
|
|
+ std::string key = operation.value("key", "");
|
|
|
+ uint32_t bytes = operation.value("bytes", 32U);
|
|
|
+
|
|
|
+ if (collection.empty() || key.empty()) {
|
|
|
+ spdlog::error("generate_secret: missing collection or key");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (bytes == 0 || bytes > 256) {
|
|
|
+ spdlog::error("generate_secret: bytes out of range (1..256), got {}", bytes);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (store_.exists(collection, key)) {
|
|
|
+ spdlog::debug("generate_secret '{}/{}': already present, preserving", collection, key);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ std::vector<unsigned char> buf(bytes);
|
|
|
+ if (RAND_bytes(buf.data(), static_cast<int>(bytes)) != 1) {
|
|
|
+ spdlog::error("generate_secret: RAND_bytes failed");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ std::stringstream hex;
|
|
|
+ hex << std::hex << std::setfill('0');
|
|
|
+ for (auto b : buf) {
|
|
|
+ hex << std::setw(2) << static_cast<int>(b);
|
|
|
+ }
|
|
|
+
|
|
|
+ Document doc;
|
|
|
+ doc.id = key;
|
|
|
+ doc.data = nlohmann::json{{"value", hex.str()}};
|
|
|
+ store_.upsert(collection, doc);
|
|
|
+ spdlog::info("generate_secret '{}/{}': {}-byte secret generated", collection, key, bytes);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
spdlog::error("Unknown operation type: {}", type);
|
|
|
return false;
|
|
|
}
|