Register and validations
This commit is contained in:
parent
36325a2de2
commit
1c6dabcd77
117
Cargo.lock
generated
117
Cargo.lock
generated
@ -857,6 +857,15 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bincode"
|
||||||
|
version = "1.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
@ -1039,6 +1048,16 @@ dependencies = [
|
|||||||
"bytes",
|
"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]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.0.79"
|
version = "1.0.79"
|
||||||
@ -1601,6 +1620,34 @@ dependencies = [
|
|||||||
"slab",
|
"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]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.7"
|
version = "0.14.7"
|
||||||
@ -1836,6 +1883,16 @@ version = "1.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
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]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@ -2062,6 +2119,15 @@ dependencies = [
|
|||||||
"value-bag",
|
"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]]
|
[[package]]
|
||||||
name = "matchers"
|
name = "matchers"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -2215,6 +2281,12 @@ version = "1.18.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "oncemutex"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "44d11de466f4a3006fe8a5e7ec84e93b79c70cb992ae0aa0eb631ad2df8abfe2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opaque-debug"
|
name = "opaque-debug"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
@ -2323,6 +2395,7 @@ dependencies = [
|
|||||||
"askama_actix",
|
"askama_actix",
|
||||||
"autometrics",
|
"autometrics",
|
||||||
"futures",
|
"futures",
|
||||||
|
"garde",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"oswilno-contract",
|
"oswilno-contract",
|
||||||
"oswilno-view",
|
"oswilno-view",
|
||||||
@ -2341,6 +2414,7 @@ name = "oswilno-view"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"askama",
|
"askama",
|
||||||
|
"garde",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2557,6 +2631,26 @@ dependencies = [
|
|||||||
"uncased",
|
"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]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.10"
|
version = "0.2.10"
|
||||||
@ -2671,6 +2765,15 @@ dependencies = [
|
|||||||
"syn 1.0.109",
|
"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]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.32"
|
version = "1.0.32"
|
||||||
@ -2777,6 +2880,18 @@ dependencies = [
|
|||||||
"regex-syntax 0.7.4",
|
"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]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.6.29"
|
version = "0.6.29"
|
||||||
@ -3950,7 +4065,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb"
|
checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"idna",
|
"idna 0.4.0",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ argon2 = "0.5.1"
|
|||||||
askama_actix = { version = "0.14.0" }
|
askama_actix = { version = "0.14.0" }
|
||||||
autometrics = { version = "0.5.0", features = ["tracing", "tracing-subscriber", "thiserror"] }
|
autometrics = { version = "0.5.0", features = ["tracing", "tracing-subscriber", "thiserror"] }
|
||||||
futures = { version = "0.3.28", features = ["thread-pool"] }
|
futures = { version = "0.3.28", features = ["thread-pool"] }
|
||||||
|
garde = { version = "0.14.0", features = ["derive"] }
|
||||||
jsonwebtoken = "8.3.0"
|
jsonwebtoken = "8.3.0"
|
||||||
oswilno-contract = { path = "../oswilno-contract" }
|
oswilno-contract = { path = "../oswilno-contract" }
|
||||||
oswilno-view = { path = "../oswilno-view" }
|
oswilno-view = { path = "../oswilno-view" }
|
||||||
|
@ -9,6 +9,7 @@ use autometrics::autometrics;
|
|||||||
use futures::channel::{mpsc, mpsc::Sender};
|
use futures::channel::{mpsc, mpsc::Sender};
|
||||||
use futures::stream::Stream;
|
use futures::stream::Stream;
|
||||||
use futures::SinkExt;
|
use futures::SinkExt;
|
||||||
|
use garde::Validate;
|
||||||
use jsonwebtoken::*;
|
use jsonwebtoken::*;
|
||||||
use oswilno_view::{Lang, TranslationStorage};
|
use oswilno_view::{Lang, TranslationStorage};
|
||||||
use ring::rand::SystemRandom;
|
use ring::rand::SystemRandom;
|
||||||
@ -99,11 +100,17 @@ impl SessionConfigurator {
|
|||||||
.add("Sign in", "Logowanie")
|
.add("Sign in", "Logowanie")
|
||||||
.add("Sign up", "Rejestracja")
|
.add("Sign up", "Rejestracja")
|
||||||
.add("Bad credentials", "Złe dane uwierzytelniające")
|
.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(
|
.add(
|
||||||
"Login or email already taken",
|
"Login or email already taken",
|
||||||
"Login lub adres e-mail jest zajęty",
|
"Login lub adres e-mail jest zajęty",
|
||||||
)
|
)
|
||||||
|
.add("Password", "Hasło")
|
||||||
.add("Submit", "Wyślij")
|
.add("Submit", "Wyślij")
|
||||||
.done();
|
.done();
|
||||||
}
|
}
|
||||||
@ -275,19 +282,16 @@ async fn logout(
|
|||||||
Ok(HttpResponse::Ok().json(EmptyResponse {}))
|
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 {
|
struct AccountInfo {
|
||||||
#[serde(skip)]
|
#[garde(length(min = 4, max = 30), custom(is_login_free))]
|
||||||
errors: Vec<String>,
|
#[serde(rename = "login")]
|
||||||
login: String,
|
input_login: String,
|
||||||
#[serde(skip)]
|
#[garde(email, custom(is_email_free))]
|
||||||
login_errors: Vec<String>,
|
|
||||||
email: String,
|
email: String,
|
||||||
#[serde(skip)]
|
#[garde(length(min = 8, max = 50), custom(is_strong_password))]
|
||||||
email_errors: Vec<String>,
|
|
||||||
password: String,
|
password: String,
|
||||||
#[serde(skip)]
|
|
||||||
password_errors: Vec<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(askama_actix::Template)]
|
#[derive(askama_actix::Template)]
|
||||||
@ -296,6 +300,7 @@ struct RegisterTemplate {
|
|||||||
form: AccountInfo,
|
form: AccountInfo,
|
||||||
t: Arc<TranslationStorage>,
|
t: Arc<TranslationStorage>,
|
||||||
lang: Lang,
|
lang: Lang,
|
||||||
|
errors: oswilno_view::Errors,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/register")]
|
#[get("/register")]
|
||||||
@ -304,6 +309,7 @@ async fn register_view(t: Data<TranslationStorage>) -> RegisterTemplate {
|
|||||||
form: AccountInfo::default(),
|
form: AccountInfo::default(),
|
||||||
t: t.into_inner(),
|
t: t.into_inner(),
|
||||||
lang: Lang::Pl,
|
lang: Lang::Pl,
|
||||||
|
errors: oswilno_view::Errors::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[get("/p/register")]
|
#[get("/p/register")]
|
||||||
@ -312,6 +318,7 @@ async fn register_partial_view(t: Data<TranslationStorage>) -> RegisterTemplate
|
|||||||
form: AccountInfo::default(),
|
form: AccountInfo::default(),
|
||||||
t: t.into_inner(),
|
t: t.into_inner(),
|
||||||
lang: Lang::Pl,
|
lang: Lang::Pl,
|
||||||
|
errors: oswilno_view::Errors::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,6 +328,7 @@ struct RegisterPartialTemplate {
|
|||||||
form: AccountInfo,
|
form: AccountInfo,
|
||||||
t: Arc<TranslationStorage>,
|
t: Arc<TranslationStorage>,
|
||||||
lang: Lang,
|
lang: Lang,
|
||||||
|
errors: oswilno_view::Errors,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[autometrics]
|
#[autometrics]
|
||||||
@ -331,14 +339,16 @@ async fn register(
|
|||||||
t: Data<oswilno_view::TranslationStorage>,
|
t: Data<oswilno_view::TranslationStorage>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
let t = t.into_inner();
|
let t = t.into_inner();
|
||||||
|
let mut errors = oswilno_view::Errors::default();
|
||||||
Ok(
|
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,
|
Ok(res) => res,
|
||||||
Err(p) => HttpResponse::BadRequest().body(
|
Err(p) => HttpResponse::BadRequest().body(
|
||||||
RegisterPartialTemplate {
|
RegisterPartialTemplate {
|
||||||
form: p,
|
form: p,
|
||||||
t,
|
t,
|
||||||
lang: Lang::Pl,
|
lang: Lang::Pl,
|
||||||
|
errors,
|
||||||
}
|
}
|
||||||
.render()
|
.render()
|
||||||
.unwrap(),
|
.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(
|
async fn register_internal(
|
||||||
db: Arc<DatabaseConnection>,
|
db: Arc<DatabaseConnection>,
|
||||||
mut p: AccountInfo,
|
p: AccountInfo,
|
||||||
t: Arc<TranslationStorage>,
|
errors: &mut oswilno_view::Errors,
|
||||||
) -> Result<HttpResponse, AccountInfo> {
|
) -> Result<HttpResponse, AccountInfo> {
|
||||||
use oswilno_contract::accounts::*;
|
use oswilno_contract::accounts::*;
|
||||||
use sea_orm::*;
|
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()) {
|
let pass = match hashing::encrypt(p.password.as_str()) {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@ -362,22 +443,9 @@ async fn register_internal(
|
|||||||
return Ok(HttpResponse::InternalServerError().body(""));
|
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 {
|
let model = match (ActiveModel {
|
||||||
id: NotSet,
|
id: NotSet,
|
||||||
login: Set(p.login.to_string()),
|
login: Set(p.input_login.to_string()),
|
||||||
email: Set(p.email.to_string()),
|
email: Set(p.email.to_string()),
|
||||||
pass_hash: Set(pass),
|
pass_hash: Set(pass),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@ -388,8 +456,7 @@ async fn register_internal(
|
|||||||
Ok(model) => model,
|
Ok(model) => model,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!("{e}");
|
tracing::warn!("{e}");
|
||||||
p.login_errors
|
errors.push_global("Login or email already taken");
|
||||||
.push(t.to_lang(Lang::Pl, "Login or email already taken"));
|
|
||||||
return Err(p);
|
return Err(p);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,26 +1,49 @@
|
|||||||
<section id="main-view" class="min-h-screen flex items-center justify-center">
|
<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">
|
<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>
|
<oswilno-error>{{error|t(lang,t)}}</oswilno-error>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<form hx-post="/register" hx-target="#main-view">
|
<form hx-post="/register" hx-target="#main-view">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="login" class="block mb-2 text-sm text-gray-600">Login</label>
|
<label for="login" class="block mb-2 text-sm text-gray-600">{{"Login"|t(lang,t)}}</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"/>
|
<input
|
||||||
{% for error in form.login_errors %}
|
id="login"
|
||||||
<oswilno-error>{{error|t(lang,t)}}</oswilno-error>
|
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 %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="email" class="block mb-2 text-sm text-gray-600">E-Mail</label>
|
<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"/>
|
<input
|
||||||
{% for error in form.email_errors %}
|
id="email"
|
||||||
<oswilno-error>{{error|t(lang,t)}}</oswilno-error>
|
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 %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="password" class="block mb-2 text-sm text-gray-600">Password</label>
|
<label for="password" class="block mb-2 text-sm text-gray-600">{{"Password"|t(lang,t)}}</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"/>
|
<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>
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<input
|
<input
|
||||||
|
@ -5,4 +5,5 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
askama = { version = "0.12.0", features = ["serde", "with-actix-web", "comrak", "mime"] }
|
askama = { version = "0.12.0", features = ["serde", "with-actix-web", "comrak", "mime"] }
|
||||||
|
garde = { version = "0.14.0", features = ["derive"] }
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
|
@ -8,6 +8,6 @@ pub fn t<T: std::fmt::Display + std::fmt::Debug>(
|
|||||||
lang: &Lang,
|
lang: &Lang,
|
||||||
t: &Arc<TranslationStorage>,
|
t: &Arc<TranslationStorage>,
|
||||||
) -> Result<String> {
|
) -> 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()))
|
Ok(t.to_lang(*lang, &s.to_string()))
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,53 @@ use std::sync::{Arc, RwLock};
|
|||||||
|
|
||||||
pub mod filters;
|
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)]
|
#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)]
|
||||||
pub enum Lang {
|
pub enum Lang {
|
||||||
Pl,
|
Pl,
|
||||||
|
@ -1,3 +1,12 @@
|
|||||||
import './elements/oswilno-price.js';
|
import './elements/oswilno-price.js';
|
||||||
import './elements/oswilno-error.js';
|
import './elements/oswilno-error.js';
|
||||||
import("https://unpkg.com/htmx.org@1.9.4/dist/htmx.min.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