feat(users): add delete user functionality and confirmation modal
feat(assets): implement S3 proxy for private asset access
This commit is contained in:
@@ -3571,6 +3571,55 @@ pub async fn update_user(
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn delete_user(
|
||||
Org(org_ctx): Org,
|
||||
claims: common::auth::Claims,
|
||||
State(pool): State<PgPool>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<StatusCode, (StatusCode, String)> {
|
||||
if claims.role != "admin" {
|
||||
return Err((StatusCode::FORBIDDEN, "Not authorized".into()));
|
||||
}
|
||||
// Prevent an admin from deleting themselves
|
||||
if claims.sub == id {
|
||||
return Err((StatusCode::BAD_REQUEST, "Cannot delete your own account".into()));
|
||||
}
|
||||
|
||||
let is_super_admin = claims.role == "admin"
|
||||
&& claims.org == Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap();
|
||||
|
||||
let result = if is_super_admin {
|
||||
sqlx::query("DELETE FROM users WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
} else {
|
||||
sqlx::query("DELETE FROM users WHERE id = $1 AND organization_id = $2")
|
||||
.bind(id)
|
||||
.bind(org_ctx.id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
}
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err((StatusCode::NOT_FOUND, "User not found".into()));
|
||||
}
|
||||
|
||||
log_action(
|
||||
&pool,
|
||||
org_ctx.id,
|
||||
claims.sub,
|
||||
"DELETE_USER",
|
||||
"User",
|
||||
id,
|
||||
serde_json::json!({}),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
// Organizations Management (Simplified for Single-Tenant)
|
||||
// Multi-tenant organization management has been removed.
|
||||
// The system now operates on a single default organization.
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, Query, State, Multipart},
|
||||
http::StatusCode,
|
||||
http::{StatusCode, HeaderMap, header},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use aws_config::BehaviorVersion;
|
||||
use aws_config::meta::region::RegionProviderChain;
|
||||
@@ -225,6 +226,49 @@ fn parse_s3_storage_path(path: &str) -> Option<(&str, &str)> {
|
||||
Some((bucket, key))
|
||||
}
|
||||
|
||||
/// GET /api/assets/s3-proxy/{bucket}/{*key}
|
||||
/// Proxies private S3 objects through CMS so frontend URLs do not depend on public-read ACLs.
|
||||
pub async fn public_s3_proxy(
|
||||
Path(params): Path<HashMap<String, String>>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let bucket = params
|
||||
.get("bucket")
|
||||
.cloned()
|
||||
.ok_or((StatusCode::BAD_REQUEST, "Missing bucket".to_string()))?;
|
||||
let key = params
|
||||
.get("key")
|
||||
.cloned()
|
||||
.ok_or((StatusCode::BAD_REQUEST, "Missing key".to_string()))?;
|
||||
|
||||
let settings = get_s3_settings().ok_or((
|
||||
StatusCode::NOT_FOUND,
|
||||
"S3 storage is not configured".to_string(),
|
||||
))?;
|
||||
|
||||
if bucket != settings.bucket {
|
||||
return Err((StatusCode::FORBIDDEN, "Bucket not allowed".to_string()));
|
||||
}
|
||||
|
||||
let storage_path = format!("s3://{}/{}", bucket, key);
|
||||
let bytes = read_storage_bytes(&storage_path).await?;
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
header::CONTENT_TYPE,
|
||||
"application/octet-stream"
|
||||
.parse()
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Invalid header: {}", e)))?,
|
||||
);
|
||||
headers.insert(
|
||||
header::CACHE_CONTROL,
|
||||
"public, max-age=3600"
|
||||
.parse()
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Invalid header: {}", e)))?,
|
||||
);
|
||||
|
||||
Ok((headers, bytes))
|
||||
}
|
||||
|
||||
async fn read_storage_bytes(storage_path: &str) -> Result<Vec<u8>, (StatusCode, String)> {
|
||||
if let Some((bucket, key)) = parse_s3_storage_path(storage_path) {
|
||||
let settings = get_s3_settings().ok_or((
|
||||
|
||||
@@ -241,7 +241,7 @@ async fn main() {
|
||||
"/users",
|
||||
get(handlers::get_all_users).post(handlers::admin_create_user),
|
||||
)
|
||||
.route("/users/{id}", axum::routing::put(handlers::update_user))
|
||||
.route("/users/{id}", axum::routing::put(handlers::update_user).delete(handlers::delete_user))
|
||||
.route("/audit-logs", get(handlers::get_audit_logs))
|
||||
.route("/api/ai/review-text", post(handlers::review_text))
|
||||
.route("/api/assets", get(handlers_assets::list_assets))
|
||||
@@ -513,6 +513,10 @@ async fn main() {
|
||||
|
||||
let public_routes = Router::new()
|
||||
.nest("/api/external", api_routes)
|
||||
.route(
|
||||
"/api/assets/s3-proxy/{bucket}/{*key}",
|
||||
get(handlers_assets::public_s3_proxy),
|
||||
)
|
||||
// Health check routes
|
||||
.merge(health::health_routes(pool.clone()).with_state(health_state))
|
||||
.nest_service("/assets", tower_http::services::ServeDir::new("uploads"))
|
||||
|
||||
Reference in New Issue
Block a user