Release v0.5.1: namespace integrity, dashboard parity and QR hardening

This commit is contained in:
thakares committed 2026-06-20 19:54:29 +05:30
1 parent 0295b4bd7c
commit 115f6e9a23
45 files changed
+5851 -1765

No files matched your search

+67 -3
View File
@@ -22,11 +22,23 @@ pub async fn run(
println!("Data directory: {:?}", config.data_dir);
println!();
let databases = ["admin", "content", "analytics", "system"];
let mut all_healthy = true;
for db_name in &databases {
let db_path = config.data_dir.join(format!("{}.db", db_name));
// Define target databases in the new layout
let admin_dir = config.data_dir.join("admin");
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")),
];
for (db_name, db_path) in dbs {
// Skip legacy databases if they don't exist
if db_name.starts_with("legacy") && !db_path.exists() {
continue;
}
if !db_path.exists() {
println!("Database: {}", db_name);
@@ -75,6 +87,58 @@ pub async fn run(
println!();
}
// Global Slug Registry Integrity Check
println!("Global Slug Registry Integrity Check");
println!("====================================");
let system_db_path = admin_dir.join("system.db");
let users_db_path = admin_dir.join("users.db");
if system_db_path.exists() && users_db_path.exists() {
match (
Connection::open(&system_db_path),
Connection::open(&users_db_path),
) {
(Ok(sys_conn), Ok(usr_conn)) => {
match crate::db::users::verify_global_slug_registry_integrity(
&sys_conn,
&usr_conn,
&config.data_dir,
) {
Ok((errors, warnings)) => {
if errors.is_empty() && warnings.is_empty() {
println!(" Status: HEALTHY (no issues found)");
} else {
if !errors.is_empty() {
println!(" Errors (Action Required):");
for err in &errors {
println!(" - {}", err);
}
all_healthy = false;
}
if !warnings.is_empty() {
println!(" Warnings (Attention Needed):");
for warn in &warnings {
println!(" - {}", warn);
}
}
}
}
Err(e) => {
println!(" Status: ERROR running integrity check: {}", e);
all_healthy = false;
}
}
}
_ => {
println!(" Status: ERROR opening system.db or users.db for integrity check");
all_healthy = false;
}
}
} else {
println!(" Status: SKIPPED (system.db/users.db not found)");
}
println!();
println!("--------------------");
if all_healthy {
println!("Overall status: HEALTHY");
+83 -22
View File
@@ -15,34 +15,95 @@ pub fn perform_restore(
let tar_gz = GzDecoder::new(f);
let mut archive = Archive::new(tar_gz);
// 2. Validate that the archive contains the expected BZOD database files
let mut has_admin = false;
let mut has_content = false;
let mut has_analytics = false;
let mut has_system = false;
// 2. Unpack to temporary directory first
let temp_dir =
std::env::temp_dir().join(format!("bzod_system_restore_{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&temp_dir)?;
for entry_res in archive.entries()? {
let entry = entry_res?;
let path = entry.path()?;
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
match file_name {
"admin.db" => has_admin = true,
"content.db" => has_content = true,
"analytics.db" => has_analytics = true,
"system.db" => has_system = true,
_ => {}
if let Err(e) = archive.unpack(&temp_dir) {
let _ = std::fs::remove_dir_all(&temp_dir);
return Err(e.into());
}
// 3. Run validation on temp_dir
let mut temp_config = Config::load();
temp_config.data_dir = temp_dir.clone();
// Namespace audit
match crate::db::users::audit_slug_namespace(&temp_config) {
Ok(report) => {
if !report.duplicates.is_empty() {
let _ = std::fs::remove_dir_all(&temp_dir);
return Err(
format!("Slug conflicts detected in backup: {:?}", report.duplicates).into(),
);
}
}
Err(e) => {
let _ = std::fs::remove_dir_all(&temp_dir);
return Err(format!("Failed to audit slug namespace in backup: {}", e).into());
}
}
if !has_admin || !has_content || !has_analytics || !has_system {
return Err("Archive is missing one or more required database files (admin.db, content.db, analytics.db, system.db)".into());
// Registry integrity check
let system_db_path = if temp_dir.join("admin/system.db").exists() {
temp_dir.join("admin/system.db")
} else {
temp_dir.join("system.db")
};
let users_db_path = if temp_dir.join("admin/users.db").exists() {
temp_dir.join("admin/users.db")
} else {
temp_dir.join("users.db")
};
if system_db_path.exists() && users_db_path.exists() {
let system_conn = rusqlite::Connection::open(&system_db_path)?;
let users_conn = rusqlite::Connection::open(&users_db_path)?;
match crate::db::users::verify_global_slug_registry_integrity(
&system_conn,
&users_conn,
&temp_dir,
) {
Ok((errors, _warnings)) => {
if !errors.is_empty() {
let _ = std::fs::remove_dir_all(&temp_dir);
return Err(format!("Registry integrity errors in backup: {:?}", errors).into());
}
}
Err(e) => {
let _ = std::fs::remove_dir_all(&temp_dir);
return Err(format!("Failed to verify registry integrity in backup: {}", e).into());
}
}
}
// 3. Unpack archive to data_dir
let f2 = File::open(file_path)?;
let tar_gz2 = GzDecoder::new(f2);
let mut archive2 = Archive::new(tar_gz2);
archive2.unpack(data_dir)?;
// 4. If validation succeeds, copy temp_dir contents to data_dir
if data_dir.exists() {
let _ = std::fs::remove_dir_all(data_dir);
}
std::fs::create_dir_all(data_dir)?;
fn copy_dir_all(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
if ty.is_dir() {
copy_dir_all(&entry.path(), &dst.join(entry.file_name()))?;
} else {
std::fs::copy(entry.path(), dst.join(entry.file_name()))?;
}
}
Ok(())
}
if let Err(e) = copy_dir_all(&temp_dir, data_dir) {
let _ = std::fs::remove_dir_all(&temp_dir);
return Err(format!("Failed to copy restored files: {}", e).into());
}
let _ = std::fs::remove_dir_all(&temp_dir);
Ok(())
}
+7 -94
View File
@@ -1,7 +1,5 @@
use crate::config::Config;
use crate::db::Db;
use chrono::Utc;
use rusqlite::OptionalExtension;
use std::fs::File;
use std::path::PathBuf;
use tar::Archive;
@@ -158,103 +156,18 @@ pub async fn run(
}
}
// 4. Register slugs in global_slugs
let restored_content_conn = rusqlite::Connection::open(dest_dir.join("content.db"))?;
// 4. Register slugs in global_slugs using the shared helper
{
let mut system_conn = db.system.lock().unwrap();
let tx = system_conn.transaction()?;
// Delete any existing global slugs owned by this user
tx.execute(
"DELETE FROM global_slugs WHERE owner_user_id = ?1;",
[target_user_id],
let system_conn = db.system.lock().unwrap();
crate::db::users::register_restored_user_slugs(
&system_conn,
target_user_id,
&dest_dir.join("content.db"),
)?;
// Register URLs
{
let mut stmt =
restored_content_conn.prepare("SELECT code, id, created_at, status FROM urls;")?;
let mut rows = stmt.query([])?;
while let Some(row) = rows.next()? {
let slug: String = row.get(0)?;
let target_id: String = row.get(1)?;
let created_at: String = row.get(2)?;
let status: String = row.get(3)?;
let now = Utc::now().to_rfc3339();
let existing_owner: Option<i64> = tx
.query_row(
"SELECT owner_user_id FROM global_slugs WHERE slug = ?1;",
[&slug],
|r| r.get(0),
)
.optional()?;
if let Some(owner) = existing_owner {
if owner != target_user_id {
error!(
"Conflict: Slug '{}' is already owned by user ID {}. Skipping.",
slug, owner
);
continue;
}
}
tx.execute(
"INSERT OR REPLACE INTO global_slugs (slug, owner_user_id, target_type, target_id, created_at, updated_at, status)
VALUES (?1, ?2, 'url', ?3, ?4, ?5, ?6);",
rusqlite::params![slug, target_user_id, target_id, created_at, now, status],
)?;
}
}
// Register Landing Pages
{
let mut stmt = restored_content_conn
.prepare("SELECT code, id, created_at, state FROM landing_pages;")?;
let mut rows = stmt.query([])?;
while let Some(row) = rows.next()? {
let slug: String = row.get(0)?;
let target_id: String = row.get(1)?;
let created_at: String = row.get(2)?;
let state: String = row.get(3)?;
let now = Utc::now().to_rfc3339();
let status = if state == "published" {
"active"
} else {
"disabled"
};
let existing_owner: Option<i64> = tx
.query_row(
"SELECT owner_user_id FROM global_slugs WHERE slug = ?1;",
[&slug],
|r| r.get(0),
)
.optional()?;
if let Some(owner) = existing_owner {
if owner != target_user_id {
error!(
"Conflict: Slug '{}' is already owned by user ID {}. Skipping.",
slug, owner
);
continue;
}
}
tx.execute(
"INSERT OR REPLACE INTO global_slugs (slug, owner_user_id, target_type, target_id, created_at, updated_at, status)
VALUES (?1, ?2, 'page', ?3, ?4, ?5, ?6);",
rusqlite::params![slug, target_user_id, target_id, created_at, now, status],
)?;
}
}
tx.commit()?;
}
// 5. Reconcile quotas for restored user
let restored_content_conn = rusqlite::Connection::open(dest_dir.join("content.db"))?;
crate::db::users::reconcile_user_quotas(
&db.users.lock().unwrap(),
target_user_id,
+29 -7
View File
@@ -33,7 +33,16 @@ pub async fn run(
None => crate::utils::random::generate_token(3),
};
// 3. Persist URL
// 3. Register slug in system.db with status 'reserving' and check availability
{
let system_conn = db.system.lock().unwrap();
if !crate::db::users::is_slug_available(&system_conn, &code)? {
return Err("Short code/slug already exists".into());
}
crate::db::users::register_global_slug(&system_conn, &code, 1, "url", "", "reserving")?;
}
// 4. Persist URL
let conn = db.content.lock().unwrap();
let res = crate::db::content::create_url_extended(
&conn,
@@ -48,7 +57,21 @@ pub async fn run(
);
match res {
Ok(_) => {
Ok(url) => {
// Activate slug in system.db
{
let system_conn = db.system.lock().unwrap();
system_conn.execute(
"UPDATE global_slugs SET target_id = ?1, status = 'active', updated_at = ?2 WHERE slug = ?3;",
rusqlite::params![url.id, chrono::Utc::now().to_rfc3339(), code],
)?;
}
// Increment quota for user ID 1
{
let users_conn = db.users.lock().unwrap();
crate::db::users::increment_quota_counter(&users_conn, 1, "urls")?;
}
let proto = if config.cookie_secure {
"https"
} else {
@@ -63,11 +86,10 @@ pub async fn run(
println!("{}/{}", base_url, code);
Ok(())
}
Err(rusqlite::Error::SqliteFailure(err, _))
if err.code == rusqlite::ErrorCode::ConstraintViolation =>
{
Err("Short code/slug already exists".into())
Err(e) => {
let system_conn = db.system.lock().unwrap();
let _ = crate::db::users::release_global_slug(&system_conn, &code, 1);
Err(e.into())
}
Err(e) => Err(e.into()),
}
}