Tasks:

🟩 Axum real-world example.

Outcomes

axum-middleware-example
utils, user model, user service

Workflow

Define Service Error, Response Body and Constant messages

After refering to actix-middleware-example developed by Eason Chai, I understood the basic workflow and skeleton of the example. I too defined a Service Error, one similar in actix-middleware-example to generalise the error and error response.

pub struct ServiceError {
     pub http_status: StatusCode,
     pub body: ResponseBody<String>,
 }

 impl ServiceError {
     pub fn new(http_status: StatusCode, message: String) -> ServiceError {
         ServiceError {
             http_status,
             body: ResponseBody {
                 message,
                 data: String::new(),
             },
         }
     }

     pub fn response(&self) -> http::Result<Response<&ResponseBody<String>>> {
         Response::builder().status(self.http_status).body(&self.body)
     }
 }

Here, ResponseBody is defined as below:

#[derive(Debug, Serialize, Deserialize)]
 pub struct ResponseBody<T> {
     pub message: String,
     pub data: T,
 }

 impl<T> ResponseBody<T> {
     pub fn new(message: &str, data: T) -> ResponseBody<T> {
         ResponseBody {
             message: message.to_string(),
             data,
         }
     }
 }

Setup Database

/model/db.rs

embed_migrations!();

  pub type Connection = PgConnection;
pub type Pool = r2d2::Pool<ConnectionManager<Connection>>;
pub fn migrate_and_config_db(url: &str, pool_size: u32) -> Pool {
     info!("Migrating and configurating database...");
     let manager = ConnectionManager::<Connection>::new(url);
     let pool = r2d2::Pool::builder()
         .connection_timeout(Duration::from_secs(10))
         .max_size(pool_size)
         .build(manager)
         .expect("Failed to create pool.");
     embedded_migrations::run(&pool.get().expect("Failed to migrate."))
         .expect("Failed to migrate.");

     pool
} 

model

  1. User Model The user is defined as shown:
#[derive(Debug, Serialize, Deserialize, Queryable)]
#[diesel(table_name = users)]
pub struct User {
    pub id: i32,
    pub username: String,
     pub email: String,
     pub password: String,
     pub role: String,
     pub login_session: String,
 }

Here, I had to set NewUser, LoginForm and LoginInfo to implement the user methods like: add_user
update_user
get_user
get_all_users
signin
signup\

#[derive(Insertable, Serialize, Deserialize, AsChangeset, Clone)]
#[table_name = "users"]
pub struct NewUser {
    pub username: String,
    pub email: String,
    pub password: String,
    // #[serde(default = "default_role")]
    pub role: String,
    pub login_session: String
}
#[derive(Serialize, Deserialize)]
 pub struct LoginForm {
     pub email: String,
     pub password: String,
 }
#[derive(Insertable)]
 #[table_name = "users"]
 pub struct LoginInfo {
     pub username: String,
     pub role: String,
     pub login_session: String,
 }

There are lot other methods define under model/user.rs which uses above structs to fetch or store data. signin is shown below for example:

pub fn signin(login: LoginForm, conn: &Connection) -> Option<LoginInfo> {
         if let Ok(user_to_verify) = users
             .filter(email.eq(&login.email))
             .get_result::<User>(conn)
         {
             if !user_to_verify.password.is_empty()
                 && compare_password(&login.password, &user_to_verify.password).unwrap()
             {
                 let login_session_str = User::generate_login_session();
                 if User::update_login_session_to_db(
                     &user_to_verify.email,
                     &login_session_str,
                     conn,
                 ) {
                     return Some(LoginInfo {
                         username: user_to_verify.username,
                         role: user_to_verify.role,
                         login_session: login_session_str,
                     });
                 }
             }
         }
         None
     }
  1. UserToken and tokenutils The usertoken is generated and validated using JWT authorisation. I used jsonwebtoken::encode to generate the user token after defining the Claims(UserToken). To encrypt the token a secret key is used, which can be generated by running head -c16 /dev/urandom > secret.key. In utils/token_utils.rs, the token is decoded and validated using the default validation(same as used for encoding).
#[derive(Serialize, Deserialize)]
 pub struct UserToken {
     // issued at
     pub iat: i64,
     // expiration
     pub exp: i64,
     // userID
     pub user_name: String,
     pub role: String,
     pub login_session: String
 }
 impl UserToken {
     pub fn generate_token(login: LoginInfo) -> String{
         let now = Utc::now().timestamp_nanos() / 1_000_000_000;
         let payload = UserToken {
             iat: now,
             exp: now + THREE_HOUR,
             user_name: login.username,
             role: login.role,
             login_session:login.login_session
         };

         jsonwebtoken::encode(
             &Header::default(),
             &payload,
             &EncodingKey::from_secret(&KEY),
         )
         .unwrap()
     }  
 }

The code for decode_token and validate_token is on the same grounds, it can be found in the above commit.

User Service

The user service defines the TokenBodyResponse and other methods to be called in api calls. Most of the methods are same as in actic-middleware-example with some small and obvious modifications. Since actix uses actix_web::web as extractor, I had to find the alternative of it in axum. I found the axum::extract::Extension, its implemented as shown:

pub fn signin(login: LoginForm, Extension(pool): Extension<Pool>) -> Result<TokenBodyResponse, ServiceError> {
  // DO SOMETHING
}

The more information about Extension can be found here.

TokenBodyResponse:

#[derive(Serialize, Deserialize)]
 pub struct TokenBodyResponse {
     pub token: String,
     pub token_type: String,
 }

The User Service implements the following methods:

pub async fn add_user() {}

 pub fn get_all_user(Extension(pool): Extension<Pool>) -> Result<Vec<User>, ServiceError>{
     //..//
 }

 pub fn get_user(user_id: i32, Extension(pool): Extension<Pool>) -> Result<User, ServiceError> {
     //..//
 }

 pub fn signin(login: LoginForm, Extension(pool): Extension<Pool>) -> Result<TokenBodyResponse, ServiceError> {
     //...//
 }

 pub fn update_user() {}

 pub fn delete_user() {}

Utils

  1. utils/bcrypt.rs It has methods to hash password and compare passwords. To hash password, it uses the HASH_ROUNDS from .env and uses bcrypt::hash. It hashes password into a String. To compare password, bcrypt::verify is used and returns a boolean. Both the method has Service Error mapped with them.
pub fn hash_password(plain: &str) -> Result<String, ServiceError> {
     let hashing_cost: u32 = match env::var("HASH_ROUNDS") {
         Ok(cost) => cost.parse().unwrap_or(DEFAULT_COST),
         _ => DEFAULT_COST,
     };
     hash(plain, hashing_cost).map_err(|_| {
         ServiceError::new(
             StatusCode::INTERNAL_SERVER_ERROR,
             constants::MESSAGE_PROCESS_TOKEN_ERROR.to_string(),
         )
     })
 }
pub fn compare_password(plain: &str, hash: &str) -> Result<bool, ServiceError>{
     verify(plain, hash).map_err(|_| {
         ServiceError::new(
             StatusCode::INTERNAL_SERVER_ERROR,
             constants::MESSAGE_PROCESS_TOKEN_ERROR.to_string(),
         )
     })
 }
  1. token_utils Defined above

Next Week target

In the later part of the week, I realised that I have used wrong response method for the ServiceError::response()

pub fn response(&self) -> http::Result<Response<&ResponseBody<String>>> {
         Response::builder().status(self.http_status).body(&self.body)
}

The above is supposed to send axum::response::Response, but here http::Response was used and hence has to be corrected. Also, since the ResponseBody is custom method and don’t have IntoResponse implemented for it, hence I need to implement IntoResponse for the ResponseBody. Also, in the coming week I plan to implement middleware, set routes, add register/signup methods and finally put all in main.rs and complete the axum-middleware-example.

Also, after completing axum-middleware-example I would make clippy happy in casbin-rs/examples and casbin-rs/axum-casbin-auth and then in the next to next week will start with casbin-openraft.