DEV Community

Adrien Chapelet
Adrien Chapelet

Posted on

Building a Rust REST API with JWT and Database

As you landed there, you are probably curious on how to use Rust in the creation of an API.
As Rust is a fairly new language, there is not so much tutorials on API development, and I went into some problems that were not so much documented (especially for the Authorization middleware).
In this tutorial, I will focus on the main struggling points I had to face.

I created a fully featured API that you can find here

Mongo DB REST controller

Source of the controller is here

Create

/// Adds a new user to the "users" collection in the database.
#[post("/")]
pub async fn create_user(
    _auth: Authenticated,
    app_state: web::Data<ProgramAppState>,
    body: web::Bytes,
) -> HttpResponse {
    //log::debug!("auth: {auth:?}");
    let json_parse_res = json::parse(std::str::from_utf8(&body).unwrap()); // return Result
    let user_in_json: json::JsonValue = match json_parse_res {
        Ok(v) => v,
        Err(e) => json::object! {"err" => e.to_string() },
    };

    match User::from_json_value(&user_in_json) {
        Some(user) => {
            let collection: Collection<User> = app_state
                .mongo_db_client
                .database(DB_NAME)
                .collection(users::REPOSITORY_NAME);
            let result = collection.insert_one(user, None).await;
            match result {
                Ok(_) => HttpResponse::Created().body(""),
                Err(err) => {
                    log::warn!("{}", err);
                    HttpResponse::InternalServerError().body("")
                }
            }
        }
        None => HttpResponse::InternalServerError().body("Parsing error"),
    }
}
Enter fullscreen mode Exit fullscreen mode

Read

/// Gets the user with the supplied email.
#[get("/{email}")]
pub async fn get_user_by_email(
    //app_data: ProgramAppState,
    auth: Authenticated,
    app_state: web::Data<ProgramAppState>,
    email: web::Path<String>,
) -> HttpResponse {
    //log::debug!("auth: {auth:?}");
    let u = auth.get_user();
    log::debug!("user: {u:?}");

    let email = email.into_inner();
    let collection: Collection<users::User> = app_state
        .mongo_db_client
        .database(DB_NAME)
        .collection(users::REPOSITORY_NAME);
    match collection.find_one(doc! { "email": &email }, None).await {
        Ok(Some(user)) => HttpResponse::Ok().json(user.sanitize()),
        Ok(None) => HttpResponse::NotFound().body(format!("No user found with email {email}")),
        Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
    }
}
Enter fullscreen mode Exit fullscreen mode

Update

/// Updates a user.
#[put("/{id}")]
pub async fn update_user(
    _auth: Authenticated,
    app_state: web::Data<ProgramAppState>,
    id: web::Path<String>,
    body: web::Bytes,
) -> HttpResponse {
    //log::debug!("auth: {auth:?}");
    let user_id = id.into_inner();

    let json_parse_res = json::parse(std::str::from_utf8(&body).unwrap()); // return Result
    let user_in_json: json::JsonValue = match json_parse_res {
        Ok(v) => v,
        Err(e) => json::object! {"err" => e.to_string() },
    };

    match User::from_json_value(&user_in_json) {
        Some(new_user) => {
            let collection: Collection<User> = app_state
                .mongo_db_client
                .database(DB_NAME)
                .collection(users::REPOSITORY_NAME);

            let user_obj_id = mongodb::bson::oid::ObjectId::from_str(&user_id).unwrap();
            let old_user: User = match collection.find_one(doc! { "_id": user_obj_id }, None).await
            {
                Ok(Some(user)) => user,
                Ok(None) => new_user.clone(),
                Err(err) => {
                    log::error!("No user found with email while updating: {err}");
                    new_user.clone()
                }
            };

            let filter = doc! {"_id": &old_user.clone()._id};
            let mut new_user_copy = new_user.clone();
            new_user_copy._id = old_user._id;
            let new_user_bson = bson::to_bson(&new_user_copy).unwrap();
            //let user_doc = new_user_bson.as_document().unwrap();
            let update = doc! {"$set": new_user_bson };

            //let update = doc! {"$set": {"first_name": new_user_copy.first_name}};
            let result = collection.update_one(filter, update, None).await;
            match result {
                Ok(_) => HttpResponse::Ok().json(new_user),
                Err(err) => {
                    log::warn!("{}", err);
                    //TODO: Handle multiple fields
                    HttpResponse::InternalServerError().body("")
                }
            }
        }
        None => HttpResponse::InternalServerError().body("User from json error"),
    }
}
Enter fullscreen mode Exit fullscreen mode

Delete

/// Deletes a user.
#[delete("/{id}")]
pub async fn delete_user_by_id(
    app_state: web::Data<ProgramAppState>,
    id: web::Path<String>,
) -> HttpResponse {
    let id = id.into_inner();
    let user_obj_id = mongodb::bson::oid::ObjectId::from_str(&id).unwrap();
    let collection: Collection<users::User> = app_state
        .mongo_db_client
        .database(DB_NAME)
        .collection(users::REPOSITORY_NAME);
    match collection
        .delete_one(doc! { "_id": &user_obj_id }, None)
        .await
    {
        Ok(res) => HttpResponse::Ok().body(res.deleted_count.to_string()),
        Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
    }
}
Enter fullscreen mode Exit fullscreen mode

Actix web middleware

Source of the middleware is here

pub struct AuthenticateMiddleware<S> {
    auth_data: Rc<AuthState>,
    service: Rc<S>,
}

impl<S, B> Service<ServiceRequest> for AuthenticateMiddleware<S>
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;

    actix_service::forward_ready!(service);

    fn call(&self, req: ServiceRequest) -> Self::Future {
        let srv = Rc::clone(&self.service);
        let auth_data = self.auth_data.clone();

        async move {
            let id = req.get_identity().ok();
            let auth = auth_data.authenticate(id, &req).await?;
            if let Some(auth) = auth {
                req.extensions_mut()
                    .insert::<Rc<AuthenticationInfo>>(Rc::new(auth));
            }

            let res = srv.call(req).await?;

            Ok(res)
        }
        .boxed_local()
    }
}
Enter fullscreen mode Exit fullscreen mode

JWT handling

Source of JWT handler is here

    let collection: Collection<users::User> = app_state
        .mongo_db_client
        .database(DB_NAME)
        .collection(users::REPOSITORY_NAME);
    match collection
        .find_one(doc! { "email": &req_body.email.to_string() }, None)
        .await
    {
        Ok(Some(user)) => {
            let pwd_correct =
                argon2::verify_encoded(user.password.as_str(), req_body.password.as_bytes())
                    .unwrap();
            log::debug!("pwd_correct: {pwd_correct}");
            if pwd_correct {
                let claims: TokenClaims = TokenClaims {
                    user_id: user._id.to_string(),
                    role: "admin".to_string(),
                    exp,
                    iat,
                };

                let token = encode(
                    &Header::default(),
                    &claims,
                    &EncodingKey::from_secret(secret_key.as_ref()),
                )
                .unwrap();

                return HttpResponse::Ok().json(token);
            } else {
                return HttpResponse::InternalServerError().body("Bad password");
            }
        }
        Ok(None) => HttpResponse::NotFound().body(format!(
            "No user found with email {}",
            &req_body.email.to_string()
        )),
        Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
    };
Enter fullscreen mode Exit fullscreen mode

Top comments (0)