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
19 changes: 12 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,18 @@ serde_json = "1.0.120"
thiserror = "1.0"
async-trait = "0.1.50"
warp = "0.3"
lazy_static = "1.4"
dotenv = "0.15"
utoipa = "4"
utoipa-swagger-ui = "7"
utoipauto = "0.1.12"
mockall = "0.13"
once_cell = "1.8"
lazy_static = "1.4"
dotenv = "0.15"
utoipa = "4"
utoipa-swagger-ui = "7"
utoipauto = "0.1.12"
mockall = "0.13"
once_cell = "1.8"
rand = "0.8"
sha2 = "0.10"
base64 = "0.21"
hex = "0.4"
chrono = { version = "0.4", features = ["serde", "clock"] }

[dev-dependencies]
reqwest = { version = "0.12", features = ["json"] }
Expand Down
40 changes: 36 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ Rust-Base-Backend is a foundational backend project written in Rust, designed wi
- RESTful API setup
- Static file serving from the `public` directory
- Docker support for containerization
- Middleware for parameter validation
- Error handling conforming to RFC 7807
- Asynchronous programming with Tokio
- Swagger integration for API documentation using OpenAPI
- Middleware for parameter validation
- Error handling conforming to RFC 7807
- Asynchronous programming with Tokio
- Swagger integration for API documentation using OpenAPI
- Secure token-based authentication

## Architecture
The project is structured to follow the principles of clean architecture, ensuring separation of concerns and maintainability. The main components include:
Expand Down Expand Up @@ -172,6 +173,37 @@ The project integrates Swagger for API documentation using OpenAPI. This allows
To enable Swagger documentation in the project, ensure that the necessary dependencies are included and configured to generate the OpenAPI specification, which can then be served and viewed using tools like Swagger UI.

The `utoipa-swagger-ui` crate downloads the Swagger UI assets at build time. Ensure network access is available, or provide an alternate archive URL via the `SWAGGER_UI_DOWNLOAD_URL` environment variable before running `cargo build` or `cargo test`.

### Token Authentication
Two additional endpoints demonstrate a secure token flow:

- `POST /api/v1/auth/token` – accepts either a username/password or a client_id/client_secret and returns a cryptographically secure, short‑lived token.
- `GET /api/v1/protected` – returns protected data and requires the token in an `Authorization: Bearer <token>` header.

To test using Swagger UI:
1. Open `/api/v1/swagger-ui` in the browser.
2. Execute the `POST /auth/token` endpoint providing credentials, for example:

```json
{
"grant_type": "user",
"username": "admin",
"password": "password"
}
```

or

```json
{
"grant_type": "client",
"client_id": "client",
"client_secret": "secret"
}
```

3. Copy the returned token and click the **Authorize** button in Swagger, entering `Bearer <token>` as the value.
4. Call `GET /protected`; it will respond only when a valid token is supplied.

## Getting Started

Expand Down
31 changes: 31 additions & 0 deletions src/controllers/auth_controller.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use std::sync::Arc;

use crate::models::{
auth_request::AuthRequestDto, error_response::ErrorResponse, token_model::TokenResponseDto,
};
use crate::services::auth_service::AuthService;

#[utoipa::path(
post,
path = "/api/v1/auth/token",
tag = "Authentication",
request_body(
content = AuthRequestDto,
description = "User/password or client credentials used to request a token",
content_type = "application/json"
),
responses(
(status = 200, description = "Token generated", body = TokenResponseDto),
(status = 401, description = "Unauthorized", body = ErrorResponse),
(status = 500, description = "Internal server error", body = ErrorResponse)
)
)]
pub async fn generate_token<S: AuthService + Send + Sync>(
service: Arc<S>,
request: AuthRequestDto,
) -> Result<impl warp::Reply, warp::Rejection> {
match service.generate_token(request).await {
Ok(token) => Ok(warp::reply::json(&token)),
Err(e) => Err(warp::reject::custom(e)),
}
}
123 changes: 105 additions & 18 deletions src/controllers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,105 @@
pub mod base_controller;

use warp::Rejection;
use std::sync::Arc;

use crate::repositories::base_Repository::InMemoryBaseRepository;
use crate::services::base_service::BaseServiceImpl;
use crate::config::Config;
use crate::router::Router;


pub fn base_routes(config: Arc<Config>) -> impl warp::Filter<Extract = impl warp::Reply, Error = Rejection> +Clone {
let repository = InMemoryBaseRepository::new();
let service = BaseServiceImpl::new(repository);
let router = Router::new(service, Arc::clone(&config));

router.routes()
}
pub mod auth_controller;
pub mod base_controller;
pub mod protected_controller;

use std::convert::Infallible;
use std::sync::Arc;
use warp::{Filter, Rejection};

use crate::config::Config;
use crate::errors::ApiError;
use crate::repositories::base_Repository::InMemoryBaseRepository;
use crate::repositories::credentials_repository::InMemoryCredentialRepository;
use crate::repositories::token_repository::InMemoryTokenRepository;
use crate::router::Router;
use crate::services::auth_service::{AuthService, AuthServiceImpl};
use crate::services::base_service::{BaseService, BaseServiceImpl};

pub fn routes(
config: Arc<Config>,
) -> impl Filter<Extract = impl warp::Reply, Error = Rejection> + Clone {
let base_repository = InMemoryBaseRepository::new();
let base_service = BaseServiceImpl::new(base_repository);
let base_router = Router::new(base_service, Arc::clone(&config)).routes();

let token_repository = InMemoryTokenRepository::new();
let credential_repository = InMemoryCredentialRepository::new();
let auth_service = Arc::new(AuthServiceImpl::new(
token_repository,
credential_repository,
));
let auth_routes = build_auth_routes(Arc::clone(&auth_service), Arc::clone(&config));
let protected_routes = build_protected_routes(Arc::clone(&auth_service), Arc::clone(&config));

base_router.or(auth_routes).or(protected_routes)
}

fn build_auth_routes<S: AuthService + Send + Sync + 'static>(
service: Arc<S>,
config: Arc<Config>,
) -> impl Filter<Extract = impl warp::Reply, Error = Rejection> + Clone {
let api_base = config.api_base.trim_matches('/').to_string();
let segments: Vec<String> = api_base.split('/').map(|s| s.to_string()).collect();

let mut api_path = warp::path(segments[0].clone()).boxed();
for seg in &segments[1..] {
api_path = api_path.and(warp::path(seg.clone())).boxed();
}

let auth_token = warp::post()
.and(api_path.clone())
.and(warp::path("auth"))
.and(warp::path("token"))
.and(warp::path::end())
.and(with_auth_service(Arc::clone(&service)))
.and(warp::body::json())
.and_then(auth_controller::generate_token);

auth_token
}

fn build_protected_routes<S: AuthService + Send + Sync + 'static>(
service: Arc<S>,
config: Arc<Config>,
) -> impl Filter<Extract = impl warp::Reply, Error = Rejection> + Clone {
let api_base = config.api_base.trim_matches('/').to_string();
let segments: Vec<String> = api_base.split('/').map(|s| s.to_string()).collect();

let mut api_path = warp::path(segments[0].clone()).boxed();
for seg in &segments[1..] {
api_path = api_path.and(warp::path(seg.clone())).boxed();
}

warp::get()
.and(api_path.clone())
.and(warp::path("protected"))
.and(warp::path::end())
.and(authorize(Arc::clone(&service)))
.and_then(protected_controller::protected_endpoint)
}

fn with_auth_service<S: AuthService + Send + Sync + 'static>(
service: Arc<S>,
) -> impl Filter<Extract = (Arc<S>,), Error = Infallible> + Clone {
warp::any().map(move || Arc::clone(&service))
}

fn authorize<S: AuthService + Send + Sync + 'static>(
service: Arc<S>,
) -> impl Filter<Extract = (), Error = Rejection> + Clone {
warp::header::optional::<String>("authorization")
.and_then(move |header: Option<String>| {
let svc = Arc::clone(&service);
async move {
if let Some(h) = header {
if let Some(token) = h.strip_prefix("Bearer ") {
if svc.validate_token(token).await {
return Ok(());
}
}
}
Err(warp::reject::custom(ApiError::Unauthorized))
}
})
.untuple_one()
}
20 changes: 20 additions & 0 deletions src/controllers/protected_controller.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use warp::{http::StatusCode, reply::with_status};

use crate::models::error_response::ErrorResponse;

#[utoipa::path(
get,
path = "/api/v1/protected",
tag = "Protected",
security(("api_key" = [])),
responses(
(status = 200, body = String),
(status = 401, description = "Unauthorized", body = ErrorResponse)
)
)]
pub async fn protected_endpoint() -> Result<impl warp::Reply, warp::Rejection> {
Ok(with_status(
warp::reply::json(&serde_json::json!({"message": "Top secret"})),
StatusCode::OK,
))
}
Loading