refactor: complete v0.8 core architecture
This commit is contained in:
1 parent
f138a645f8
commit
d7e0ac7679
98 files changed
+8413
-3044
No files matched your search
@@ -1,12 +1,13 @@
|
||||
//! Cross-tenant slug transfer business logic.
|
||||
//!
|
||||
//! Copies URL/page content between tenant content DBs, then updates global_slugs
|
||||
//! ownership. Handlers own admin auth and HTTP mapping.
|
||||
//! Copies URL/page content between tenant content DBs, then updates v0.8 slug
|
||||
//! databases ownership. Handlers own admin auth and HTTP mapping.
|
||||
|
||||
use crate::db::tenant::TenantOpenMode;
|
||||
use crate::identity::TenantId;
|
||||
use crate::state::{AppState, UserDbs};
|
||||
use crate::utils::lock_db;
|
||||
use chrono::Utc;
|
||||
use rusqlite::OptionalExtension;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TransferError {
|
||||
@@ -34,31 +35,21 @@ pub struct SlugTransferRequest {
|
||||
pub struct SlugTransferResult {
|
||||
pub old_owner_user_id: i64,
|
||||
pub new_owner_user_id: i64,
|
||||
pub old_owner_tenant_id: TenantId,
|
||||
pub new_owner_tenant_id: TenantId,
|
||||
pub target_type: String,
|
||||
pub new_target_id: String,
|
||||
}
|
||||
|
||||
/// Look up slug ownership in `global_slugs`.
|
||||
pub fn lookup_slug(state: &AppState, slug: &str) -> Result<(i64, String, String), TransferError> {
|
||||
let system_conn = lock_db(&state.system_db, "system_db")
|
||||
.map_err(|e| TransferError::Internal(e.to_string()))?;
|
||||
let mut stmt = system_conn
|
||||
.prepare("SELECT owner_user_id, target_type, target_id FROM global_slugs WHERE slug = ?1;")
|
||||
.map_err(|e| TransferError::Internal(e.to_string()))?;
|
||||
let row_opt = stmt
|
||||
.query_row([slug], |row| {
|
||||
Ok((
|
||||
row.get::<_, i64>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, String>(2)?,
|
||||
))
|
||||
})
|
||||
.optional()
|
||||
.map_err(|e| TransferError::Internal(e.to_string()))?;
|
||||
|
||||
match row_opt {
|
||||
Some(r) => Ok(r),
|
||||
None => Err(TransferError::NotFound("Slug not found")),
|
||||
/// Look up slug ownership in v0.8 slug databases (`global_urls.db` / `global_landing_pages.db`).
|
||||
pub fn lookup_slug(
|
||||
state: &AppState,
|
||||
slug: &str,
|
||||
) -> Result<crate::db::slugs::ResolvedSlugInfo, TransferError> {
|
||||
match state.lookup_slug(slug) {
|
||||
Ok(Some(info)) => Ok(info),
|
||||
Ok(None) => Err(TransferError::NotFound("Slug not found")),
|
||||
Err(e) => Err(TransferError::Internal(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,25 +149,68 @@ fn copy_content(
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform a full slug transfer (content + registry + quotas + history).
|
||||
/// Perform a full slug transfer (content + v0.8 slug registry + quotas + history).
|
||||
pub fn transfer_slug(
|
||||
state: &AppState,
|
||||
req: &SlugTransferRequest,
|
||||
admin_username: &str,
|
||||
) -> Result<SlugTransferResult, TransferError> {
|
||||
let (old_owner_user_id, target_type, _target_id) = lookup_slug(state, &req.slug)?;
|
||||
let slug_info = lookup_slug(state, &req.slug)?;
|
||||
let target_type = slug_info.target_type.as_str().to_string();
|
||||
|
||||
if old_owner_user_id == req.new_owner_user_id {
|
||||
let old_owner_tenant_id = TenantId::parse(&slug_info.owner_tenant_id).map_err(|_| {
|
||||
TransferError::Internal(format!(
|
||||
"Invalid owner tenant ID '{}' on slug '{}'",
|
||||
slug_info.owner_tenant_id, req.slug
|
||||
))
|
||||
})?;
|
||||
|
||||
// Look up old owner user row in users.db
|
||||
let (old_owner_user_id, new_owner_tenant_id) = {
|
||||
let users_conn = lock_db(&state.users_db, "users_db")
|
||||
.map_err(|e| TransferError::Internal(e.to_string()))?;
|
||||
|
||||
let old_user = crate::db::users::get_user_by_tenant_id(&users_conn, old_owner_tenant_id)
|
||||
.map_err(|e| TransferError::Internal(e.to_string()))?
|
||||
.ok_or_else(|| {
|
||||
TransferError::Internal(format!(
|
||||
"Current owner user for tenant {old_owner_tenant_id} not found"
|
||||
))
|
||||
})?;
|
||||
|
||||
let new_user = crate::db::users::get_user_by_id(&users_conn, req.new_owner_user_id)
|
||||
.map_err(|e| TransferError::Internal(e.to_string()))?
|
||||
.ok_or_else(|| {
|
||||
TransferError::BadRequest(format!(
|
||||
"Target user ID {} not found",
|
||||
req.new_owner_user_id
|
||||
))
|
||||
})?;
|
||||
|
||||
if new_user.account_type == "admin" {
|
||||
return Err(TransferError::BadRequest(
|
||||
"Target user cannot be an Admin account (must be a tenant account)".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let new_tid = new_user.tenant_id.ok_or_else(|| {
|
||||
TransferError::BadRequest("Target user has no TenantId allocated".into())
|
||||
})?;
|
||||
|
||||
(old_user.id, new_tid)
|
||||
};
|
||||
|
||||
if old_owner_tenant_id == new_owner_tenant_id {
|
||||
return Err(TransferError::BadRequest(
|
||||
"New owner must be different from the current owner".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let old_dbs = state
|
||||
.get_user_dbs(old_owner_user_id)
|
||||
.open_tenant(old_owner_tenant_id, TenantOpenMode::CoreJob)
|
||||
.map_err(|_| TransferError::Internal("Failed to load current owner's database".into()))?;
|
||||
let new_dbs = state
|
||||
.get_user_dbs(req.new_owner_user_id)
|
||||
.open_tenant(new_owner_tenant_id, TenantOpenMode::Provision)
|
||||
.map_err(|_| TransferError::Internal("Failed to load new owner's database".into()))?;
|
||||
|
||||
let new_target_id = copy_content(
|
||||
@@ -188,16 +222,29 @@ pub fn transfer_slug(
|
||||
req.new_owner_user_id,
|
||||
)?;
|
||||
|
||||
// Update authoritative v0.8 slug databases
|
||||
{
|
||||
let urls_conn = lock_db(&state.db.global_urls, "global_urls")
|
||||
.map_err(|e| TransferError::Internal(e.to_string()))?;
|
||||
let pages_conn = lock_db(&state.db.global_landing_pages, "global_landing_pages")
|
||||
.map_err(|e| TransferError::Internal(e.to_string()))?;
|
||||
|
||||
crate::db::slugs::transfer_slug_owner(
|
||||
&urls_conn,
|
||||
&pages_conn,
|
||||
&req.slug,
|
||||
&new_owner_tenant_id,
|
||||
&new_target_id,
|
||||
)
|
||||
.map_err(|e| TransferError::Internal(format!("Failed to update slug registry: {e}")))?;
|
||||
}
|
||||
|
||||
// Write audit event and history
|
||||
{
|
||||
let system_conn = lock_db(&state.system_db, "system_db")
|
||||
.map_err(|e| TransferError::Internal(e.to_string()))?;
|
||||
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![req.new_owner_user_id, new_target_id, now, req.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);",
|
||||
@@ -228,8 +275,8 @@ pub fn transfer_slug(
|
||||
"slug",
|
||||
&req.slug,
|
||||
Some(&format!(
|
||||
"From owner {} to owner {}",
|
||||
old_owner_user_id, req.new_owner_user_id
|
||||
"From tenant {} (user {}) to tenant {} (user {})",
|
||||
old_owner_tenant_id, old_owner_user_id, new_owner_tenant_id, req.new_owner_user_id
|
||||
)),
|
||||
);
|
||||
}
|
||||
@@ -237,6 +284,8 @@ pub fn transfer_slug(
|
||||
Ok(SlugTransferResult {
|
||||
old_owner_user_id,
|
||||
new_owner_user_id: req.new_owner_user_id,
|
||||
old_owner_tenant_id,
|
||||
new_owner_tenant_id,
|
||||
target_type,
|
||||
new_target_id,
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user