Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update to use modern Rust dependencies #4

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Updated to use modern Rust dependencies
ggrossman committed Jul 9, 2024
commit b880c9984fdcb11be281d967a34c30a946bc70ce
20 changes: 10 additions & 10 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
[package]

name = "rust-users"
version = "0.0.1"
authors = [ "Ryan Chenkie <ryanchenkie@gmail.com>" ]
version = "0.1.0"
edition = "2021"

[dependencies]
nickel = "*"
mongodb = "*"
bson = "*"
rustc-serialize = "*"
hyper = "*"
jwt = "*"
rust-crypto = "*"
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"
336 changes: 120 additions & 216 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -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<Document>) -> Result<Json, String> {
static AUTH_SECRET: &str = "your_secret_key";

async fn get_data_string(result: mongodb::error::Result<Document>) -> Result<web::Json<Document>, 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<Output = Result<HttpResponse, actix_web::Error>>,
>,
) -> 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::<Authorization<Bearer>>() {
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::<Header, Registered>::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::<UserLogin>(&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::<UserLogin>().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<UserLogin>) -> 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::<Document>("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::<User>().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<User>) -> 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::<Document>("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::<Document>("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");
}
match collection.delete_one(doc! {"_id": id}, None).await {
Ok(_) => HttpResponse::Ok().body("Item deleted!"),
Err(e) => HttpResponse::InternalServerError().body(format!("{}", e)),
}
}