Skip to main content

reinhardt_admin/server/
import.rs

1//! Import operation Server Function
2//!
3//! Provides import operations for admin models from various formats (JSON, CSV, TSV).
4
5#[cfg(server)]
6use super::admin_auth::AdminAuthenticatedUser;
7use crate::adapters::{AdminDatabase, AdminRecord, AdminSite, ImportFormat, ImportResponse};
8#[cfg(server)]
9use crate::core::{AdminDatabaseKey, AdminSiteKey};
10#[cfg(server)]
11use reinhardt_di::Depends;
12#[cfg(server)]
13use reinhardt_pages::server_fn::ServerFnRequest;
14use reinhardt_pages::server_fn::{ServerFnError, server_fn};
15#[cfg(server)]
16use std::collections::HashMap;
17
18#[cfg(server)]
19use super::error::{AdminAuth, MapServerFnError, ModelPermission};
20#[cfg(server)]
21use super::limits::{MAX_IMPORT_FILE_SIZE, MAX_IMPORT_RECORDS};
22
23/// Import model data from various formats
24///
25/// Imports records from uploaded data in the specified format (JSON, CSV, TSV).
26/// Each record is inserted as a new entry. Returns statistics about the import operation.
27///
28/// # Server Function
29///
30/// This function is automatically exposed as an HTTP endpoint by the `#[server_fn]` macro.
31/// AdminSite and AdminDatabase dependencies are automatically injected via the DI system.
32///
33/// # Authentication
34///
35/// Requires staff (admin) permission and add permission for the model.
36///
37/// # Example
38///
39/// ```ignore
40/// use reinhardt_admin::server::import_data;
41/// use reinhardt_admin::types::ImportFormat;
42///
43/// // Client-side usage (automatically generates HTTP request)
44/// let file_data = vec![/* binary data */];
45/// let response = import_data(
46///     "User".to_string(),
47///     ImportFormat::JSON,
48///     file_data
49/// ).await?;
50/// println!("Imported {} records", response.imported);
51/// ```
52#[server_fn]
53pub async fn import_data(
54	model_name: String,
55	format: crate::adapters::ImportFormat,
56	data: Vec<u8>,
57	#[inject] site: Depends<AdminSiteKey, AdminSite>,
58	#[inject] db: Depends<AdminDatabaseKey, AdminDatabase>,
59	#[inject] http_request: ServerFnRequest,
60	#[inject] AdminAuthenticatedUser(user): AdminAuthenticatedUser,
61) -> Result<crate::adapters::ImportResponse, ServerFnError> {
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::Add)
66		.await?;
67
68	// Validate import file size to prevent memory exhaustion
69	if data.len() > MAX_IMPORT_FILE_SIZE {
70		return Err(ServerFnError::application(format!(
71			"Import file size ({} bytes) exceeds maximum allowed size ({} bytes)",
72			data.len(),
73			MAX_IMPORT_FILE_SIZE
74		)));
75	}
76
77	let table_name = model_admin.table_name();
78	let pk_field = model_admin.pk_field();
79
80	// Parse data based on format
81	// Sanitize error messages to avoid exposing internal details (schema, SQL, etc.)
82	let records: Vec<HashMap<String, serde_json::Value>> = match format {
83		ImportFormat::JSON => serde_json::from_slice(&data)
84			.map_err(|_| ServerFnError::deserialization("Invalid JSON format in import data"))?,
85		ImportFormat::CSV => {
86			let mut rdr = csv::Reader::from_reader(&data[..]);
87			rdr.deserialize()
88				.collect::<Result<Vec<_>, _>>()
89				.map_err(|_| ServerFnError::deserialization("Invalid CSV format in import data"))?
90		}
91		ImportFormat::TSV => {
92			let mut rdr = csv::ReaderBuilder::new()
93				.delimiter(b'\t')
94				.from_reader(&data[..]);
95			rdr.deserialize()
96				.collect::<Result<Vec<_>, _>>()
97				.map_err(|_| ServerFnError::deserialization("Invalid TSV format in import data"))?
98		}
99	};
100
101	// Validate record count to prevent database overload
102	if records.len() > MAX_IMPORT_RECORDS {
103		return Err(ServerFnError::application(format!(
104			"Import record count ({}) exceeds maximum allowed count ({})",
105			records.len(),
106			MAX_IMPORT_RECORDS
107		)));
108	}
109
110	// Import records
111	let mut imported = 0;
112	let mut failed = 0;
113	let mut errors = Vec::new();
114
115	for (index, record) in records.into_iter().enumerate() {
116		match db
117			.create::<AdminRecord>(table_name, Some(pk_field), record)
118			.await
119		{
120			Ok(_) => imported += 1,
121			Err(_) => {
122				// Hide internal error details (SQL fragments, table structures, column names)
123				// to prevent information disclosure aiding reconnaissance attacks
124				failed += 1;
125				errors.push(format!("Record {}: import failed", index + 1));
126			}
127		}
128	}
129
130	Ok(ImportResponse {
131		success: failed == 0,
132		imported,
133		updated: 0, // Not supporting updates in basic import
134		skipped: 0,
135		failed,
136		message: if failed == 0 {
137			format!("Successfully imported {} {} records", imported, model_name)
138		} else {
139			format!(
140				"Imported {} {} records, {} failed",
141				imported, model_name, failed
142			)
143		},
144		errors: if errors.is_empty() {
145			None
146		} else {
147			Some(errors)
148		},
149	})
150}