Release v0.5.2

- Add admin content consistency diagnostics
- Add admin-migrate CLI
- Harden RBAC for API endpoints
- Normalize multi-tenant storage paths
- Improve backup and restore compatibility
- Fix administrator routing consistency
- Improve doctor and stats commands
This commit is contained in:
thakares committed 2026-06-29 16:11:43 +05:30
1 parent a32c0fd7ca
commit 2761863c14
14 files changed
+437 -75

No files matched your search

+152
View File
@@ -0,0 +1,152 @@
use crate::config::Config;
use crate::db::Db;
use rusqlite::Connection;
use std::path::PathBuf;
use tracing::{error, info};
pub async fn run(
target_admin_id: i64,
data_dir: Option<String>,
dry_run: bool,
force: bool,
mut config: Config,
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(d) = data_dir {
config.data_dir = PathBuf::from(d);
}
let db = Db::init(&config)?;
// 1. Verify target admin exists and is an admin
let target_user = {
let conn = db.users.lock().unwrap();
crate::db::users::get_user_by_id(&conn, target_admin_id)?
};
let target_user = match target_user {
Some(u) => u,
None => {
error!("Target admin ID {} not found", target_admin_id);
return Ok(());
}
};
if target_user.account_type != "admin" {
error!(
"Target user '{}' (ID {}) is not an admin account.",
target_user.username, target_admin_id
);
return Ok(());
}
if target_admin_id == 1 {
error!("Target admin ID cannot be 1 (legacy admin).");
return Ok(());
}
// 2. Open databases
let legacy_content_path = config.data_dir.join("users").join("1").join("content.db");
if !legacy_content_path.exists() {
info!(
"No legacy admin content database found at {:?}",
legacy_content_path
);
return Ok(());
}
db.init_user_databases(target_admin_id)?;
let target_content_path = config
.data_dir
.join("users")
.join(target_admin_id.to_string())
.join("content.db");
let mut legacy_conn = Connection::open(&legacy_content_path)?;
let mut target_conn = Connection::open(&target_content_path)?;
let mut system_conn = db.system.lock().unwrap();
println!("Scanning legacy admin content database...");
// 3. Count items
let urls = {
let mut stmt = legacy_conn.prepare("SELECT * FROM urls;")?;
let mut rows = stmt.query([])?;
let mut data = Vec::new();
while let Ok(Some(_)) = rows.next() {
data.push(1);
}
data
};
let url_count = urls.len();
let pages = {
let mut stmt = legacy_conn.prepare("SELECT * FROM landing_pages;")?;
let mut rows = stmt.query([])?;
let mut data = Vec::new();
while let Ok(Some(_)) = rows.next() {
data.push(1);
}
data
};
let page_count = pages.len();
println!(
"Found {} URLs and {} Landing Pages owned by legacy admin (ID 1).",
url_count, page_count
);
if dry_run {
println!("Dry run mode enabled. No changes will be made.");
return Ok(());
}
if !force {
println!("Migration requires the --force flag to execute. Aborting.");
return Ok(());
}
println!(
"Starting migration to Admin '{}' (ID {})...",
target_user.username, target_admin_id
);
// 4. Perform Migration (using ATTACH DATABASE for fast copy)
// We attach the legacy db to the target db to do INSERT INTO ... SELECT * FROM
target_conn.execute(
"ATTACH DATABASE ?1 AS legacy;",
rusqlite::params![legacy_content_path.to_string_lossy()],
)?;
let tx = target_conn.transaction()?;
tx.execute("INSERT OR IGNORE INTO urls SELECT * FROM legacy.urls;", [])?;
tx.execute(
"INSERT OR IGNORE INTO landing_pages SELECT * FROM legacy.landing_pages;",
[],
)?;
tx.commit()?;
target_conn.execute("DETACH DATABASE legacy;", [])?;
// 5. Update global registry
let sys_tx = system_conn.transaction()?;
let updated_slugs = sys_tx.execute(
"UPDATE global_slugs SET owner_user_id = ?1 WHERE owner_user_id = 1;",
rusqlite::params![target_admin_id],
)?;
sys_tx.commit()?;
// 6. Delete from legacy
let legacy_tx = legacy_conn.transaction()?;
legacy_tx.execute("DELETE FROM urls;", [])?;
legacy_tx.execute("DELETE FROM landing_pages;", [])?;
legacy_tx.commit()?;
println!("Migration Complete!");
println!("-------------------");
println!("Migrated {} URLs.", url_count);
println!("Migrated {} Landing Pages.", page_count);
println!("Updated {} slugs in global registry.", updated_slugs);
println!("Cleared legacy content database.");
Ok(())
}
+4 -2
View File
@@ -26,12 +26,14 @@ pub async fn run(
// Define target databases in the new layout
let admin_dir = config.data_dir.join("admin");
let legacy_user_dir = config.data_dir.join("users").join("1");
let dbs = vec![
("admin", admin_dir.join("admin.db")),
("system", admin_dir.join("system.db")),
("users", admin_dir.join("users.db")),
("legacy content", config.data_dir.join("content.db")),
("legacy analytics", config.data_dir.join("analytics.db")),
("legacy content", legacy_user_dir.join("content.db")),
("legacy analytics", legacy_user_dir.join("analytics.db")),
];
for (db_name, db_path) in dbs {
+14
View File
@@ -11,6 +11,7 @@ pub mod shorten;
pub mod stats;
pub mod validate;
pub mod admin_migrate;
pub mod backup_user;
pub mod create_user;
pub mod delete_user;
@@ -165,4 +166,17 @@ pub enum Commands {
#[arg(long)]
data_dir: Option<String>,
},
/// FUTURE: Migrate legacy admin content to a specific admin tenant database
AdminMigrate {
/// Target Admin ID
target_admin_id: i64,
#[arg(long)]
data_dir: Option<String>,
/// Preview what would be moved without making changes
#[arg(long)]
dry_run: bool,
/// Force the migration to execute
#[arg(long)]
force: bool,
},
}
+18 -6
View File
@@ -14,14 +14,26 @@ pub async fn run(
println!("=== BZOD Database Stats ===");
println!("Storage Directory: {:?}", config.data_dir);
let files = vec!["admin.db", "content.db", "analytics.db", "system.db"];
for f in files {
let p = config.data_dir.join(f);
if p.exists() {
let sz = std::fs::metadata(&p)?.len();
let files = vec![
("admin.db", config.data_dir.join("admin/admin.db")),
("system.db", config.data_dir.join("admin/system.db")),
("users.db", config.data_dir.join("admin/users.db")),
(
"legacy content.db",
config.data_dir.join("users/1/content.db"),
),
(
"legacy analytics.db",
config.data_dir.join("users/1/analytics.db"),
),
];
for (name, path) in files {
if path.exists() {
let sz = std::fs::metadata(&path)?.len();
println!(
" File: {} - Size: {} bytes ({:.2} MB)",
f,
name,
sz,
sz as f64 / 1_048_576.0
);