Skip to main content

reinhardt_admin/server/
update.rs

1//! Update operation Server Function
2//!
3//! Provides update operations for admin models.
4
5#[cfg(server)]
6use super::admin_auth::AdminAuthenticatedUser;
7use crate::adapters::{AdminDatabase, AdminRecord, AdminSite};
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};
21#[cfg(server)]
22use super::security::{require_csrf_token, sanitize_mutation_values};
23#[cfg(server)]
24use super::validation::validate_mutation_data;
25
26/// Update an existing model instance
27///
28/// Updates a record in the database by ID using the provided field data.
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 change permission for the model.
39///
40/// # Example
41///
42/// ```ignore
43/// use reinhardt_admin::server::update_record;
44/// use reinhardt_admin::types::MutationRequest;
45/// use std::collections::HashMap;
46///
47/// // Client-side usage (automatically generates HTTP request)
48/// let mut data = HashMap::new();
49/// data.insert("email".to_string(), serde_json::json!("alice.new@example.com"));
50///
51/// let request = MutationRequest { csrf_token: "token".to_string(), data };
52/// let response = update_record("User".to_string(), "42".to_string(), request).await?;
53/// println!("Updated: {}", response.message);
54/// ```
55#[server_fn]
56pub async fn update_record(
57	model_name: String,
58	id: String,
59	request: crate::types::MutationRequest,
60	#[inject] site: Depends<AdminSiteKey, AdminSite>,
61	#[inject] db: Depends<AdminDatabaseKey, AdminDatabase>,
62	#[inject] http_request: ServerFnRequest,
63	#[inject] AdminAuthenticatedUser(user): AdminAuthenticatedUser,
64) -> Result<crate::types::MutationResponse, ServerFnError> {
65	// CSRF token validation (double-submit cookie pattern)
66	require_csrf_token(&request.csrf_token, &http_request.inner().headers)?;
67
68	// Authentication and authorization check
69	let auth = AdminAuth::from_request(&http_request);
70	let model_admin = site.get_model_admin(&model_name).map_server_fn_error()?;
71	auth.require_model_permission(model_admin.as_ref(), user.as_ref(), ModelPermission::Change)
72		.await?;
73
74	let table_name = model_admin.table_name();
75	let pk_field = model_admin.pk_field();
76
77	// Validate input data before database operation
78	validate_mutation_data(&request.data, model_admin.as_ref(), true).map_server_fn_error()?;
79
80	// Sanitize string values to prevent stored XSS
81	let mut sanitized_data = request.data;
82	sanitize_mutation_values(&mut sanitized_data);
83
84	// Inject current timestamp for auto_now fields (updated on every save)
85	super::create::inject_auto_now_timestamps(&mut sanitized_data, table_name);
86
87	let user_id = auth.user_id().unwrap_or("unknown").to_string();
88
89	let result = db
90		.update::<AdminRecord>(table_name, pk_field, &id, sanitized_data.clone())
91		.await
92		.map_server_fn_error();
93
94	// Check for database errors first, logging failure before returning
95	let affected = match result {
96		Err(e) => {
97			audit::log_update(&user_id, &model_name, &id, &sanitized_data, false);
98			return Err(e);
99		}
100		Ok(n) => n,
101	};
102
103	// Return 404 error when no record was found with the given ID.
104	// Only log success=true after confirming the record was actually updated.
105	if affected == 0 {
106		audit::log_update(&user_id, &model_name, &id, &sanitized_data, false);
107		return Err(ServerFnError::server(
108			404,
109			format!("{} not found", model_name),
110		));
111	}
112
113	audit::log_update(&user_id, &model_name, &id, &sanitized_data, true);
114
115	Ok(MutationResponse {
116		success: true,
117		message: format!("{} updated successfully", model_name),
118		affected: Some(affected),
119		data: None,
120	})
121}