Skip to main content

reinhardt_admin/server/
create.rs

1//! Create operation Server Function
2//!
3//! Provides create 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/// Create a new model instance
27///
28/// Inserts a new record into the database 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 authentication and add permission for the model.
39///
40/// # Example
41///
42/// ```ignore
43/// use reinhardt_admin::server::create_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("username".to_string(), serde_json::json!("alice"));
50/// data.insert("email".to_string(), serde_json::json!("alice@example.com"));
51///
52/// let request = MutationRequest { csrf_token: "token".to_string(), data };
53/// let response = create_record("User".to_string(), request).await?;
54/// println!("Created: {}", response.message);
55/// ```
56#[server_fn]
57pub async fn create_record(
58	model_name: 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::Add)
72		.await?;
73	let table_name = model_admin.table_name();
74	let pk_field = model_admin.pk_field();
75
76	// Validate input data before database operation
77	validate_mutation_data(&request.data, model_admin.as_ref(), false).map_server_fn_error()?;
78
79	// Sanitize string values to prevent stored XSS
80	let mut sanitized_data = request.data;
81	sanitize_mutation_values(&mut sanitized_data);
82
83	// Inject current timestamp for auto_now and auto_now_add fields.
84	// These fields are typically readonly in the admin form, so the client
85	// does not submit values for them. Without this injection the database
86	// would raise a NOT NULL violation.
87	inject_auto_timestamps(&mut sanitized_data, table_name);
88
89	let user_id = auth.user_id().unwrap_or("unknown").to_string();
90
91	let result = db
92		.create::<AdminRecord>(table_name, Some(pk_field), sanitized_data.clone())
93		.await
94		.map_server_fn_error();
95
96	let success = result.is_ok();
97	audit::log_create(&user_id, &model_name, &sanitized_data, success);
98
99	let affected = result?;
100
101	Ok(MutationResponse {
102		success: true,
103		message: format!("{} created successfully", model_name),
104		affected: Some(affected),
105		data: None,
106	})
107}
108
109/// Injects the current UTC timestamp for fields with `auto_now` or `auto_now_add`.
110///
111/// This mirrors Django's behavior: `auto_now_add` sets the timestamp on creation,
112/// and `auto_now` sets it on every save (both apply during creation). Any existing
113/// value for these fields is overwritten — they are always server-controlled.
114///
115/// For updates, call [`inject_auto_now_timestamps`] instead, which only handles
116/// `auto_now` fields.
117#[cfg(server)]
118pub(crate) fn inject_auto_timestamps(
119	data: &mut std::collections::HashMap<String, serde_json::Value>,
120	table_name: &str,
121) {
122	use crate::server::type_inference::find_model_by_table_name;
123
124	let Some(model) = find_model_by_table_name(table_name) else {
125		return;
126	};
127
128	let now = chrono::Utc::now();
129
130	for (field_name, meta) in &model.fields {
131		let is_auto_now = meta
132			.params
133			.get("auto_now")
134			.is_some_and(|v| v == "true" || v == "True");
135		let is_auto_now_add = meta
136			.params
137			.get("auto_now_add")
138			.is_some_and(|v| v == "true" || v == "True");
139
140		if is_auto_now || is_auto_now_add {
141			// Format based on field type: Date, Time, or DateTime
142			let value = match &meta.field_type {
143				reinhardt_db::migrations::FieldType::Date => {
144					serde_json::Value::String(now.format("%Y-%m-%d").to_string())
145				}
146				reinhardt_db::migrations::FieldType::Time => {
147					serde_json::Value::String(now.format("%H:%M:%S").to_string())
148				}
149				_ => {
150					// DateTime and other types: ISO 8601 format
151					serde_json::Value::String(now.format("%Y-%m-%dT%H:%M:%S%.6fZ").to_string())
152				}
153			};
154			data.insert(field_name.clone(), value);
155		}
156	}
157}
158
159/// Injects the current UTC timestamp for fields with `auto_now` only.
160///
161/// Used during updates — `auto_now_add` fields are not touched because they
162/// should only be set on initial creation.
163#[cfg(server)]
164pub(crate) fn inject_auto_now_timestamps(
165	data: &mut std::collections::HashMap<String, serde_json::Value>,
166	table_name: &str,
167) {
168	use crate::server::type_inference::find_model_by_table_name;
169
170	let Some(model) = find_model_by_table_name(table_name) else {
171		return;
172	};
173
174	let now = chrono::Utc::now();
175
176	for (field_name, meta) in &model.fields {
177		let is_auto_now = meta
178			.params
179			.get("auto_now")
180			.is_some_and(|v| v == "true" || v == "True");
181
182		if is_auto_now {
183			let value = match &meta.field_type {
184				reinhardt_db::migrations::FieldType::Date => {
185					serde_json::Value::String(now.format("%Y-%m-%d").to_string())
186				}
187				reinhardt_db::migrations::FieldType::Time => {
188					serde_json::Value::String(now.format("%H:%M:%S").to_string())
189				}
190				_ => serde_json::Value::String(now.format("%Y-%m-%dT%H:%M:%S%.6fZ").to_string()),
191			};
192			data.insert(field_name.clone(), value);
193		}
194	}
195}