refactor: http-axum split into api/html/federation modules, router.rs, templates per page group

This commit is contained in:
2026-10-25 21:46:03 +01:00
parent a931fb548f
commit 614aec8df9
97 changed files with 5317 additions and 5160 deletions

View File

@@ -0,0 +1,149 @@
use crate::html::helpers::encode_domain_error;
use crate::html::render::render_page;
use crate::html::templates::ImportMappingTemplate;
use crate::support::str_to_domain_field;
use crate::{extractors::RequiredCookieUser, html::csrf::CsrfToken, state::AppState};
use application::import::{
apply_mapping as apply_import_mapping, commands::ApplyImportMappingCommand, get_mapping_stage,
};
use axum::{
Extension, Form,
extract::{Path, State},
response::{IntoResponse, Redirect},
};
use domain::models::{
FieldMapping,
import::{DomainField, Transform},
};
use domain::value_objects::ImportSessionId;
use std::collections::HashMap;
fn parse_mapping_form(form: &HashMap<String, String>) -> Vec<FieldMapping> {
let mut mappings = Vec::new();
let mut i = 0usize;
loop {
if i > 64 {
break;
}
let col_key = format!("mapping_{i}_col");
let Some(col) = form.get(&col_key).cloned() else {
break;
};
let field_str = form
.get(&format!("mapping_{i}_field"))
.map(|s| s.as_str())
.unwrap_or("");
if let Some(domain_field) = str_to_domain_field(field_str) {
let transform = if domain_field == DomainField::Rating {
let scale: f64 = form
.get(&format!("mapping_{i}_scale"))
.and_then(|s| s.parse().ok())
.unwrap_or(1.0);
Transform::RatingScale(scale)
} else if domain_field == DomainField::WatchedAt {
form.get(&format!("mapping_{i}_datefmt"))
.filter(|s| !s.is_empty())
.cloned()
.map(Transform::DateFormat)
.unwrap_or(Transform::Identity)
} else {
Transform::Identity
};
mappings.push(FieldMapping {
source_column: col,
domain_field,
transform,
});
}
i += 1;
}
mappings
}
pub async fn get_mapping_page(
State(state): State<AppState>,
RequiredCookieUser(user_id): RequiredCookieUser,
Path(session_id_str): Path<String>,
Extension(csrf): Extension<CsrfToken>,
) -> impl IntoResponse {
let Ok(session_id) = session_id_str
.parse::<uuid::Uuid>()
.map(ImportSessionId::from_uuid)
else {
return Redirect::to("/import").into_response();
};
let Ok(stage) = get_mapping_stage::execute(
&state.app_ctx.deps.import.get_mapping_stage,
session_id,
user_id.value(),
)
.await
else {
return Redirect::to("/import").into_response();
};
let ctx = crate::html::helpers::build_page_context(&state, Some(user_id), csrf.0).await;
let domain_fields: Vec<(&str, &str)> = vec![
("title", "Title"),
("release_year", "Release Year"),
("director", "Director"),
("rating", "Rating"),
("watched_at", "Watched At"),
("comment", "Comment"),
("external_metadata_id", "External ID"),
];
render_page(ImportMappingTemplate {
ctx: &ctx,
session_id: &session_id_str,
columns: &stage.columns,
sample_rows: &stage.sample_rows,
domain_fields: &domain_fields,
error: None,
})
.into_response()
}
pub async fn post_mapping(
State(state): State<AppState>,
RequiredCookieUser(user_id): RequiredCookieUser,
Path(session_id_str): Path<String>,
Extension(csrf): Extension<CsrfToken>,
Form(form): Form<HashMap<String, String>>,
) -> impl IntoResponse {
let csrf_token = form.get("_csrf").map(|s| s.as_str()).unwrap_or("");
if crate::html::csrf::mismatch(&csrf, csrf_token) {
return Redirect::to("/import").into_response();
}
let Ok(session_id) = session_id_str
.parse::<uuid::Uuid>()
.map(ImportSessionId::from_uuid)
else {
return Redirect::to("/import").into_response();
};
let mappings = parse_mapping_form(&form);
if mappings.is_empty() {
return Redirect::to(&format!(
"/import/{}/mapping?error=select+at+least+one+mapping",
session_id_str
))
.into_response();
}
match apply_import_mapping::execute(
&state.app_ctx.deps.import.apply_mapping,
ApplyImportMappingCommand {
user_id: user_id.value(),
session_id: session_id.value(),
mappings,
},
)
.await
{
Ok(_) => Redirect::to(&format!("/import/{}/preview", session_id_str)).into_response(),
Err(e) => Redirect::to(&format!(
"/import/{}/mapping?error={}",
session_id_str,
encode_domain_error(&e)
))
.into_response(),
}
}