Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/api/rest_api_routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ pub fn rest_api_routes(state: AppState) -> crate::error::Result<Router> {
let env_str_allowed_cors = state.config.cors_allowed_app_url.clone();

for origin in &env_str_allowed_cors {
origins.push(HeaderValue::from_str(origin).unwrap());
origins.push(HeaderValue::from_str(origin)?);
}

let cors = CorsLayer::new()
Expand Down
26 changes: 16 additions & 10 deletions src/core/domain/entities/workflow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,28 +58,31 @@ pub struct WorkflowExecutionModel {

#[cfg(test)]
mod tests {
use crate::error::Result;
use super::{WorkflowAction, WorkflowEvent, WorkflowExecutionStatus};

#[test]
fn workflow_events_use_stable_snake_case_names() {
fn workflow_events_use_stable_snake_case_names() -> Result<()> {
assert_eq!(
serde_json::to_value(WorkflowEvent::Created).unwrap(),
serde_json::to_value(WorkflowEvent::Created)?,
serde_json::json!("created")
);
assert_eq!(
serde_json::to_value(WorkflowEvent::Updated).unwrap(),
serde_json::to_value(WorkflowEvent::Updated)?,
serde_json::json!("updated")
);

Ok(())
}

#[test]
fn workflow_actions_use_a_discriminated_ordered_action_shape() {
fn workflow_actions_use_a_discriminated_ordered_action_shape() -> Result<()> {
let action = WorkflowAction::Email {
template_id: "welcome".to_string(),
recipient: "admin@example.test".to_string(),
};

let encoded = serde_json::to_value(&action).unwrap();
let encoded = serde_json::to_value(&action)?;
assert_eq!(
encoded,
serde_json::json!({
Expand All @@ -89,24 +92,27 @@ mod tests {
})
);
assert_eq!(
serde_json::from_value::<WorkflowAction>(encoded).unwrap(),
serde_json::from_value::<WorkflowAction>(encoded)?,
action
);

Ok(())
}

#[test]
fn workflow_execution_status_names_are_stable() {
fn workflow_execution_status_names_are_stable() -> Result<()> {
assert_eq!(
serde_json::to_value(WorkflowExecutionStatus::Running).unwrap(),
serde_json::to_value(WorkflowExecutionStatus::Running)?,
serde_json::json!("running")
);
assert_eq!(
serde_json::to_value(WorkflowExecutionStatus::Success).unwrap(),
serde_json::to_value(WorkflowExecutionStatus::Success)?,
serde_json::json!("success")
);
assert_eq!(
serde_json::to_value(WorkflowExecutionStatus::Failed).unwrap(),
serde_json::to_value(WorkflowExecutionStatus::Failed)?,
serde_json::json!("failed")
);
Ok(())
}
}
45 changes: 45 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use axum::http::{self, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::Json;
use rust_i18n::t;
use tracing::error;

Expand All @@ -9,6 +10,22 @@ use crate::core::domain::entities::ErrorMessageResponse;
/// This is custom Result type for the application.
pub type Result<T> = core::result::Result<T, Error>;

#[derive(Debug, serde::Serialize, Default)]
pub struct AuthErrorResponse {
pub status: bool,
pub message: String,
}

pub fn unauthorized(message: impl Into<String>) -> (StatusCode, Json<AuthErrorResponse>) {
(
StatusCode::UNAUTHORIZED,
Json(AuthErrorResponse {
status: false,
message: message.into(),
}),
)
}

/// This is the custom error type for the application.
#[derive(Debug, Clone)]
pub enum Error {
Expand Down Expand Up @@ -69,6 +86,34 @@ impl From<jsonwebtoken::errors::Error> for Error {
}
}

impl From<leptos::config::errors::LeptosConfigError> for Error {
fn from(actual_error: leptos::config::errors::LeptosConfigError) -> Self {
error!("leptos config error: {actual_error:?}");
Self::Generic(format!("there is an issue with leptos config"))
}
}

impl From<std::io::Error> for Error {
fn from(actual_error: std::io::Error) -> Self {
error!("IO error: {actual_error:?}");
Self::Generic(format!("there is an issue with IO"))
}
}

impl From<reqwest::header::InvalidHeaderValue> for Error {
fn from(actual_error: reqwest::header::InvalidHeaderValue) -> Self {
error!("Invalid header value error: {actual_error:?}");
Self::Generic(format!("there is an issue with the header value"))
}
}

impl From<std::net::AddrParseError> for Error {
fn from(actual_error: std::net::AddrParseError) -> Self {
error!("Address parse error: {actual_error:?}");
Self::Generic(format!("there is an issue with the address"))
}
}

impl IntoResponse for Error {
fn into_response(self) -> Response {
match self {
Expand Down
69 changes: 31 additions & 38 deletions src/infrastructure/middleware/auth_middleware.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,11 @@
use crate::avored_state::AppState;
use crate::core::domain::entities::user::TokenClaims;
use crate::error::Error;
use crate::error::{unauthorized, AuthErrorResponse as ErrorResponse};
use axum::body::Body;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::{http::Request, middleware::Next, Json};
use jsonwebtoken::{decode, DecodingKey, Validation};
use serde::Serialize;
use std::borrow::ToOwned;

#[derive(Debug, Serialize, Default)]
/// error response struct
pub struct ErrorResponse {
/// status of the response
pub status: bool,
/// message of the response
pub message: String,
}

/// Middleware to require JWT authentication for incoming requests
pub async fn check_auth(
Expand All @@ -25,12 +14,20 @@ pub async fn check_auth(
next: Next,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
if let Some(t) = req.headers().get("authorization") {
let auth_value = t.to_str().map_err(|_e| Error::Authentication).unwrap();
let Ok(auth_value) = t.to_str() else {
return Err(unauthorized(
"Authentication failed: Invalid authorization header",
));
};

let jwt_token = &state.config.jwt_secret_key;
let token = auth_value.strip_prefix("Bearer ").map(ToOwned::to_owned);
let Some(token) = auth_value.strip_prefix("Bearer ") else {
return Err(unauthorized(
"Authentication failed: Invalid authorization header",
));
};
let claims = match decode::<TokenClaims>(
&token.unwrap_or_default(),
token,
&DecodingKey::from_secret(jwt_token.as_ref()),
&Validation::default(),
) {
Expand All @@ -43,23 +40,17 @@ pub async fn check_auth(
_ => "Token validation failed",
};

let json_error = ErrorResponse {
status: false,
message: format!("Authentication failed: {error_message}"),
};
return Err((StatusCode::UNAUTHORIZED, Json(json_error)));
return Err(unauthorized(format!(
"Authentication failed: {error_message}"
)));
}
};

req.extensions_mut().insert(claims);

Ok(next.run(req).await)
} else {
let json_error = ErrorResponse {
status: false,
message: "You are not logged in, please provide token".to_string(),
};
Err((StatusCode::UNAUTHORIZED, Json(json_error)))
Err(unauthorized("You are not logged in, please provide token"))
}
}

Expand All @@ -70,12 +61,20 @@ pub async fn check_customer_auth(
next: Next,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
if let Some(t) = req.headers().get("authorization") {
let auth_value = t.to_str().map_err(|_e| Error::Authentication).unwrap();
let Ok(auth_value) = t.to_str() else {
return Err(unauthorized(
"Authentication failed: Invalid authorization header",
));
};

let jwt_token = &state.config.jwt_secret_key;
let token = auth_value.strip_prefix("Bearer ").map(ToOwned::to_owned);
let Some(token) = auth_value.strip_prefix("Bearer ") else {
return Err(unauthorized(
"Authentication failed: Invalid authorization header",
));
};
let claims = match decode::<TokenClaims>(
&token.unwrap_or_default(),
token,
&DecodingKey::from_secret(jwt_token.as_ref()),
&Validation::default(),
) {
Expand All @@ -88,22 +87,16 @@ pub async fn check_customer_auth(
_ => "Token validation failed",
};

let json_error = ErrorResponse {
status: false,
message: format!("Authentication failed: {error_message}"),
};
return Err((StatusCode::UNAUTHORIZED, Json(json_error)));
return Err(unauthorized(format!(
"Authentication failed: {error_message}"
)));
}
};

req.extensions_mut().insert(claims);

Ok(next.run(req).await)
} else {
let json_error = ErrorResponse {
status: false,
message: "You are not logged in, please provide token".to_string(),
};
Err((StatusCode::UNAUTHORIZED, Json(json_error)))
Err(unauthorized("You are not logged in, please provide token"))
}
}
15 changes: 9 additions & 6 deletions src/infrastructure/workflow_dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ mod tests {
use super::{HttpWebhookExecutor, WorkflowActionExecutor};
use crate::core::domain::entities::WorkflowAction;
use serde_json::json;
use crate::error::Result;

#[test]
fn rejects_non_public_ipv4_addresses() {
Expand All @@ -278,22 +279,24 @@ mod tests {
}

#[test]
fn rejects_non_public_ipv6_addresses() {
fn rejects_non_public_ipv6_addresses() -> Result<()> {
for address in [
Ipv6Addr::LOCALHOST,
"fc00::1".parse().unwrap(),
"fe80::1".parse().unwrap(),
"2001:db8::1".parse().unwrap(),
"2002::1".parse().unwrap(),
"fc00::1".parse()?,
"fe80::1".parse()?,
"2001:db8::1".parse()?,
"2002::1".parse()?,
] {
assert!(
!is_public_ip(IpAddr::V6(address)),
"{address} must be blocked"
);
}
assert!(is_public_ip(IpAddr::V6(
"2606:4700:4700::1111".parse().unwrap()
"2606:4700:4700::1111".parse()?
)));

Ok(())
}

#[tokio::test]
Expand Down
32 changes: 21 additions & 11 deletions src/interfaces/web/pages/auth/login_page.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,23 @@ pub struct ApiValidationError {
pub message: String,
}

async fn send_login_request(
login_request: LoginRequest,
) -> Result<gloo_net::http::Response, String> {
let request_body = serde_json::to_string(&login_request)
.map_err(|err| format!("Failed to serialize login request: {err}"))?;

let request = gloo_net::http::Request::post("/api/auth/login")
.header("Content-Type", "application/json")
.body(request_body)
.map_err(|err| format!("Failed to build login request: {err}"))?;

request
.send()
.await
.map_err(|err| format!("Unable to reach login API: {err}"))
}

#[component]
pub fn LoginPage() -> impl IntoView {
let email = RwSignal::new("admin@avored.com".to_string());
Expand Down Expand Up @@ -67,18 +84,13 @@ pub fn LoginPage() -> impl IntoView {
field_errors.set(HashMap::new());

leptos::task::spawn_local(async move {
let request_body = serde_json::to_string(&LoginRequest {
let response = send_login_request(LoginRequest {
email: email_value,
password: password_value,
})
.unwrap_or_default();
.await;

let request = gloo_net::http::Request::post("/api/auth/login")
.header("Content-Type", "application/json")
.body(request_body)
.unwrap();

match request.send().await {
match response {
Ok(response) if response.ok() => match response.json::<LoginApiResponse>().await {
Ok(login_response) => {
if login_response.authenticated {
Expand Down Expand Up @@ -126,9 +138,7 @@ pub fn LoginPage() -> impl IntoView {
}
}
}
Err(err) => {
error.set(format!("Unable to reach login API: {err}"));
}
Err(err) => error.set(err),
}

submitting.set(false);
Expand Down
7 changes: 3 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,17 @@ use avored_rust_cms::{api::rest_api_routes::rest_api_routes, avored_state::AppSt

#[tokio::main]
async fn main() -> error::Result<()> {
let conf = leptos::config::get_configuration(None).unwrap();
let conf = leptos::config::get_configuration(None)?;
let leptos_options = conf.leptos_options;

let state = AppState::new(leptos_options).await?;

let app: axum::Router = rest_api_routes(state)?;

println!("listening on http://{}", 3000);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app.into_make_service())
.await
.unwrap();
.await?;

Ok(())
}
Expand Down
Loading
Loading