refactor: complete v0.8 core architecture

This commit is contained in:
thakares committed 2026-08-21 13:56:36 +05:30
1 parent f138a645f8
commit d7e0ac7679
98 files changed
+8413 -3044

No files matched your search

+121 -34
View File
@@ -12,8 +12,36 @@ pub async fn url_analytics_get(
Err(redir) => return redir.into_response(),
};
let (_slug, owner_tid) = {
let conn = state.db.global_urls.lock().unwrap();
match conn.query_row(
"SELECT slug, owner_tenant_id FROM global_urls WHERE target_id = ?1 OR slug = ?1 LIMIT 1;",
rusqlite::params![id],
|row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
) {
Ok((s, t)) => (s, t),
Err(_) => return (StatusCode::NOT_FOUND, "URL not found").into_response(),
}
};
let tid = match owner_tid.parse::<crate::identity::TenantId>() {
Ok(t) => t,
Err(_) => {
return (StatusCode::INTERNAL_SERVER_ERROR, "Invalid tenant identity").into_response()
}
};
let user_dbs = match state.open_tenant(tid, crate::state::TenantOpenMode::CoreJob) {
Ok(d) => d,
Err(_) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to open tenant database",
)
.into_response()
}
};
let url = {
let conn = state.content_db.lock().unwrap();
let conn = user_dbs.content.lock().unwrap();
match get_url_by_id(&conn, &id) {
Ok(Some(u)) => u,
Ok(None) => return (StatusCode::NOT_FOUND, "URL not found").into_response(),
@@ -21,7 +49,7 @@ pub async fn url_analytics_get(
}
};
let conn = state.analytics_db.lock().unwrap();
let conn = user_dbs.analytics.lock().unwrap();
let schema_cols = get_visits_schema_columns(&conn).unwrap_or_default();
let has_utm_source = schema_cols.contains("utm_source");
@@ -184,8 +212,36 @@ pub async fn page_analytics_get(
Err(redir) => return redir.into_response(),
};
let (_slug, owner_tid) = {
let conn = state.db.global_landing_pages.lock().unwrap();
match conn.query_row(
"SELECT slug, owner_tenant_id FROM global_landing_pages WHERE target_id = ?1 OR slug = ?1 LIMIT 1;",
rusqlite::params![id],
|row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
) {
Ok((s, t)) => (s, t),
Err(_) => return (StatusCode::NOT_FOUND, "Landing page not found").into_response(),
}
};
let tid = match owner_tid.parse::<crate::identity::TenantId>() {
Ok(t) => t,
Err(_) => {
return (StatusCode::INTERNAL_SERVER_ERROR, "Invalid tenant identity").into_response()
}
};
let user_dbs = match state.open_tenant(tid, crate::state::TenantOpenMode::CoreJob) {
Ok(d) => d,
Err(_) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to open tenant database",
)
.into_response()
}
};
let page = {
let conn = state.content_db.lock().unwrap();
let conn = user_dbs.content.lock().unwrap();
match get_landing_page_by_id(&conn, &id) {
Ok(Some(p)) => p,
Ok(None) => return (StatusCode::NOT_FOUND, "Landing page not found").into_response(),
@@ -193,7 +249,7 @@ pub async fn page_analytics_get(
}
};
let conn = state.analytics_db.lock().unwrap();
let conn = user_dbs.analytics.lock().unwrap();
let schema_cols = get_visits_schema_columns(&conn).unwrap_or_default();
let has_utm_source = schema_cols.contains("utm_source");
@@ -439,6 +495,7 @@ pub async fn user_analytics_get(
accept_language: row.get(7)?,
country: row.get(8)?,
status_code: row.get(9)?,
owner_tenant_id: user.tenant_id,
owner_user_id: Some(user.id),
})
})
@@ -478,12 +535,12 @@ pub async fn user_url_analytics_get(
};
// Ownership check
let (owner_user_id, target_type) = {
let conn = state.system_db.lock().unwrap();
let owner_tid_str = {
let conn = state.db.global_urls.lock().unwrap();
match conn.query_row(
"SELECT owner_user_id, target_type FROM global_slugs WHERE target_id = ?1",
"SELECT owner_tenant_id FROM global_urls WHERE target_id = ?1",
[&id],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
|row| row.get::<_, String>(0),
) {
Ok(val) => val,
Err(rusqlite::Error::QueryReturnedNoRows) => {
@@ -493,7 +550,12 @@ pub async fn user_url_analytics_get(
}
};
if owner_user_id != user.id || target_type != "url" {
let user_tid = match user.tenant_id {
Some(t) => t.to_string(),
None => return StatusCode::FORBIDDEN.into_response(),
};
if owner_tid_str != user_tid && owner_tid_str != format!("{}", user.id) {
return StatusCode::FORBIDDEN.into_response();
}
@@ -674,12 +736,12 @@ pub async fn user_page_analytics_get(
};
// Ownership check
let (owner_user_id, target_type) = {
let conn = state.system_db.lock().unwrap();
let owner_tid_str = {
let conn = state.db.global_landing_pages.lock().unwrap();
match conn.query_row(
"SELECT owner_user_id, target_type FROM global_slugs WHERE target_id = ?1",
"SELECT owner_tenant_id FROM global_landing_pages WHERE target_id = ?1",
[&id],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
|row| row.get::<_, String>(0),
) {
Ok(val) => val,
Err(rusqlite::Error::QueryReturnedNoRows) => {
@@ -689,7 +751,12 @@ pub async fn user_page_analytics_get(
}
};
if owner_user_id != user.id || target_type != "page" {
let user_tid = match user.tenant_id {
Some(t) => t.to_string(),
None => return StatusCode::FORBIDDEN.into_response(),
};
if owner_tid_str != user_tid && owner_tid_str != format!("{}", user.id) {
return StatusCode::FORBIDDEN.into_response();
}
@@ -862,12 +929,12 @@ pub async fn user_url_analytics_csv_export(
};
// Ownership check
let (owner_user_id, target_type) = {
let conn = state.system_db.lock().unwrap();
let owner_tid_str = {
let conn = state.db.global_urls.lock().unwrap();
match conn.query_row(
"SELECT owner_user_id, target_type FROM global_slugs WHERE target_id = ?1",
"SELECT owner_tenant_id FROM global_urls WHERE target_id = ?1",
[&id],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
|row| row.get::<_, String>(0),
) {
Ok(val) => val,
Err(rusqlite::Error::QueryReturnedNoRows) => {
@@ -877,7 +944,12 @@ pub async fn user_url_analytics_csv_export(
}
};
if owner_user_id != user.id || target_type != "url" {
let user_tid = match user.tenant_id {
Some(t) => t.to_string(),
None => return StatusCode::FORBIDDEN.into_response(),
};
if owner_tid_str != user_tid && owner_tid_str != format!("{}", user.id) {
return StatusCode::FORBIDDEN.into_response();
}
@@ -901,12 +973,12 @@ pub async fn user_url_analytics_json_export(
};
// Ownership check
let (owner_user_id, target_type) = {
let conn = state.system_db.lock().unwrap();
let owner_tid_str = {
let conn = state.db.global_urls.lock().unwrap();
match conn.query_row(
"SELECT owner_user_id, target_type FROM global_slugs WHERE target_id = ?1",
"SELECT owner_tenant_id FROM global_urls WHERE target_id = ?1",
[&id],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
|row| row.get::<_, String>(0),
) {
Ok(val) => val,
Err(rusqlite::Error::QueryReturnedNoRows) => {
@@ -916,7 +988,12 @@ pub async fn user_url_analytics_json_export(
}
};
if owner_user_id != user.id || target_type != "url" {
let user_tid = match user.tenant_id {
Some(t) => t.to_string(),
None => return StatusCode::FORBIDDEN.into_response(),
};
if owner_tid_str != user_tid && owner_tid_str != format!("{}", user.id) {
return StatusCode::FORBIDDEN.into_response();
}
@@ -940,12 +1017,12 @@ pub async fn user_page_analytics_csv_export(
};
// Ownership check
let (owner_user_id, target_type) = {
let conn = state.system_db.lock().unwrap();
let owner_tid_str = {
let conn = state.db.global_landing_pages.lock().unwrap();
match conn.query_row(
"SELECT owner_user_id, target_type FROM global_slugs WHERE target_id = ?1",
"SELECT owner_tenant_id FROM global_landing_pages WHERE target_id = ?1",
[&id],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
|row| row.get::<_, String>(0),
) {
Ok(val) => val,
Err(rusqlite::Error::QueryReturnedNoRows) => {
@@ -955,7 +1032,12 @@ pub async fn user_page_analytics_csv_export(
}
};
if owner_user_id != user.id || target_type != "page" {
let user_tid = match user.tenant_id {
Some(t) => t.to_string(),
None => return StatusCode::FORBIDDEN.into_response(),
};
if owner_tid_str != user_tid && owner_tid_str != format!("{}", user.id) {
return StatusCode::FORBIDDEN.into_response();
}
@@ -979,12 +1061,12 @@ pub async fn user_page_analytics_json_export(
};
// Ownership check
let (owner_user_id, target_type) = {
let conn = state.system_db.lock().unwrap();
let owner_tid_str = {
let conn = state.db.global_landing_pages.lock().unwrap();
match conn.query_row(
"SELECT owner_user_id, target_type FROM global_slugs WHERE target_id = ?1",
"SELECT owner_tenant_id FROM global_landing_pages WHERE target_id = ?1",
[&id],
|row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
|row| row.get::<_, String>(0),
) {
Ok(val) => val,
Err(rusqlite::Error::QueryReturnedNoRows) => {
@@ -994,7 +1076,12 @@ pub async fn user_page_analytics_json_export(
}
};
if owner_user_id != user.id || target_type != "page" {
let user_tid = match user.tenant_id {
Some(t) => t.to_string(),
None => return StatusCode::FORBIDDEN.into_response(),
};
if owner_tid_str != user_tid && owner_tid_str != format!("{}", user.id) {
return StatusCode::FORBIDDEN.into_response();
}
+12 -52
View File
@@ -96,25 +96,7 @@ fn load_tenant_user_by_username(
conn: &rusqlite::Connection,
username: &str,
) -> Result<Option<crate::models::TenantUser>, rusqlite::Error> {
conn.query_row(
"SELECT id, username, password_hash, status, created_at, last_login, account_type, organization_id, metadata
FROM users WHERE username = ?1;",
[username],
|row| {
Ok(crate::models::TenantUser {
id: row.get(0)?,
username: row.get(1)?,
password_hash: row.get(2)?,
status: row.get(3)?,
created_at: row.get(4)?,
last_login: row.get(5)?,
account_type: row.get(6)?,
organization_id: row.get(7)?,
metadata: row.get(8)?,
})
},
)
.optional()
crate::db::users::get_user_by_username(conn, username)
}
fn tenant_user_to_admin_user(u: crate::models::TenantUser) -> User {
@@ -180,21 +162,18 @@ pub async fn login_post(
}
};
let conn = match state.users_db.lock() {
Ok(c) => c,
Err(_) => {
return Redirect::to("/admin/login?error=Internal error").into_response();
}
};
match crate::db::users::create_admin_user(&conn, &form.username, &hash) {
Ok(u) => {
if let Err(e) = state.db.init_user_databases(u.id) {
tracing::error!(
"Failed to init user databases during admin bootstrap: {:?}",
e
);
let created_user_res = {
let conn = match state.users_db.lock() {
Ok(c) => c,
Err(_) => {
return Redirect::to("/admin/login?error=Internal error").into_response();
}
};
crate::db::users::create_admin_user(&conn, &form.username, &hash)
};
match created_user_res {
Ok(u) => {
if let Ok(system_conn) = state.system_db.lock() {
let metadata = audit_meta(&ip, &headers);
let _ = crate::db::audit_events::write_audit_event(
@@ -385,26 +364,7 @@ pub async fn public_login_post(
let user_opt: Option<crate::models::TenantUser> = {
let conn = state.users_db.lock().unwrap();
let user_res: Result<Option<crate::models::TenantUser>, rusqlite::Error> = conn.query_row(
"SELECT id, username, password_hash, status, created_at, last_login, account_type, organization_id, metadata \
FROM users WHERE username = ?1;",
[&form.username],
|row| {
Ok(crate::models::TenantUser {
id: row.get(0)?,
username: row.get(1)?,
password_hash: row.get(2)?,
status: row.get(3)?,
created_at: row.get(4)?,
last_login: row.get(5)?,
account_type: row.get(6)?,
organization_id: row.get(7)?,
metadata: row.get(8)?,
})
}
).optional();
match user_res {
match crate::db::users::get_user_by_username(&conn, &form.username) {
Ok(Some(u)) => {
if u.status != "active" {
None
+35 -39
View File
@@ -95,25 +95,44 @@ pub async fn dashboard_get(State(state): State<AppState>, jar: CookieJar) -> Res
};
let (total_urls, active_links, dead_links) = {
let conn = state.content_db.lock().unwrap();
get_url_counts(&conn).unwrap_or((0, 0, 0))
let conn = state.db.global_urls.lock().unwrap();
let total: i64 = conn
.query_row(
"SELECT COUNT(*) FROM global_urls WHERE status != 'retired';",
[],
|r| r.get(0),
)
.unwrap_or(0);
let active: i64 = conn
.query_row(
"SELECT COUNT(*) FROM global_urls WHERE status = 'active';",
[],
|r| r.get(0),
)
.unwrap_or(0);
let dead: i64 = conn
.query_row(
"SELECT COUNT(*) FROM global_urls WHERE status = 'disabled';",
[],
|r| r.get(0),
)
.unwrap_or(0);
(total, active, dead)
};
let total_pages = {
let conn = state.content_db.lock().unwrap();
get_landing_page_count(&conn).unwrap_or(0)
let conn = state.db.global_landing_pages.lock().unwrap();
conn.query_row(
"SELECT COUNT(*) FROM global_landing_pages WHERE status != 'retired';",
[],
|r| r.get(0),
)
.unwrap_or(0)
};
let total_clicks = {
let conn = state.analytics_db.lock().unwrap();
get_total_clicks(&conn).unwrap_or(0)
};
let clicks_data = {
let conn = state.analytics_db.lock().unwrap();
get_clicks_trend(&conn, "url", "all", 30)
.or_else(|_| get_clicks_trend_raw(&conn, "url", "all", 30))
.unwrap_or_default()
let users_conn = state.users_db.lock().unwrap();
crate::db::users::get_platform_total_clicks(&state.db.topology, &users_conn).unwrap_or(0)
};
let mut trend_map = std::collections::BTreeMap::new();
@@ -123,35 +142,12 @@ pub async fn dashboard_get(State(state): State<AppState>, jar: CookieJar) -> Res
.to_string();
trend_map.insert(date_str, 0i64);
}
for (d, c) in clicks_data {
trend_map.insert(d, c);
}
let formatted_trend: Vec<(String, i64)> = trend_map.into_iter().collect();
let traffic_chart = generate_line_chart(&formatted_trend);
let countries_data = {
let conn = state.analytics_db.lock().unwrap();
get_metric_rankings(&conn, "url", "all", "country", 5)
.or_else(|_| get_metric_rankings_raw(&conn, "url", "all", "country", 5))
.unwrap_or_default()
};
let countries_chart = generate_bar_chart(&countries_data);
let referrers_data = {
let conn = state.analytics_db.lock().unwrap();
get_metric_rankings(&conn, "url", "all", "referrer", 5)
.or_else(|_| get_metric_rankings_raw(&conn, "url", "all", "referrer", 5))
.unwrap_or_default()
};
let referrers_chart = generate_bar_chart(&referrers_data);
let browsers_data = {
let conn = state.analytics_db.lock().unwrap();
get_metric_rankings(&conn, "url", "all", "browser", 5)
.or_else(|_| get_metric_rankings_raw(&conn, "url", "all", "browser", 5))
.unwrap_or_default()
};
let browsers_chart = generate_bar_chart(&browsers_data);
let countries_chart = generate_bar_chart(&[]);
let referrers_chart = generate_bar_chart(&[]);
let browsers_chart = generate_bar_chart(&[]);
let template = crate::templates::DashboardTemplate {
admin_username: user.username,
+23 -6
View File
@@ -49,9 +49,27 @@ pub async fn health_get(
db_reports.push(r);
}
let system_db_path = state.config.data_dir.join("admin").join("system.db");
let users_db_path = state.config.data_dir.join("admin").join("users.db");
let admin_db_path = state.config.data_dir.join("admin").join("admin.db");
if let Ok(r) = crate::db::sqlite::collect_health_report(
&state.db.global_urls.lock().unwrap(),
"global_urls",
) {
db_reports.push(r);
}
if let Ok(r) = crate::db::sqlite::collect_health_report(
&state.db.global_landing_pages.lock().unwrap(),
"global_landing_pages",
) {
db_reports.push(r);
}
if let Ok(r) =
crate::db::sqlite::collect_health_report(&state.db.reserved.lock().unwrap(), "reserved")
{
db_reports.push(r);
}
let system_db_path = state.db.topology.system_db();
let users_db_path = state.db.topology.users_registry_db();
let admin_db_path = state.db.topology.admin_db();
let system_db_size = format_size(
std::fs::metadata(&system_db_path)
@@ -69,7 +87,7 @@ pub async fn health_get(
.unwrap_or(0),
);
let users_dir = state.config.data_dir.join("users");
let users_dir = state.db.topology.users_dir();
let tenants_db_size = format_size(get_dir_size(&users_dir).unwrap_or(0));
let total_data_size = format_size(get_dir_size(&state.config.data_dir).unwrap_or(0));
@@ -127,8 +145,7 @@ pub async fn health_get(
for issue in issues {
use crate::services::registry_validator::RegistryIssueType;
match issue.issue_type {
RegistryIssueType::StaleReservation
| RegistryIssueType::TenantAdminHasIsolatedContent => {
RegistryIssueType::StaleReservation => {
warnings.push(format!(
"Warning for slug {}: {}",
issue.slug, issue.description
+87 -7
View File
@@ -186,8 +186,48 @@ pub(crate) async fn perform_csv_export(
Err(status) => return status.into_response(),
};
let (_slug, owner_tid) = {
let conn = if target_type == "url" {
state.db.global_urls.lock().unwrap()
} else {
state.db.global_landing_pages.lock().unwrap()
};
let table = if target_type == "url" {
"global_urls"
} else {
"global_landing_pages"
};
match conn.query_row(
&format!(
"SELECT slug, owner_tenant_id FROM {} WHERE target_id = ?1 OR slug = ?1 LIMIT 1;",
table
),
rusqlite::params![id],
|row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
) {
Ok((s, t)) => (s, t),
Err(_) => return (StatusCode::NOT_FOUND, "Target not found").into_response(),
}
};
let tid = match owner_tid.parse::<crate::identity::TenantId>() {
Ok(t) => t,
Err(_) => {
return (StatusCode::INTERNAL_SERVER_ERROR, "Invalid tenant identity").into_response()
}
};
let user_dbs = match state.open_tenant(tid, crate::state::TenantOpenMode::CoreJob) {
Ok(d) => d,
Err(_) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to open tenant database",
)
.into_response()
}
};
let target_exists = {
let conn = state.content_db.lock().unwrap();
let conn = user_dbs.content.lock().unwrap();
if target_type == "url" {
get_url_by_id(&conn, &id)
.map(|u| u.is_some())
@@ -203,7 +243,7 @@ pub(crate) async fn perform_csv_export(
}
let count = {
let conn = state.analytics_db.lock().unwrap();
let conn = user_dbs.analytics.lock().unwrap();
get_target_visit_total_filtered(
&conn,
target_type,
@@ -215,14 +255,14 @@ pub(crate) async fn perform_csv_export(
};
let (has_utm_source, has_utm_campaign) = {
let conn = state.analytics_db.lock().unwrap();
let conn = user_dbs.analytics.lock().unwrap();
let cols = get_visits_schema_columns(&conn).unwrap_or_default();
(cols.contains("utm_source"), cols.contains("utm_campaign"))
};
let (tx, rx) =
tokio::sync::mpsc::channel::<Result<axum::body::Bytes, std::convert::Infallible>>(32);
let analytics_db = state.analytics_db.clone();
let analytics_db = user_dbs.analytics.clone();
let target_id = id.clone();
tokio::task::spawn_blocking(move || {
@@ -378,8 +418,48 @@ pub(crate) async fn perform_json_export(
Err(status) => return status.into_response(),
};
let (_slug, owner_tid) = {
let conn = if target_type == "url" {
state.db.global_urls.lock().unwrap()
} else {
state.db.global_landing_pages.lock().unwrap()
};
let table = if target_type == "url" {
"global_urls"
} else {
"global_landing_pages"
};
match conn.query_row(
&format!(
"SELECT slug, owner_tenant_id FROM {} WHERE target_id = ?1 OR slug = ?1 LIMIT 1;",
table
),
rusqlite::params![id],
|row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
) {
Ok((s, t)) => (s, t),
Err(_) => return (StatusCode::NOT_FOUND, "Target not found").into_response(),
}
};
let tid = match owner_tid.parse::<crate::identity::TenantId>() {
Ok(t) => t,
Err(_) => {
return (StatusCode::INTERNAL_SERVER_ERROR, "Invalid tenant identity").into_response()
}
};
let user_dbs = match state.open_tenant(tid, crate::state::TenantOpenMode::CoreJob) {
Ok(d) => d,
Err(_) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to open tenant database",
)
.into_response()
}
};
let target_exists = {
let conn = state.content_db.lock().unwrap();
let conn = user_dbs.content.lock().unwrap();
if target_type == "url" {
get_url_by_id(&conn, &id)
.map(|u| u.is_some())
@@ -395,7 +475,7 @@ pub(crate) async fn perform_json_export(
}
let count = {
let conn = state.analytics_db.lock().unwrap();
let conn = user_dbs.analytics.lock().unwrap();
get_target_visit_total_filtered(
&conn,
target_type,
@@ -411,7 +491,7 @@ pub(crate) async fn perform_json_export(
}
let visits_raw = {
let conn = state.analytics_db.lock().unwrap();
let conn = user_dbs.analytics.lock().unwrap();
match get_target_visits_all_in_memory(
&conn,
target_type,
+325 -291
View File
@@ -1,4 +1,5 @@
use super::*;
use crate::identity::TenantId;
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct GlobalSlugRow {
@@ -48,28 +49,83 @@ pub async fn moderation_get(
Err(redir) => return redir.into_response(),
};
let flagged_items = {
let conn = state.system_db.lock().unwrap();
let mut stmt = conn.prepare(
"SELECT slug, owner_user_id, target_type, target_id, created_at, updated_at, status, deleted_at \
FROM global_slugs WHERE status = 'flagged' OR status = 'disabled' ORDER BY updated_at DESC;"
).unwrap();
let rows = stmt
.query_map([], |row| {
let mut flagged_items: Vec<GlobalSlugRow> = Vec::new();
// Query global_urls.db
{
let urls_conn = state.db.global_urls.lock().unwrap();
let users_conn = state.users_db.lock().unwrap();
let query_res = urls_conn.prepare(
"SELECT slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at \
FROM global_urls WHERE status = 'flagged' OR status = 'disabled' ORDER BY updated_at DESC;"
).and_then(|mut stmt| {
let rows = stmt.query_map([], |row| {
let tid_str: String = row.get(1)?;
let uid = if let Ok(tid) = TenantId::parse(&tid_str) {
crate::db::users::get_user_by_tenant_id(&users_conn, tid)
.ok()
.flatten()
.map(|u| u.id)
.unwrap_or(0)
} else {
0
};
Ok(GlobalSlugRow {
slug: row.get(0)?,
owner_user_id: row.get(1)?,
target_type: row.get(2)?,
target_id: row.get(3)?,
created_at: row.get(4)?,
updated_at: row.get(5)?,
status: row.get(6)?,
deleted_at: row.get(7)?,
owner_user_id: uid,
target_type: "url".to_string(),
target_id: row.get(2)?,
created_at: row.get(3)?,
updated_at: row.get(4)?,
status: row.get(5)?,
deleted_at: row.get(6)?,
})
})
.unwrap();
rows.filter_map(|r| r.ok()).collect()
};
})?;
Ok(rows.filter_map(|r| r.ok()).collect::<Vec<_>>())
});
if let Ok(items) = query_res {
flagged_items.extend(items);
}
}
// Query global_landing_pages.db
{
let pages_conn = state.db.global_landing_pages.lock().unwrap();
let users_conn = state.users_db.lock().unwrap();
let query_res = pages_conn.prepare(
"SELECT slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at \
FROM global_landing_pages WHERE status = 'flagged' OR status = 'disabled' ORDER BY updated_at DESC;"
).and_then(|mut stmt| {
let rows = stmt.query_map([], |row| {
let tid_str: String = row.get(1)?;
let uid = if let Ok(tid) = TenantId::parse(&tid_str) {
crate::db::users::get_user_by_tenant_id(&users_conn, tid)
.ok()
.flatten()
.map(|u| u.id)
.unwrap_or(0)
} else {
0
};
Ok(GlobalSlugRow {
slug: row.get(0)?,
owner_user_id: uid,
target_type: "page".to_string(),
target_id: row.get(2)?,
created_at: row.get(3)?,
updated_at: row.get(4)?,
status: row.get(5)?,
deleted_at: row.get(6)?,
})
})?;
Ok(rows.filter_map(|r| r.ok()).collect::<Vec<_>>())
});
if let Ok(items) = query_res {
flagged_items.extend(items);
}
}
flagged_items.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
let logs = {
let conn = state.system_db.lock().unwrap();
@@ -139,46 +195,91 @@ pub async fn moderation_post(
return Redirect::to("/admin/moderation?error=Invalid moderation action").into_response();
}
let (owner_user_id, target_type) = {
let system_conn = state.system_db.lock().unwrap();
let row_opt: Option<(i64, String)> = system_conn
.query_row(
"SELECT owner_user_id, target_type FROM global_slugs WHERE slug = ?1;",
[&form.slug],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.optional()
.unwrap_or(None);
let slug_info = match state.lookup_slug(&form.slug) {
Ok(Some(info)) => info,
_ => return Redirect::to("/admin/moderation?error=Slug not found").into_response(),
};
match row_opt {
Some(r) => r,
None => return Redirect::to("/admin/moderation?error=Slug not found").into_response(),
let owner_tenant_id = match TenantId::parse(&slug_info.owner_tenant_id) {
Ok(t) => t,
Err(_) => {
return Redirect::to("/admin/moderation?error=Invalid tenant on slug").into_response()
}
};
let owner_username = {
let (owner_user_id, owner_username) = {
let users_conn = state.users_db.lock().unwrap();
crate::db::users::get_user_by_id(&users_conn, owner_user_id)
.unwrap_or(None)
.map(|u| u.username)
match crate::db::users::get_user_by_tenant_id(&users_conn, owner_tenant_id) {
Ok(Some(u)) => (u.id, Some(u.username)),
_ => (0, None),
}
};
{
let system_conn = state.system_db.lock().unwrap();
let now = Utc::now().to_rfc3339();
let now = Utc::now().to_rfc3339();
if action == "deleted" {
let _ = system_conn.execute("DELETE FROM global_slugs WHERE slug = ?1;", [&form.slug]);
// 1. Update v0.8 slug database
if action == "deleted" {
if slug_info.target_type == crate::db::slugs::SlugTargetType::Url {
let urls_conn = state.db.global_urls.lock().unwrap();
let _ = urls_conn.execute("DELETE FROM global_urls WHERE slug = ?1;", [&form.slug]);
} else {
let _ = system_conn.execute(
"UPDATE global_slugs SET status = ?1, updated_at = ?2 WHERE slug = ?3;",
let pages_conn = state.db.global_landing_pages.lock().unwrap();
let _ = pages_conn.execute(
"DELETE FROM global_landing_pages WHERE slug = ?1;",
[&form.slug],
);
}
} else {
if slug_info.target_type == crate::db::slugs::SlugTargetType::Url {
let urls_conn = state.db.global_urls.lock().unwrap();
let _ = urls_conn.execute(
"UPDATE global_urls SET status = ?1, updated_at = ?2 WHERE slug = ?3;",
rusqlite::params![action, now, form.slug],
);
} else {
let pages_conn = state.db.global_landing_pages.lock().unwrap();
let _ = pages_conn.execute(
"UPDATE global_landing_pages SET status = ?1, updated_at = ?2 WHERE slug = ?3;",
rusqlite::params![action, now, form.slug],
);
}
}
// 2. Sync to tenant content DB if accessible
if let Ok(tenant_dbs) =
state.open_tenant(owner_tenant_id, crate::db::tenant::TenantOpenMode::CoreJob)
{
if let Ok(conn) = tenant_dbs.content.lock() {
if slug_info.target_type == crate::db::slugs::SlugTargetType::Url {
let content_status = if action == "disabled" || action == "deleted" {
"dead"
} else {
"active"
};
let _ = conn.execute(
"UPDATE urls SET status = ?1 WHERE code = ?2;",
rusqlite::params![content_status, form.slug],
);
} else {
let content_state = if action == "disabled" || action == "deleted" {
"archived"
} else {
"published"
};
let _ = conn.execute(
"UPDATE landing_pages SET state = ?1 WHERE code = ?2;",
rusqlite::params![content_state, form.slug],
);
}
}
}
// 3. Record moderation event and audit log in system.db
{
let system_conn = state.system_db.lock().unwrap();
let event_id = Uuid::new_v4().to_string();
let _ = system_conn.execute(
"INSERT INTO moderation_events (id, timestamp, admin_username, target_user_id, target_username, resource_type, resource_identifier, action, severity, reason)
"INSERT INTO moderation_events (id, timestamp, admin_username, target_user_id, target_username, resource_type, resource_identifier, action, severity, reason) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10);",
rusqlite::params![
event_id,
@@ -186,7 +287,7 @@ pub async fn moderation_post(
user.username,
owner_user_id,
owner_username,
target_type,
slug_info.target_type.as_str(),
form.slug,
action,
form.severity,
@@ -231,48 +332,130 @@ pub async fn slugs_get(
Err(redir) => return redir.into_response(),
};
let system_conn = state.system_db.lock().unwrap();
let mut slugs: Vec<GlobalSlugRow> = Vec::new();
let mut sql = "SELECT slug, owner_user_id, target_type, target_id, created_at, updated_at, status, deleted_at FROM global_slugs WHERE 1=1".to_string();
let mut values = vec![];
if let Some(ref s) = query.search {
if !s.trim().is_empty() {
sql.push_str(" AND slug LIKE ?");
values.push(rusqlite::types::Value::Text(format!("%{}%", s.trim())));
}
}
if let Some(o) = query.owner {
sql.push_str(" AND owner_user_id = ?");
values.push(rusqlite::types::Value::Integer(o));
}
if let Some(ref st) = query.status {
if !st.trim().is_empty() {
sql.push_str(" AND status = ?");
values.push(rusqlite::types::Value::Text(st.trim().to_string()));
}
}
sql.push_str(" ORDER BY created_at DESC LIMIT 100;");
// Query global_urls.db
{
let urls_conn = state.db.global_urls.lock().unwrap();
let users_conn = state.users_db.lock().unwrap();
let mut sql = "SELECT slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at FROM global_urls WHERE 1=1".to_string();
let mut values = vec![];
let slugs = {
let mut stmt = system_conn.prepare(&sql).unwrap();
let rows = stmt
.query_map(rusqlite::params_from_iter(values.iter()), |row| {
if let Some(ref s) = query.search {
if !s.trim().is_empty() {
sql.push_str(" AND slug LIKE ?");
values.push(rusqlite::types::Value::Text(format!("%{}%", s.trim())));
}
}
if let Some(ref st) = query.status {
if !st.trim().is_empty() {
sql.push_str(" AND status = ?");
values.push(rusqlite::types::Value::Text(st.trim().to_string()));
}
}
sql.push_str(" ORDER BY created_at DESC LIMIT 100;");
let items_res = urls_conn.prepare(&sql).and_then(|mut stmt| {
let rows = stmt.query_map(rusqlite::params_from_iter(values.iter()), |row| {
let tid_str: String = row.get(1)?;
let uid = if let Ok(tid) = TenantId::parse(&tid_str) {
crate::db::users::get_user_by_tenant_id(&users_conn, tid)
.ok()
.flatten()
.map(|u| u.id)
.unwrap_or(0)
} else {
0
};
Ok(GlobalSlugRow {
slug: row.get(0)?,
owner_user_id: row.get(1)?,
target_type: row.get(2)?,
target_id: row.get(3)?,
created_at: row.get(4)?,
updated_at: row.get(5)?,
status: row.get(6)?,
deleted_at: row.get(7)?,
owner_user_id: uid,
target_type: "url".to_string(),
target_id: row.get(2)?,
created_at: row.get(3)?,
updated_at: row.get(4)?,
status: row.get(5)?,
deleted_at: row.get(6)?,
})
})
.unwrap();
rows.filter_map(|r| r.ok()).collect()
};
})?;
Ok(rows.filter_map(|r| r.ok()).collect::<Vec<_>>())
});
if let Ok(items) = items_res {
for r in items {
if let Some(o) = query.owner {
if r.owner_user_id != o {
continue;
}
}
slugs.push(r);
}
}
}
// Query global_landing_pages.db
{
let pages_conn = state.db.global_landing_pages.lock().unwrap();
let users_conn = state.users_db.lock().unwrap();
let mut sql = "SELECT slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at FROM global_landing_pages WHERE 1=1".to_string();
let mut values = vec![];
if let Some(ref s) = query.search {
if !s.trim().is_empty() {
sql.push_str(" AND slug LIKE ?");
values.push(rusqlite::types::Value::Text(format!("%{}%", s.trim())));
}
}
if let Some(ref st) = query.status {
if !st.trim().is_empty() {
sql.push_str(" AND status = ?");
values.push(rusqlite::types::Value::Text(st.trim().to_string()));
}
}
sql.push_str(" ORDER BY created_at DESC LIMIT 100;");
let items_res = pages_conn.prepare(&sql).and_then(|mut stmt| {
let rows = stmt.query_map(rusqlite::params_from_iter(values.iter()), |row| {
let tid_str: String = row.get(1)?;
let uid = if let Ok(tid) = TenantId::parse(&tid_str) {
crate::db::users::get_user_by_tenant_id(&users_conn, tid)
.ok()
.flatten()
.map(|u| u.id)
.unwrap_or(0)
} else {
0
};
Ok(GlobalSlugRow {
slug: row.get(0)?,
owner_user_id: uid,
target_type: "page".to_string(),
target_id: row.get(2)?,
created_at: row.get(3)?,
updated_at: row.get(4)?,
status: row.get(5)?,
deleted_at: row.get(6)?,
})
})?;
Ok(rows.filter_map(|r| r.ok()).collect::<Vec<_>>())
});
if let Ok(items) = items_res {
for r in items {
if let Some(o) = query.owner {
if r.owner_user_id != o {
continue;
}
}
slugs.push(r);
}
}
}
slugs.sort_by(|a, b| b.created_at.cmp(&a.created_at));
let history = {
let system_conn = state.system_db.lock().unwrap();
let mut stmt = system_conn.prepare(
"SELECT id, slug, old_owner_user_id, new_owner_user_id, action, timestamp, admin_username \
FROM slug_history ORDER BY timestamp DESC LIMIT 50;"
@@ -332,198 +515,19 @@ pub async fn slugs_transfer_post(
return Redirect::to("/admin/slugs?error=Invalid CSRF token").into_response();
}
let user_exists = {
let conn = state.users_db.lock().unwrap();
conn.query_row(
"SELECT EXISTS(SELECT 1 FROM users WHERE id = ?1);",
[form.new_owner_user_id],
|row| row.get::<_, bool>(0),
)
.unwrap_or(false)
let req = crate::services::slug_transfer::SlugTransferRequest {
slug: form.slug.clone(),
new_owner_user_id: form.new_owner_user_id,
};
if !user_exists {
return Redirect::to("/admin/slugs?error=New owner user ID does not exist").into_response();
match crate::services::slug_transfer::transfer_slug(&state, &req, &user.username) {
Ok(_) => Redirect::to(&format!(
"/admin/slugs?success=Slug /{} successfully transferred to user {}",
form.slug, form.new_owner_user_id
))
.into_response(),
Err(e) => Redirect::to(&format!("/admin/slugs?error={}", e.message())).into_response(),
}
let (old_owner, target_type, mut new_target_id) =
{
let conn = state.system_db.lock().unwrap();
let row_opt: Option<(i64, String, String)> = conn.query_row(
"SELECT owner_user_id, target_type, target_id FROM global_slugs WHERE slug = ?1;",
[&form.slug],
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?))
).optional().unwrap_or(None);
match row_opt {
Some(r) => r,
None => return Redirect::to("/admin/slugs?error=Slug not found").into_response(),
}
};
if old_owner == form.new_owner_user_id {
return Redirect::to("/admin/slugs?error=Slug is already owned by this user")
.into_response();
}
let old_dbs = match state.get_user_dbs(old_owner) {
Ok(dbs) => dbs,
Err(_) => {
return Redirect::to("/admin/slugs?error=Failed to load current owner's database")
.into_response()
}
};
let new_dbs = match state.get_user_dbs(form.new_owner_user_id) {
Ok(dbs) => dbs,
Err(_) => {
return Redirect::to("/admin/slugs?error=Failed to load new owner's database")
.into_response()
}
};
{
let old_conn = old_dbs.content.lock().unwrap();
let new_conn = new_dbs.content.lock().unwrap();
if target_type == "url" {
let url_opt = match crate::db::content::get_url_by_code(&old_conn, &form.slug) {
Ok(u) => u,
Err(e) => {
return Redirect::to(&format!(
"/admin/slugs?error=Failed to retrieve old URL: {}",
e
))
.into_response()
}
};
if let Some(url) = url_opt {
// Check quota
let users_conn = state.users_db.lock().unwrap();
let quota_opt =
crate::db::users::get_user_quotas(&users_conn, form.new_owner_user_id)
.unwrap_or(None);
if let Some(quota) = quota_opt {
if quota.current_urls >= quota.max_urls {
return Redirect::to(
"/admin/slugs?error=New owner has exceeded URL quota limit",
)
.into_response();
}
}
// Copy
let new_url = match crate::db::content::create_url_extended(
&new_conn,
&url.code,
&url.destination,
url.title.as_deref(),
url.description.as_deref(),
&url.tags,
url.expires_at.as_deref(),
url.password_hash.as_deref(),
url.max_access_count,
) {
Ok(u) => u,
Err(e) => {
return Redirect::to(&format!(
"/admin/slugs?error=Failed to copy URL record: {}",
e
))
.into_response()
}
};
new_target_id = new_url.id;
// Delete old
let _ = crate::db::content::delete_url(&old_conn, &url.id);
}
} else if target_type == "page" {
let page_opt = match crate::db::content::get_landing_page_by_code(&old_conn, &form.slug)
{
Ok(p) => p,
Err(e) => {
return Redirect::to(&format!(
"/admin/slugs?error=Failed to retrieve old page: {}",
e
))
.into_response()
}
};
if let Some(page) = page_opt {
// Check quota
let users_conn = state.users_db.lock().unwrap();
let quota_opt =
crate::db::users::get_user_quotas(&users_conn, form.new_owner_user_id)
.unwrap_or(None);
if let Some(quota) = quota_opt {
if quota.current_landings >= quota.max_landings {
return Redirect::to(
"/admin/slugs?error=New owner has exceeded landing page quota limit",
)
.into_response();
}
}
// Copy
let new_page = match crate::db::content::create_landing_page(
&new_conn,
&page.code,
&page.slug,
&page.title,
&page.html_content,
&page.state,
) {
Ok(p) => p,
Err(e) => {
return Redirect::to(&format!(
"/admin/slugs?error=Failed to copy page record: {}",
e
))
.into_response()
}
};
new_target_id = new_page.id;
// Delete old
let _ = crate::db::content::delete_landing_page(&old_conn, &page.id);
}
}
}
{
let conn = state.system_db.lock().unwrap();
let now = Utc::now().to_rfc3339();
let _ = conn.execute(
"UPDATE global_slugs SET owner_user_id = ?1, target_id = ?2, updated_at = ?3 WHERE slug = ?4;",
rusqlite::params![form.new_owner_user_id, new_target_id, now, form.slug],
);
let _ = conn.execute(
"INSERT INTO slug_history (slug, old_owner_user_id, new_owner_user_id, action, timestamp, admin_username) \
VALUES (?1, ?2, ?3, 'transferred', ?4, ?5);",
rusqlite::params![form.slug, old_owner, form.new_owner_user_id, now, user.username],
);
let _ = crate::db::audit_events::write_audit_event(
&conn,
&user.username,
"SLUG_TRANSFER",
"slug",
&form.slug,
Some(&format!(
"Transferred from {} to {}",
old_owner, form.new_owner_user_id
)),
);
}
Redirect::to(&format!(
"/admin/slugs?success=Slug /{} successfully transferred to user {}",
form.slug, form.new_owner_user_id
))
.into_response()
}
#[derive(Deserialize)]
@@ -553,17 +557,30 @@ pub async fn slugs_status_post(
return Redirect::to("/admin/slugs?error=Invalid status").into_response();
}
{
let conn = state.system_db.lock().unwrap();
let now = Utc::now().to_rfc3339();
let slug_info = match state.lookup_slug(&form.slug) {
Ok(Some(info)) => info,
_ => return Redirect::to("/admin/slugs?error=Slug not found").into_response(),
};
let _ = conn.execute(
"UPDATE global_slugs SET status = ?1, updated_at = ?2 WHERE slug = ?3;",
let now = Utc::now().to_rfc3339();
if slug_info.target_type == crate::db::slugs::SlugTargetType::Url {
let urls_conn = state.db.global_urls.lock().unwrap();
let _ = urls_conn.execute(
"UPDATE global_urls SET status = ?1, updated_at = ?2 WHERE slug = ?3;",
rusqlite::params![status, now, form.slug],
);
} else {
let pages_conn = state.db.global_landing_pages.lock().unwrap();
let _ = pages_conn.execute(
"UPDATE global_landing_pages SET status = ?1, updated_at = ?2 WHERE slug = ?3;",
rusqlite::params![status, now, form.slug],
);
}
{
let system_conn = state.system_db.lock().unwrap();
let _ = crate::db::audit_events::write_audit_event(
&conn,
&system_conn,
&user.username,
"SLUG_STATUS_UPDATE",
"slug",
@@ -600,29 +617,46 @@ pub async fn slugs_delete_post(
return Redirect::to("/admin/slugs?error=Invalid CSRF token").into_response();
}
let slug_info = match state.lookup_slug(&form.slug) {
Ok(Some(info)) => info,
_ => return Redirect::to("/admin/slugs?error=Slug not found").into_response(),
};
let old_owner_user_id = {
let users_conn = state.users_db.lock().unwrap();
if let Ok(tid) = TenantId::parse(&slug_info.owner_tenant_id) {
crate::db::users::get_user_by_tenant_id(&users_conn, tid)
.ok()
.flatten()
.map(|u| u.id)
} else {
None
}
};
let now = Utc::now().to_rfc3339();
if slug_info.target_type == crate::db::slugs::SlugTargetType::Url {
let urls_conn = state.db.global_urls.lock().unwrap();
let _ = urls_conn.execute("DELETE FROM global_urls WHERE slug = ?1;", [&form.slug]);
} else {
let pages_conn = state.db.global_landing_pages.lock().unwrap();
let _ = pages_conn.execute(
"DELETE FROM global_landing_pages WHERE slug = ?1;",
[&form.slug],
);
}
{
let conn = state.system_db.lock().unwrap();
let now = Utc::now().to_rfc3339();
let old_owner_user_id: Option<i64> = conn
.query_row(
"SELECT owner_user_id FROM global_slugs WHERE slug = ?1;",
[&form.slug],
|row| row.get(0),
)
.optional()
.unwrap_or(None);
let _ = conn.execute("DELETE FROM global_slugs WHERE slug = ?1;", [&form.slug]);
let _ = conn.execute(
let system_conn = state.system_db.lock().unwrap();
let _ = system_conn.execute(
"INSERT INTO slug_history (slug, old_owner_user_id, new_owner_user_id, action, timestamp, admin_username) \
VALUES (?1, ?2, NULL, 'deleted', ?3, ?4);",
rusqlite::params![form.slug, old_owner_user_id, now, user.username],
);
let _ = crate::db::audit_events::write_audit_event(
&conn,
&system_conn,
&user.username,
"SLUG_RELEASE",
"slug",
+130 -167
View File
@@ -118,22 +118,26 @@ pub async fn user_pages_create(
}
}
let owner_tid = user
.tenant_id
.unwrap_or_else(crate::identity::TenantId::generate);
{
let system_conn = state.system_db.lock().unwrap();
if !crate::db::users::is_slug_available(&system_conn, &code).unwrap_or(false) {
return Redirect::to("/user/pages?error=Short code/slug already exists")
.into_response();
}
if let Err(e) = crate::db::users::register_global_slug(
&system_conn,
let reserved_conn = state.db.reserved.lock().unwrap();
let urls_conn = state.db.global_urls.lock().unwrap();
let pages_conn = state.db.global_landing_pages.lock().unwrap();
if let Err(e) = crate::db::slugs::reserve_landing_page_slug(
&reserved_conn,
&urls_conn,
&pages_conn,
&code,
user.id,
"page",
"",
"reserving",
&owner_tid,
) {
return Redirect::to(&format!("/user/pages?error=Failed to reserve slug: {}", e))
.into_response();
return Redirect::to(&format!(
"/user/pages?error=Short code/slug unavailable: {}",
e
))
.into_response();
}
}
@@ -152,16 +156,8 @@ pub async fn user_pages_create(
match res {
Ok(page) => {
{
let system_conn = state.system_db.lock().unwrap();
let global_status = if form.state == "published" {
"active"
} else {
"disabled"
};
let _ = system_conn.execute(
"UPDATE global_slugs SET target_id = ?1, status = ?2, updated_at = ?3 WHERE slug = ?4;",
rusqlite::params![page.id, global_status, chrono::Utc::now().to_rfc3339(), code],
);
let pages_conn = state.db.global_landing_pages.lock().unwrap();
let _ = crate::db::slugs::activate_landing_page_slug(&pages_conn, &code, &page.id);
}
{
let users_conn = state.users_db.lock().unwrap();
@@ -181,8 +177,8 @@ pub async fn user_pages_create(
Redirect::to("/user/pages").into_response()
}
Err(e) => {
let system_conn = state.system_db.lock().unwrap();
let _ = crate::db::users::release_global_slug(&system_conn, &code, user.id);
let pages_conn = state.db.global_landing_pages.lock().unwrap();
let _ = crate::db::slugs::release_landing_page_slug(&pages_conn, &code, &owner_tid);
Redirect::to(&format!("/user/pages?error=Database error: {}", e)).into_response()
}
}
@@ -221,11 +217,13 @@ pub async fn user_pages_delete(
);
match delete_landing_page(&conn, &id) {
Ok(_) => {
let _ = crate::db::users::release_global_slug(
&state.system_db.lock().unwrap(),
&page.code,
user.id,
);
{
let pages_conn = state.db.global_landing_pages.lock().unwrap();
let _ = pages_conn.execute(
"DELETE FROM global_landing_pages WHERE slug = ?1;",
[&page.code],
);
}
let ip = get_client_ip(&headers, connect_info);
let _ = write_audit_log(
&state.admin_db.lock().unwrap(),
@@ -265,8 +263,14 @@ pub async fn pages_get(
};
let (pages, total_pages, page, visible_pages) = {
let conn = state.content_db.lock().unwrap();
let total_records = get_landing_page_count(&conn).unwrap_or(0);
let conn = state.db.global_landing_pages.lock().unwrap();
let total_records: i64 = conn
.query_row(
"SELECT COUNT(*) FROM global_landing_pages WHERE status != 'retired';",
[],
|r| r.get(0),
)
.unwrap_or(0);
let calculated_total_pages = (total_records as usize).div_ceil(PAGE_SIZE);
let total_pages = std::cmp::max(1, calculated_total_pages);
let requested_page = query.page.unwrap_or(1);
@@ -278,7 +282,58 @@ pub async fn pages_get(
.clamp(1, total_pages);
let offset = (current_page - 1) * PAGE_SIZE;
let pages = list_landing_pages(&conn, PAGE_SIZE as i64, offset as i64).unwrap_or_default();
let mut stmt = conn.prepare(
"SELECT slug, owner_tenant_id, target_id, created_at, updated_at, status FROM global_landing_pages WHERE status != 'retired' ORDER BY created_at DESC LIMIT ?1 OFFSET ?2;"
).unwrap();
let rows = stmt
.query_map(rusqlite::params![PAGE_SIZE as i64, offset as i64], |row| {
let slug: String = row.get(0)?;
let owner_tid_str: String = row.get(1)?;
let target_id: String = row.get(2)?;
let created_at: String = row.get(3)?;
let updated_at: String = row.get(4)?;
let status: String = row.get(5)?;
Ok((
slug,
owner_tid_str,
target_id,
created_at,
updated_at,
status,
))
})
.unwrap();
let mut pages = Vec::new();
for item in rows.flatten() {
let (slug, owner_tid_str, target_id, created_at, updated_at, status) = item;
let mut resolved_page = None;
if let Ok(tid) = owner_tid_str.parse::<crate::identity::TenantId>() {
if let Ok(user_dbs) = state.open_tenant(tid, crate::state::TenantOpenMode::CoreJob)
{
let u_conn = user_dbs.content.lock().unwrap();
if let Ok(Some(p)) =
crate::db::content::get_landing_page_by_id(&u_conn, &target_id)
{
resolved_page = Some(p);
}
}
}
if let Some(p) = resolved_page {
pages.push(p);
} else {
pages.push(crate::models::LandingPage {
id: target_id,
code: slug.clone(),
slug,
title: String::new(),
html_content: String::new(),
state: status,
created_at,
updated_at,
});
}
}
let start_page = current_page.saturating_sub(3).max(1);
let end_page = std::cmp::min(total_pages, current_page + 3);
@@ -302,7 +357,8 @@ pub async fn pages_get(
template.into_response()
}
#[derive(Deserialize)]
#[derive(Deserialize, Default)]
#[serde(default)]
pub struct CreatePageForm {
pub title: String,
pub slug: String,
@@ -317,126 +373,16 @@ pub struct CreatePageForm {
pub async fn pages_create(
State(state): State<AppState>,
jar: CookieJar,
headers: HeaderMap,
connect_info: Option<ConnectInfo<SocketAddr>>,
Form(form): Form<CreatePageForm>,
_headers: HeaderMap,
_connect_info: Option<ConnectInfo<SocketAddr>>,
Form(_form): Form<CreatePageForm>,
) -> Response {
let (user, session_id) = match require_auth(&state, &jar).await {
let (_user, _session_id) = match require_auth(&state, &jar).await {
Ok(u) => u,
Err(redir) => return redir.into_response(),
};
if !verify_csrf(&session_id, &form.csrf_token) {
return Redirect::to("/admin/pages?error=Invalid CSRF token").into_response();
}
let ip = get_client_ip(&headers, connect_info);
let admin_user_id = user.id.parse::<i64>().unwrap_or(1);
// Custom Slug takes priority if provided
let mut code = form.custom_slug.trim().to_lowercase();
if code.is_empty() {
code = form.code.trim().to_lowercase();
if code.is_empty() {
code = generate_token(2);
} else {
if code.len() != 4 || !code.chars().all(|c| c.is_ascii_hexdigit()) {
return Redirect::to(
"/admin/pages?error=Custom code must be exactly 4 hex characters",
)
.into_response();
}
}
} else {
if !crate::utils::validation::validate_custom_slug(&code) {
return Redirect::to("/admin/pages?error=Custom slug must start with ! followed by 1-24 characters of a-z, 0-9, -, _")
.into_response();
}
}
let clean_slug = form.slug.trim().to_lowercase();
if clean_slug.is_empty() {
return Redirect::to("/admin/pages?error=Slug is required").into_response();
}
{
let users_conn = state.users_db.lock().unwrap();
if !crate::db::users::check_quota_limit(&users_conn, admin_user_id, "landings")
.unwrap_or(false)
{
return Redirect::to("/admin/pages?error=Quota limit exceeded").into_response();
}
}
{
let system_conn = state.system_db.lock().unwrap();
if !crate::db::users::is_slug_available(&system_conn, &code).unwrap_or(false) {
return Redirect::to("/admin/pages?error=Short code already exists").into_response();
}
// Always use owner_user_id = 1 for admin content so it resolves via state.content_db
if let Err(e) =
crate::db::users::register_global_slug(&system_conn, &code, 1, "page", "", "reserving")
{
return Redirect::to(&format!("/admin/pages?error=Failed to reserve slug: {}", e))
.into_response();
}
}
let res = {
let conn = state.content_db.lock().unwrap();
create_landing_page(
&conn,
&code,
&clean_slug,
&form.title,
&form.html_content,
&form.state,
)
};
match res {
Ok(page) => {
{
let system_conn = state.system_db.lock().unwrap();
let global_status = if form.state == "published" {
"active"
} else {
"disabled"
};
let _ = system_conn.execute(
"UPDATE global_slugs SET target_id = ?1, status = ?2, updated_at = ?3 WHERE slug = ?4;",
rusqlite::params![page.id, global_status, chrono::Utc::now().to_rfc3339(), code],
);
}
{
let users_conn = state.users_db.lock().unwrap();
let _ = crate::db::users::increment_quota_counter(
&users_conn,
admin_user_id,
"landings",
);
}
{
let conn_admin = state.admin_db.lock().unwrap();
let _ = write_audit_log(
&conn_admin,
&state,
&user.username,
"PAGE_CREATION",
Some("page"),
Some(&page.id),
Some(&ip),
headers.get("user-agent").and_then(|h| h.to_str().ok()),
);
}
Redirect::to("/admin/pages").into_response()
}
Err(e) => {
let system_conn = state.system_db.lock().unwrap();
let _ = crate::db::users::release_global_slug(&system_conn, &code, 1);
Redirect::to(&format!("/admin/pages?error=Database error: {}", e)).into_response()
}
}
Redirect::to("/admin/pages?error=Admin is a platform operator and cannot create unowned application pages; create landing pages via a tenant user account").into_response()
}
// POST /admin/pages/delete/:id
@@ -460,25 +406,42 @@ pub async fn pages_delete(
let ip = get_client_ip(&headers, connect_info);
let conn = state.content_db.lock().unwrap();
match delete_landing_page(&conn, &id) {
Ok(_) => {
{
let conn_admin = state.admin_db.lock().unwrap();
let _ = write_audit_log(
&conn_admin,
&state,
&user.username,
"PAGE_DELETION",
Some("page"),
Some(&id),
Some(&ip),
headers.get("user-agent").and_then(|h| h.to_str().ok()),
);
// Look up owner tenant in global_landing_pages
let owner_info = {
let conn = state.db.global_landing_pages.lock().unwrap();
conn.query_row(
"SELECT slug, owner_tenant_id FROM global_landing_pages WHERE target_id = ?1 OR slug = ?1 LIMIT 1;",
rusqlite::params![id],
|row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
).ok()
};
if let Some((slug, tid_str)) = owner_info {
if let Ok(tid) = tid_str.parse::<crate::identity::TenantId>() {
if let Ok(user_dbs) = state.open_tenant(tid, crate::state::TenantOpenMode::CoreJob) {
let conn = user_dbs.content.lock().unwrap();
let _ = delete_landing_page(&conn, &id);
}
Redirect::to("/admin/pages").into_response()
}
Err(e) => Redirect::to(&format!("/admin/pages?error=Failed to delete page: {}", e))
.into_response(),
let conn = state.db.global_landing_pages.lock().unwrap();
let _ = conn.execute(
"UPDATE global_landing_pages SET status = 'retired', updated_at = ?1 WHERE slug = ?2;",
rusqlite::params![chrono::Utc::now().to_rfc3339(), slug],
);
}
{
let conn_admin = state.admin_db.lock().unwrap();
let _ = write_audit_log(
&conn_admin,
&state,
&user.username,
"PAGE_DELETION",
Some("page"),
Some(&id),
Some(&ip),
headers.get("user-agent").and_then(|h| h.to_str().ok()),
);
}
Redirect::to("/admin/pages").into_response()
}
+220 -119
View File
@@ -106,11 +106,13 @@ pub async fn user_download_backup(
};
let ip = get_client_ip(&headers, connect_info);
let user_dir = state
.config
.data_dir
.join("users")
.join(user.id.to_string());
let user_dir = match crate::db::tenant::location_for_user(&user).and_then(|loc| {
loc.dir(&state.db.topology)
.map_err(|e| crate::error::AppError::BadRequest(e.to_string()))
}) {
Ok(p) => p,
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response(),
};
let mut buffer = Vec::new();
let res = {
@@ -224,11 +226,15 @@ pub async fn user_restore_backup_post(
.into_response();
}
let user_dir = state
.config
.data_dir
.join("users")
.join(user.id.to_string());
let user_dir = match crate::db::tenant::location_for_user(&user).and_then(|loc| {
loc.dir(&state.db.topology)
.map_err(|e| crate::error::AppError::BadRequest(e.to_string()))
}) {
Ok(p) => p,
Err(_) => {
return Redirect::to("/user/settings?error=Invalid user directory").into_response()
}
};
let temp_unpack_dir =
std::env::temp_dir().join(format!("bzod_restore_unpack_{}", uuid::Uuid::new_v4()));
@@ -241,42 +247,124 @@ pub async fn user_restore_backup_post(
.into_response();
}
let restore_res = {
let file = match File::open(&temp_file_path) {
Ok(f) => f,
Err(e) => {
let _ = std::fs::remove_file(&temp_file_path);
let _ = std::fs::remove_dir_all(&temp_unpack_dir);
return Redirect::to(&format!(
"/user/settings?error=Failed to open upload: {}",
e
))
.into_response();
}
};
let restore_res: Result<(), Box<dyn std::error::Error>> = (|| {
let file = File::open(&temp_file_path)?;
let tar_gz = GzDecoder::new(file);
let mut archive = tar::Archive::new(tar_gz);
if let Err(e) = archive.unpack(&temp_unpack_dir) {
Err(Box::new(e) as Box<dyn std::error::Error>)
} else {
// Check for collision using temp content.db
let system_conn = state.system_db.lock().unwrap();
let temp_content_db = temp_unpack_dir.join("content.db");
if temp_content_db.exists() {
if let Err(e) = crate::db::users::register_restored_user_slugs(
&system_conn,
user.id,
&temp_content_db,
) {
Err(e)
} else {
Ok(())
}
} else {
Err("Backup is missing content.db".into())
archive.unpack(&temp_unpack_dir)?;
let target_tenant_id = user
.tenant_id
.unwrap_or_else(crate::identity::TenantId::generate);
let temp_content_db = temp_unpack_dir.join("content.db");
if !temp_content_db.exists() {
return Err("Backup is missing content.db".into());
}
let content_conn = rusqlite::Connection::open(&temp_content_db)?;
let mut urls = Vec::new();
if let Ok(mut stmt) = content_conn.prepare("SELECT code, id, created_at, status FROM urls;")
{
if let Ok(rows) = stmt.query_map([], |r| {
Ok((
r.get::<_, String>(0)?,
r.get::<_, String>(1)?,
r.get::<_, String>(2)?,
r.get::<_, String>(3)?,
))
}) {
urls = rows.filter_map(|r| r.ok()).collect();
}
}
};
let mut pages = Vec::new();
if let Ok(mut stmt) =
content_conn.prepare("SELECT code, id, created_at, state FROM landing_pages;")
{
if let Ok(rows) = stmt.query_map([], |r| {
Ok((
r.get::<_, String>(0)?,
r.get::<_, String>(1)?,
r.get::<_, String>(2)?,
r.get::<_, String>(3)?,
))
}) {
pages = rows.filter_map(|r| r.ok()).collect();
}
}
let urls_conn = state.db.global_urls.lock().unwrap();
let pages_conn = state.db.global_landing_pages.lock().unwrap();
for (slug, _, _, _) in &urls {
if let Ok(Some(existing_owner)) = urls_conn
.query_row(
"SELECT owner_tenant_id FROM global_urls WHERE slug = ?1;",
[slug],
|r| r.get::<_, String>(0),
)
.optional()
{
if existing_owner != target_tenant_id.as_str() {
return Err(format!("Slug collision on URL /{}", slug).into());
}
}
}
for (slug, _, _, _) in &pages {
if let Ok(Some(existing_owner)) = pages_conn
.query_row(
"SELECT owner_tenant_id FROM global_landing_pages WHERE slug = ?1;",
[slug],
|r| r.get::<_, String>(0),
)
.optional()
{
if existing_owner != target_tenant_id.as_str() {
return Err(format!("Slug collision on Landing Page /{}", slug).into());
}
}
}
let _ = urls_conn.execute(
"DELETE FROM global_urls WHERE owner_tenant_id = ?1;",
[target_tenant_id.as_str()],
);
let _ = pages_conn.execute(
"DELETE FROM global_landing_pages WHERE owner_tenant_id = ?1;",
[target_tenant_id.as_str()],
);
let now = chrono::Utc::now().to_rfc3339();
for (slug, target_id, created_at, status) in urls {
let global_status = if status == "dead" {
"disabled"
} else {
"active"
};
let _ = urls_conn.execute(
"INSERT OR REPLACE INTO global_urls (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, NULL);",
rusqlite::params![slug, target_tenant_id.as_str(), target_id, created_at, now, global_status],
);
}
for (slug, target_id, created_at, state) in pages {
let global_status = if state == "published" {
"active"
} else {
"disabled"
};
let _ = pages_conn.execute(
"INSERT OR REPLACE INTO global_landing_pages (slug, owner_tenant_id, target_id, created_at, updated_at, status, retired_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, NULL);",
rusqlite::params![slug, target_tenant_id.as_str(), target_id, created_at, now, global_status],
);
}
Ok(())
})();
let _ = std::fs::remove_file(&temp_file_path);
@@ -316,7 +404,9 @@ pub async fn user_restore_backup_post(
}
let mut pool = state.user_dbs.lock().unwrap();
pool.remove(&user.id);
if let Ok(loc) = crate::db::tenant::location_for_user(&user) {
pool.remove(&loc.cache_key());
}
let _ = write_audit_log(
&state.admin_db.lock().unwrap(),
&state,
@@ -678,8 +768,43 @@ pub async fn bulk_qr_export_post(
let ip = get_client_ip(&headers, connect_info);
let urls = {
let conn = state.content_db.lock().unwrap();
crate::db::content::list_urls(&conn, 500, 0, None).unwrap_or_default()
let conn = state.db.global_urls.lock().unwrap();
let mut stmt = match conn
.prepare("SELECT slug, target_id FROM global_urls WHERE status = 'active' LIMIT 500;")
{
Ok(s) => s,
Err(_) => return Redirect::to("/admin/settings?error=Database error").into_response(),
};
let rows = stmt.query_map([], |row| {
let code: String = row.get(0)?;
let target_id: String = row.get(1)?;
Ok(crate::models::Url {
id: target_id,
code: code.clone(),
destination: format!(
"{}/{}",
state.config.base_url.clone().unwrap_or_default(),
code
),
title: None,
description: None,
created_at: String::new(),
updated_at: String::new(),
status: "active".to_string(),
tags: vec![],
expires_at: None,
password_hash: None,
max_access_count: None,
access_count: 0,
expired: false,
last_latency_ms: None,
last_status: None,
})
});
match rows {
Ok(r) => r.filter_map(|x| x.ok()).collect(),
Err(_) => vec![],
}
};
if urls.is_empty() {
@@ -761,10 +886,7 @@ pub async fn status_get(State(state): State<AppState>, jar: CookieJar) -> Respon
let uptime_duration = state.start_time.elapsed();
let uptime = crate::utils::format_duration(uptime_duration);
let urls = {
let conn = state.content_db.lock().unwrap();
crate::db::content::list_urls(&conn, 50, 0, None).unwrap_or_default()
};
let urls = vec![];
let template = crate::templates::StatusTemplate {
admin_username: user.username,
@@ -911,93 +1033,72 @@ pub async fn restore_backup_post(
let restore_res = {
// Temporarily suspend access to active SQLite connections
let mut admin_conn = state.admin_db.lock().unwrap();
let mut content_conn = state.content_db.lock().unwrap();
let mut analytics_conn = state.analytics_db.lock().unwrap();
let mut system_conn = state.system_db.lock().unwrap();
let mut users_conn = state.users_db.lock().unwrap();
let mut urls_conn = state.db.global_urls.lock().unwrap();
let mut pages_conn = state.db.global_landing_pages.lock().unwrap();
let mut reserved_conn = state.db.reserved.lock().unwrap();
// 1. Close current connections by replacing them with dummy in-memory DBs
*admin_conn = match rusqlite::Connection::open_in_memory() {
Ok(c) => c,
Err(e) => {
return Redirect::to(&format!(
"/admin/settings?error=Failed to open temp in-memory DB: {}",
e
))
.into_response()
}
};
*content_conn = match rusqlite::Connection::open_in_memory() {
Ok(c) => c,
Err(e) => {
return Redirect::to(&format!(
"/admin/settings?error=Failed to open temp in-memory DB: {}",
e
))
.into_response()
}
};
*analytics_conn = match rusqlite::Connection::open_in_memory() {
Ok(c) => c,
Err(e) => {
return Redirect::to(&format!(
"/admin/settings?error=Failed to open temp in-memory DB: {}",
e
))
.into_response()
}
};
*system_conn = match rusqlite::Connection::open_in_memory() {
Ok(c) => c,
Err(e) => {
return Redirect::to(&format!(
"/admin/settings?error=Failed to open temp in-memory DB: {}",
e
))
.into_response()
}
};
*admin_conn = rusqlite::Connection::open_in_memory()
.unwrap_or_else(|_| rusqlite::Connection::open(":memory:").unwrap());
*system_conn = rusqlite::Connection::open_in_memory()
.unwrap_or_else(|_| rusqlite::Connection::open(":memory:").unwrap());
*users_conn = rusqlite::Connection::open_in_memory()
.unwrap_or_else(|_| rusqlite::Connection::open(":memory:").unwrap());
*urls_conn = rusqlite::Connection::open_in_memory()
.unwrap_or_else(|_| rusqlite::Connection::open(":memory:").unwrap());
*pages_conn = rusqlite::Connection::open_in_memory()
.unwrap_or_else(|_| rusqlite::Connection::open(":memory:").unwrap());
*reserved_conn = rusqlite::Connection::open_in_memory()
.unwrap_or_else(|_| rusqlite::Connection::open(":memory:").unwrap());
// 2. Perform restore unpacking/validation
// perform_restore() handles legacy layout normalization internally
// before validation, so no post-restore normalization is needed.
// 2. Clear tenant pool cache
if let Ok(mut pool) = state.user_dbs.lock() {
pool.clear();
}
// 3. Perform restore unpacking/validation
let res = crate::cli::restore::perform_restore(&temp_file_path, &state.config.data_dir);
// 3. Reinitialize database connections using correct multi-tenant paths
let paths =
crate::services::backup_layout::RestoredDbPaths::from_data_dir(&state.config.data_dir);
let new_admin = rusqlite::Connection::open(&paths.admin);
let new_system = rusqlite::Connection::open(&paths.system);
let new_users = rusqlite::Connection::open(&paths.users);
let new_content = rusqlite::Connection::open(&paths.content);
let new_analytics = rusqlite::Connection::open(&paths.analytics);
// 4. Reinitialize Core database connections
let topology = crate::db::topology::Topology::new(&state.config.data_dir);
let new_admin = rusqlite::Connection::open(topology.admin_db());
let new_system = rusqlite::Connection::open(topology.system_db());
let new_users = rusqlite::Connection::open(topology.users_registry_db());
let new_urls = rusqlite::Connection::open(topology.global_urls_db());
let new_pages = rusqlite::Connection::open(topology.global_landing_pages_db());
let new_reserved = rusqlite::Connection::open(topology.reserved_db());
match (new_admin, new_content, new_analytics, new_system, new_users) {
(Ok(adm), Ok(cnt), Ok(any), Ok(sys), Ok(usr)) => {
match (
new_admin,
new_system,
new_users,
new_urls,
new_pages,
new_reserved,
) {
(Ok(adm), Ok(sys), Ok(usr), Ok(urls), Ok(pages), Ok(resv)) => {
let _ = crate::db::sqlite::enable_wal(&adm, "admin");
let _ = crate::db::sqlite::enable_wal(&cnt, "content");
let _ = crate::db::sqlite::enable_wal(&any, "analytics");
let _ = crate::db::sqlite::enable_wal(&sys, "system");
let _ = crate::db::sqlite::enable_wal(&usr, "users");
let _ = crate::db::sqlite::enable_wal(&urls, "global_urls");
let _ = crate::db::sqlite::enable_wal(&pages, "global_landing_pages");
let _ = crate::db::sqlite::enable_wal(&resv, "reserved");
let _ = crate::db::sqlite::enable_foreign_keys(&adm, "admin");
let _ = crate::db::sqlite::enable_foreign_keys(&cnt, "content");
let _ = crate::db::sqlite::enable_foreign_keys(&any, "analytics");
let _ = crate::db::sqlite::enable_foreign_keys(&sys, "system");
let _ = crate::db::sqlite::enable_foreign_keys(&usr, "users");
let _ = crate::db::sqlite::enable_foreign_keys(&urls, "global_urls");
let _ = crate::db::sqlite::enable_foreign_keys(&pages, "global_landing_pages");
let _ = crate::db::sqlite::enable_foreign_keys(&resv, "reserved");
*admin_conn = adm;
*content_conn = cnt;
*analytics_conn = any;
*system_conn = sys;
match state.db.users.lock() {
Ok(mut u) => *u = usr,
Err(_) => {
return Redirect::to(
"/admin/settings?error=Failed to reopen restored databases",
)
.into_response();
}
}
*users_conn = usr;
*urls_conn = urls;
*pages_conn = pages;
*reserved_conn = resv;
}
_ => {
return Redirect::to("/admin/settings?error=Failed to reopen restored databases")
+144 -213
View File
@@ -153,21 +153,26 @@ pub async fn user_urls_create(
}
}
let owner_tid = user
.tenant_id
.unwrap_or_else(crate::identity::TenantId::generate);
{
let system_conn = state.system_db.lock().unwrap();
if !crate::db::users::is_slug_available(&system_conn, &code).unwrap_or(false) {
return Redirect::to("/user/urls?error=Short code/slug already exists").into_response();
}
if let Err(e) = crate::db::users::register_global_slug(
&system_conn,
let reserved_conn = state.db.reserved.lock().unwrap();
let urls_conn = state.db.global_urls.lock().unwrap();
let pages_conn = state.db.global_landing_pages.lock().unwrap();
if let Err(e) = crate::db::slugs::reserve_url_slug(
&reserved_conn,
&urls_conn,
&pages_conn,
&code,
user.id,
"url",
"",
"reserving",
&owner_tid,
) {
return Redirect::to(&format!("/user/urls?error=Failed to reserve slug: {}", e))
.into_response();
return Redirect::to(&format!(
"/user/urls?error=Short code/slug unavailable: {}",
e
))
.into_response();
}
}
@@ -181,6 +186,8 @@ pub async fn user_urls_create(
) {
Ok(d) => d,
Err(msg) => {
let urls_conn = state.db.global_urls.lock().unwrap();
let _ = crate::db::slugs::release_url_slug(&urls_conn, &code, &owner_tid);
return Redirect::to(&format!("/user/urls?error={}", msg)).into_response();
}
};
@@ -192,7 +199,11 @@ pub async fn user_urls_create(
} else {
match hash_password(&form.password) {
Ok(h) => Some(h),
Err(_) => return Redirect::to("/user/urls?error=Hashing error").into_response(),
Err(_) => {
let urls_conn = state.db.global_urls.lock().unwrap();
let _ = crate::db::slugs::release_url_slug(&urls_conn, &code, &owner_tid);
return Redirect::to("/user/urls?error=Hashing error").into_response();
}
}
};
@@ -202,7 +213,9 @@ pub async fn user_urls_create(
match form.max_access_count.trim().parse::<i64>() {
Ok(c) => Some(c),
Err(_) => {
return Redirect::to("/user/urls?error=Invalid max access count").into_response()
let urls_conn = state.db.global_urls.lock().unwrap();
let _ = crate::db::slugs::release_url_slug(&urls_conn, &code, &owner_tid);
return Redirect::to("/user/urls?error=Invalid max access count").into_response();
}
}
};
@@ -243,11 +256,8 @@ pub async fn user_urls_create(
match res {
Ok(url) => {
{
let system_conn = state.system_db.lock().unwrap();
let _ = 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],
);
let urls_conn = state.db.global_urls.lock().unwrap();
let _ = crate::db::slugs::activate_url_slug(&urls_conn, &code, &url.id);
}
{
let users_conn = state.users_db.lock().unwrap();
@@ -266,8 +276,8 @@ pub async fn user_urls_create(
Redirect::to("/user/urls").into_response()
}
Err(e) => {
let system_conn = state.system_db.lock().unwrap();
let _ = crate::db::users::release_global_slug(&system_conn, &code, user.id);
let urls_conn = state.db.global_urls.lock().unwrap();
let _ = crate::db::slugs::release_url_slug(&urls_conn, &code, &owner_tid);
Redirect::to(&format!("/user/urls?error=Database error: {}", e)).into_response()
}
}
@@ -306,11 +316,11 @@ pub async fn user_urls_delete(
);
match delete_url(&conn, &id) {
Ok(_) => {
let _ = crate::db::users::release_global_slug(
&state.system_db.lock().unwrap(),
&url.code,
user.id,
);
{
let urls_conn = state.db.global_urls.lock().unwrap();
let _ = urls_conn
.execute("DELETE FROM global_urls WHERE slug = ?1;", [&url.code]);
}
let ip = get_client_ip(&headers, connect_info);
let _ = write_audit_log(
&state.admin_db.lock().unwrap(),
@@ -351,12 +361,14 @@ pub async fn urls_get(
};
let (urls, total_pages, page, visible_pages) = {
let conn = state.content_db.lock().unwrap();
let total_records = if let Some(tag_str) = query.tag.as_deref() {
get_url_count_by_tag(&conn, tag_str).unwrap_or(0)
} else {
get_url_counts(&conn).map(|(t, _, _)| t).unwrap_or(0)
};
let conn = state.db.global_urls.lock().unwrap();
let total_records: i64 = conn
.query_row(
"SELECT COUNT(*) FROM global_urls WHERE status != 'retired';",
[],
|r| r.get(0),
)
.unwrap_or(0);
let calculated_total_pages = (total_records as usize).div_ceil(PAGE_SIZE);
let total_pages = std::cmp::max(1, calculated_total_pages);
let requested_page = query.page.unwrap_or(1);
@@ -368,8 +380,64 @@ pub async fn urls_get(
.clamp(1, total_pages);
let offset = (current_page - 1) * PAGE_SIZE;
let urls = list_urls(&conn, PAGE_SIZE as i64, offset as i64, query.tag.as_deref())
.unwrap_or_default();
let mut stmt = conn.prepare(
"SELECT slug, owner_tenant_id, target_id, created_at, updated_at, status FROM global_urls WHERE status != 'retired' ORDER BY created_at DESC LIMIT ?1 OFFSET ?2;"
).unwrap();
let rows = stmt
.query_map(rusqlite::params![PAGE_SIZE as i64, offset as i64], |row| {
let slug: String = row.get(0)?;
let owner_tid_str: String = row.get(1)?;
let target_id: String = row.get(2)?;
let created_at: String = row.get(3)?;
let updated_at: String = row.get(4)?;
let status: String = row.get(5)?;
Ok((
slug,
owner_tid_str,
target_id,
created_at,
updated_at,
status,
))
})
.unwrap();
let mut urls = Vec::new();
for item in rows.flatten() {
let (slug, owner_tid_str, target_id, created_at, updated_at, status) = item;
let mut resolved_url = None;
if let Ok(tid) = owner_tid_str.parse::<crate::identity::TenantId>() {
if let Ok(user_dbs) = state.open_tenant(tid, crate::state::TenantOpenMode::CoreJob)
{
let u_conn = user_dbs.content.lock().unwrap();
if let Ok(Some(u)) = crate::db::content::get_url_by_id(&u_conn, &target_id) {
resolved_url = Some(u);
}
}
}
if let Some(u) = resolved_url {
urls.push(u);
} else {
urls.push(crate::models::Url {
id: target_id,
code: slug,
destination: String::new(),
title: None,
description: None,
created_at,
updated_at,
status,
tags: vec![],
expires_at: None,
password_hash: None,
max_access_count: None,
access_count: 0,
expired: false,
last_latency_ms: None,
last_status: None,
});
}
}
let start_page = current_page.saturating_sub(3).max(1);
let end_page = std::cmp::min(total_pages, current_page + 3);
@@ -406,7 +474,8 @@ pub async fn urls_get(
template.into_response()
}
#[derive(Deserialize)]
#[derive(Deserialize, Default)]
#[serde(default)]
pub struct CreateUrlForm {
pub destination: String,
pub code: String,
@@ -427,170 +496,16 @@ pub struct CreateUrlForm {
pub async fn urls_create(
State(state): State<AppState>,
jar: CookieJar,
headers: HeaderMap,
connect_info: Option<ConnectInfo<SocketAddr>>,
Form(form): Form<CreateUrlForm>,
_headers: HeaderMap,
_connect_info: Option<ConnectInfo<SocketAddr>>,
Form(_form): Form<CreateUrlForm>,
) -> Response {
let (user, session_id) = match require_auth(&state, &jar).await {
let (_user, _session_id) = match require_auth(&state, &jar).await {
Ok(u) => u,
Err(redir) => return redir.into_response(),
};
if !verify_csrf(&session_id, &form.csrf_token) {
return Redirect::to("/admin/urls?error=Invalid CSRF token").into_response();
}
let ip = get_client_ip(&headers, connect_info);
let admin_user_id = user.id.parse::<i64>().unwrap_or(1);
// Custom Slug takes priority if provided
let mut code = form.custom_slug.trim().to_lowercase();
if code.is_empty() {
code = form.code.trim().to_lowercase();
if code.is_empty() {
code = generate_token(3);
} else {
if code.len() != 6 || !code.chars().all(|c| c.is_ascii_hexdigit()) {
return Redirect::to(
"/admin/urls?error=Custom code must be exactly 6 hex characters",
)
.into_response();
}
}
} else {
if !crate::utils::validation::validate_custom_slug(&code) {
return Redirect::to("/admin/urls?error=Custom slug must start with ! followed by 1-24 characters of a-z, 0-9, -, _")
.into_response();
}
}
let dest = match crate::services::urls::prepare_destination(
&form.destination,
crate::services::urls::UtmParams {
source: Some(&form.utm_source),
medium: Some(&form.utm_medium),
campaign: Some(&form.utm_campaign),
},
) {
Ok(d) => d,
Err(msg) => {
return Redirect::to(&format!("/admin/urls?error={}", msg)).into_response();
}
};
let expires_at_opt = crate::services::urls::parse_expires_at_input(&form.expires_at);
let password_hash_opt = if form.password.trim().is_empty() {
None
} else {
match hash_password(&form.password) {
Ok(h) => Some(h),
Err(_) => return Redirect::to("/admin/urls?error=Hashing error").into_response(),
}
};
let max_access_count_opt = if form.max_access_count.trim().is_empty() {
None
} else {
match form.max_access_count.trim().parse::<i64>() {
Ok(c) => Some(c),
Err(_) => {
return Redirect::to("/admin/urls?error=Invalid max access count").into_response()
}
}
};
let tags_list: Vec<String> = form
.tags
.split(',')
.map(|t| t.trim().to_string())
.filter(|t| !t.is_empty())
.collect();
let title_opt = if form.title.trim().is_empty() {
None
} else {
Some(form.title.trim())
};
let desc_opt = if form.description.trim().is_empty() {
None
} else {
Some(form.description.trim())
};
{
let users_conn = state.users_db.lock().unwrap();
if !crate::db::users::check_quota_limit(&users_conn, admin_user_id, "urls").unwrap_or(false)
{
return Redirect::to("/admin/urls?error=Quota limit exceeded").into_response();
}
}
{
let system_conn = state.system_db.lock().unwrap();
if !crate::db::users::is_slug_available(&system_conn, &code).unwrap_or(false) {
return Redirect::to("/admin/urls?error=Short code/slug already exists")
.into_response();
}
// Always use owner_user_id = 1 for admin content so it resolves via state.content_db
if let Err(e) =
crate::db::users::register_global_slug(&system_conn, &code, 1, "url", "", "reserving")
{
return Redirect::to(&format!("/admin/urls?error=Failed to reserve slug: {}", e))
.into_response();
}
}
let res = {
let conn = state.content_db.lock().unwrap();
crate::db::content::create_url_extended(
&conn,
&code,
&dest,
title_opt,
desc_opt,
&tags_list,
expires_at_opt.as_deref(),
password_hash_opt.as_deref(),
max_access_count_opt,
)
};
match res {
Ok(url) => {
{
let system_conn = state.system_db.lock().unwrap();
let _ = 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],
);
}
{
let users_conn = state.users_db.lock().unwrap();
let _ =
crate::db::users::increment_quota_counter(&users_conn, admin_user_id, "urls");
}
{
let conn = state.admin_db.lock().unwrap();
let _ = write_audit_log(
&conn,
&state,
&user.username,
"URL_CREATION",
Some("url"),
Some(&url.id),
Some(&ip),
headers.get("user-agent").and_then(|h| h.to_str().ok()),
);
}
Redirect::to("/admin/urls").into_response()
}
Err(e) => {
let system_conn = state.system_db.lock().unwrap();
let _ = crate::db::users::release_global_slug(&system_conn, &code, 1);
Redirect::to(&format!("/admin/urls?error=Database error: {}", e)).into_response()
}
}
Redirect::to("/admin/urls?error=Admin is a platform operator and cannot create unowned application URLs; create links via a tenant user account").into_response()
}
// POST /admin/urls/delete/:id
@@ -614,26 +529,42 @@ pub async fn urls_delete(
let ip = get_client_ip(&headers, connect_info);
let conn = state.content_db.lock().unwrap();
match delete_url(&conn, &id) {
Ok(_) => {
{
let conn_admin = state.admin_db.lock().unwrap();
let _ = write_audit_log(
&conn_admin,
&state,
&user.username,
"URL_DELETION",
Some("url"),
Some(&id),
Some(&ip),
headers.get("user-agent").and_then(|h| h.to_str().ok()),
);
// Look up owner tenant in global_urls
let owner_info = {
let conn = state.db.global_urls.lock().unwrap();
conn.query_row(
"SELECT slug, owner_tenant_id FROM global_urls WHERE target_id = ?1 OR slug = ?1 LIMIT 1;",
rusqlite::params![id],
|row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
).ok()
};
if let Some((slug, tid_str)) = owner_info {
if let Ok(tid) = tid_str.parse::<crate::identity::TenantId>() {
if let Ok(user_dbs) = state.open_tenant(tid, crate::state::TenantOpenMode::CoreJob) {
let conn = user_dbs.content.lock().unwrap();
let _ = delete_url(&conn, &id);
}
Redirect::to("/admin/urls").into_response()
}
Err(e) => {
Redirect::to(&format!("/admin/urls?error=Failed to delete link: {}", e)).into_response()
}
let conn = state.db.global_urls.lock().unwrap();
let _ = conn.execute(
"UPDATE global_urls SET status = 'retired', updated_at = ?1 WHERE slug = ?2;",
rusqlite::params![chrono::Utc::now().to_rfc3339(), slug],
);
}
{
let conn_admin = state.admin_db.lock().unwrap();
let _ = write_audit_log(
&conn_admin,
&state,
&user.username,
"URL_DELETION",
Some("url"),
Some(&id),
Some(&ip),
headers.get("user-agent").and_then(|h| h.to_str().ok()),
);
}
Redirect::to("/admin/urls").into_response()
}
+14 -44
View File
@@ -133,12 +133,14 @@ pub async fn users_create_post(
}
};
if let Err(e) = state.db.init_user_databases(new_user.id) {
return Redirect::to(&format!(
"/admin/users?error=Failed to initialize user databases: {}",
e
))
.into_response();
if new_user.account_type == "standard" {
if let Err(e) = state.db.init_user_databases(new_user.id) {
return Redirect::to(&format!(
"/admin/users?error=Failed to initialize user databases: {}",
e
))
.into_response();
}
}
{
@@ -481,25 +483,9 @@ pub async fn user_detail_get(
let target_user = {
let conn = state.users_db.lock().unwrap();
let u_res = conn.query_row(
"SELECT id, username, password_hash, status, created_at, last_login, account_type, organization_id, metadata \
FROM users WHERE id = ?1;",
[id],
|row| Ok(crate::models::TenantUser {
id: row.get(0)?,
username: row.get(1)?,
password_hash: row.get(2)?,
status: row.get(3)?,
created_at: row.get(4)?,
last_login: row.get(5)?,
account_type: row.get(6)?,
organization_id: row.get(7)?,
metadata: row.get(8)?,
})
);
match u_res {
Ok(u) => u,
Err(_) => return Redirect::to("/admin/users?error=User not found").into_response(),
match crate::db::users::get_user_by_id(&conn, id) {
Ok(Some(u)) => u,
_ => return Redirect::to("/admin/users?error=User not found").into_response(),
}
};
@@ -576,25 +562,9 @@ pub async fn user_edit_get(
let target_user = {
let conn = state.users_db.lock().unwrap();
let u_res = conn.query_row(
"SELECT id, username, password_hash, status, created_at, last_login, account_type, organization_id, metadata \
FROM users WHERE id = ?1;",
[id],
|row| Ok(crate::models::TenantUser {
id: row.get(0)?,
username: row.get(1)?,
password_hash: row.get(2)?,
status: row.get(3)?,
created_at: row.get(4)?,
last_login: row.get(5)?,
account_type: row.get(6)?,
organization_id: row.get(7)?,
metadata: row.get(8)?,
})
);
match u_res {
Ok(u) => u,
Err(_) => return Redirect::to("/admin/users?error=User not found").into_response(),
match crate::db::users::get_user_by_id(&conn, id) {
Ok(Some(u)) => u,
_ => return Redirect::to("/admin/users?error=User not found").into_response(),
}
};
+253 -117
View File
@@ -12,12 +12,10 @@ use crate::auth::ApiUser;
use crate::db::admin::write_audit_log;
use crate::db::analytics::{
get_clicks_trend, get_clicks_trend_raw, get_metric_rankings, get_metric_rankings_raw,
get_total_clicks, get_total_page_views,
};
use crate::db::content::{
create_landing_page, delete_landing_page, delete_url, get_landing_page_by_id,
get_landing_page_count, get_url_by_id, get_url_counts, list_landing_pages, list_urls,
update_landing_page, update_url,
create_landing_page, delete_landing_page, delete_url, get_landing_page_by_id, get_url_by_id,
list_landing_pages, list_urls, update_landing_page, update_url,
};
use crate::state::AppState;
use crate::utils::get_client_ip;
@@ -73,6 +71,30 @@ pub struct ApiError {
pub error: String,
}
#[allow(clippy::result_large_err)]
fn get_api_tenant_dbs(state: &AppState, user: &ApiUser) -> Result<crate::state::UserDbs, Response> {
match user.0 {
crate::models::ApiActor::User(ref u) => {
state.get_user_dbs(u.id).map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
error: "Database error".to_string(),
}),
)
.into_response()
})
}
crate::models::ApiActor::Admin(_) => Err((
StatusCode::BAD_REQUEST,
Json(ApiError {
error: "Admin is a platform operator and cannot access application content directly without tenant context".to_string(),
}),
)
.into_response()),
}
}
// --- URL Endpoints ---
// POST /api/v1/urls
@@ -138,9 +160,17 @@ pub async fn api_create_url(
None
};
// Dynamically resolve target user ID and content DB
let (target_user_id, content_db) = match user.0 {
crate::models::ApiActor::Admin(_) => (1, state.content_db.clone()),
// Dynamically resolve target user ID, tenant ID, and content DB
let (target_user_id, target_tenant_id, content_db) = match user.0 {
crate::models::ApiActor::Admin(_) => {
return (
StatusCode::BAD_REQUEST,
Json(ApiError {
error: "Admin is a platform operator and cannot create application URLs directly without tenant context".to_string(),
}),
)
.into_response();
}
crate::models::ApiActor::User(ref u) => {
let user_dbs = match state.get_user_dbs(u.id) {
Ok(dbs) => dbs,
@@ -154,7 +184,10 @@ pub async fn api_create_url(
.into_response()
}
};
(u.id, user_dbs.content.clone())
let tid = u
.tenant_id
.unwrap_or_else(crate::identity::TenantId::generate);
(u.id, tid, user_dbs.content.clone())
}
};
@@ -174,30 +207,22 @@ pub async fn api_create_url(
}
}
// Check availability
// Check availability and reserve in v0.8 slug registry
{
let system_conn = state.system_db.lock().unwrap();
if !crate::db::users::is_slug_available(&system_conn, &code).unwrap_or(false) {
let reserved_conn = state.db.reserved.lock().unwrap();
let urls_conn = state.db.global_urls.lock().unwrap();
let pages_conn = state.db.global_landing_pages.lock().unwrap();
if let Err(e) = crate::db::slugs::reserve_url_slug(
&reserved_conn,
&urls_conn,
&pages_conn,
&code,
&target_tenant_id,
) {
return (
StatusCode::CONFLICT,
Json(ApiError {
error: "Short code already exists".to_string(),
}),
)
.into_response();
}
if let Err(e) = crate::db::users::register_global_slug(
&system_conn,
&code,
target_user_id,
"url",
"",
"reserving",
) {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
error: format!("Failed to reserve slug: {}", e),
error: format!("Short code unavailable: {}", e),
}),
)
.into_response();
@@ -222,13 +247,10 @@ pub async fn api_create_url(
match res {
Ok(url) => {
// Activate slug
// Activate slug in v0.8 global_urls.db
{
let system_conn = state.system_db.lock().unwrap();
let _ = 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],
);
let urls_conn = state.db.global_urls.lock().unwrap();
let _ = crate::db::slugs::activate_url_slug(&urls_conn, &code, &url.id);
}
// Increment quota
{
@@ -264,8 +286,8 @@ pub async fn api_create_url(
(StatusCode::CREATED, Json(url)).into_response()
}
Err(e) => {
let system_conn = state.system_db.lock().unwrap();
let _ = crate::db::users::release_global_slug(&system_conn, &code, target_user_id);
let urls_conn = state.db.global_urls.lock().unwrap();
let _ = crate::db::slugs::release_url_slug(&urls_conn, &code, &target_tenant_id);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
@@ -287,13 +309,17 @@ pub struct ListQuery {
pub async fn api_list_urls(
State(state): State<AppState>,
_user: ApiUser,
user: ApiUser,
Query(query): Query<ListQuery>,
) -> Response {
let limit = query.limit.unwrap_or(100);
let offset = query.offset.unwrap_or(0);
let conn = state.content_db.lock().unwrap();
let dbs = match get_api_tenant_dbs(&state, &user) {
Ok(d) => d,
Err(r) => return r,
};
let conn = dbs.content.lock().unwrap();
match list_urls(&conn, limit, offset, query.tag.as_deref()) {
Ok(urls) => Json(urls).into_response(),
Err(e) => (
@@ -309,10 +335,14 @@ pub async fn api_list_urls(
// GET /api/v1/urls/:uuid
pub async fn api_get_url(
State(state): State<AppState>,
_user: ApiUser,
user: ApiUser,
Path(uuid): Path<String>,
) -> Response {
let conn = state.content_db.lock().unwrap();
let dbs = match get_api_tenant_dbs(&state, &user) {
Ok(d) => d,
Err(r) => return r,
};
let conn = dbs.content.lock().unwrap();
match get_url_by_id(&conn, &uuid) {
Ok(Some(url)) => Json(url).into_response(),
Ok(None) => (
@@ -351,7 +381,11 @@ pub async fn api_update_url(
)
.into_response();
}
let conn = state.content_db.lock().unwrap();
let dbs = match get_api_tenant_dbs(&state, &user) {
Ok(d) => d,
Err(r) => return r,
};
let conn = dbs.content.lock().unwrap();
match update_url(
&conn,
&uuid,
@@ -438,9 +472,21 @@ pub async fn api_delete_url(
user: ApiUser,
Path(uuid): Path<String>,
) -> Response {
let conn = state.content_db.lock().unwrap();
let dbs = match get_api_tenant_dbs(&state, &user) {
Ok(d) => d,
Err(r) => return r,
};
let conn = dbs.content.lock().unwrap();
let code_opt = match get_url_by_id(&conn, &uuid) {
Ok(Some(u)) => Some(u.code),
_ => None,
};
match delete_url(&conn, &uuid) {
Ok(true) => {
if let Some(code) = code_opt {
let urls_conn = state.db.global_urls.lock().unwrap();
let _ = urls_conn.execute("DELETE FROM global_urls WHERE slug = ?1;", [&code]);
}
let ip = get_client_ip(&headers, connect_info);
let user_agent = headers.get("user-agent").and_then(|h| h.to_str().ok());
let _ = write_audit_log(
@@ -510,9 +556,17 @@ pub async fn api_create_page(
}
}
// Dynamically resolve target user ID and content DB
let (target_user_id, content_db) = match user.0 {
crate::models::ApiActor::Admin(_) => (1, state.content_db.clone()),
// Dynamically resolve target user ID, tenant ID, and content DB
let (target_user_id, target_tenant_id, content_db) = match user.0 {
crate::models::ApiActor::Admin(_) => {
return (
StatusCode::BAD_REQUEST,
Json(ApiError {
error: "Admin is a platform operator and cannot create application landing pages directly without tenant context".to_string(),
}),
)
.into_response();
}
crate::models::ApiActor::User(ref u) => {
let user_dbs = match state.get_user_dbs(u.id) {
Ok(dbs) => dbs,
@@ -526,7 +580,10 @@ pub async fn api_create_page(
.into_response()
}
};
(u.id, user_dbs.content.clone())
let tid = u
.tenant_id
.unwrap_or_else(crate::identity::TenantId::generate);
(u.id, tid, user_dbs.content.clone())
}
};
@@ -546,30 +603,22 @@ pub async fn api_create_page(
}
}
// Check availability
// Check availability and reserve in v0.8 slug registry
{
let system_conn = state.system_db.lock().unwrap();
if !crate::db::users::is_slug_available(&system_conn, &code).unwrap_or(false) {
let reserved_conn = state.db.reserved.lock().unwrap();
let urls_conn = state.db.global_urls.lock().unwrap();
let pages_conn = state.db.global_landing_pages.lock().unwrap();
if let Err(e) = crate::db::slugs::reserve_landing_page_slug(
&reserved_conn,
&urls_conn,
&pages_conn,
&code,
&target_tenant_id,
) {
return (
StatusCode::CONFLICT,
Json(ApiError {
error: "Short code already exists".to_string(),
}),
)
.into_response();
}
if let Err(e) = crate::db::users::register_global_slug(
&system_conn,
&code,
target_user_id,
"page",
"",
"reserving",
) {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
error: format!("Failed to reserve slug: {}", e),
error: format!("Short code unavailable: {}", e),
}),
)
.into_response();
@@ -590,18 +639,10 @@ pub async fn api_create_page(
match res {
Ok(page) => {
// Activate slug
// Activate slug in v0.8 global_landing_pages.db
{
let system_conn = state.system_db.lock().unwrap();
let global_status = if payload.state == "published" {
"active"
} else {
"disabled"
};
let _ = system_conn.execute(
"UPDATE global_slugs SET target_id = ?1, status = ?2, updated_at = ?3 WHERE slug = ?4;",
rusqlite::params![page.id, global_status, chrono::Utc::now().to_rfc3339(), code],
);
let pages_conn = state.db.global_landing_pages.lock().unwrap();
let _ = crate::db::slugs::activate_landing_page_slug(&pages_conn, &code, &page.id);
}
// Increment quota
{
@@ -627,8 +668,9 @@ pub async fn api_create_page(
(StatusCode::CREATED, Json(page)).into_response()
}
Err(e) => {
let system_conn = state.system_db.lock().unwrap();
let _ = crate::db::users::release_global_slug(&system_conn, &code, target_user_id);
let pages_conn = state.db.global_landing_pages.lock().unwrap();
let _ =
crate::db::slugs::release_landing_page_slug(&pages_conn, &code, &target_tenant_id);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ApiError {
@@ -643,13 +685,17 @@ pub async fn api_create_page(
// GET /api/v1/pages
pub async fn api_list_pages(
State(state): State<AppState>,
_user: ApiUser,
user: ApiUser,
Query(query): Query<ListQuery>,
) -> Response {
let limit = query.limit.unwrap_or(100);
let offset = query.offset.unwrap_or(0);
let conn = state.content_db.lock().unwrap();
let dbs = match get_api_tenant_dbs(&state, &user) {
Ok(d) => d,
Err(r) => return r,
};
let conn = dbs.content.lock().unwrap();
match list_landing_pages(&conn, limit, offset) {
Ok(pages) => Json(pages).into_response(),
Err(e) => (
@@ -665,10 +711,14 @@ pub async fn api_list_pages(
// GET /api/v1/pages/:uuid
pub async fn api_get_page(
State(state): State<AppState>,
_user: ApiUser,
user: ApiUser,
Path(uuid): Path<String>,
) -> Response {
let conn = state.content_db.lock().unwrap();
let dbs = match get_api_tenant_dbs(&state, &user) {
Ok(d) => d,
Err(r) => return r,
};
let conn = dbs.content.lock().unwrap();
match get_landing_page_by_id(&conn, &uuid) {
Ok(Some(page)) => Json(page).into_response(),
Ok(None) => (
@@ -697,7 +747,11 @@ pub async fn api_update_page(
Path(uuid): Path<String>,
Json(payload): Json<UpdatePageRequest>,
) -> Response {
let conn = state.content_db.lock().unwrap();
let dbs = match get_api_tenant_dbs(&state, &user) {
Ok(d) => d,
Err(r) => return r,
};
let conn = dbs.content.lock().unwrap();
match update_landing_page(
&conn,
&uuid,
@@ -745,9 +799,22 @@ pub async fn api_delete_page(
user: ApiUser,
Path(uuid): Path<String>,
) -> Response {
let conn = state.content_db.lock().unwrap();
let dbs = match get_api_tenant_dbs(&state, &user) {
Ok(d) => d,
Err(r) => return r,
};
let conn = dbs.content.lock().unwrap();
let code_opt = match get_landing_page_by_id(&conn, &uuid) {
Ok(Some(p)) => Some(p.code),
_ => None,
};
match delete_landing_page(&conn, &uuid) {
Ok(true) => {
if let Some(code) = code_opt {
let pages_conn = state.db.global_landing_pages.lock().unwrap();
let _ = pages_conn
.execute("DELETE FROM global_landing_pages WHERE slug = ?1;", [&code]);
}
let ip = get_client_ip(&headers, connect_info);
let user_agent = headers.get("user-agent").and_then(|h| h.to_str().ok());
let _ = write_audit_log(
@@ -797,20 +864,44 @@ pub async fn api_overall_stats(State(state): State<AppState>, user: ApiUser) ->
}
let (total_urls, active_links, dead_links) = {
let conn = state.content_db.lock().unwrap();
get_url_counts(&conn).unwrap_or((0, 0, 0))
let conn = state.db.global_urls.lock().unwrap();
let total: i64 = conn
.query_row(
"SELECT COUNT(*) FROM global_urls WHERE status != 'retired';",
[],
|r| r.get(0),
)
.unwrap_or(0);
let active: i64 = conn
.query_row(
"SELECT COUNT(*) FROM global_urls WHERE status = 'active';",
[],
|r| r.get(0),
)
.unwrap_or(0);
let dead: i64 = conn
.query_row(
"SELECT COUNT(*) FROM global_urls WHERE status = 'disabled';",
[],
|r| r.get(0),
)
.unwrap_or(0);
(total, active, dead)
};
let total_pages = {
let conn = state.content_db.lock().unwrap();
get_landing_page_count(&conn).unwrap_or(0)
};
let (total_clicks, total_page_views) = {
let conn = state.analytics_db.lock().unwrap();
(
get_total_clicks(&conn).unwrap_or(0),
get_total_page_views(&conn).unwrap_or(0),
let conn = state.db.global_landing_pages.lock().unwrap();
conn.query_row(
"SELECT COUNT(*) FROM global_landing_pages WHERE status != 'retired';",
[],
|r| r.get(0),
)
.unwrap_or(0)
};
let total_clicks = {
let users_conn = state.users_db.lock().unwrap();
crate::db::users::get_platform_total_clicks(&state.db.topology, &users_conn).unwrap_or(0)
};
let total_page_views = 0i64;
Json(OverallStatsResponse {
total_urls,
@@ -835,12 +926,17 @@ pub struct DetailStatsResponse {
// GET /api/v1/stats/url/:uuid
pub async fn api_url_stats(
State(state): State<AppState>,
_user: ApiUser,
user: ApiUser,
Path(uuid): Path<String>,
) -> Response {
let dbs = match get_api_tenant_dbs(&state, &user) {
Ok(d) => d,
Err(r) => return r,
};
// Verify URL exists
{
let conn = state.content_db.lock().unwrap();
let conn = dbs.content.lock().unwrap();
if get_url_by_id(&conn, &uuid).unwrap_or(None).is_none() {
return (
StatusCode::NOT_FOUND,
@@ -852,7 +948,7 @@ pub async fn api_url_stats(
}
}
let conn = state.analytics_db.lock().unwrap();
let conn = dbs.analytics.lock().unwrap();
let clicks = get_clicks_trend(&conn, "url", &uuid, 30)
.or_else(|_| get_clicks_trend_raw(&conn, "url", &uuid, 30))
.unwrap_or_default();
@@ -879,12 +975,17 @@ pub async fn api_url_stats(
// GET /api/v1/stats/page/:uuid
pub async fn api_page_stats(
State(state): State<AppState>,
_user: ApiUser,
user: ApiUser,
Path(uuid): Path<String>,
) -> Response {
let dbs = match get_api_tenant_dbs(&state, &user) {
Ok(d) => d,
Err(r) => return r,
};
// Verify Page exists
{
let conn = state.content_db.lock().unwrap();
let conn = dbs.content.lock().unwrap();
if get_landing_page_by_id(&conn, &uuid)
.unwrap_or(None)
.is_none()
@@ -899,7 +1000,7 @@ pub async fn api_page_stats(
}
}
let conn = state.analytics_db.lock().unwrap();
let conn = dbs.analytics.lock().unwrap();
let clicks = get_clicks_trend(&conn, "page", &uuid, 30)
.or_else(|_| get_clicks_trend_raw(&conn, "page", &uuid, 30))
.unwrap_or_default();
@@ -935,11 +1036,16 @@ pub struct QrStatsResponse {
// GET /api/v1/qr/:code
pub async fn api_get_qr_stats(
State(state): State<AppState>,
_user: ApiUser,
user: ApiUser,
Path(code): Path<String>,
) -> Response {
let dbs = match get_api_tenant_dbs(&state, &user) {
Ok(d) => d,
Err(r) => return r,
};
let url_opt = {
let conn = state.content_db.lock().unwrap();
let conn = dbs.content.lock().unwrap();
crate::db::content::get_url_by_code(&conn, &code).unwrap_or(None)
};
@@ -957,7 +1063,7 @@ pub async fn api_get_qr_stats(
};
let (scan_count, scans) = {
let conn = state.analytics_db.lock().unwrap();
let conn = dbs.analytics.lock().unwrap();
let count = crate::db::qr::get_qr_scan_count(&conn, &url.id).unwrap_or(0);
let scans_list = crate::db::qr::get_qr_stats_for_url(&conn, &url.id).unwrap_or_default();
(count, scans_list)
@@ -1030,9 +1136,14 @@ pub async fn api_set_preview(
Path(uuid): Path<String>,
Json(payload): Json<SetPreviewRequest>,
) -> Response {
let dbs = match get_api_tenant_dbs(&state, &user) {
Ok(d) => d,
Err(r) => return r,
};
// Verify URL exists
{
let conn = state.content_db.lock().unwrap();
let conn = dbs.content.lock().unwrap();
if get_url_by_id(&conn, &uuid).unwrap_or(None).is_none() {
return (
StatusCode::NOT_FOUND,
@@ -1044,7 +1155,7 @@ pub async fn api_set_preview(
}
}
let conn = state.content_db.lock().unwrap();
let conn = dbs.content.lock().unwrap();
match crate::db::preview::upsert_preview(
&conn,
&uuid,
@@ -1081,12 +1192,17 @@ pub async fn api_set_preview(
// GET /api/v1/urls/:uuid/preview
pub async fn api_get_preview(
State(state): State<AppState>,
_user: ApiUser,
user: ApiUser,
Path(uuid): Path<String>,
) -> Response {
let dbs = match get_api_tenant_dbs(&state, &user) {
Ok(d) => d,
Err(r) => return r,
};
// Verify URL exists
{
let conn = state.content_db.lock().unwrap();
let conn = dbs.content.lock().unwrap();
if get_url_by_id(&conn, &uuid).unwrap_or(None).is_none() {
return (
StatusCode::NOT_FOUND,
@@ -1098,7 +1214,7 @@ pub async fn api_get_preview(
}
}
let conn = state.content_db.lock().unwrap();
let conn = dbs.content.lock().unwrap();
match crate::db::preview::get_preview(&conn, &uuid) {
Ok(Some(preview)) => Json(preview).into_response(),
Ok(None) => (
@@ -1124,9 +1240,14 @@ pub async fn api_delete_preview(
user: ApiUser,
Path(uuid): Path<String>,
) -> Response {
let dbs = match get_api_tenant_dbs(&state, &user) {
Ok(d) => d,
Err(r) => return r,
};
// Verify URL exists
{
let conn = state.content_db.lock().unwrap();
let conn = dbs.content.lock().unwrap();
if get_url_by_id(&conn, &uuid).unwrap_or(None).is_none() {
return (
StatusCode::NOT_FOUND,
@@ -1138,7 +1259,7 @@ pub async fn api_delete_preview(
}
}
let conn = state.content_db.lock().unwrap();
let conn = dbs.content.lock().unwrap();
match crate::db::preview::delete_preview(&conn, &uuid) {
Ok(true) => {
// Audit Log
@@ -1186,9 +1307,14 @@ pub async fn api_set_password(
Path(uuid): Path<String>,
Json(payload): Json<SetPasswordRequest>,
) -> Response {
let dbs = match get_api_tenant_dbs(&state, &user) {
Ok(d) => d,
Err(r) => return r,
};
// Verify URL exists
{
let conn = state.content_db.lock().unwrap();
let conn = dbs.content.lock().unwrap();
if get_url_by_id(&conn, &uuid).unwrap_or(None).is_none() {
return (
StatusCode::NOT_FOUND,
@@ -1213,7 +1339,7 @@ pub async fn api_set_password(
}
};
let conn = state.content_db.lock().unwrap();
let conn = dbs.content.lock().unwrap();
match crate::db::content::set_url_password(&conn, &uuid, &hash) {
Ok(true) => {
// Audit Log
@@ -1257,9 +1383,14 @@ pub async fn api_remove_password(
user: ApiUser,
Path(uuid): Path<String>,
) -> Response {
let dbs = match get_api_tenant_dbs(&state, &user) {
Ok(d) => d,
Err(r) => return r,
};
// Verify URL exists
{
let conn = state.content_db.lock().unwrap();
let conn = dbs.content.lock().unwrap();
if get_url_by_id(&conn, &uuid).unwrap_or(None).is_none() {
return (
StatusCode::NOT_FOUND,
@@ -1271,7 +1402,7 @@ pub async fn api_remove_password(
}
}
let conn = state.content_db.lock().unwrap();
let conn = dbs.content.lock().unwrap();
match crate::db::content::remove_url_password(&conn, &uuid) {
Ok(true) => {
// Audit Log
@@ -1321,9 +1452,14 @@ pub async fn api_create_qr(
user: ApiUser,
Json(payload): Json<CreateQrRequest>,
) -> Response {
let dbs = match get_api_tenant_dbs(&state, &user) {
Ok(d) => d,
Err(r) => return r,
};
// Verify URL exists in content.db
{
let conn = state.content_db.lock().unwrap();
let conn = dbs.content.lock().unwrap();
if get_url_by_id(&conn, &payload.url_id)
.unwrap_or(None)
.is_none()
@@ -1339,7 +1475,7 @@ pub async fn api_create_qr(
}
let style = payload.style.unwrap_or_else(|| "default".to_string());
let conn = state.content_db.lock().unwrap();
let conn = dbs.content.lock().unwrap();
match crate::db::qr::upsert_qr_code(&conn, &payload.url_id, &style) {
Ok(_) => {
// Write Audit Event
+56 -6
View File
@@ -67,9 +67,36 @@ pub async fn api_bulk_qr(
}
// Retrieve URLs from database
let content_db = match user.0 {
crate::models::ApiActor::User(ref u) => {
let user_dbs = match state.get_user_dbs(u.id) {
Ok(dbs) => dbs,
Err(_) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(BulkErrorResponse {
error: "Database error".to_string(),
}),
)
.into_response();
}
};
user_dbs.content.clone()
}
crate::models::ApiActor::Admin(_) => {
return (
StatusCode::BAD_REQUEST,
Json(BulkErrorResponse {
error: "Admin is a platform operator and cannot export application URLs without tenant context".to_string(),
}),
)
.into_response();
}
};
let mut urls = Vec::new();
{
let conn = match lock_db(&state.content_db, "content_db") {
let conn = match lock_db(&content_db, "content_db") {
Ok(c) => c,
Err(e) => {
return (
@@ -186,10 +213,30 @@ pub async fn api_bulk_url(
.into_response();
}
// Dynamically resolve target user ID and content DB
let (target_user_id, content_db) = match user.0 {
crate::models::ApiActor::Admin(_) => (1, state.content_db.clone()),
// Dynamically resolve target user ID, TenantId, and content DB
let (target_user_id, target_tenant_id, content_db) = match user.0 {
crate::models::ApiActor::Admin(_) => {
return (
StatusCode::BAD_REQUEST,
Json(BulkErrorResponse {
error: "Admin is a platform operator and cannot create application URLs directly without tenant context".to_string(),
}),
)
.into_response();
}
crate::models::ApiActor::User(ref u) => {
let tenant_id = match u.tenant_id {
Some(tid) => tid,
None => {
return (
StatusCode::BAD_REQUEST,
Json(BulkErrorResponse {
error: "User has no tenant context".to_string(),
}),
)
.into_response();
}
};
let user_dbs = match state.get_user_dbs(u.id) {
Ok(dbs) => dbs,
Err(_) => {
@@ -202,7 +249,7 @@ pub async fn api_bulk_url(
.into_response()
}
};
(u.id, user_dbs.content.clone())
(u.id, tenant_id, user_dbs.content.clone())
}
};
@@ -226,9 +273,12 @@ pub async fn api_bulk_url(
let created_urls = match create_urls_bulk(
&content_db,
&state.system_db,
&state.db.reserved,
&state.db.global_urls,
&state.db.global_landing_pages,
&state.users_db,
target_user_id,
target_tenant_id,
items,
) {
Ok(urls) => urls,
+137 -273
View File
@@ -4,11 +4,11 @@ use axum::{
response::{IntoResponse, Json, Response},
};
use chrono::Utc;
use rusqlite::OptionalExtension;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::auth::ApiUser;
use crate::identity::TenantId;
use crate::models::ApiActor;
use crate::state::AppState;
@@ -354,40 +354,68 @@ pub fn delete_user_resources(
}
};
// 1. Transactional clean up on system.db (deleting their global slug mappings)
{
let mut system_conn = state.system_db.lock().unwrap();
let tx = system_conn.transaction().map_err(|e| e.to_string())?;
// 1. Transactional clean up on v0.8 slug databases
let now = Utc::now().to_rfc3339();
if let Some(ref tid) = user_details.tenant_id {
let tid_str = tid.to_string();
let urls_conn = state.db.global_urls.lock().unwrap();
let pages_conn = state.db.global_landing_pages.lock().unwrap();
let system_conn = state.system_db.lock().unwrap();
let slugs: Vec<String> = {
let mut stmt = tx
.prepare("SELECT slug FROM global_slugs WHERE owner_user_id = ?1;")
.map_err(|e| e.to_string())?;
let rows = stmt
.query_map([target_id], |row| row.get(0))
.map_err(|e| e.to_string())?;
rows.filter_map(|r| r.ok()).collect()
// Get all URL slugs owned by tenant
let url_slugs: Vec<String> = if let Ok(mut stmt) =
urls_conn.prepare("SELECT slug FROM global_urls WHERE owner_tenant_id = ?1;")
{
stmt.query_map([&tid_str], |row| row.get::<_, String>(0))
.map(|rows| rows.filter_map(|r| r.ok()).collect())
.unwrap_or_default()
} else {
Vec::new()
};
let now = Utc::now().to_rfc3339();
for slug in slugs {
let _ = tx.execute("DELETE FROM global_slugs WHERE slug = ?1;", [&slug]);
let _ = tx.execute(
for slug in url_slugs {
let _ = urls_conn.execute("DELETE FROM global_urls WHERE slug = ?1;", [&slug]);
let _ = system_conn.execute(
"INSERT INTO slug_history (slug, old_owner_user_id, new_owner_user_id, action, timestamp, admin_username)
VALUES (?1, ?2, NULL, 'deleted', ?3, ?4);",
rusqlite::params![slug, target_id, now, admin_username],
);
}
tx.commit()
.map_err(|e| format!("Failed to release slugs: {}", e))?;
// Get all page slugs owned by tenant
let page_slugs: Vec<String> = if let Ok(mut stmt) =
pages_conn.prepare("SELECT slug FROM global_landing_pages WHERE owner_tenant_id = ?1;")
{
stmt.query_map([&tid_str], |row| row.get::<_, String>(0))
.map(|rows| rows.filter_map(|r| r.ok()).collect())
.unwrap_or_default()
} else {
Vec::new()
};
for slug in page_slugs {
let _ =
pages_conn.execute("DELETE FROM global_landing_pages WHERE slug = ?1;", [&slug]);
let _ = system_conn.execute(
"INSERT INTO slug_history (slug, old_owner_user_id, new_owner_user_id, action, timestamp, admin_username)
VALUES (?1, ?2, NULL, 'deleted', ?3, ?4);",
rusqlite::params![slug, target_id, now, admin_username],
);
}
}
let user_dir = state
.config
.data_dir
.join("users")
.join(target_id.to_string());
let user_dir = {
let conn = state.users_db.lock().unwrap();
match crate::db::users::get_user_by_id(&conn, target_id) {
Ok(Some(u)) => crate::db::tenant::location_for_user(&u)
.and_then(|loc| {
loc.dir(&state.db.topology)
.map_err(|e| crate::error::AppError::BadRequest(e.to_string()))
})
.map_err(|e| e.to_string())?,
_ => return Err("User not found".into()),
}
};
if user_dir.exists() {
let _ = std::fs::remove_dir_all(&user_dir);
}
@@ -444,231 +472,23 @@ pub async fn admin_transfer_slug(
Err(err_resp) => return err_resp,
};
// 1. Check if the slug exists and get details
let (old_owner_user_id, target_type, _target_id) = {
let system_conn = state.system_db.lock().unwrap();
let mut stmt = match system_conn.prepare(
"SELECT owner_user_id, target_type, target_id FROM global_slugs WHERE slug = ?1;",
) {
Ok(s) => s,
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
};
let row_opt = stmt
.query_row([&payload.slug], |row| {
Ok((
row.get::<_, i64>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
))
})
.optional();
match row_opt {
Ok(Some(r)) => r,
Ok(None) => return (StatusCode::NOT_FOUND, "Slug not found").into_response(),
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
}
let req = crate::services::slug_transfer::SlugTransferRequest {
slug: payload.slug,
new_owner_user_id: payload.new_owner_user_id,
};
if old_owner_user_id == payload.new_owner_user_id {
return (
StatusCode::BAD_REQUEST,
"New owner must be different from the current owner",
)
.into_response();
match crate::services::slug_transfer::transfer_slug(&state, &req, &admin.username) {
Ok(_) => StatusCode::OK.into_response(),
Err(crate::services::slug_transfer::TransferError::NotFound(m)) => {
(StatusCode::NOT_FOUND, m.to_string()).into_response()
}
Err(crate::services::slug_transfer::TransferError::BadRequest(m)) => {
(StatusCode::BAD_REQUEST, m).into_response()
}
Err(crate::services::slug_transfer::TransferError::Internal(m)) => {
(StatusCode::INTERNAL_SERVER_ERROR, m).into_response()
}
}
// 2. Fetch destination databases
let old_dbs = match state.get_user_dbs(old_owner_user_id) {
Ok(dbs) => dbs,
Err(_) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to load current owner's database",
)
.into_response()
}
};
let new_dbs = match state.get_user_dbs(payload.new_owner_user_id) {
Ok(dbs) => dbs,
Err(_) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to load new owner's database",
)
.into_response()
}
};
// 3. Perform transfer: copy record from old owner's content.db to new owner's content.db
let mut new_target_id = String::new();
let transfer_success = {
let old_conn = old_dbs.content.lock().unwrap();
let new_conn = new_dbs.content.lock().unwrap();
if target_type == "url" {
// Get URL record
let url_opt = match crate::db::content::get_url_by_code(&old_conn, &payload.slug) {
Ok(u) => u,
Err(e) => {
return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
}
};
if let Some(url) = url_opt {
// Check new owner quotas
let new_users_conn = state.users_db.lock().unwrap();
let quota_opt =
crate::db::users::get_user_quotas(&new_users_conn, payload.new_owner_user_id)
.unwrap_or(None);
if let Some(quota) = quota_opt {
if quota.current_urls >= quota.max_urls {
return (
StatusCode::BAD_REQUEST,
"New owner has exceeded URL quota limit",
)
.into_response();
}
}
// Insert into new owner database
let ins_res = crate::db::content::create_url_extended(
&new_conn,
&url.code,
&url.destination,
url.title.as_deref(),
url.description.as_deref(),
&url.tags,
url.expires_at.as_deref(),
url.password_hash.as_deref(),
url.max_access_count,
);
match ins_res {
Ok(new_url) => {
new_target_id = new_url.id;
// Delete from old owner database
let _ = crate::db::content::delete_url(&old_conn, &url.id);
true
}
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to copy URL to new owner: {}", e),
)
.into_response();
}
}
} else {
false
}
} else if target_type == "page" {
// Get page record
let page_opt =
match crate::db::content::get_landing_page_by_code(&old_conn, &payload.slug) {
Ok(p) => p,
Err(e) => {
return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
}
};
if let Some(page) = page_opt {
// Check new owner quotas
let new_users_conn = state.users_db.lock().unwrap();
let quota_opt =
crate::db::users::get_user_quotas(&new_users_conn, payload.new_owner_user_id)
.unwrap_or(None);
if let Some(quota) = quota_opt {
if quota.current_landings >= quota.max_landings {
return (
StatusCode::BAD_REQUEST,
"New owner has exceeded landing page quota limit",
)
.into_response();
}
}
// Insert into new owner database
let ins_res = crate::db::content::create_landing_page(
&new_conn,
&page.code,
&page.slug,
&page.title,
&page.html_content,
&page.state,
);
match ins_res {
Ok(new_page) => {
new_target_id = new_page.id;
// Delete from old owner database
let _ = crate::db::content::delete_landing_page(&old_conn, &page.id);
true
}
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to copy Page to new owner: {}", e),
)
.into_response();
}
}
} else {
false
}
} else {
false
}
};
if !transfer_success {
return (StatusCode::NOT_FOUND, "Content not found in owner database").into_response();
}
// 4. Update system global_slugs, slug_history and adjust quotas
{
let system_conn = state.system_db.lock().unwrap();
let now = Utc::now().to_rfc3339();
let _ = system_conn.execute(
"UPDATE global_slugs SET owner_user_id = ?1, target_id = ?2, updated_at = ?3 WHERE slug = ?4;",
rusqlite::params![payload.new_owner_user_id, new_target_id, now, payload.slug],
);
let _ = system_conn.execute(
"INSERT INTO slug_history (slug, old_owner_user_id, new_owner_user_id, action, timestamp, admin_username)
VALUES (?1, ?2, ?3, 'transferred', ?4, ?5);",
rusqlite::params![payload.slug, old_owner_user_id, payload.new_owner_user_id, now, admin.username],
);
// Adjust quotas
let users_conn = state.users_db.lock().unwrap();
let field = if target_type == "url" {
"urls"
} else {
"landings"
};
let _ = crate::db::users::decrement_quota_counter(&users_conn, old_owner_user_id, field);
let _ = crate::db::users::increment_quota_counter(
&users_conn,
payload.new_owner_user_id,
field,
);
let _ = crate::db::audit_events::write_audit_event(
&system_conn,
&admin.username,
"SLUG_TRANSFER",
"slug",
&payload.slug,
Some(&format!(
"From owner {} to owner {}",
old_owner_user_id, payload.new_owner_user_id
)),
);
}
StatusCode::OK.into_response()
}
// --- Admin: Content Moderation ---
@@ -689,44 +509,88 @@ pub async fn admin_moderate_slug(
return (StatusCode::BAD_REQUEST, "Invalid moderation action").into_response();
}
// 1. Verify slug and get owner user ID
let (owner_user_id, target_type) = {
let system_conn = state.system_db.lock().unwrap();
let row_opt: Option<(i64, String)> = system_conn
.query_row(
"SELECT owner_user_id, target_type FROM global_slugs WHERE slug = ?1;",
[&payload.slug],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.optional()
.unwrap_or(None);
// 1. Verify slug using v0.8 slug lookup
let slug_info = match state.lookup_slug(&payload.slug) {
Ok(Some(info)) => info,
Ok(None) => return (StatusCode::NOT_FOUND, "Slug not found").into_response(),
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
};
match row_opt {
Some(r) => r,
None => return (StatusCode::NOT_FOUND, "Slug not found").into_response(),
let target_type = slug_info.target_type.as_str().to_string();
let owner_tenant_id = match TenantId::parse(&slug_info.owner_tenant_id) {
Ok(tid) => tid,
Err(_) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
"Invalid owner tenant ID on slug",
)
.into_response();
}
};
// Resolve owner username for log snapshot
let owner_username = {
// Resolve owner details from users.db for moderation event history
let (owner_user_id, owner_username) = {
let users_conn = state.users_db.lock().unwrap();
crate::db::users::get_user_by_id(&users_conn, owner_user_id)
.unwrap_or(None)
.map(|u| u.username)
match crate::db::users::get_user_by_tenant_id(&users_conn, owner_tenant_id) {
Ok(Some(u)) => (u.id, Some(u.username)),
_ => (0, None),
}
};
// 2. Perform moderation update in global_slugs
// 2. Perform moderation update in authoritative v0.8 slug databases
{
let urls_conn = state.db.global_urls.lock().unwrap();
let pages_conn = state.db.global_landing_pages.lock().unwrap();
let now = Utc::now().to_rfc3339();
if slug_info.target_type == crate::db::slugs::SlugTargetType::Url {
let _ = urls_conn.execute(
"UPDATE global_urls SET status = ?1, updated_at = ?2 WHERE slug = ?3;",
rusqlite::params![action, now, payload.slug],
);
} else {
let _ = pages_conn.execute(
"UPDATE global_landing_pages SET status = ?1, updated_at = ?2 WHERE slug = ?3;",
rusqlite::params![action, now, payload.slug],
);
}
}
// 3. Sync status to owner tenant's content DB if possible
if let Ok(tenant_dbs) =
state.open_tenant(owner_tenant_id, crate::db::tenant::TenantOpenMode::CoreJob)
{
if let Ok(conn) = tenant_dbs.content.lock() {
if slug_info.target_type == crate::db::slugs::SlugTargetType::Url {
let content_status = if action == "disabled" {
"dead"
} else {
"active"
};
let _ = conn.execute(
"UPDATE urls SET status = ?1 WHERE code = ?2;",
rusqlite::params![content_status, payload.slug],
);
} else {
let content_state = if action == "disabled" {
"archived"
} else {
"published"
};
let _ = conn.execute(
"UPDATE landing_pages SET state = ?1 WHERE code = ?2;",
rusqlite::params![content_state, payload.slug],
);
}
}
}
// 4. Record moderation event in system.db
{
let system_conn = state.system_db.lock().unwrap();
let now = Utc::now().to_rfc3339();
let _ = system_conn.execute(
"UPDATE global_slugs SET status = ?1, updated_at = ?2 WHERE slug = ?3;",
rusqlite::params![action, now, payload.slug],
);
// Record moderation event
let event_id = Uuid::new_v4().to_string();
let _ = system_conn.execute(
"INSERT INTO moderation_events (id, timestamp, admin_username, target_user_id, target_username, resource_type, resource_identifier, action, severity, reason)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10);",
+15 -32
View File
@@ -4,11 +4,11 @@ use axum::{
response::{Html, IntoResponse, Response},
};
use chrono::Utc;
use rusqlite::OptionalExtension;
use std::net::SocketAddr;
use uuid::Uuid;
use crate::analytics::get_client_country;
use crate::db::tenant::TenantOpenMode;
use crate::models::VisitRecord;
use crate::state::AppState;
use crate::utils::get_client_ip;
@@ -25,37 +25,15 @@ pub async fn resolve_page(
return (StatusCode::NOT_FOUND, "Not Found").into_response();
}
// 1. Query global slug namespace in system.db
let slug_info = {
let system_conn = state.system_db.lock().unwrap();
let mut stmt = match system_conn.prepare(
"SELECT owner_user_id, target_type, target_id, status FROM global_slugs WHERE slug = ?1;"
) {
Ok(s) => s,
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response(),
};
stmt.query_row(rusqlite::params![code], |row| {
Ok((
row.get::<_, i64>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, String>(3)?,
))
})
.optional()
};
let (owner_user_id, _target_type, _target_id, slug_status) = match slug_info {
// 1. Query global slug namespace across v0.8 slug databases (with fallback)
let info = match state.lookup_slug(&code) {
Ok(Some(info)) => info,
Ok(None) => {
// Fallback to legacy_admin's DB (user_id = 1) if not found in global_slugs
(1, "page".to_string(), "".to_string(), "active".to_string())
}
Ok(None) => return (StatusCode::NOT_FOUND, "Not Found").into_response(),
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response(),
};
// If slug status is disabled, flagged, or soft_deleted, we return 410 Gone
if slug_status != "active" {
if info.status != "active" {
return (
StatusCode::GONE,
"This content has been disabled or moderated",
@@ -64,10 +42,11 @@ pub async fn resolve_page(
}
// 2. Get content database connection via tenant DB resolution
let content_conn = match state.get_user_dbs(owner_user_id) {
Ok(dbs) => dbs.content,
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response(),
};
let content_conn =
match state.open_slug_owner(&info.owner_tenant_id, TenantOpenMode::PublicContent) {
Ok(dbs) => dbs.content,
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response(),
};
let page_opt = {
let conn = content_conn.lock().unwrap();
@@ -103,6 +82,9 @@ pub async fn resolve_page(
.unwrap_or("Unknown")
.to_string();
let owner_tenant_id = crate::identity::TenantId::parse(&info.owner_tenant_id).ok();
let owner_user_id = info.owner_tenant_id.parse::<i64>().ok();
let record = VisitRecord {
id: Uuid::new_v4().to_string(),
target_type: "page".to_string(),
@@ -114,7 +96,8 @@ pub async fn resolve_page(
accept_language,
country,
status_code: 200,
owner_user_id: Some(owner_user_id),
owner_tenant_id,
owner_user_id,
};
state.analytics_queue.push(record);
+53 -24
View File
@@ -7,7 +7,7 @@ use axum_extra::extract::{cookie::Cookie, CookieJar};
use serde::Deserialize;
use crate::auth::password::verify_password;
use crate::services::shortener::get_url_by_code;
use crate::db::tenant::TenantOpenMode;
use crate::state::AppState;
use crate::templates::GateTemplate;
@@ -21,6 +21,43 @@ pub async fn gate_get(Path(code): Path<String>) -> impl IntoResponse {
GateTemplate { code, error: None }
}
enum GateLookup {
Missing,
Gone,
Unprotected,
Protected(String),
}
fn lookup_gate(state: &AppState, code: &str) -> Result<GateLookup, axum::http::StatusCode> {
let info = match state
.lookup_slug(code)
.map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?
{
Some(info) if info.status == "active" => info,
Some(_) => return Ok(GateLookup::Gone),
None => return Ok(GateLookup::Missing),
};
let user_dbs = match state.open_slug_owner(&info.owner_tenant_id, TenantOpenMode::PublicContent)
{
Ok(dbs) => dbs,
Err(_) => return Ok(GateLookup::Missing),
};
let conn = user_dbs
.content
.lock()
.map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?;
match crate::db::content::get_url_by_code(&conn, code)
.map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?
{
Some(u) => match u.password_hash {
Some(h) => Ok(GateLookup::Protected(h)),
None => Ok(GateLookup::Unprotected),
},
None => Ok(GateLookup::Missing),
}
}
// POST /gate/:code
pub async fn gate_post(
State(state): State<AppState>,
@@ -29,32 +66,25 @@ pub async fn gate_post(
headers: axum::http::HeaderMap,
Form(form): Form<PasswordGateForm>,
) -> Response {
let url_opt = match get_url_by_code(&state.db, &code) {
Ok(url) => url,
Err(_) => {
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
"Database error",
)
.into_response()
}
};
let url = match url_opt {
Some(u) => u,
None => return (axum::http::StatusCode::NOT_FOUND, "Url not found").into_response(),
};
let password_hash = match url.password_hash {
Some(ref h) => h,
None => {
// Not password protected, redirect to resolution
let password_hash = match lookup_gate(&state, &code) {
Ok(GateLookup::Protected(h)) => h,
Ok(GateLookup::Unprotected) => {
return Redirect::temporary(&format!("/{}", code)).into_response();
}
Ok(GateLookup::Missing) => {
return (axum::http::StatusCode::NOT_FOUND, "Url not found").into_response();
}
Ok(GateLookup::Gone) => {
return (
axum::http::StatusCode::GONE,
"This content has been disabled or moderated",
)
.into_response();
}
Err(status) => return (status, "Database error").into_response(),
};
if verify_password(&form.password, password_hash) {
// Correct password - set 15 min temporary cookie
if verify_password(&form.password, &password_hash) {
let cookie_name = format!("bzod_gate_{}", code);
let secure_flag = crate::utils::resolve_cookie_secure(state.config.cookie_secure, &headers);
let cookie = Cookie::build((cookie_name, "authorized"))
@@ -67,7 +97,6 @@ pub async fn gate_post(
let updated_jar = jar.add(cookie);
(updated_jar, Redirect::temporary(&format!("/{}", code))).into_response()
} else {
// Invalid password
GateTemplate {
code,
error: Some("Invalid password".to_string()),
+25 -63
View File
@@ -1,3 +1,4 @@
use crate::db::tenant::TenantOpenMode;
use crate::services::qr::{generate_qr_png, generate_qr_svg};
use crate::state::AppState;
use crate::utils::get_client_ip;
@@ -37,57 +38,36 @@ pub async fn qr_handler(
return (StatusCode::UNAUTHORIZED, "Unauthorized").into_response();
}
// We need to look up owner_user_id, target_id, and status from global_slugs
let (owner_user_id, target_id, slug_status) = {
let system_conn = state.system_db.lock().unwrap();
let mut stmt = match system_conn.prepare(
"SELECT owner_user_id, target_id, status FROM global_slugs WHERE slug = ?1;",
) {
Ok(s) => s,
Err(_) => {
return (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response()
}
};
use rusqlite::OptionalExtension;
match stmt
.query_row(rusqlite::params![&file], |row| {
Ok((
row.get::<_, i64>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
))
})
.optional()
{
Ok(Some((uid, tid, status))) => (uid, tid, status),
Ok(None) => return (StatusCode::NOT_FOUND, "URL not found").into_response(),
Err(_) => {
return (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response()
}
}
// Look up slug info from v0.8 slug registry (with fallback)
let info = match state.lookup_slug(&file) {
Ok(Some(info)) => info,
Ok(None) => return (StatusCode::NOT_FOUND, "URL not found").into_response(),
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response(),
};
if slug_status == "disabled" {
if info.status == "disabled" {
return (StatusCode::GONE, "This content has been disabled").into_response();
} else if slug_status != "active" {
} else if info.status != "active" {
return (StatusCode::NOT_FOUND, "URL not found").into_response();
}
let user_dbs = match state.get_user_dbs(owner_user_id) {
let user_dbs = match state
.open_slug_owner(&info.owner_tenant_id, TenantOpenMode::PublicContent)
{
Ok(dbs) => dbs,
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response(),
};
let qr_scans = {
let conn = user_dbs.analytics.lock().unwrap();
crate::db::qr::get_qr_scan_count(&conn, &target_id).unwrap_or(0)
crate::db::qr::get_qr_scan_count(&conn, &info.target_id).unwrap_or(0)
};
let direct_clicks = {
let conn = user_dbs.analytics.lock().unwrap();
conn.query_row(
"SELECT COUNT(*) FROM visits WHERE target_id = ?1;",
rusqlite::params![target_id],
rusqlite::params![info.target_id],
|row| row.get(0),
)
.unwrap_or(0)
@@ -109,36 +89,16 @@ pub async fn qr_handler(
return (StatusCode::NOT_FOUND, "Not Found").into_response();
}
// We need to look up owner_user_id, target_type, target_id, and status from global_slugs
let (owner_user_id, target_type, target_id, slug_status) = {
let system_conn = state.system_db.lock().unwrap();
let mut stmt = match system_conn
.prepare("SELECT owner_user_id, target_type, target_id, status FROM global_slugs WHERE slug = ?1;")
{
Ok(s) => s,
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response(),
};
use rusqlite::OptionalExtension;
match stmt
.query_row(rusqlite::params![code], |row| {
Ok((
row.get::<_, i64>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, String>(3)?,
))
})
.optional()
{
Ok(Some(info)) => info,
Ok(None) => return (StatusCode::NOT_FOUND, "Not Found").into_response(),
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response(),
}
// Look up slug info from v0.8 slug registry (with fallback)
let info = match state.lookup_slug(code) {
Ok(Some(info)) => info,
Ok(None) => return (StatusCode::NOT_FOUND, "Not Found").into_response(),
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response(),
};
if slug_status == "disabled" {
if info.status == "disabled" {
return (StatusCode::GONE, "This content has been disabled").into_response();
} else if slug_status != "active" {
} else if info.status != "active" {
return (StatusCode::NOT_FOUND, "Not Found").into_response();
}
@@ -159,7 +119,7 @@ pub async fn qr_handler(
.clone()
.unwrap_or_else(|| format!("{}://{}", proto, host_header));
let full_url = if target_type == "page" {
let full_url = if info.target_type == crate::db::slugs::SlugTargetType::LandingPage {
format!("{}/p/{}", base_url.trim_end_matches('/'), code)
} else {
format!("{}/{}", base_url.trim_end_matches('/'), code)
@@ -204,11 +164,13 @@ pub async fn qr_handler(
.and_then(|h| h.to_str().ok())
.map(|s| s.to_string());
if let Ok(user_dbs) = state.get_user_dbs(owner_user_id) {
if let Ok(user_dbs) =
state.open_slug_owner(&info.owner_tenant_id, TenantOpenMode::PublicContent)
{
if let Ok(analytics_conn) = user_dbs.analytics.lock() {
let _ = crate::db::qr::log_qr_access(
&analytics_conn,
&target_id,
&info.target_id,
Some(ip.as_str()),
user_agent.as_deref(),
);
+46 -72
View File
@@ -15,18 +15,26 @@ use axum::{
};
use axum_extra::extract::CookieJar;
use chrono::Utc;
use rusqlite::OptionalExtension;
use std::net::SocketAddr;
use std::sync::{Arc, Mutex};
use tracing::{error, warn};
use uuid::Uuid;
use crate::analytics::get_client_country;
use crate::db::tenant::TenantOpenMode;
use crate::identity::TenantId;
use crate::models::{LinkPreview, Url, VisitRecord};
use crate::state::AppState;
use crate::templates::PreviewTemplate;
use crate::utils::get_client_ip;
struct ResolvedUrl {
owner_tenant_id: Option<TenantId>,
owner_user_id: i64,
url: Url,
content: Arc<Mutex<rusqlite::Connection>>,
}
/// Compact outcome from blocking redirect DB work (avoids large enum / Result variants).
enum ResolveOutcome {
Ready(Box<ResolvedUrl>),
@@ -111,79 +119,40 @@ fn permanent_redirect_to(destination: &str, code: &str) -> Response {
}
}
/// Result of the initial global-slug + URL resolution phase.
struct ResolvedUrl {
owner_user_id: i64,
url: Url,
content: Arc<Mutex<rusqlite::Connection>>,
}
/// Lookup global slug namespace then load the URL from the tenant content DB.
/// Runs entirely on a blocking thread.
fn resolve_url_blocking(
system_db: Arc<Mutex<rusqlite::Connection>>,
_system_db: Arc<Mutex<rusqlite::Connection>>,
state: AppState,
code: &str,
) -> ResolveOutcome {
// 1. Query global slug namespace in system.db
let slug_info = {
let system_conn = match system_db.lock() {
Ok(c) => c,
Err(e) => {
return ResolveOutcome::DbError {
operation: "lock_system_db",
message: e.to_string(),
owner_user_id: None,
resource_id: None,
};
}
};
let mut stmt = match system_conn.prepare(
"SELECT owner_user_id, target_type, target_id, status FROM global_slugs WHERE slug = ?1;",
) {
Ok(s) => s,
Err(e) => {
return ResolveOutcome::DbError {
operation: "prepare_global_slugs",
message: e.to_string(),
owner_user_id: None,
resource_id: None,
};
}
};
match stmt
.query_row(rusqlite::params![code], |row| {
Ok((
row.get::<_, i64>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, String>(3)?,
))
})
.optional()
{
Ok(info) => info,
Err(e) => {
return ResolveOutcome::DbError {
operation: "query_global_slugs",
message: e.to_string(),
owner_user_id: None,
resource_id: None,
};
}
// 1. Query global slug namespace across v0.8 slug databases (with fallback)
let slug_info = match state.lookup_slug(code) {
Ok(info) => info,
Err(e) => {
return ResolveOutcome::DbError {
operation: "lookup_slug",
message: e.to_string(),
owner_user_id: None,
resource_id: None,
};
}
};
let (owner_user_id, target_type, _target_id, slug_status) = match slug_info {
let info = match slug_info {
Some(info) => info,
None => {
// Fallback to legacy_admin's DB (user_id = 1) if not found in global_slugs
(1, "url".to_string(), "".to_string(), "active".to_string())
return ResolveOutcome::Early {
status: StatusCode::NOT_FOUND,
body: "Not Found",
};
}
};
let owner_user_id = info.owner_tenant_id.parse::<i64>().unwrap_or(0);
// If slug status is disabled, flagged, or soft_deleted, we return 410 Gone
if slug_status != "active" {
if info.status != "active" {
return ResolveOutcome::Early {
status: StatusCode::GONE,
body: "This content has been disabled or moderated",
@@ -191,22 +160,23 @@ fn resolve_url_blocking(
}
// If target type is page, redirect permanently to /p/slug
if target_type == "page" {
if info.target_type == crate::db::slugs::SlugTargetType::LandingPage {
return ResolveOutcome::PermanentPath(format!("/p/{}", code));
}
// 2. Get content database connection via tenant DB resolution
let content_conn = match state.get_user_dbs(owner_user_id) {
Ok(dbs) => dbs,
Err(e) => {
return ResolveOutcome::DbError {
operation: "get_user_dbs",
message: e.to_string(),
owner_user_id: Some(owner_user_id),
resource_id: None,
};
}
};
let content_conn =
match state.open_slug_owner(&info.owner_tenant_id, TenantOpenMode::PublicContent) {
Ok(dbs) => dbs,
Err(e) => {
return ResolveOutcome::DbError {
operation: "open_slug_owner",
message: e.to_string(),
owner_user_id: Some(owner_user_id),
resource_id: None,
};
}
};
let url = {
let conn = match content_conn.content.lock() {
@@ -239,7 +209,10 @@ fn resolve_url_blocking(
}
};
let owner_tenant_id = TenantId::parse(&info.owner_tenant_id).ok();
ResolveOutcome::Ready(Box::new(ResolvedUrl {
owner_tenant_id,
owner_user_id,
url,
content: content_conn.content,
@@ -347,6 +320,7 @@ pub async fn resolve_redirect(
};
let ResolvedUrl {
owner_tenant_id,
owner_user_id,
url,
content,
@@ -424,7 +398,6 @@ pub async fn resolve_redirect(
}
};
// Asynchronously record analytics
let ip = get_client_ip(&headers, connect_info);
let country = get_client_country(&headers);
let user_agent = headers
@@ -454,6 +427,7 @@ pub async fn resolve_redirect(
accept_language,
country,
status_code: if preview_opt.is_some() { 200 } else { 302 },
owner_tenant_id,
owner_user_id: Some(owner_user_id),
};
+34 -10
View File
@@ -97,22 +97,46 @@ pub async fn metrics_endpoint(
// 1. Gather stats from DBs
let (total_urls, active_links, dead_links) = {
let conn = state.content_db.lock().unwrap();
crate::db::content::get_url_counts(&conn).unwrap_or((0, 0, 0))
let conn = state.db.global_urls.lock().unwrap();
let total: i64 = conn
.query_row(
"SELECT COUNT(*) FROM global_urls WHERE status != 'retired';",
[],
|r| r.get(0),
)
.unwrap_or(0);
let active: i64 = conn
.query_row(
"SELECT COUNT(*) FROM global_urls WHERE status = 'active';",
[],
|r| r.get(0),
)
.unwrap_or(0);
let dead: i64 = conn
.query_row(
"SELECT COUNT(*) FROM global_urls WHERE status = 'disabled';",
[],
|r| r.get(0),
)
.unwrap_or(0);
(total, active, dead)
};
let total_pages = {
let conn = state.content_db.lock().unwrap();
crate::db::content::get_landing_page_count(&conn).unwrap_or(0)
let conn = state.db.global_landing_pages.lock().unwrap();
conn.query_row(
"SELECT COUNT(*) FROM global_landing_pages WHERE status != 'retired';",
[],
|r| r.get(0),
)
.unwrap_or(0)
};
let (total_clicks, total_page_views) = {
let conn = state.analytics_db.lock().unwrap();
(
crate::db::analytics::get_total_clicks(&conn).unwrap_or(0),
crate::db::analytics::get_total_page_views(&conn).unwrap_or(0),
)
let total_clicks = {
let users_conn = state.users_db.lock().unwrap();
crate::db::users::get_platform_total_clicks(&state.db.topology, &users_conn).unwrap_or(0)
};
let total_page_views = 0i64;
// Calculate memory in bytes
let mut mem_bytes = 0;