From b880c9984fdcb11be281d967a34c30a946bc70ce Mon Sep 17 00:00:00 2001 From: Gary Grossman Date: Tue, 9 Jul 2024 14:14:08 -0700 Subject: [PATCH 1/8] Updated to use modern Rust dependencies --- Cargo.toml | 20 ++-- src/main.rs | 336 +++++++++++++++++++--------------------------------- 2 files changed, 130 insertions(+), 226 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b9ae09f..e04a8c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,14 @@ [package] - name = "rust-users" -version = "0.0.1" -authors = [ "Ryan Chenkie " ] +version = "0.1.0" +edition = "2021" [dependencies] -nickel = "*" -mongodb = "*" -bson = "*" -rustc-serialize = "*" -hyper = "*" -jwt = "*" -rust-crypto = "*" \ No newline at end of file +actix-web = "4.0" +actix-service = "2.0" +env_logger = "0.10" +mongodb = "2.3" +serde = { version = "1.0", features = ["derive"] } +jsonwebtoken = "8.1" +futures = "0.3" +sha2 = "0.10" diff --git a/src/main.rs b/src/main.rs index 0bc5152..85dc923 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,256 +1,160 @@ -#[macro_use] -extern crate nickel; -extern crate rustc_serialize; +use actix_web::{web, App, HttpServer, HttpRequest, HttpResponse, Responder, middleware::Logger}; +use mongodb::{Client, options::ClientOptions, bson::{doc, oid::ObjectId, Document}}; +use serde::{Deserialize, Serialize}; +use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey}; +use futures::StreamExt; -#[macro_use(bson, doc)] -extern crate bson; -extern crate mongodb; -extern crate hyper; -extern crate crypto; -extern crate jwt; - -// Nickel -use nickel::{Nickel, JsonBody, HttpRouter, Request, Response, MiddlewareResult, MediaType}; -use nickel::status::StatusCode::{self, Forbidden}; - -// MongoDB -use mongodb::{Client, ThreadedClient}; -use mongodb::db::ThreadedDatabase; -use mongodb::error::Result as MongoResult; - -// bson -use bson::{Bson, Document}; -use bson::oid::ObjectId; - -// rustc_serialize -use rustc_serialize::json::{Json, ToJson}; -use rustc_serialize::base64; -use rustc_serialize::base64::{FromBase64}; - -// hyper -use hyper::header; -use hyper::header::{Authorization, Bearer}; -use hyper::method::Method; - -// jwt -use std::default::Default; -use crypto::sha2::Sha256; -use jwt::{ - Header, - Registered, - Token, -}; - -#[derive(RustcDecodable, RustcEncodable)] +#[derive(Serialize, Deserialize)] struct User { firstname: String, lastname: String, - email: String + email: String, } -static AUTH_SECRET: &'static str = "your_secret_key"; - -#[derive(RustcDecodable, RustcEncodable)] +#[derive(Serialize, Deserialize)] struct UserLogin { email: String, - password: String + password: String, } -fn get_data_string(result: MongoResult) -> Result { +static AUTH_SECRET: &str = "your_secret_key"; + +async fn get_data_string(result: mongodb::error::Result) -> Result, String> { match result { - Ok(doc) => Ok(Bson::Document(doc).to_json()), - Err(e) => Err(format!("{}", e)) + Ok(doc) => Ok(web::Json(doc)), + Err(e) => Err(format!("{}", e)), } } -fn authenticator<'mw>(request: &mut Request, response: Response<'mw>, ) -> MiddlewareResult<'mw> { - - // Check if we are getting an OPTIONS request - if request.origin.method.to_string() == "OPTIONS".to_string() { - - // The middleware shouldn't be used for OPTIONS, so continue - response.next_middleware() - - } else { +async fn authenticator( + req: HttpRequest, + srv: &dyn actix_service::Service< + HttpRequest, + Response = HttpResponse, + Error = actix_web::Error, + Future = impl std::future::Future>, + >, +) -> impl Responder { + if req.method() == "OPTIONS" { + return srv.call(req).await; + } - // We don't want to apply the middleware to the login route - if request.origin.uri.to_string() == "/login".to_string() { + if req.path() == "/login" { + return srv.call(req).await; + } - response.next_middleware() + let auth_header = match req.headers().get("Authorization") { + Some(header) => header.to_str().unwrap_or(""), + None => "", + }; + let jwt = if auth_header.starts_with("Bearer ") { + &auth_header[7..] } else { + "" + }; - // Get the full Authorization header from the incoming request headers - let auth_header = match request.origin.headers.get::>() { - Some(header) => header, - None => panic!("No authorization header found") - }; - - // Format the header to only take the value - let jwt = header::HeaderFormatter(auth_header).to_string(); - - // We don't need the Bearer part, - // so get whatever is after an index of 7 - let jwt_slice = &jwt[7..]; - - // Parse the token - let token = Token::::parse(jwt_slice).unwrap(); - - // Get the secret key as bytes - let secret = AUTH_SECRET.as_bytes(); - - // Generic example - // Verify the token - if token.verify(&secret, Sha256::new()) { - - response.next_middleware() - - } else { - - response.error(Forbidden, "Access denied") - - } + let token_data = decode::(&jwt, &DecodingKey::from_secret(AUTH_SECRET.as_ref()), &Validation::default()); + match token_data { + Ok(_) => srv.call(req).await, + Err(_) => Ok(HttpResponse::Forbidden().finish()), } - } } -fn main() { - - let mut server = Nickel::new(); - let mut router = Nickel::router(); - - server.utilize(authenticator); - - router.post("/login", middleware! { |request| - - // Accept a JSON string that corresponds to the User struct - let user = request.json_as::().unwrap(); - - // Get the email and password - let email = user.email.to_string(); - let password = user.password.to_string(); - - // Simple password checker - if password == "secret".to_string() { - - let header: Header = Default::default(); - - // For the example, we just have one claim - // You would also want iss, exp, iat etc - let claims = Registered { - sub: Some(email.into()), - ..Default::default() - }; - - let token = Token::new(header, claims); - - // Sign the token - let jwt = token.signed(AUTH_SECRET.as_bytes(), Sha256::new()).unwrap(); - - format!("{}", jwt) - - } else { - format!("Incorrect username or password") - } - - }); - - router.get("/users", middleware! { |request, mut response| +#[actix_web::main] +async fn main() -> std::io::Result<()> { + env_logger::init(); + + HttpServer::new(|| { + App::new() + .wrap(Logger::default()) + .service( + web::resource("/login") + .route(web::post().to(login)) + ) + .service( + web::resource("/users") + .route(web::get().to(get_users)) + .route(web::post().to(new_user)) + ) + .service( + web::resource("/users/{id}") + .route(web::delete().to(delete_user)) + ) + }) + .bind("127.0.0.1:9000")? + .run() + .await +} - // Connect to the database - let client = Client::connect("localhost", 27017) - .ok().expect("Error establishing connection."); +async fn login(info: web::Json) -> impl Responder { + let email = info.email.clone(); + let password = info.password.clone(); - // The users collection - let coll = client.db("rust-users").collection("users"); + if password == "secret" { + let claims = UserLogin { email, password }; + let token = encode(&Header::default(), &claims, &EncodingKey::from_secret(AUTH_SECRET.as_ref())).unwrap(); - // Create cursor that finds all documents - let cursor = coll.find(None, None).unwrap(); + HttpResponse::Ok().body(token) + } else { + HttpResponse::BadRequest().body("Incorrect username or password") + } +} - // Opening for the JSON string to be returned - let mut data_result = "{\"data\":[".to_owned(); +async fn get_users() -> impl Responder { + let client_options = ClientOptions::parse("mongodb://localhost:27017").await.unwrap(); + let client = Client::with_options(client_options).unwrap(); - for (i, result) in cursor.enumerate() { - match get_data_string(result) { - Ok(data) => { - let string_data = if i == 0 { - format!("{}", data) - } else { - format!("{},", data) - }; + let collection = client.database("rust-users").collection::("users"); + let mut cursor = collection.find(None, None).await.unwrap(); - data_result.push_str(&string_data); - }, + let mut data_result = "{\"data\":[".to_owned(); - Err(e) => return response.send(format!("{}", e)) + while let Some(result) = cursor.next().await { + match get_data_string(result).await { + Ok(data) => { + let string_data = format!("{},", data.into_inner()); + data_result.push_str(&string_data); } + Err(e) => return HttpResponse::InternalServerError().body(e), } + } - // Close the JSON string - data_result.push_str("]}"); - - // Set the returned type as JSON - response.set(MediaType::Json); - - // Send back the result - format!("{}", data_result) - - }); - - router.post("/users/new", middleware! { |request, response| - - // Accept a JSON string that corresponds to the User struct - let user = request.json_as::().unwrap(); - - let firstname = user.firstname.to_string(); - let lastname = user.lastname.to_string(); - let email = user.email.to_string(); - - // Connect to the database - let client = Client::connect("localhost", 27017) - .ok().expect("Error establishing connection."); - - // The users collection - let coll = client.db("rust-users").collection("users"); - - // Insert one user - match coll.insert_one(doc! { - "firstname" => firstname, - "lastname" => lastname, - "email" => email - }, None) { - Ok(_) => (StatusCode::Ok, "Item saved!"), - Err(e) => return response.send(format!("{}", e)) - } - - }); - - router.delete("/users/:id", middleware! { |request, response| - - let client = Client::connect("localhost", 27017) - .ok().expect("Failed to initialize standalone client."); + data_result.push_str("]}"); + HttpResponse::Ok() + .content_type("application/json") + .body(data_result) +} - // The users collection - let coll = client.db("rust-users").collection("users"); +async fn new_user(info: web::Json) -> impl Responder { + let client_options = ClientOptions::parse("mongodb://localhost:27017").await.unwrap(); + let client = Client::with_options(client_options).unwrap(); - // Get the user_id from the request params - let object_id = request.param("id").unwrap(); + let collection = client.database("rust-users").collection::("users"); + let user = doc! { + "firstname": &info.firstname, + "lastname": &info.lastname, + "email": &info.email, + }; - // Match the user id to an bson ObjectId - let id = match ObjectId::with_string(object_id) { - Ok(oid) => oid, - Err(e) => return response.send(format!("{}", e)) - }; + match collection.insert_one(user, None).await { + Ok(_) => HttpResponse::Ok().body("Item saved!"), + Err(e) => HttpResponse::InternalServerError().body(format!("{}", e)), + } +} - match coll.delete_one(doc! {"_id" => id}, None) { - Ok(_) => (StatusCode::Ok, "Item deleted!"), - Err(e) => return response.send(format!("{}", e)) - } +async fn delete_user(req: HttpRequest) -> impl Responder { + let client_options = ClientOptions::parse("mongodb://localhost:27017").await.unwrap(); + let client = Client::with_options(client_options).unwrap(); - }); + let collection = client.database("rust-users").collection::("users"); + let object_id = req.match_info().get("id").unwrap(); - server.utilize(router); + let id = ObjectId::parse_str(object_id).unwrap(); - server.listen("127.0.0.1:9000"); -} \ No newline at end of file + match collection.delete_one(doc! {"_id": id}, None).await { + Ok(_) => HttpResponse::Ok().body("Item deleted!"), + Err(e) => HttpResponse::InternalServerError().body(format!("{}", e)), + } +} From 7ab9f644224e358fd235bd9bde8874d51823515e Mon Sep 17 00:00:00 2001 From: Gary Grossman Date: Tue, 9 Jul 2024 17:46:21 -0700 Subject: [PATCH 2/8] Authentication almost working --- Cargo.toml | 4 +-- src/main.rs | 97 +++++++++++++++++++++++++++++++++++------------------ 2 files changed, 66 insertions(+), 35 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e04a8c9..a5ecc8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,5 +10,5 @@ env_logger = "0.10" mongodb = "2.3" serde = { version = "1.0", features = ["derive"] } jsonwebtoken = "8.1" -futures = "0.3" -sha2 = "0.10" +futures-util = "0.3" +futures = "0.3.30" diff --git a/src/main.rs b/src/main.rs index 85dc923..71b3720 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,14 @@ -use actix_web::{web, App, HttpServer, HttpRequest, HttpResponse, Responder, middleware::Logger}; +use actix_service::{Service, Transform}; +use actix_web::{dev::ServiceRequest, dev::ServiceResponse, web, App, Error, HttpServer, HttpRequest, HttpResponse, Responder, middleware::Logger, body::EitherBody}; +use futures_util::future::{ok, Ready}; +use futures_util::TryStreamExt; use mongodb::{Client, options::ClientOptions, bson::{doc, oid::ObjectId, Document}}; use serde::{Deserialize, Serialize}; use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey}; -use futures::StreamExt; +use std::pin::Pin; +use std::future::Future; +use std::boxed::Box; +use futures_util::TryFutureExt; #[derive(Serialize, Deserialize)] struct User { @@ -17,7 +23,7 @@ struct UserLogin { password: String, } -static AUTH_SECRET: &str = "your_secret_key"; +static AUTH_SECRET: &str = "ef3b28c9951edd3151d9abb14e8e2909b5fc535c986e7cb588b32b0d2082a9b9"; async fn get_data_string(result: mongodb::error::Result) -> Result, String> { match result { @@ -26,39 +32,63 @@ async fn get_data_string(result: mongodb::error::Result) -> Result>, - >, -) -> impl Responder { - if req.method() == "OPTIONS" { - return srv.call(req).await; - } - - if req.path() == "/login" { - return srv.call(req).await; +struct Authenticator; + +impl Transform for Authenticator +where + S: Service, Error = Error> + 'static, + B: 'static, +{ + type Response = ServiceResponse>; + type Error = Error; + type Transform = AuthenticatorMiddleware; + type InitError = (); + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ok(AuthenticatorMiddleware { service }) } +} - let auth_header = match req.headers().get("Authorization") { - Some(header) => header.to_str().unwrap_or(""), - None => "", - }; +struct AuthenticatorMiddleware { + service: S, +} - let jwt = if auth_header.starts_with("Bearer ") { - &auth_header[7..] - } else { - "" - }; +impl Service for AuthenticatorMiddleware +where + S: Service, Error = Error> + 'static, + B: 'static, +{ + type Response = ServiceResponse>; + type Error = Error; + type Future = Pin>>>; + + actix_service::forward_ready!(service); + + fn call(&self, req: ServiceRequest) -> Self::Future { + let auth_header = match req.headers().get("Authorization") { + Some(header) => header.to_str().unwrap_or(""), + None => "", + }; + + let jwt = if auth_header.starts_with("Bearer ") { + &auth_header[7..] + } else { + "" + }; + + if req.method() == "OPTIONS" || req.path() == "/login" { + return Box::pin(self.service.call(req).map_ok(|res| res.map_into_left_body())); + } - let token_data = decode::(&jwt, &DecodingKey::from_secret(AUTH_SECRET.as_ref()), &Validation::default()); + let token_data = decode::(&jwt, &DecodingKey::from_secret(AUTH_SECRET.as_ref()), &Validation::default()); - match token_data { - Ok(_) => srv.call(req).await, - Err(_) => Ok(HttpResponse::Forbidden().finish()), + if let Ok(_) = token_data { + Box::pin(self.service.call(req).map_ok(|res| res.map_into_left_body())) + } else { + let res = req.into_response(HttpResponse::Forbidden().finish().map_into_right_body()); + Box::pin(async { Ok(res) }) + } } } @@ -69,6 +99,7 @@ async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .wrap(Logger::default()) + .wrap(Authenticator) .service( web::resource("/login") .route(web::post().to(login)) @@ -111,8 +142,8 @@ async fn get_users() -> impl Responder { let mut data_result = "{\"data\":[".to_owned(); - while let Some(result) = cursor.next().await { - match get_data_string(result).await { + while let Some(result) = cursor.try_next().await.unwrap() { + match get_data_string(Ok(result)).await { Ok(data) => { let string_data = format!("{},", data.into_inner()); data_result.push_str(&string_data); From d2ea37c962adee27f4270ca580ac76c2901d9ba4 Mon Sep 17 00:00:00 2001 From: Gary Grossman Date: Wed, 10 Jul 2024 06:18:54 -0700 Subject: [PATCH 3/8] Use separate structs for session JWT and user login JSON --- src/main.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 71b3720..a630ebc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ use std::pin::Pin; use std::future::Future; use std::boxed::Box; use futures_util::TryFutureExt; +use std::time::{SystemTime, UNIX_EPOCH}; #[derive(Serialize, Deserialize)] struct User { @@ -17,12 +18,20 @@ struct User { email: String, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] struct UserLogin { email: String, password: String, } +#[derive(Serialize, Deserialize, Debug)] +struct SessionJWT { + iat: u64, + exp: u64, + email: String, + password: String, +} + static AUTH_SECRET: &str = "ef3b28c9951edd3151d9abb14e8e2909b5fc535c986e7cb588b32b0d2082a9b9"; async fn get_data_string(result: mongodb::error::Result) -> Result, String> { @@ -81,7 +90,7 @@ where return Box::pin(self.service.call(req).map_ok(|res| res.map_into_left_body())); } - let token_data = decode::(&jwt, &DecodingKey::from_secret(AUTH_SECRET.as_ref()), &Validation::default()); + let token_data = decode::(&jwt, &DecodingKey::from_secret(AUTH_SECRET.as_ref()), &Validation::default()); if let Ok(_) = token_data { Box::pin(self.service.call(req).map_ok(|res| res.map_into_left_body())) @@ -124,7 +133,14 @@ async fn login(info: web::Json) -> impl Responder { let password = info.password.clone(); if password == "secret" { - let claims = UserLogin { email, password }; + let start = SystemTime::now(); + let since_the_epoch = start + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + println!("{:?}", since_the_epoch); + + let claims = SessionJWT { iat: since_the_epoch, exp: since_the_epoch+3600, email, password }; let token = encode(&Header::default(), &claims, &EncodingKey::from_secret(AUTH_SECRET.as_ref())).unwrap(); HttpResponse::Ok().body(token) From 1513b811a290dc0bb788ece5a013d17a5e7cdb4a Mon Sep 17 00:00:00 2001 From: Gary Grossman Date: Wed, 10 Jul 2024 06:20:20 -0700 Subject: [PATCH 4/8] Accidentally removed authors --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index a5ecc8d..222cdc9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "rust-users" version = "0.1.0" +authors = [ "Ryan Chenkie " ] edition = "2021" [dependencies] From f330af82cb6581b32814d92e53930d543606dd18 Mon Sep 17 00:00:00 2001 From: Gary Grossman Date: Wed, 10 Jul 2024 06:28:56 -0700 Subject: [PATCH 5/8] Change AUTH_SECRET back to "your_secret_key" --- src/main.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index a630ebc..f9eb2fc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,7 +32,8 @@ struct SessionJWT { password: String, } -static AUTH_SECRET: &str = "ef3b28c9951edd3151d9abb14e8e2909b5fc535c986e7cb588b32b0d2082a9b9"; +static AUTH_SECRET: &str = "your_secret_key"; +static JWT_EXPIRATION_SECS: u64 = 3600; async fn get_data_string(result: mongodb::error::Result) -> Result, String> { match result { @@ -138,9 +139,8 @@ async fn login(info: web::Json) -> impl Responder { .duration_since(UNIX_EPOCH) .expect("Time went backwards") .as_secs(); - println!("{:?}", since_the_epoch); - let claims = SessionJWT { iat: since_the_epoch, exp: since_the_epoch+3600, email, password }; + let claims = SessionJWT { iat: since_the_epoch, exp: since_the_epoch+JWT_EXPIRATION_SECS, email, password }; let token = encode(&Header::default(), &claims, &EncodingKey::from_secret(AUTH_SECRET.as_ref())).unwrap(); HttpResponse::Ok().body(token) From edc345bbe0f00aebeca782fd6127e5720f4c036f Mon Sep 17 00:00:00 2001 From: Gary Grossman Date: Wed, 10 Jul 2024 06:42:57 -0700 Subject: [PATCH 6/8] Update readme.md --- readme.md | 120 +++++++++++++++++++----------------------------------- 1 file changed, 43 insertions(+), 77 deletions(-) diff --git a/readme.md b/readme.md index d400175..ac4e67d 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ # Rust API Example -This repo shows how to implement a RESTful API in Rust with **[Nickel.rs](http://nickel.rs/)** and the **[MongoDB Rust Driver](https://github.com/mongodb-labs/mongo-rust-driver-prototype)**. +This repo shows how to implement a RESTful API in Rust with **[Actix Web](https://actix.rs/)** and the **[MongoDB Rust Driver](https://www.mongodb.com/docs/drivers/rust/current/)**. ## Important Snippets @@ -13,51 +13,37 @@ The **GET** `/users` route searches MongoDB for all users and then returns a JSO ... -fn get_data_string(result: MongoResult) -> Result { +async fn get_data_string(result: mongodb::error::Result) -> Result, String> { match result { - Ok(doc) => Ok(Bson::Document(doc).to_json()), - Err(e) => Err(format!("{}", e)) + Ok(doc) => Ok(web::Json(doc)), + Err(e) => Err(format!("{}", e)), } } -router.get("/users", middleware! { |request, response| +async fn get_users() -> impl Responder { + let client_options = ClientOptions::parse("mongodb://localhost:27017").await.unwrap(); + let client = Client::with_options(client_options).unwrap(); - // Connect to the database - let client = Client::connect("localhost", 27017) - .ok().expect("Error establishing connection."); + let collection = client.database("rust-users").collection::("users"); + let mut cursor = collection.find(None, None).await.unwrap(); - // The users collection - let coll = client.db("rust-users").collection("users"); - - // Create cursor that finds all documents - let cursor = coll.find(None, None).unwrap(); - - // Opening for the JSON string to be returned let mut data_result = "{\"data\":[".to_owned(); - for (i, result) in cursor.enumerate() { - match get_data_string(result) { + while let Some(result) = cursor.try_next().await.unwrap() { + match get_data_string(Ok(result)).await { Ok(data) => { - let string_data = if i == 0 { - format!("{}", data) - } else { - format!("{},", data) - }; - + let string_data = format!("{},", data.into_inner()); data_result.push_str(&string_data); - }, - - Err(e) => return response.send(format!("{}", e)) + } + Err(e) => return HttpResponse::InternalServerError().body(e), } } - // Close the JSON string data_result.push_str("]}"); - - // Send back the result - format!("{}", data_result) - -}); + HttpResponse::Ok() + .content_type("application/json") + .body(data_result) +} ... ``` @@ -69,42 +55,31 @@ The **POST** `/users/new` route takes JSON data and saves in the database. The d ... -#[derive(RustcDecodable, RustcEncodable)] +#[derive(Serialize, Deserialize)] struct User { firstname: String, lastname: String, - email: String + email: String, } ... -router.post("/users/new", middleware! { |request, response| - - // Accept a JSON string that corresponds to the User struct - let user = request.json_as::().unwrap(); - - let firstname = user.firstname.to_string(); - let lastname = user.lastname.to_string(); - let email = user.email.to_string(); +async fn new_user(info: web::Json) -> impl Responder { + let client_options = ClientOptions::parse("mongodb://localhost:27017").await.unwrap(); + let client = Client::with_options(client_options).unwrap(); - // Connect to the database - let client = Client::connect("localhost", 27017) - .ok().expect("Error establishing connection."); - - // The users collection - let coll = client.db("rust-users").collection("users"); + let collection = client.database("rust-users").collection::("users"); + let user = doc! { + "firstname": &info.firstname, + "lastname": &info.lastname, + "email": &info.email, + }; - // Insert one user - match coll.insert_one(doc! { - "firstname" => firstname, - "lastname" => lastname, - "email" => email - }, None) { - Ok(_) => (StatusCode::Ok, "Item saved!"), - Err(e) => return response.send(format!("{}", e)) + match collection.insert_one(user, None).await { + Ok(_) => HttpResponse::Ok().body("Item saved!"), + Err(e) => HttpResponse::InternalServerError().body(format!("{}", e)), } - -}); +} ... ``` @@ -116,29 +91,20 @@ The **DELETE** `/users/:user_id` takes an `objectid` as a parameter, decodes it ... -router.delete("/users/:user_id", middleware! { |request, response| - - let client = Client::connect("localhost", 27017) - .ok().expect("Failed to initialize standalone client."); +async fn delete_user(req: HttpRequest) -> impl Responder { + let client_options = ClientOptions::parse("mongodb://localhost:27017").await.unwrap(); + let client = Client::with_options(client_options).unwrap(); - // The users collection - let coll = client.db("rust-users").collection("users"); + let collection = client.database("rust-users").collection::("users"); + let object_id = req.match_info().get("id").unwrap(); - // Get the user_id from the request params - let user_id = request.param("user_id").unwrap(); + let id = ObjectId::parse_str(object_id).unwrap(); - // Match the user id to an bson ObjectId - let id = match ObjectId::with_string(user_id) { - Ok(oid) => oid, - Err(e) => return response.send(format!("{}", e)) - }; - - match coll.delete_one(doc! {"_id" => id}, None) { - Ok(_) => (StatusCode::Ok, "Item deleted!"), - Err(e) => return response.send(format!("{}", e)) + match collection.delete_one(doc! {"_id": id}, None).await { + Ok(_) => HttpResponse::Ok().body("Item deleted!"), + Err(e) => HttpResponse::InternalServerError().body(format!("{}", e)), } - -}); +} ... ``` From 6d2956976fe47281c4db9a57ca928c0d810d81ce Mon Sep 17 00:00:00 2001 From: Gary Grossman Date: Wed, 10 Jul 2024 07:26:17 -0700 Subject: [PATCH 7/8] Add more instructions to README --- readme.md | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index ac4e67d..7d8d171 100644 --- a/readme.md +++ b/readme.md @@ -2,6 +2,76 @@ This repo shows how to implement a RESTful API in Rust with **[Actix Web](https://actix.rs/)** and the **[MongoDB Rust Driver](https://www.mongodb.com/docs/drivers/rust/current/)**. +This REST API uses simple JWT authentication. Note that it does not actually integrate with Auth0! + +## Prerequisites + +You will need to have MongoDB installed and running on `localhost:27017`. If on macOS, for instance, follow MongoDB's [installation instructions](https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-os-x/). + +## Usage + +Issue a POST to the `/login` route to obtain a JWT bearer token. + +``` +% curl -v http://127.0.0.1:9000/login -X POST -d '{"email": "Joe User", "password": "secret"}' -H "Content-Type: application/json" +Note: Unnecessary use of -X or --request, POST is already inferred. +* Trying 127.0.0.1:9000... +* Connected to 127.0.0.1 (127.0.0.1) port 9000 +> POST /login HTTP/1.1 +> Host: 127.0.0.1:9000 +> User-Agent: curl/8.4.0 +> Accept: */* +> Content-Type: application/json +> Content-Length: 43 +> +< HTTP/1.1 200 OK +< content-length: 180 +< date: Wed, 10 Jul 2024 14:20:55 GMT +< +* Connection #0 to host 127.0.0.1 left intact +eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MjA2MjEyNTYsImV4cCI6MTcyMDYyNDg1NiwiZW1haWwiOiJKb2UgVXNlciIsInBhc3N3b3JkIjoic2VjcmV0In0.CLi9Jc34GUOMuHuK7KDN2BUI2-vX6KI4yfnIN6ngm0E +``` + +The JWT bearer token can now be specified to the protected routes such as `/users` and `/users/new`. + +``` +% curl -v -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MjA2MjEyNTYsImV4cCI6MTcyMDYyNDg1NiwiZW1haWwiOiJKb2UgVXNlciIsInBhc3N3b3JkIjoic2VjcmV0In0.CLi9Jc34GUOMuHuK7KDN2BUI2-vX6KI4yfnIN6ngm0E" http://127.0.0.1:9000/users -X POST -d '{"firstname": "Joe", "lastname": "User", "email": "joe@example.org"}' -H "Content-Type: application/json" +Note: Unnecessary use of -X or --request, POST is already inferred. +* Trying 127.0.0.1:9000... +* Connected to 127.0.0.1 (127.0.0.1) port 9000 +> POST /users HTTP/1.1 +> Host: 127.0.0.1:9000 +> User-Agent: curl/8.4.0 +> Accept: */* +> Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MjA2MjEyNTYsImV4cCI6MTcyMDYyNDg1NiwiZW1haWwiOiJKb2UgVXNlciIsInBhc3N3b3JkIjoic2VjcmV0In0.CLi9Jc34GUOMuHuK7KDN2BUI2-vX6KI4yfnIN6ngm0E +> Content-Type: application/json +> Content-Length: 68 +> +< HTTP/1.1 200 OK +< content-length: 11 +< date: Wed, 10 Jul 2024 14:23:07 GMT +< +* Connection #0 to host 127.0.0.1 left intact +Item saved! + +% curl -v -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MjA2MjEyNTYsImV4cCI6MTcyMDYyNDg1NiwiZW1haWwiOiJKb2UgVXNlciIsInBhc3N3b3JkIjoic2VjcmV0In0.CLi9Jc34GUOMuHuK7KDN2BUI2-vX6KI4yfnIN6ngm0E" http://127.0.0.1:9000/users +* Trying 127.0.0.1:9000... +* Connected to 127.0.0.1 (127.0.0.1) port 9000 +> GET /users HTTP/1.1 +> Host: 127.0.0.1:9000 +> User-Agent: curl/8.4.0 +> Accept: */* +> Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MjA2MjEyNTYsImV4cCI6MTcyMDYyNDg1NiwiZW1haWwiOiJKb2UgVXNlciIsInBhc3N3b3JkIjoic2VjcmV0In0.CLi9Jc34GUOMuHuK7KDN2BUI2-vX6KI4yfnIN6ngm0E +> +< HTTP/1.1 200 OK +< content-length: 127 +< content-type: application/json +< date: Wed, 10 Jul 2024 14:23:51 GMT +< +* Connection #0 to host 127.0.0.1 left intact +{"data":[{ "_id": ObjectId("668e994c7eba267f28496f8a"), "firstname": "Joe", "lastname": "User", "email": "joe@example.org" },]} +``` + ## Important Snippets The simple example has `GET`, `POST`, and `DELETE` routes in the `main` function. @@ -48,7 +118,7 @@ async fn get_users() -> impl Responder { ... ``` -The **POST** `/users/new` route takes JSON data and saves in the database. The data conforms to the `User` struct. +The **POST** `/users` route takes JSON data and saves in the database. The data conforms to the `User` struct. ```rust // src/main.rs From dce7c8d13459d68a6c3f13b7c8640e32b8b51635 Mon Sep 17 00:00:00 2001 From: Gary Grossman Date: Wed, 10 Jul 2024 07:27:29 -0700 Subject: [PATCH 8/8] There is no /users/new route anymore --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 7d8d171..f92ecc5 100644 --- a/readme.md +++ b/readme.md @@ -32,7 +32,7 @@ Note: Unnecessary use of -X or --request, POST is already inferred. eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MjA2MjEyNTYsImV4cCI6MTcyMDYyNDg1NiwiZW1haWwiOiJKb2UgVXNlciIsInBhc3N3b3JkIjoic2VjcmV0In0.CLi9Jc34GUOMuHuK7KDN2BUI2-vX6KI4yfnIN6ngm0E ``` -The JWT bearer token can now be specified to the protected routes such as `/users` and `/users/new`. +The JWT bearer token can now be specified to the protected routes such as `/users`. ``` % curl -v -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MjA2MjEyNTYsImV4cCI6MTcyMDYyNDg1NiwiZW1haWwiOiJKb2UgVXNlciIsInBhc3N3b3JkIjoic2VjcmV0In0.CLi9Jc34GUOMuHuK7KDN2BUI2-vX6KI4yfnIN6ngm0E" http://127.0.0.1:9000/users -X POST -d '{"firstname": "Joe", "lastname": "User", "email": "joe@example.org"}' -H "Content-Type: application/json"