Skip to content
Closed
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
8 changes: 5 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ lazy_static = "1.4"
dotenv = "0.15"
utoipa = "4"
utoipa-swagger-ui = "7"
utoipauto = "0.1.12"
mockall = "0.13"
once_cell = "1.8"
utoipauto = "0.1.12"
mockall = "0.13"
once_cell = "1.8"
jsonwebtoken = "9.3"
chrono = { version = "0.4", features = ["serde"] }

[dev-dependencies]
reqwest = { version = "0.12", features = ["json"] }
Expand Down
86 changes: 86 additions & 0 deletions src/controllers/auth_controller.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use warp::reply::with_status;
use crate::models::auth_model::{LoginRequest, TokenResponse, Claims};
use crate::middleware::auth_middleware::create_jwt;
use crate::models::error_response::ErrorResponse;

const VALID_USERNAME: &str = "admin";
const VALID_PASSWORD: &str = "password123";

#[utoipa::path(
post,
path = "/api/v1/auth/login",
tag = "Authentication",
responses(
(status = 200, body = TokenResponse),
(status = 401, description="Unauthorized", body = ErrorResponse),
(status = 400, description="Bad request", body = ErrorResponse),
(status = 500, body = ErrorResponse)
),
request_body(content = LoginRequest, description = "Login credentials", content_type = "application/json")
)]
pub async fn handle_login(
login_request: LoginRequest,
) -> Result<Box<dyn warp::Reply>, warp::Rejection> {
// Simple credential validation (in production, use proper authentication)
if login_request.username == VALID_USERNAME && login_request.password == VALID_PASSWORD {
match create_jwt(&login_request.username) {
Ok(token) => {
let response = TokenResponse {
access_token: token,
token_type: "Bearer".to_string(),
expires_in: 86400, // 24 hours in seconds
};
Ok(Box::new(warp::reply::json(&response)))
}
Err(_) => {
let error_response = ErrorResponse {
instance: Some("/api/v1/auth/login".to_string()),
title: "Internal Server Error".to_string(),
detail: "Failed to generate token".to_string(),
status: 500,
details: None,
};
Ok(Box::new(with_status(
warp::reply::json(&error_response),
warp::http::StatusCode::INTERNAL_SERVER_ERROR,
)))
}
}
} else {
let error_response = ErrorResponse {
instance: Some("/api/v1/auth/login".to_string()),
title: "Unauthorized".to_string(),
detail: "Invalid username or password".to_string(),
status: 401,
details: None,
};
Ok(Box::new(with_status(
warp::reply::json(&error_response),
warp::http::StatusCode::UNAUTHORIZED,
)))
}
}

#[utoipa::path(
get,
path = "/api/v1/test/protected",
tag = "Test",
responses(
(status = 200, description = "Success", body = String),
(status = 401, description="Unauthorized", body = ErrorResponse),
(status = 500, body = ErrorResponse)
),
security(
("Bearer" = [])
)
)]
pub async fn handle_protected_test(
claims: Claims,
) -> Result<Box<dyn warp::Reply>, warp::Rejection> {
let response = serde_json::json!({
"message": "This is a protected endpoint",
"user": claims.sub,
"timestamp": chrono::Utc::now().to_rfc3339()
});
Ok(Box::new(warp::reply::json(&response)))
}
3 changes: 2 additions & 1 deletion src/controllers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod base_controller;
pub mod base_controller;
pub mod auth_controller;

use warp::Rejection;
use std::sync::Arc;
Expand Down
102 changes: 58 additions & 44 deletions src/errors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,48 +27,62 @@ impl Reject for ApiError {}



pub async fn handle_rejection(err: Rejection) -> Result<impl Reply, Infallible> {
let dict = ERROR_CODES.read().unwrap();
let errors: ErrorResponse = if err.is_not_found() {
ErrorResponse { title: "Not Found".to_string(), status: StatusCode::NOT_FOUND.as_u16(), instance: None, details: None}
} else if let Some(e) = err.find::<ApiError>() {
match e {
ApiError::NotFound => ErrorResponse { title: e.to_string(), status: StatusCode::NOT_FOUND.as_u16(), instance: None, details: None},
ApiError::BadRequest(details, code) => ErrorResponse { title: "Bad request".to_string(), status: StatusCode::BAD_REQUEST.as_u16(), instance: None, details: Some(vec![ValidationProblem {field: None, message: details.clone(), error_code: code.clone()}])},
ApiError::InternalServerError => ErrorResponse { title: "Internal server error".to_string(), status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(), instance: None, details: Some(vec![ValidationProblem {field: None, message: e.to_string(), error_code: 0}])},
ApiError::ErrorCode(code) => {
if let Some(errorcode) = dict.get(code){
ErrorResponse { title: errorcode.message.clone(), status: errorcode.status_code.as_u16(), instance: None, details: Some(vec![ValidationProblem {field: None, message: errorcode.message.clone(), error_code: errorcode.code}])}//(errorcode.status_code, errorcode.code, errorcode.message.to_string())
} else {
ErrorResponse { title: "Internal server error".to_string(), status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(), instance: None, details: Some(vec![ValidationProblem {field: None, message: e.to_string(), error_code: 0}])}
}
},
ApiError::MultipleErrors(errors, field, instance) => {
let mut validation_problems: Option<Vec<ValidationProblem>> = Some(vec![]);
let mut status_code: u16 = 0;
if let Some(error_list) = errors {
for code in error_list {
if let Some(errorcode) = dict.get(code){
status_code = errorcode.status_code.as_u16();
if let Some(ref mut problems) = validation_problems {
problems.push(ValidationProblem {field: field.clone(), message: errorcode.message.clone(), error_code: errorcode.code});
}
}
}
}
ErrorResponse { title: e.to_string(), status: status_code, instance: instance.clone(), details: validation_problems}
}
}
} else {
ErrorResponse { title: "Internal server error".to_string(), status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(), instance: None, details: None}
};

let json = warp::reply::json(&errors);
let res_status_code: StatusCode;
match StatusCode::from_u16(errors.status) {
Ok(status_code)=> {res_status_code = status_code},
Err(_)=> {res_status_code = StatusCode::INTERNAL_SERVER_ERROR}
}

Ok(warp::reply::with_status(json, res_status_code))
pub async fn handle_rejection(err: Rejection) -> Result<impl Reply, Infallible> {
let dict = ERROR_CODES.read().unwrap();

// Handle authentication errors
if let Some(_) = err.find::<crate::middleware::auth_middleware::AuthError>() {
let error_response = ErrorResponse {
title: "Unauthorized".to_string(),
status: StatusCode::UNAUTHORIZED.as_u16(),
instance: None,
detail: "Authentication failed".to_string(),
details: None,
};
let json = warp::reply::json(&error_response);
return Ok(warp::reply::with_status(json, StatusCode::UNAUTHORIZED));
}

let errors: ErrorResponse = if err.is_not_found() {
ErrorResponse { title: "Not Found".to_string(), status: StatusCode::NOT_FOUND.as_u16(), instance: None, detail: "Resource not found".to_string(), details: None}
} else if let Some(e) = err.find::<ApiError>() {
match e {
ApiError::NotFound => ErrorResponse { title: e.to_string(), status: StatusCode::NOT_FOUND.as_u16(), instance: None, detail: e.to_string(), details: None},
ApiError::BadRequest(details, code) => ErrorResponse { title: "Bad request".to_string(), status: StatusCode::BAD_REQUEST.as_u16(), instance: None, detail: details.clone(), details: Some(vec![ValidationProblem {field: None, message: details.clone(), error_code: code.clone()}])},
ApiError::InternalServerError => ErrorResponse { title: "Internal server error".to_string(), status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(), instance: None, detail: e.to_string(), details: Some(vec![ValidationProblem {field: None, message: e.to_string(), error_code: 0}])},
ApiError::ErrorCode(code) => {
if let Some(errorcode) = dict.get(code){
ErrorResponse { title: errorcode.message.clone(), status: errorcode.status_code.as_u16(), instance: None, detail: errorcode.message.clone(), details: Some(vec![ValidationProblem {field: None, message: errorcode.message.clone(), error_code: errorcode.code}])}//(errorcode.status_code, errorcode.code, errorcode.message.to_string())
} else {
ErrorResponse { title: "Internal server error".to_string(), status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(), instance: None, detail: e.to_string(), details: Some(vec![ValidationProblem {field: None, message: e.to_string(), error_code: 0}])}
}
},
ApiError::MultipleErrors(errors, field, instance) => {
let mut validation_problems: Option<Vec<ValidationProblem>> = Some(vec![]);
let mut status_code: u16 = 0;
if let Some(error_list) = errors {
for code in error_list {
if let Some(errorcode) = dict.get(code){
status_code = errorcode.status_code.as_u16();
if let Some(ref mut problems) = validation_problems {
problems.push(ValidationProblem {field: field.clone(), message: errorcode.message.clone(), error_code: errorcode.code});
}
}
}
}
ErrorResponse { title: e.to_string(), status: status_code, instance: instance.clone(), detail: e.to_string(), details: validation_problems}
}
}
} else {
ErrorResponse { title: "Internal server error".to_string(), status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(), instance: None, detail: "Internal server error".to_string(), details: None}
};

let json = warp::reply::json(&errors);
let res_status_code: StatusCode;
match StatusCode::from_u16(errors.status) {
Ok(status_code)=> {res_status_code = status_code},
Err(_)=> {res_status_code = StatusCode::INTERNAL_SERVER_ERROR}
}

Ok(warp::reply::with_status(json, res_status_code))
}
59 changes: 59 additions & 0 deletions src/middleware/auth_middleware.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use jsonwebtoken::{decode, DecodingKey, Validation, Algorithm};
use warp::{Filter, Rejection};
use crate::models::auth_model::Claims;
use std::convert::Infallible;

const JWT_SECRET: &[u8] = b"your-secret-key"; // In production, use environment variable

pub fn with_auth() -> impl Filter<Extract = (Claims,), Error = Rejection> + Clone {
warp::header::optional::<String>("authorization")
.and_then(|auth_header: Option<String>| async move {
match auth_header {
Some(header) => {
if let Some(token) = header.strip_prefix("Bearer ") {
match decode_jwt(token) {
Ok(claims) => Ok(claims),
Err(_) => Err(warp::reject::custom(AuthError::InvalidToken)),
}
} else {
Err(warp::reject::custom(AuthError::MissingToken))
}
}
None => Err(warp::reject::custom(AuthError::MissingToken)),
}
})
}

fn decode_jwt(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
let decoding_key = DecodingKey::from_secret(JWT_SECRET);
let validation = Validation::new(Algorithm::HS256);

decode::<Claims>(token, &decoding_key, &validation)
.map(|data| data.claims)
}

pub fn create_jwt(username: &str) -> Result<String, jsonwebtoken::errors::Error> {
use jsonwebtoken::{encode, EncodingKey, Header};
use chrono::{Utc, Duration};

let now = Utc::now();
let expires_in = Duration::hours(24);
let exp = (now + expires_in).timestamp();

let claims = Claims {
sub: username.to_owned(),
exp,
iat: now.timestamp(),
};

let encoding_key = EncodingKey::from_secret(JWT_SECRET);
encode(&Header::default(), &claims, &encoding_key)
}

#[derive(Debug)]
pub enum AuthError {
InvalidToken,
MissingToken,
}

impl warp::reject::Reject for AuthError {}
3 changes: 2 additions & 1 deletion src/middleware/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod validator;
pub mod validator;
pub mod auth_middleware;
22 changes: 22 additions & 0 deletions src/models/auth_model.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

#[derive(Debug, Serialize, Deserialize, ToSchema, Clone)]
pub struct LoginRequest {
pub username: String,
pub password: String,
}

#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct TokenResponse {
pub access_token: String,
pub token_type: String,
pub expires_in: i64,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub sub: String,
pub exp: i64,
pub iat: i64,
}
13 changes: 7 additions & 6 deletions src/models/error_response.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use serde::Serialize;

#[derive(Serialize, utoipa::ToResponse,utoipa::ToSchema)]
pub struct ErrorResponse {
pub title:String,
pub status:u16,
pub instance: Option<String>,
pub details: Option<Vec<ValidationProblem>>
#[derive(Serialize, utoipa::ToResponse,utoipa::ToSchema)]
pub struct ErrorResponse {
pub title:String,
pub status:u16,
pub instance: Option<String>,
pub detail: String,
pub details: Option<Vec<ValidationProblem>>
}

#[derive(Debug, Serialize, utoipa::ToResponse, utoipa::ToSchema)]
Expand Down
5 changes: 3 additions & 2 deletions src/models/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod messageModel;
pub mod error_response;
pub mod messageModel;
pub mod error_response;
pub mod auth_model;
Loading