error messages
This commit is contained in:
@@ -62,3 +62,19 @@ pub(crate) enum LoadError {
|
||||
Forbidden,
|
||||
Backend(String),
|
||||
}
|
||||
|
||||
impl LoadError {
|
||||
/// Hands the page load's failure to the one place that decides statuses.
|
||||
/// `permission_key` names the message shown when the load was refused.
|
||||
pub(crate) fn into_form_error(
|
||||
self,
|
||||
locale: crate::i18n::Locale,
|
||||
permission_key: &str,
|
||||
) -> crate::ui::FormError {
|
||||
match self {
|
||||
Self::Unauthenticated => crate::ui::FormError::Unauthenticated,
|
||||
Self::Forbidden => crate::ui::FormError::Forbidden(locale.lookup(permission_key)),
|
||||
Self::Backend(message) => crate::ui::FormError::Unavailable(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use axum::{
|
||||
Form,
|
||||
extract::State,
|
||||
http::{HeaderMap, StatusCode},
|
||||
http::HeaderMap,
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
{i18n::Locale, tr},
|
||||
i18n::Locale,
|
||||
services::{authenticated_request, reject_cross_site},
|
||||
ui::FormError,
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -38,12 +39,7 @@ pub(crate) async fn create_logic(
|
||||
}
|
||||
let request = match form.into_request(Locale::from_headers(&headers)) {
|
||||
Ok(request) => request,
|
||||
Err(message) => {
|
||||
return render_loaded(
|
||||
&headers,
|
||||
load_page(state, &headers, submitted, Some(message)).await,
|
||||
);
|
||||
}
|
||||
Err(message) => return reject(&headers, message),
|
||||
};
|
||||
let mut scripts = state.scripts;
|
||||
let request = match authenticated_request(&headers, request) {
|
||||
@@ -57,34 +53,25 @@ pub(crate) async fn create_logic(
|
||||
&response.get_ref().warnings,
|
||||
))
|
||||
.into_response(),
|
||||
Err(error) => (
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
Html(ui::render_submission_error(
|
||||
Locale::from_headers(&headers),
|
||||
error.message(),
|
||||
)),
|
||||
)
|
||||
.into_response(),
|
||||
Err(error) => FormError::from_status(&error)
|
||||
.into_response(Locale::from_headers(&headers), ui::render_submission_error),
|
||||
}
|
||||
}
|
||||
|
||||
/// A submission this crate rejected before it reached the backend. Same
|
||||
/// response as a backend rejection: the user has to change something either
|
||||
/// way, and the form's `hx-target` is the status block in both cases.
|
||||
fn reject(headers: &HeaderMap, message: String) -> Response {
|
||||
FormError::Rejected(message)
|
||||
.into_response(Locale::from_headers(headers), ui::render_submission_error)
|
||||
}
|
||||
|
||||
fn render_loaded(headers: &HeaderMap, result: Result<super::state::AddLogicPageState, LoadError>) -> Response {
|
||||
let locale = Locale::from_headers(headers);
|
||||
match result {
|
||||
Ok(page) => Html(ui::render_page(&page)).into_response(),
|
||||
Err(LoadError::Unauthenticated) => Redirect::to("/login").into_response(),
|
||||
Err(LoadError::Forbidden) => {
|
||||
let message = tr!(locale, "add-logic-err-permission");
|
||||
(
|
||||
StatusCode::FORBIDDEN,
|
||||
Html(ui::render_submission_error(locale, &message)),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
Err(LoadError::Backend(message)) => (
|
||||
StatusCode::BAD_GATEWAY,
|
||||
Html(ui::render_submission_error(locale, &message)),
|
||||
)
|
||||
.into_response(),
|
||||
Err(error) => error
|
||||
.into_form_error(locale, "add-logic-err-permission")
|
||||
.into_response(locale, ui::render_submission_error),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,3 +68,19 @@ pub(crate) enum LoadError {
|
||||
Forbidden,
|
||||
Backend(String),
|
||||
}
|
||||
|
||||
impl LoadError {
|
||||
/// Hands the page load's failure to the one place that decides statuses.
|
||||
/// `permission_key` names the message shown when the load was refused.
|
||||
pub(crate) fn into_form_error(
|
||||
self,
|
||||
locale: crate::i18n::Locale,
|
||||
permission_key: &str,
|
||||
) -> crate::ui::FormError {
|
||||
match self {
|
||||
Self::Unauthenticated => crate::ui::FormError::Unauthenticated,
|
||||
Self::Forbidden => crate::ui::FormError::Forbidden(locale.lookup(permission_key)),
|
||||
Self::Backend(message) => crate::ui::FormError::Unavailable(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use axum::{
|
||||
Form,
|
||||
extract::State,
|
||||
http::{HeaderMap, StatusCode},
|
||||
http::HeaderMap,
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
{i18n::Locale, tr},
|
||||
i18n::Locale,
|
||||
services::{authenticated_request, reject_cross_site},
|
||||
ui::FormError,
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -17,6 +18,9 @@ use super::{
|
||||
ui,
|
||||
};
|
||||
|
||||
/// The message shown when the page load itself was refused.
|
||||
const PERMISSION_KEY: &str = "validation-err-permission";
|
||||
|
||||
pub(crate) async fn new_field_validation_page(State(state): State<AppState>, headers: HeaderMap) -> Response {
|
||||
render_loaded(&headers, load_page(state, &headers, ValidationForm::default(), false, None).await)
|
||||
}
|
||||
@@ -36,85 +40,51 @@ pub(crate) async fn save_field_validation(
|
||||
return rejection;
|
||||
}
|
||||
let submitted = form.clone();
|
||||
if let Err(error) = load_page(state.clone(), &headers, submitted.clone(), false, None).await {
|
||||
return render_loaded(&headers, Err(error));
|
||||
if let Err(error) = load_page(state.clone(), &headers, submitted, false, None).await {
|
||||
return load_failed(&headers, error);
|
||||
}
|
||||
// A set name turns this into "apply that set to this column" instead of
|
||||
// "save these checks on this column".
|
||||
if !form.validation_set_name.trim().is_empty() {
|
||||
let request = match form.apply_set_request(Locale::from_headers(&headers)) {
|
||||
Ok(request) => request,
|
||||
Err(message) => {
|
||||
return render_loaded(
|
||||
&headers,
|
||||
load_page(state, &headers, submitted, false, Some(message)).await,
|
||||
)
|
||||
}
|
||||
Err(message) => return reject(&headers, message),
|
||||
};
|
||||
let mut validations = state.validations;
|
||||
let request = match authenticated_request(&headers, request) {
|
||||
Ok(request) => request,
|
||||
Err(_) => return Redirect::to("/login").into_response(),
|
||||
};
|
||||
return match validations.apply_validation_set(request).await {
|
||||
Ok(response) if response.get_ref().success => Html(ui::render_success(
|
||||
Locale::from_headers(&headers),
|
||||
&response.get_ref().message,
|
||||
))
|
||||
.into_response(),
|
||||
Ok(response) => (
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
Html(ui::render_error(
|
||||
Locale::from_headers(&headers),
|
||||
&response.get_ref().message,
|
||||
)),
|
||||
)
|
||||
.into_response(),
|
||||
Err(error) => (
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
Html(ui::render_error(
|
||||
Locale::from_headers(&headers),
|
||||
error.message(),
|
||||
)),
|
||||
)
|
||||
.into_response(),
|
||||
};
|
||||
return answer(
|
||||
&headers,
|
||||
validations
|
||||
.apply_validation_set(request)
|
||||
.await
|
||||
.map(|response| {
|
||||
let response = response.into_inner();
|
||||
(response.success, response.message)
|
||||
}),
|
||||
);
|
||||
}
|
||||
let request = match form.field_request(Locale::from_headers(&headers)) {
|
||||
Ok(request) => request,
|
||||
Err(message) => {
|
||||
return render_loaded(
|
||||
&headers,
|
||||
load_page(state, &headers, submitted, false, Some(message)).await,
|
||||
)
|
||||
}
|
||||
Err(message) => return reject(&headers, message),
|
||||
};
|
||||
let mut validations = state.validations;
|
||||
let request = match authenticated_request(&headers, request) {
|
||||
Ok(request) => request,
|
||||
Err(_) => return Redirect::to("/login").into_response(),
|
||||
};
|
||||
match validations.update_field_validation(request).await {
|
||||
Ok(response) if response.get_ref().success => Html(ui::render_success(
|
||||
Locale::from_headers(&headers),
|
||||
&response.get_ref().message,
|
||||
))
|
||||
.into_response(),
|
||||
Ok(response) => (
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
Html(ui::render_error(
|
||||
Locale::from_headers(&headers),
|
||||
&response.get_ref().message,
|
||||
)),
|
||||
)
|
||||
.into_response(),
|
||||
Err(error) => (
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
Html(ui::render_error(
|
||||
Locale::from_headers(&headers),
|
||||
error.message(),
|
||||
)),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
answer(
|
||||
&headers,
|
||||
validations
|
||||
.update_field_validation(request)
|
||||
.await
|
||||
.map(|response| {
|
||||
let response = response.into_inner();
|
||||
(response.success, response.message)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) async fn save_validation_rule(
|
||||
@@ -124,46 +94,28 @@ pub(crate) async fn save_validation_rule(
|
||||
return rejection;
|
||||
}
|
||||
let submitted = form.clone();
|
||||
if let Err(error) = load_page(state.clone(), &headers, submitted.clone(), true, None).await {
|
||||
return render_loaded(&headers, Err(error));
|
||||
if let Err(error) = load_page(state.clone(), &headers, submitted, true, None).await {
|
||||
return load_failed(&headers, error);
|
||||
}
|
||||
let request = match form.rule_request(Locale::from_headers(&headers)) {
|
||||
Ok(request) => request,
|
||||
Err(message) => {
|
||||
return render_loaded(
|
||||
&headers,
|
||||
load_page(state, &headers, submitted, true, Some(message)).await,
|
||||
)
|
||||
}
|
||||
Err(message) => return reject(&headers, message),
|
||||
};
|
||||
let mut validations = state.validations;
|
||||
let request = match authenticated_request(&headers, request) {
|
||||
Ok(request) => request,
|
||||
Err(_) => return Redirect::to("/login").into_response(),
|
||||
};
|
||||
match validations.upsert_validation_rule(request).await {
|
||||
Ok(response) if response.get_ref().success => Html(ui::render_success(
|
||||
Locale::from_headers(&headers),
|
||||
&response.get_ref().message,
|
||||
))
|
||||
.into_response(),
|
||||
Ok(response) => (
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
Html(ui::render_error(
|
||||
Locale::from_headers(&headers),
|
||||
&response.get_ref().message,
|
||||
)),
|
||||
)
|
||||
.into_response(),
|
||||
Err(error) => (
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
Html(ui::render_error(
|
||||
Locale::from_headers(&headers),
|
||||
error.message(),
|
||||
)),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
answer(
|
||||
&headers,
|
||||
validations
|
||||
.upsert_validation_rule(request)
|
||||
.await
|
||||
.map(|response| {
|
||||
let response = response.into_inner();
|
||||
(response.success, response.message)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) async fn save_validation_set(
|
||||
@@ -173,48 +125,59 @@ pub(crate) async fn save_validation_set(
|
||||
return rejection;
|
||||
}
|
||||
let submitted = form.clone();
|
||||
if let Err(error) = load_set_page(state.clone(), &headers, submitted.clone(), None).await {
|
||||
return render_set_loaded(&headers, Err(error));
|
||||
if let Err(error) = load_set_page(state.clone(), &headers, submitted, None).await {
|
||||
return load_failed(&headers, error);
|
||||
}
|
||||
let request = match form.request(Locale::from_headers(&headers)) {
|
||||
Ok(request) => request,
|
||||
Err(message) => {
|
||||
return render_set_loaded(
|
||||
&headers,
|
||||
load_set_page(state, &headers, submitted, Some(message)).await,
|
||||
)
|
||||
}
|
||||
Err(message) => return reject(&headers, message),
|
||||
};
|
||||
let mut validations = state.validations;
|
||||
let request = match authenticated_request(&headers, request) {
|
||||
Ok(request) => request,
|
||||
Err(_) => return Redirect::to("/login").into_response(),
|
||||
};
|
||||
match validations.upsert_validation_set(request).await {
|
||||
Ok(response) if response.get_ref().success => Html(ui::render_success(
|
||||
Locale::from_headers(&headers),
|
||||
&response.get_ref().message,
|
||||
))
|
||||
.into_response(),
|
||||
Ok(response) => (
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
Html(ui::render_error(
|
||||
Locale::from_headers(&headers),
|
||||
&response.get_ref().message,
|
||||
)),
|
||||
)
|
||||
.into_response(),
|
||||
Err(error) => (
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
Html(ui::render_error(
|
||||
Locale::from_headers(&headers),
|
||||
error.message(),
|
||||
)),
|
||||
)
|
||||
.into_response(),
|
||||
answer(
|
||||
&headers,
|
||||
validations
|
||||
.upsert_validation_set(request)
|
||||
.await
|
||||
.map(|response| {
|
||||
let response = response.into_inner();
|
||||
(response.success, response.message)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// What every one of these three POSTs does with the backend's answer. The
|
||||
/// endpoints report a refusal in the body (`success: false`) as readily as
|
||||
/// through a gRPC status, and both are the same thing to the user.
|
||||
fn answer(headers: &HeaderMap, outcome: Result<(bool, String), tonic::Status>) -> Response {
|
||||
let locale = Locale::from_headers(headers);
|
||||
match outcome {
|
||||
Ok((true, message)) => Html(ui::render_success(locale, &message)).into_response(),
|
||||
Ok((false, message)) => {
|
||||
FormError::Rejected(message).into_response(locale, ui::render_error)
|
||||
}
|
||||
Err(status) => FormError::from_status(&status).into_response(locale, ui::render_error),
|
||||
}
|
||||
}
|
||||
|
||||
/// A submission this crate rejected before it reached the backend — a pattern
|
||||
/// line in the wrong shape, a missing column. It answers exactly like a
|
||||
/// backend rejection: the form's `hx-target` is the status block either way,
|
||||
/// and the user has to change something either way.
|
||||
fn reject(headers: &HeaderMap, message: String) -> Response {
|
||||
FormError::Rejected(message).into_response(Locale::from_headers(headers), ui::render_error)
|
||||
}
|
||||
|
||||
fn load_failed(headers: &HeaderMap, error: LoadError) -> Response {
|
||||
let locale = Locale::from_headers(headers);
|
||||
error
|
||||
.into_form_error(locale, PERMISSION_KEY)
|
||||
.into_response(locale, ui::render_error)
|
||||
}
|
||||
|
||||
fn render_set_loaded(
|
||||
headers: &HeaderMap,
|
||||
result: Result<(crate::ui::Nav, ValidationSetForm, Option<String>), LoadError>,
|
||||
@@ -223,23 +186,7 @@ fn render_set_loaded(
|
||||
Ok((nav, form, error)) => {
|
||||
Html(ui::render_set_page(nav, &form, error.as_deref())).into_response()
|
||||
}
|
||||
Err(LoadError::Unauthenticated) => Redirect::to("/login").into_response(),
|
||||
Err(LoadError::Forbidden) => (
|
||||
StatusCode::FORBIDDEN,
|
||||
Html(ui::render_error(
|
||||
Locale::from_headers(headers),
|
||||
&tr!(
|
||||
Locale::from_headers(headers),
|
||||
"validation-err-permission"
|
||||
),
|
||||
)),
|
||||
)
|
||||
.into_response(),
|
||||
Err(LoadError::Backend(message)) => (
|
||||
StatusCode::BAD_GATEWAY,
|
||||
Html(ui::render_error(Locale::from_headers(headers), &message)),
|
||||
)
|
||||
.into_response(),
|
||||
Err(error) => load_failed(headers, error),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,22 +196,6 @@ fn render_loaded(
|
||||
) -> Response {
|
||||
match result {
|
||||
Ok(page) => Html(ui::render_page(&page)).into_response(),
|
||||
Err(LoadError::Unauthenticated) => Redirect::to("/login").into_response(),
|
||||
Err(LoadError::Forbidden) => (
|
||||
StatusCode::FORBIDDEN,
|
||||
Html(ui::render_error(
|
||||
Locale::from_headers(headers),
|
||||
&tr!(
|
||||
Locale::from_headers(headers),
|
||||
"validation-err-permission"
|
||||
),
|
||||
)),
|
||||
)
|
||||
.into_response(),
|
||||
Err(LoadError::Backend(message)) => (
|
||||
StatusCode::BAD_GATEWAY,
|
||||
Html(ui::render_error(Locale::from_headers(headers), &message)),
|
||||
)
|
||||
.into_response(),
|
||||
Err(error) => load_failed(headers, error),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,3 +113,19 @@ pub(crate) enum LoadError {
|
||||
Forbidden,
|
||||
Backend(String),
|
||||
}
|
||||
|
||||
impl LoadError {
|
||||
/// Hands the page load's failure to the one place that decides statuses.
|
||||
/// `permission_key` names the message shown when the load was refused.
|
||||
pub(crate) fn into_form_error(
|
||||
self,
|
||||
locale: crate::i18n::Locale,
|
||||
permission_key: &str,
|
||||
) -> crate::ui::FormError {
|
||||
match self {
|
||||
Self::Unauthenticated => crate::ui::FormError::Unauthenticated,
|
||||
Self::Forbidden => crate::ui::FormError::Forbidden(locale.lookup(permission_key)),
|
||||
Self::Backend(message) => crate::ui::FormError::Unavailable(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use axum::{
|
||||
Form,
|
||||
extract::State,
|
||||
http::{HeaderMap, HeaderValue, StatusCode, header},
|
||||
http::{HeaderMap, HeaderValue, header},
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
};
|
||||
|
||||
@@ -53,39 +53,22 @@ pub(crate) async fn export_csv(
|
||||
};
|
||||
let (profile_name, table_names) = match form.targets(Locale::from_headers(&headers)) {
|
||||
Ok(targets) => targets,
|
||||
Err(message) => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Html(ui::render_error(
|
||||
Locale::from_headers(&headers),
|
||||
&message,
|
||||
)),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
Err(message) => return reject(&headers, message),
|
||||
};
|
||||
let Some(profile) = catalog.profiles.iter().find(|profile| profile.name == profile_name) else {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Html(ui::render_error(
|
||||
Locale::from_headers(&headers),
|
||||
&tr!(Locale::from_headers(&headers), "export-err-unknown-profile"),
|
||||
)),
|
||||
)
|
||||
.into_response();
|
||||
return reject(
|
||||
&headers,
|
||||
tr!(Locale::from_headers(&headers), "export-err-unknown-profile"),
|
||||
);
|
||||
};
|
||||
if table_names.iter().any(|name| !profile.tables.contains(name)) {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Html(ui::render_error(
|
||||
return reject(
|
||||
&headers,
|
||||
tr!(
|
||||
Locale::from_headers(&headers),
|
||||
&tr!(
|
||||
Locale::from_headers(&headers),
|
||||
"export-err-tables-not-in-profile"
|
||||
),
|
||||
)),
|
||||
)
|
||||
.into_response();
|
||||
"export-err-tables-not-in-profile"
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
let mut tables = Vec::new();
|
||||
@@ -103,7 +86,7 @@ pub(crate) async fn export_csv(
|
||||
.await
|
||||
{
|
||||
Ok(response) => response.into_inner().table_structures.remove(table_name),
|
||||
Err(error) => return backend_error(&headers, error.message()),
|
||||
Err(error) => return grpc_error(&headers, &error),
|
||||
};
|
||||
let Some(structure) = structure else {
|
||||
return backend_error(
|
||||
@@ -127,7 +110,7 @@ pub(crate) async fn export_csv(
|
||||
.await
|
||||
{
|
||||
Ok(response) => response.into_inner().count,
|
||||
Err(error) => return backend_error(&headers, error.message()),
|
||||
Err(error) => return grpc_error(&headers, &error),
|
||||
};
|
||||
let Ok(count) = u64::try_from(count) else {
|
||||
return backend_error(
|
||||
@@ -170,7 +153,7 @@ pub(crate) async fn export_csv(
|
||||
Err(_) => return Redirect::to("/login").into_response(),
|
||||
}).await {
|
||||
Ok(response) => response.into_inner(),
|
||||
Err(error) => return backend_error(&headers, error.message()),
|
||||
Err(error) => return grpc_error(&headers, &error),
|
||||
};
|
||||
row.extend(table.columns.iter().map(|column| response.data.get(column).cloned().unwrap_or_default()));
|
||||
}
|
||||
@@ -187,27 +170,29 @@ pub(crate) async fn export_csv(
|
||||
}
|
||||
|
||||
fn load_error(headers: &HeaderMap, error: LoadError) -> Response {
|
||||
match error {
|
||||
LoadError::Unauthenticated => Redirect::to("/login").into_response(),
|
||||
LoadError::Forbidden => (
|
||||
StatusCode::FORBIDDEN,
|
||||
Html(ui::render_error(
|
||||
Locale::from_headers(headers),
|
||||
&tr!(
|
||||
Locale::from_headers(headers),
|
||||
"export-err-permission"
|
||||
),
|
||||
)),
|
||||
)
|
||||
.into_response(),
|
||||
LoadError::Backend(message) => backend_error(headers, &message),
|
||||
}
|
||||
let locale = Locale::from_headers(headers);
|
||||
error
|
||||
.into_form_error(locale, "export-err-permission")
|
||||
.into_response(locale, ui::render_error)
|
||||
}
|
||||
|
||||
fn backend_error(headers: &HeaderMap, message: &str) -> Response {
|
||||
(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
Html(ui::render_error(Locale::from_headers(headers), message)),
|
||||
)
|
||||
.into_response()
|
||||
/// An export the user has to change: a profile that is not theirs to read, a
|
||||
/// table that is not in it.
|
||||
fn reject(headers: &HeaderMap, message: String) -> Response {
|
||||
crate::ui::FormError::Rejected(message)
|
||||
.into_response(Locale::from_headers(headers), ui::render_error)
|
||||
}
|
||||
|
||||
/// A gRPC failure, classified by its code rather than assumed to be a dead
|
||||
/// backend.
|
||||
fn grpc_error(headers: &HeaderMap, error: &tonic::Status) -> Response {
|
||||
crate::ui::FormError::from_status(error)
|
||||
.into_response(Locale::from_headers(headers), ui::render_error)
|
||||
}
|
||||
|
||||
/// The backend answered, with something the export cannot use — a structure
|
||||
/// it does not know, a row count that is negative or past `i32`.
|
||||
fn backend_error(headers: &HeaderMap, message: &str) -> Response {
|
||||
crate::ui::FormError::Unavailable(message.to_string())
|
||||
.into_response(Locale::from_headers(headers), ui::render_error)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet};
|
||||
use axum::{
|
||||
Form,
|
||||
extract::State,
|
||||
http::{HeaderMap, StatusCode},
|
||||
http::HeaderMap,
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
};
|
||||
|
||||
@@ -53,61 +53,31 @@ pub(crate) async fn import_csv(
|
||||
};
|
||||
let (profile_name, table_names) = match form.targets(Locale::from_headers(&headers)) {
|
||||
Ok(targets) => targets,
|
||||
Err(message) => {
|
||||
return render_loaded(
|
||||
&headers,
|
||||
load_page(state, &headers, submitted, Some(message)).await,
|
||||
)
|
||||
}
|
||||
Err(message) => return reject(&headers, message),
|
||||
};
|
||||
let Some(profile) = page.catalog.profiles.iter().find(|profile| profile.name == profile_name) else {
|
||||
return render_loaded(
|
||||
return reject(
|
||||
&headers,
|
||||
load_page(
|
||||
state,
|
||||
&headers,
|
||||
submitted,
|
||||
Some(tr!(
|
||||
Locale::from_headers(&headers),
|
||||
"import-err-unknown-profile"
|
||||
)),
|
||||
)
|
||||
.await,
|
||||
tr!(Locale::from_headers(&headers), "import-err-unknown-profile"),
|
||||
);
|
||||
};
|
||||
if table_names.iter().any(|name| !profile.tables.contains(name)) {
|
||||
return render_loaded(
|
||||
return reject(
|
||||
&headers,
|
||||
load_page(
|
||||
state,
|
||||
&headers,
|
||||
submitted,
|
||||
Some(tr!(
|
||||
Locale::from_headers(&headers),
|
||||
"import-err-tables-not-in-profile"
|
||||
)),
|
||||
)
|
||||
.await,
|
||||
tr!(
|
||||
Locale::from_headers(&headers),
|
||||
"import-err-tables-not-in-profile"
|
||||
),
|
||||
);
|
||||
}
|
||||
let rows = match parse_csv(Locale::from_headers(&headers), &form.csv_data) {
|
||||
Ok(rows) => rows,
|
||||
Err(message) => {
|
||||
return render_loaded(
|
||||
&headers,
|
||||
load_page(state, &headers, submitted, Some(message)).await,
|
||||
)
|
||||
}
|
||||
Err(message) => return reject(&headers, message),
|
||||
};
|
||||
let (table_headers, columns, data_rows) =
|
||||
match split_headers(Locale::from_headers(&headers), rows, &table_names) {
|
||||
Ok(parts) => parts,
|
||||
Err(message) => {
|
||||
return render_loaded(
|
||||
&headers,
|
||||
load_page(state, &headers, submitted, Some(message)).await,
|
||||
)
|
||||
}
|
||||
Err(message) => return reject(&headers, message),
|
||||
};
|
||||
|
||||
let mut tables = Vec::new();
|
||||
@@ -122,10 +92,10 @@ pub(crate) async fn import_csv(
|
||||
Err(_) => return Redirect::to("/login").into_response(),
|
||||
}).await {
|
||||
Ok(response) => response.into_inner().table_structures.remove(table_name),
|
||||
Err(error) => return backend_error(&headers, error.message().to_string()),
|
||||
Err(error) => return grpc_error(&headers, &error),
|
||||
};
|
||||
let Some(structure) = structure else {
|
||||
return backend_error(
|
||||
return unavailable(
|
||||
&headers,
|
||||
tr!(
|
||||
Locale::from_headers(&headers),
|
||||
@@ -153,38 +123,26 @@ pub(crate) async fn import_csv(
|
||||
.map(|(index, column)| (index, column.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
if positions.is_empty() {
|
||||
return render_loaded(
|
||||
return reject(
|
||||
&headers,
|
||||
load_page(
|
||||
state,
|
||||
&headers,
|
||||
submitted,
|
||||
Some(tr!(
|
||||
Locale::from_headers(&headers),
|
||||
"import-err-no-importable-columns",
|
||||
"table" => table.name.clone(),
|
||||
)),
|
||||
)
|
||||
.await,
|
||||
tr!(
|
||||
Locale::from_headers(&headers),
|
||||
"import-err-no-importable-columns",
|
||||
"table" => table.name.clone(),
|
||||
),
|
||||
);
|
||||
}
|
||||
for (index, column) in columns.iter().enumerate() {
|
||||
let belongs = table_headers.as_ref().map_or(table_names.len() == 1, |headers| headers.get(index).is_some_and(|name| name == &table.name));
|
||||
if belongs && !table.columns.contains(column) {
|
||||
return render_loaded(
|
||||
return reject(
|
||||
&headers,
|
||||
load_page(
|
||||
state,
|
||||
&headers,
|
||||
submitted,
|
||||
Some(tr!(
|
||||
Locale::from_headers(&headers),
|
||||
"import-err-column-not-importable",
|
||||
"column" => column.clone(),
|
||||
"table" => table.name.clone(),
|
||||
)),
|
||||
)
|
||||
.await,
|
||||
tr!(
|
||||
Locale::from_headers(&headers),
|
||||
"import-err-column-not-importable",
|
||||
"column" => column.clone(),
|
||||
"table" => table.name.clone(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -206,12 +164,7 @@ pub(crate) async fn import_csv(
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
{
|
||||
Ok(rows) => rows,
|
||||
Err(message) => {
|
||||
return render_loaded(
|
||||
&headers,
|
||||
load_page(state, &headers, submitted, Some(message)).await,
|
||||
)
|
||||
}
|
||||
Err(message) => return reject(&headers, message),
|
||||
};
|
||||
for chunk in converted.chunks(1_000) {
|
||||
let request = PostTableDataBulkRequest {
|
||||
@@ -225,7 +178,7 @@ pub(crate) async fn import_csv(
|
||||
Err(_) => return Redirect::to("/login").into_response(),
|
||||
}).await {
|
||||
Ok(response) => response.into_inner(),
|
||||
Err(error) => return backend_error(&headers, error.message().to_string()),
|
||||
Err(error) => return grpc_error(&headers, &error),
|
||||
};
|
||||
inserted += response.responses.iter().filter(|row| row.inserted_id > 0).count();
|
||||
}
|
||||
@@ -309,30 +262,38 @@ fn render_loaded(headers: &HeaderMap, result: Result<super::state::ImportPageSta
|
||||
}
|
||||
}
|
||||
|
||||
fn load_error(headers: &HeaderMap, error: LoadError) -> Response {
|
||||
match error {
|
||||
LoadError::Unauthenticated => Redirect::to("/login").into_response(),
|
||||
LoadError::Forbidden => (
|
||||
StatusCode::FORBIDDEN,
|
||||
Html(ui::render_error(
|
||||
Locale::from_headers(headers),
|
||||
&tr!(
|
||||
Locale::from_headers(headers),
|
||||
"import-err-permission"
|
||||
),
|
||||
)),
|
||||
)
|
||||
.into_response(),
|
||||
LoadError::Backend(message) => backend_error(headers, message),
|
||||
}
|
||||
/// A CSV this crate rejected before any of it reached the backend: an unknown
|
||||
/// profile, a column the table has no place for, an unparseable cell. The
|
||||
/// form's `hx-target` is `#submission-status`, so the answer is the alert
|
||||
/// fragment — re-rendering the page here sent a whole `<html>` document to be
|
||||
/// swapped into a status block.
|
||||
fn reject(headers: &HeaderMap, message: String) -> Response {
|
||||
crate::ui::FormError::Rejected(message)
|
||||
.into_response(Locale::from_headers(headers), ui::render_error)
|
||||
}
|
||||
|
||||
fn backend_error(headers: &HeaderMap, message: String) -> Response {
|
||||
(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
Html(ui::render_error(Locale::from_headers(headers), &message)),
|
||||
)
|
||||
.into_response()
|
||||
/// A gRPC failure, classified by its code. An import that the server refused
|
||||
/// because of what was *in* the CSV — a script-consistency mismatch, a
|
||||
/// validation pattern the value does not satisfy — is the user's to fix and
|
||||
/// answers 422; only an actually unreachable backend answers 502.
|
||||
fn grpc_error(headers: &HeaderMap, error: &tonic::Status) -> Response {
|
||||
crate::ui::FormError::from_status(error)
|
||||
.into_response(Locale::from_headers(headers), ui::render_error)
|
||||
}
|
||||
|
||||
fn load_error(headers: &HeaderMap, error: LoadError) -> Response {
|
||||
let locale = Locale::from_headers(headers);
|
||||
error
|
||||
.into_form_error(locale, "import-err-permission")
|
||||
.into_response(locale, ui::render_error)
|
||||
}
|
||||
|
||||
/// The backend answered, but not with what the import needs — a table the
|
||||
/// catalogue lists and the structure service does not know. Nothing the user
|
||||
/// can fix from the form.
|
||||
fn unavailable(headers: &HeaderMap, message: String) -> Response {
|
||||
crate::ui::FormError::Unavailable(message)
|
||||
.into_response(Locale::from_headers(headers), ui::render_error)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -84,9 +84,11 @@ pub(crate) async fn login(
|
||||
Locale::from_headers(&headers),
|
||||
"login-error-identifier-required"
|
||||
);
|
||||
// 422, not 400: the form parsed fine, its contents were refused —
|
||||
// the same class as every other rejected submission in the app.
|
||||
return error(
|
||||
Locale::from_headers(&headers),
|
||||
StatusCode::BAD_REQUEST,
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
&message,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use axum::{
|
||||
Form,
|
||||
extract::State,
|
||||
http::{HeaderMap, StatusCode},
|
||||
http::HeaderMap,
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
use tonic::Request;
|
||||
@@ -44,14 +44,15 @@ pub(crate) async fn register(
|
||||
{
|
||||
Ok(response) => response.into_inner(),
|
||||
Err(status) => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Html(ui::render_error(
|
||||
Locale::from_headers(&headers),
|
||||
status.message(),
|
||||
)),
|
||||
)
|
||||
.into_response();
|
||||
// /register is public, so `Unauthenticated` here is the backend
|
||||
// refusing this submission, not a lost session to redirect on.
|
||||
let failure = match crate::ui::FormError::from_status(&status) {
|
||||
crate::ui::FormError::Unauthenticated => {
|
||||
crate::ui::FormError::Rejected(status.message().to_string())
|
||||
}
|
||||
other => other,
|
||||
};
|
||||
return failure.into_response(Locale::from_headers(&headers), ui::render_error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user