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"),
}
}
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()),
}
}
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"),
}
}
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()),
}
}
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()
}
}
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()),
};
Top comments (0)