Skip to content

Commit 57edd88

Browse files
committed
improvements
1 parent bc15177 commit 57edd88

8 files changed

Lines changed: 178 additions & 10 deletions

File tree

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ wasm-bindgen = { version = "0.2.106", optional = true }
3232
tonic = { version = "0.14", optional = true }
3333
jsonwebtoken = { version = "10.2.0", features = ["aws_lc_rs"], optional = true }
3434
chrono = { version = "0.4.42", features = [], optional = true }
35+
email_address = { version = "0.2.9", optional = false}
3536
prost = { version = "0.14", optional = true }
3637
prost-types = { version = "0.14", optional = true }
3738
tonic-prost = { version = "0.14", optional = true }

resources/locales/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
"email_address": "Email address",
44
"password": "Password",
55
"forgot_your_password": "Forgot your password?",
6-
"sign_in": "Sign in"
6+
"sign_in": "Sign in",
7+
"validation_required": "%{attribute} is required"
78
}

src/adapters/grpc_api/auth_user_api.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use crate::application::use_cases::login_user_use_case::LoginUserUseCase;
2+
use crate::domain::models::validation_error::{ErrorMessage, ErrorResponse, Validate};
23
use crate::infra::grpc::auth_user::{auth_server::Auth, LoginRequest, LoginResponse};
4+
use rust_i18n::t;
35
use tonic::{Request, Response, Status};
46

57
pub struct AuthUserGrpcApi {
@@ -22,6 +24,8 @@ impl Auth for AuthUserGrpcApi {
2224
) -> Result<Response<LoginResponse>, Status> {
2325
let req = request.into_inner();
2426

27+
req.validate().await?;
28+
2529
let login_response_data = self
2630
.login_user_use_case
2731
.execute(&req.email, &req.password)
@@ -35,3 +39,59 @@ impl Auth for AuthUserGrpcApi {
3539
Ok(Response::new(response))
3640
}
3741
}
42+
43+
44+
45+
46+
impl LoginRequest {
47+
/// validate
48+
pub async fn validate(&self) -> crate::error::Result<()> {
49+
let mut errors: Vec<ErrorMessage> = vec![];
50+
let mut valid = true;
51+
let locale = "en";
52+
53+
if !self.email.required()? {
54+
let error_message = ErrorMessage {
55+
key: String::from("email"),
56+
message: t!("validation_required", locale = locale, attribute = t!("email", locale = locale)).to_string(),
57+
};
58+
valid = false;
59+
errors.push(error_message);
60+
}
61+
62+
if !self.email.validate_email()? {
63+
let error_message = ErrorMessage {
64+
key: String::from("email"),
65+
message: t!("email_address_not_valid", locale = locale).to_string(),
66+
};
67+
68+
valid = false;
69+
errors.push(error_message);
70+
}
71+
72+
73+
74+
// if profile photo exist then certain type of photo is only allowed
75+
if !self.password.required()? {
76+
let error_message = ErrorMessage {
77+
key: String::from("password"),
78+
message: t!("validation_required", locale = locale, attribute = t!("password", locale = locale)).to_string(),
79+
};
80+
81+
valid = false;
82+
errors.push(error_message);
83+
}
84+
85+
86+
if !valid {
87+
let error_response = ErrorResponse {
88+
status: valid,
89+
errors,
90+
};
91+
let error_string = serde_json::to_string(&error_response)?;
92+
return Err(crate::error::Error::InvalidArgument(error_string));
93+
}
94+
95+
Ok(())
96+
}
97+
}

src/domain/models/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use surrealdb::types::{Datetime, RecordIdKey, Value};
44

55
pub mod user;
66
pub mod admin_user;
7+
pub mod validation_error;
78

89

910

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
use email_address::EmailAddress;
2+
use serde::Serialize;
3+
4+
5+
/// Represents an error message with a key and a message
6+
#[derive(Debug, Serialize, Clone)]
7+
pub struct ErrorMessage {
8+
9+
/// Key associated with the error message
10+
pub key: String,
11+
12+
/// The error message itself
13+
pub message: String,
14+
}
15+
16+
/// Represents a response containing errors
17+
#[derive(Debug, Serialize, Clone)]
18+
pub struct ErrorResponse {
19+
20+
/// Indicates whether the validation was successful
21+
pub status: bool,
22+
23+
/// A list of error messages
24+
pub errors: Vec<ErrorMessage>,
25+
}
26+
27+
/// Trait for validating input data
28+
pub trait Validate {
29+
30+
/// Checks if the input is required
31+
fn required(&self) -> crate::error::Result<bool>;
32+
33+
/// Validates if the input is a valid email address
34+
fn validate_email(&self) -> crate::error::Result<bool>;
35+
}
36+
37+
impl Validate for String {
38+
fn required(&self) -> crate::error::Result<bool> {
39+
if !self.is_empty() {
40+
return Ok(true);
41+
}
42+
Ok(false)
43+
}
44+
45+
fn validate_email(&self) -> crate::error::Result<bool> {
46+
if !EmailAddress::is_valid(self) {
47+
return Ok(false);
48+
}
49+
Ok(true)
50+
}
51+
}

src/error.rs

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1-
use leptos::{config::errors::LeptosConfigError};
1+
use axum::{
2+
http::StatusCode,
3+
response::{IntoResponse, Response},
4+
};
5+
use leptos::config::errors::LeptosConfigError;
26
use tonic::Status;
37
use tracing::error;
48

9+
use crate::domain::models::validation_error::ErrorResponse;
10+
511
/// This is custom Result type for the application.
612
pub type Result<T> = core::result::Result<T, Error>;
713

@@ -13,6 +19,11 @@ pub enum Error {
1319
/// Error when the password encryption has some issue.
1420
Argon2(Box<argon2::password_hash::Error>),
1521

22+
/// Error when the request is bad.
23+
BadRequest(ErrorResponse),
24+
25+
/// Error when the request is forbidden.
26+
InvalidArgument(String),
1627
}
1728

1829
impl core::fmt::Display for Error {
@@ -56,8 +67,25 @@ impl From<jsonwebtoken::errors::Error> for Error {
5667
}
5768
}
5869

70+
impl From<std::io::Error> for Error {
71+
fn from(error: std::io::Error) -> Self {
72+
error!("IO error: {error:?}");
73+
Self::Generic(format!("IO error: {error}"))
74+
}
75+
}
76+
77+
impl From<serde_json::Error> for Error {
78+
fn from(error: serde_json::Error) -> Self {
79+
error!("serde_json error: {error:?}");
80+
Self::Generic(format!("serde_json error: {error}"))
81+
}
82+
}
83+
5984
//
6085

86+
87+
//
88+
6189
impl From<Error> for Status {
6290
fn from(val: Error) -> Self {
6391
match val {
@@ -69,19 +97,29 @@ impl From<Error> for Status {
6997
// Error::Unauthenticated(error_message) => {
7098
// Self::unauthenticated(error_message)
7199
// },
72-
Error::Argon2(boxed_error) => {
73-
Self::internal(format!("Argon2 error: {boxed_error:?}"))
74-
},
75-
_ => Self::invalid_argument("500 Internal server error")
100+
Error::Argon2(boxed_error) => Self::internal(format!("Argon2 error: {boxed_error:?}")),
101+
Error::InvalidArgument(error_response) => Self::invalid_argument(error_response),
102+
Error::BadRequest(str) => {
103+
let validation_errors = match serde_json::to_string(&str) {
104+
Ok(str) => str,
105+
_ => "validation error 400.".to_string(),
106+
};
107+
108+
Self::invalid_argument(validation_errors)
109+
}
110+
_ => Self::internal("500 Internal server error"),
76111
}
77112
}
78113
}
79114

115+
impl IntoResponse for ErrorResponse {
116+
fn into_response(self) -> Response {
117+
let validation_errors = match serde_json::to_string(&self) {
118+
Ok(str) => str,
119+
_ => "validation error 400.".to_string(),
120+
};
80121

81-
impl From<std::io::Error> for Error {
82-
fn from(error: std::io::Error) -> Self {
83-
error!("IO error: {error:?}");
84-
Self::Generic(format!("IO error: {error}"))
122+
(StatusCode::BAD_REQUEST, validation_errors).into_response()
85123
}
86124
}
87125

src/pages/app_layout.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ pub fn AppLayout() -> impl IntoView {
6262
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-500 uppercase tracking-wider">
6363
"System"
6464
</div>
65+
<a href="/admin/entity" class="flex items-center gap-3 px-3 py-2 rounded-lg text-gray-300 hover:bg-slate-800 hover:text-white transition-colors">
66+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
67+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />
68+
</svg>
69+
<span>"Entity"</span>
70+
</a>
6571
<a href="/admin/users" class="flex items-center gap-3 px-3 py-2 rounded-lg text-gray-300 hover:bg-slate-800 hover:text-white transition-colors">
6672
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
6773
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13.471 14.172a4 4 0 015.658 0L21 17.5" />

0 commit comments

Comments
 (0)