Register and validations
This commit is contained in:
parent
1317863cec
commit
daf472fd3e
117
Cargo.lock
generated
117
Cargo.lock
generated
@ -857,6 +857,15 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
@ -1039,6 +1048,16 @@ dependencies = [
|
||||
"bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "card-validate"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6894706eb61c98c868965ca508ea1a13c68d293acb68a85db1ba327e7c55b31c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.79"
|
||||
@ -1601,6 +1620,34 @@ dependencies = [
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "garde"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b9d6ea59620728a8e9fb67bba68dbd88bff7744de0c7149027612334ed6a40b"
|
||||
dependencies = [
|
||||
"card-validate",
|
||||
"garde_derive",
|
||||
"idna 0.3.0",
|
||||
"once_cell",
|
||||
"phonenumber",
|
||||
"regex",
|
||||
"serde",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "garde_derive"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d9df466e06c7de0744e5f14048ba5331d7a4d31e98542046d869e953fc3420f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"syn 2.0.28",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@ -1836,6 +1883,16 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
|
||||
dependencies = [
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.4.0"
|
||||
@ -2062,6 +2119,15 @@ dependencies = [
|
||||
"value-bag",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru-cache"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
|
||||
dependencies = [
|
||||
"linked-hash-map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
@ -2215,6 +2281,12 @@ version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||
|
||||
[[package]]
|
||||
name = "oncemutex"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44d11de466f4a3006fe8a5e7ec84e93b79c70cb992ae0aa0eb631ad2df8abfe2"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.3.0"
|
||||
@ -2323,6 +2395,7 @@ dependencies = [
|
||||
"askama_actix",
|
||||
"autometrics",
|
||||
"futures",
|
||||
"garde",
|
||||
"jsonwebtoken",
|
||||
"oswilno-contract",
|
||||
"oswilno-view",
|
||||
@ -2341,6 +2414,7 @@ name = "oswilno-view"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"garde",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@ -2557,6 +2631,26 @@ dependencies = [
|
||||
"uncased",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phonenumber"
|
||||
version = "0.3.2+8.13.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34749f64ea9d76f10cdc8a859588b57775f59177c7dd91f744d620bd62982d6f"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"either",
|
||||
"fnv",
|
||||
"itertools",
|
||||
"lazy_static",
|
||||
"nom",
|
||||
"quick-xml",
|
||||
"regex",
|
||||
"regex-cache",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.10"
|
||||
@ -2671,6 +2765,15 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.28.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.32"
|
||||
@ -2777,6 +2880,18 @@ dependencies = [
|
||||
"regex-syntax 0.7.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-cache"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f7b62d69743b8b94f353b6b7c3deb4c5582828328bcb8d5fedf214373808793"
|
||||
dependencies = [
|
||||
"lru-cache",
|
||||
"oncemutex",
|
||||
"regex",
|
||||
"regex-syntax 0.6.29",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.29"
|
||||
@ -3950,7 +4065,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"idna 0.4.0",
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
|
@ -12,6 +12,7 @@ askama = { version = "0.12.0", features = ["serde", "with-actix-web", "comrak",
|
||||
askama_actix = { version = "0.14.0" }
|
||||
autometrics = { version = "0.5.0", features = ["tracing", "tracing-subscriber", "thiserror"] }
|
||||
futures = { version = "0.3.28", features = ["thread-pool"] }
|
||||
garde = { version = "0.14.0", features = ["derive"] }
|
||||
jsonwebtoken = "8.3.0"
|
||||
oswilno-contract = { path = "../oswilno-contract" }
|
||||
oswilno-view = { path = "../oswilno-view" }
|
||||
|
@ -9,6 +9,7 @@ use autometrics::autometrics;
|
||||
use futures::channel::{mpsc, mpsc::Sender};
|
||||
use futures::stream::Stream;
|
||||
use futures::SinkExt;
|
||||
use garde::Validate;
|
||||
use jsonwebtoken::*;
|
||||
use oswilno_view::{Lang, TranslationStorage};
|
||||
use ring::rand::SystemRandom;
|
||||
@ -99,11 +100,17 @@ impl SessionConfigurator {
|
||||
.add("Sign in", "Logowanie")
|
||||
.add("Sign up", "Rejestracja")
|
||||
.add("Bad credentials", "Złe dane uwierzytelniające")
|
||||
.add("Login already taken", "Login jest zajęty")
|
||||
.add("is taken", "jest zajęty")
|
||||
.add("is not strong enough", "jest za słabe")
|
||||
.add(
|
||||
"length is lower than 8",
|
||||
"długość jest mniejsza niż 8 znaków",
|
||||
)
|
||||
.add(
|
||||
"Login or email already taken",
|
||||
"Login lub adres e-mail jest zajęty",
|
||||
)
|
||||
.add("Password", "Hasło")
|
||||
.add("Submit", "Wyślij")
|
||||
.done();
|
||||
}
|
||||
@ -275,19 +282,16 @@ async fn logout(
|
||||
Ok(HttpResponse::Ok().json(EmptyResponse {}))
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq)]
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq, garde::Validate)]
|
||||
#[garde(context(RegisterContext))]
|
||||
struct AccountInfo {
|
||||
#[serde(skip)]
|
||||
errors: Vec<String>,
|
||||
login: String,
|
||||
#[serde(skip)]
|
||||
login_errors: Vec<String>,
|
||||
#[garde(length(min = 4, max = 30), custom(is_login_free))]
|
||||
#[serde(rename = "login")]
|
||||
input_login: String,
|
||||
#[garde(email, custom(is_email_free))]
|
||||
email: String,
|
||||
#[serde(skip)]
|
||||
email_errors: Vec<String>,
|
||||
#[garde(length(min = 8, max = 50), custom(is_strong_password))]
|
||||
password: String,
|
||||
#[serde(skip)]
|
||||
password_errors: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(askama_actix::Template)]
|
||||
@ -296,6 +300,7 @@ struct RegisterTemplate {
|
||||
form: AccountInfo,
|
||||
t: Arc<TranslationStorage>,
|
||||
lang: Lang,
|
||||
errors: oswilno_view::Errors,
|
||||
}
|
||||
|
||||
#[get("/register")]
|
||||
@ -304,6 +309,7 @@ async fn register_view(t: Data<TranslationStorage>) -> RegisterTemplate {
|
||||
form: AccountInfo::default(),
|
||||
t: t.into_inner(),
|
||||
lang: Lang::Pl,
|
||||
errors: oswilno_view::Errors::default(),
|
||||
}
|
||||
}
|
||||
#[get("/p/register")]
|
||||
@ -312,6 +318,7 @@ async fn register_partial_view(t: Data<TranslationStorage>) -> RegisterTemplate
|
||||
form: AccountInfo::default(),
|
||||
t: t.into_inner(),
|
||||
lang: Lang::Pl,
|
||||
errors: oswilno_view::Errors::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -321,6 +328,7 @@ struct RegisterPartialTemplate {
|
||||
form: AccountInfo,
|
||||
t: Arc<TranslationStorage>,
|
||||
lang: Lang,
|
||||
errors: oswilno_view::Errors,
|
||||
}
|
||||
|
||||
#[autometrics]
|
||||
@ -331,14 +339,16 @@ async fn register(
|
||||
t: Data<oswilno_view::TranslationStorage>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let t = t.into_inner();
|
||||
let mut errors = oswilno_view::Errors::default();
|
||||
Ok(
|
||||
match register_internal(db.into_inner(), payload.into_inner(), t.clone()).await {
|
||||
match register_internal(db.into_inner(), payload.into_inner(), &mut errors).await {
|
||||
Ok(res) => res,
|
||||
Err(p) => HttpResponse::BadRequest().body(
|
||||
RegisterPartialTemplate {
|
||||
form: p,
|
||||
t,
|
||||
lang: Lang::Pl,
|
||||
errors,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
@ -347,14 +357,85 @@ async fn register(
|
||||
)
|
||||
}
|
||||
|
||||
struct RegisterContext {
|
||||
login_taken: bool,
|
||||
email_taken: bool,
|
||||
}
|
||||
|
||||
fn is_email_free(_value: &str, context: &RegisterContext) -> garde::Result {
|
||||
if context.email_taken {
|
||||
return Err(garde::Error::new("is taken"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn is_login_free(_value: &str, context: &RegisterContext) -> garde::Result {
|
||||
if context.login_taken {
|
||||
return Err(garde::Error::new("is taken"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
static WEAK_PASS: &str = "is not strong enough";
|
||||
fn is_strong_password(value: &str, _context: &RegisterContext) -> garde::Result {
|
||||
if !(8..50).contains(&value.len()) {
|
||||
return Err(garde::Error::new(WEAK_PASS));
|
||||
}
|
||||
let mut num = false;
|
||||
let mut low = false;
|
||||
let mut up = false;
|
||||
let mut spec = false;
|
||||
for c in value.chars() {
|
||||
if num && low && up && spec {
|
||||
return Ok(());
|
||||
}
|
||||
num = num || c.is_numeric();
|
||||
low = low || c.is_lowercase();
|
||||
up = up || c.is_uppercase();
|
||||
spec = spec || !c.is_alphanumeric();
|
||||
}
|
||||
|
||||
return Err(garde::Error::new(WEAK_PASS));
|
||||
}
|
||||
|
||||
async fn register_internal(
|
||||
db: Arc<DatabaseConnection>,
|
||||
mut p: AccountInfo,
|
||||
t: Arc<TranslationStorage>,
|
||||
p: AccountInfo,
|
||||
errors: &mut oswilno_view::Errors,
|
||||
) -> Result<HttpResponse, AccountInfo> {
|
||||
use oswilno_contract::accounts::*;
|
||||
use sea_orm::*;
|
||||
|
||||
let query_result = db
|
||||
.query_one(sea_orm::Statement::from_sql_and_values(
|
||||
sea_orm::DbBackend::Postgres,
|
||||
"select login = $1 as login_taken, email = $2 as email_taken from accounts",
|
||||
[p.input_login.clone().into(), p.email.clone().into()],
|
||||
))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("{e}");
|
||||
errors.push_global("Something went wrong");
|
||||
p.clone()
|
||||
})?;
|
||||
let (login_taken, email_taken) = if let Some(query_result) = query_result {
|
||||
let Ok((login_taken, email_taken)): Result<(bool,bool), _> = query_result.try_get_many("", &["login_taken".into(), "email_taken".into()]) else {
|
||||
tracing::warn!("Failed to fetch fields from query result while checking if account info exists in db");
|
||||
errors.push_global("Something went wrong");
|
||||
return Err(p);
|
||||
};
|
||||
(login_taken, email_taken)
|
||||
} else {
|
||||
(false, false)
|
||||
};
|
||||
|
||||
if let Err(e) = p.validate(&RegisterContext {
|
||||
login_taken,
|
||||
email_taken,
|
||||
}) {
|
||||
errors.consume_garde(e);
|
||||
}
|
||||
tracing::warn!("{errors:#?}");
|
||||
|
||||
let pass = match hashing::encrypt(p.password.as_str()) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
@ -362,22 +443,9 @@ async fn register_internal(
|
||||
return Ok(HttpResponse::InternalServerError().body(""));
|
||||
}
|
||||
};
|
||||
|
||||
match Entity::find()
|
||||
.filter(Column::Login.eq(&p.login).or(Column::Email.eq(&p.email)))
|
||||
.one(&*db)
|
||||
.await
|
||||
{
|
||||
Ok(None) | Err(_) => {
|
||||
p.login_errors.push("Login already taken".into());
|
||||
return Err(p);
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
|
||||
let model = match (ActiveModel {
|
||||
id: NotSet,
|
||||
login: Set(p.login.to_string()),
|
||||
login: Set(p.input_login.to_string()),
|
||||
email: Set(p.email.to_string()),
|
||||
pass_hash: Set(pass),
|
||||
..Default::default()
|
||||
@ -388,8 +456,7 @@ async fn register_internal(
|
||||
Ok(model) => model,
|
||||
Err(e) => {
|
||||
tracing::warn!("{e}");
|
||||
p.login_errors
|
||||
.push(t.to_lang(Lang::Pl, "Login or email already taken"));
|
||||
errors.push_global("Login or email already taken");
|
||||
return Err(p);
|
||||
}
|
||||
};
|
||||
|
@ -1,26 +1,49 @@
|
||||
<section id="main-view" class="min-h-screen flex items-center justify-center">
|
||||
<section class="max-w-md w-full p-6 bg-white rounded-lg shadow-lg">
|
||||
{% for error in form.errors %}
|
||||
{% for error in errors.global() %}
|
||||
<oswilno-error>{{error|t(lang,t)}}</oswilno-error>
|
||||
{% endfor %}
|
||||
<form hx-post="/register" hx-target="#main-view">
|
||||
<div class="mb-4">
|
||||
<label for="login" class="block mb-2 text-sm text-gray-600">Login</label>
|
||||
<input id="login" name="login" value="{{form.login}}" required class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-500"/>
|
||||
{% for error in form.login_errors %}
|
||||
<oswilno-error>{{error|t(lang,t)}}</oswilno-error>
|
||||
<label for="login" class="block mb-2 text-sm text-gray-600">{{"Login"|t(lang,t)}}</label>
|
||||
<input
|
||||
id="login"
|
||||
name="login"
|
||||
value="{{form.input_login}}"
|
||||
required
|
||||
class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-500"
|
||||
/>
|
||||
{% for error in errors.field("input_login") %}
|
||||
<oswilno-error class="mb-2 mt-2">{{error|t(lang,t)}}</oswilno-error>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="email" class="block mb-2 text-sm text-gray-600">E-Mail</label>
|
||||
<input id="email" name="email" type="email" value="{{form.email}}" required class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-500"/>
|
||||
{% for error in form.email_errors %}
|
||||
<oswilno-error>{{error|t(lang,t)}}</oswilno-error>
|
||||
<label for="email" class="block mb-2 text-sm text-gray-600">{{"E-Mail"|t(lang,t)}}</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value="{{form.email}}"
|
||||
required
|
||||
class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-500"
|
||||
/>
|
||||
{% for error in errors.field("email") %}
|
||||
<oswilno-error class="mb-2 mt-2">{{error|t(lang,t)}}</oswilno-error>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="password" class="block mb-2 text-sm text-gray-600">Password</label>
|
||||
<input id="password" name="password" type="password" required class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-500"/>
|
||||
<label for="password" class="block mb-2 text-sm text-gray-600">{{"Password"|t(lang,t)}}</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
value="{{form.password}}"
|
||||
required
|
||||
class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-500"
|
||||
/>
|
||||
{% for error in errors.field("password") %}
|
||||
<oswilno-error class="mb-2 mt-2">{{error|t(lang,t)}}</oswilno-error>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<input
|
||||
|
@ -5,4 +5,5 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
askama = { version = "0.12.0", features = ["serde", "with-actix-web", "comrak", "mime"] }
|
||||
garde = { version = "0.14.0", features = ["derive"] }
|
||||
tracing = "0.1.37"
|
||||
|
@ -8,6 +8,6 @@ pub fn t<T: std::fmt::Display + std::fmt::Debug>(
|
||||
lang: &Lang,
|
||||
t: &Arc<TranslationStorage>,
|
||||
) -> Result<String> {
|
||||
tracing::debug!("translating {s:?} to lang {lang:?} with {t:?}");
|
||||
// tracing::debug!("translating {s:?} to lang {lang:?} with {t:?}");
|
||||
Ok(t.to_lang(*lang, &s.to_string()))
|
||||
}
|
||||
|
@ -3,6 +3,53 @@ use std::sync::{Arc, RwLock};
|
||||
|
||||
pub mod filters;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Errors {
|
||||
errors: Vec<String>,
|
||||
field_errors: Vec<(String, garde::Error)>,
|
||||
}
|
||||
|
||||
impl Errors {
|
||||
pub fn push_global<S: Into<String>>(&mut self, s: S) {
|
||||
self.errors.push(s.into());
|
||||
}
|
||||
|
||||
pub fn push_field<S: Into<String>>(&mut self, field: S, error: &'static str) {
|
||||
self.field_errors
|
||||
.push((field.into(), garde::Error::new(error)));
|
||||
}
|
||||
|
||||
pub fn consume_garde(&mut self, g: garde::Errors) {
|
||||
self.field_errors = g.flatten();
|
||||
}
|
||||
|
||||
pub fn global(&self) -> &[String] {
|
||||
&self.errors
|
||||
}
|
||||
|
||||
pub fn field(&self, name: &str) -> Vec<String> {
|
||||
self.field_errors
|
||||
.iter()
|
||||
.filter_map(|(field, error)| {
|
||||
if field.contains(name) {
|
||||
Some(error.message.clone().into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<garde::Errors> for Errors {
|
||||
fn from(value: garde::Errors) -> Self {
|
||||
Errors {
|
||||
errors: vec![],
|
||||
field_errors: value.flatten(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)]
|
||||
pub enum Lang {
|
||||
Pl,
|
||||
|
@ -1,3 +1,12 @@
|
||||
import './elements/oswilno-price.js';
|
||||
import './elements/oswilno-error.js';
|
||||
import("https://unpkg.com/htmx.org@1.9.4/dist/htmx.min.js");
|
||||
|
||||
document.body.addEventListener('htmx:beforeOnLoad', function (evt) {
|
||||
const status = evt.detail.xhr.status;
|
||||
if (status === 422 || status === 400) {
|
||||
evt.detail.shouldSwap = true;
|
||||
evt.detail.isError = false;
|
||||
}
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user