Skip to main content

reinhardt_admin/server/
delete.rs

1//! Delete operation Server Functions
2//!
3//! Provides delete operations for admin models (single and bulk).
4
5#[cfg(server)]
6use super::admin_auth::AdminAuthenticatedUser;
7use crate::adapters::{AdminDatabase, AdminRecord, AdminSite, BulkDeleteResponse};
8#[cfg(server)]
9use crate::core::{AdminDatabaseKey, AdminSiteKey};
10use crate::types::MutationResponse;
11#[cfg(server)]
12use reinhardt_di::Depends;
13#[cfg(server)]
14use reinhardt_pages::server_fn::ServerFnRequest;
15use reinhardt_pages::server_fn::{ServerFnError, server_fn};
16
17#[cfg(server)]
18use super::audit;
19#[cfg(server)]
20use super::error::{AdminAuth, MapServerFnError, ModelPermission, require_object_filters};
21#[cfg(server)]
22use super::limits::MAX_BULK_DELETE_IDS;
23#[cfg(server)]
24use super::security::require_csrf_token;
25
26/// Delete a single model instance by ID
27///
28/// Removes a record from the database by its primary key.
29/// Returns the number of affected rows (typically 1) on success.
30///
31/// # Server Function
32///
33/// This function is automatically exposed as an HTTP endpoint by the `#[server_fn]` macro.
34/// AdminSite and AdminDatabase dependencies are automatically injected via the DI system.
35///
36/// # Authentication
37///
38/// Requires staff (admin) permission and delete permission for the model.
39///
40/// # Example
41///
42/// ```ignore
43/// use reinhardt_admin::server::delete_record;
44///
45/// // Client-side usage (automatically generates HTTP request)
46/// let response = delete_record("User".to_string(), "42".to_string(), "token".to_string()).await?;
47/// println!("Deleted: {}", response.message);
48/// ```
49#[server_fn]
50pub async fn delete_record(
51	model_name: String,
52	id: String,
53	csrf_token: String,
54	#[inject] site: Depends<AdminSiteKey, AdminSite>,
55	#[inject] db: Depends<AdminDatabaseKey, AdminDatabase>,
56	#[inject] http_request: ServerFnRequest,
57	#[inject] AdminAuthenticatedUser(user): AdminAuthenticatedUser,
58) -> Result<crate::types::MutationResponse, ServerFnError> {
59	// CSRF token validation (double-submit cookie pattern)
60	require_csrf_token(&csrf_token, &http_request.inner().headers)?;
61
62	// Authentication and authorization check
63	let auth = AdminAuth::from_request(&http_request);
64	let model_admin = site.get_model_admin(&model_name).map_server_fn_error()?;
65	auth.require_model_permission(model_admin.as_ref(), user.as_ref(), ModelPermission::Delete)
66		.await?;
67
68	let table_name = model_admin.table_name();
69	let pk_field = model_admin.pk_field();
70	let object_filters = require_object_filters(model_admin.as_ref(), user.as_ref())?;
71
72	let user_id = auth.user_id().unwrap_or("unknown").to_string();
73
74	let result = db
75		.delete_with_filters::<AdminRecord>(table_name, pk_field, &id, object_filters)
76		.await
77		.map_server_fn_error();
78
79	// Check for database errors first, logging failure before returning
80	let affected = match result {
81		Err(e) => {
82			audit::log_delete(&user_id, &model_name, &id, false);
83			return Err(e);
84		}
85		Ok(n) => n,
86	};
87
88	// Return 404 error when no record was found with the given ID.
89	// Only log success=true after confirming the record was actually deleted.
90	if affected == 0 {
91		audit::log_delete(&user_id, &model_name, &id, false);
92		return Err(ServerFnError::server(
93			404,
94			format!("{} not found", model_name),
95		));
96	}
97
98	audit::log_delete(&user_id, &model_name, &id, true);
99
100	Ok(MutationResponse {
101		success: true,
102		message: format!("{} deleted successfully", model_name),
103		affected: Some(affected),
104		data: None,
105	})
106}
107
108/// Delete multiple model instances by IDs (bulk delete)
109///
110/// Removes multiple records from the database using their primary keys.
111/// Returns the total number of deleted rows.
112///
113/// # Server Function
114///
115/// This function is automatically exposed as an HTTP endpoint by the `#[server_fn]` macro.
116/// AdminSite and AdminDatabase dependencies are automatically injected via the DI system.
117///
118/// # Authentication
119///
120/// Requires staff (admin) permission and delete permission for the model.
121///
122/// # Example
123///
124/// ```ignore
125/// use reinhardt_admin::server::bulk_delete_records;
126/// use reinhardt_admin::types::BulkDeleteRequest;
127///
128/// // Client-side usage (automatically generates HTTP request)
129/// let request = BulkDeleteRequest {
130///     csrf_token: "token".to_string(),
131///     ids: vec!["1".to_string(), "2".to_string(), "3".to_string()],
132/// };
133/// let response = bulk_delete_records("User".to_string(), request).await?;
134/// println!("Deleted {} items", response.deleted);
135/// ```
136#[server_fn]
137pub async fn bulk_delete_records(
138	model_name: String,
139	request: crate::adapters::BulkDeleteRequest,
140	#[inject] site: Depends<AdminSiteKey, AdminSite>,
141	#[inject] db: Depends<AdminDatabaseKey, AdminDatabase>,
142	#[inject] http_request: ServerFnRequest,
143	#[inject] AdminAuthenticatedUser(user): AdminAuthenticatedUser,
144) -> Result<crate::adapters::BulkDeleteResponse, ServerFnError> {
145	// CSRF token validation (double-submit cookie pattern)
146	require_csrf_token(&request.csrf_token, &http_request.inner().headers)?;
147
148	// Authentication and authorization check
149	let auth = AdminAuth::from_request(&http_request);
150	let model_admin = site.get_model_admin(&model_name).map_server_fn_error()?;
151	auth.require_model_permission(model_admin.as_ref(), user.as_ref(), ModelPermission::Delete)
152		.await?;
153
154	let table_name = model_admin.table_name();
155	let pk_field = model_admin.pk_field();
156	let object_filters = require_object_filters(model_admin.as_ref(), user.as_ref())?;
157
158	let user_id = auth.user_id().unwrap_or("unknown").to_string();
159
160	let ids = request.ids;
161	if ids.len() > MAX_BULK_DELETE_IDS {
162		return Err(ServerFnError::application(format!(
163			"Too many IDs for bulk delete: {} exceeds maximum of {}",
164			ids.len(),
165			MAX_BULK_DELETE_IDS
166		)));
167	}
168
169	let result = db
170		.bulk_delete_with_filters::<AdminRecord>(table_name, pk_field, ids.clone(), object_filters)
171		.await
172		.map_server_fn_error();
173
174	let success = result.is_ok();
175	let affected_count = result.as_ref().copied().unwrap_or(0);
176	audit::log_bulk_delete(&user_id, &model_name, &ids, affected_count, success);
177
178	let affected = result?;
179
180	Ok(BulkDeleteResponse {
181		success: affected > 0,
182		deleted: affected,
183		message: format!("Deleted {} {} items", affected, model_name),
184	})
185}