5 minutes
GSoC: Week 6
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
- 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
}
- 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 runninghead -c16 /dev/urandom > secret.key
. Inutils/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
- utils/bcrypt.rs
It has methods to hash password and compare passwords. To hash password, it uses the
HASH_ROUNDS
from.env
and usesbcrypt::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(),
)
})
}
- 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
.
900 Words
2022-07-24 23:57