├── .env ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── diesel.toml ├── docs └── images │ ├── ddb.png │ └── lambda.png ├── migrations ├── .gitkeep ├── 00000000000000_diesel_initial_setup │ ├── down.sql │ └── up.sql └── 2020-12-30-082829_rust_api │ ├── down.sql │ └── up.sql └── src ├── controller ├── awsc.rs ├── home.rs └── mod.rs ├── main.rs ├── middleware ├── auth.rs └── mod.rs ├── model ├── dbmethods.rs ├── mod.rs └── structs.rs ├── routes ├── awsr.rs ├── dieselr.rs └── mod.rs ├── schema.rs ├── test ├── integration.rs ├── mod.rs └── unit.rs └── vars.rs /.env: -------------------------------------------------------------------------------- 1 | RUST_LOG=actix_web=info,actix_server=info,rest_api=info,warn 2 | DATABASE_URL= 3 | DOMAIN=0.0.0.0 4 | PORT=9000 5 | JWT_SECRET= 6 | PASS_SECRET=<> 7 | AWS_ACCESS_KEY_ID=< > 8 | AWS_SECRET_ACCESS_KEY=< > -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .idea 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rest-api" 3 | version = "0.1.0" 4 | authors = ["kaustubh babar "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | actix-web = { version = "3", features = ["openssl"] } 11 | actix-rt = "1.0" 12 | rusoto_core = "0.46.0" 13 | rusoto_lambda = "0.46.0" 14 | actix-service = "1.0.6" 15 | actix-cors = "0.5.4" 16 | serde = { version = "1.0", features = ["derive"] } 17 | serde_json = "1.0" 18 | json = "0.12" 19 | diesel = { version = "1.4.4", features = ["postgres", "uuid", "r2d2", "chrono"] } 20 | dotenv = "0.15.0" 21 | env_logger = "0.8.2" 22 | jsonwebtoken = "7.2.0" 23 | futures = "0.3.8" 24 | log = "0.4.11" 25 | chrono = { version = "0.4", features = ["serde"] } 26 | easy_password = "0.1.2" 27 | http = "0.2.2" 28 | openssl = { version = "0.10", features = ["vendored"] } 29 | actix-multipart = "0.3.0" 30 | rusoto_s3 = "0.46.0" 31 | failure = "0.1.8" 32 | rusoto_dynamodb = "0.46.0" 33 | tokio = { version= "1.0.1",features = ["full"]} 34 | tokio-stream = "0.1.3" 35 | 36 | [profile.release] 37 | lto = true 38 | codegen-units = 1 39 | opt-level = 3 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 IntelliConnect Technologies 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RUST Actix-Web Microservice 2 | ---------------------------------------------------- 3 | Our Rust Beginners Kit for Application Development 4 | --------------------------------------------------- 5 | 6 | #### A collection of sample code using the actix rust framework to 7 | 8 | ### A) Develop REST API 9 | 10 | #### Sample Code : 11 | Functionality | API | Description 12 | :---------|:--------|:-------- 13 | Post JSON data and recieve JSON data | http://0.0.0.0:9000/register_user| Recieve user details, query to create new user and respond with status of request 14 | Post JSON data and recieve jwt token for auth |http://0.0.0.0:9000/login | Recieve username and passwrod, query and verify if credentials are correct, generate and respond with jwt 15 | Fetch JSON Array | http://0.0.0.0:9000/view_holidays | Recieve input year, query database for holiday dates and respond with results 16 | Raw Query example| http://0.0.0.0:9000/list_users | Raw sql query and Manually specifying sql datatypes for structs 17 | AWS Lambda function invoke | http://0.0.0.0:9000/lambda_example | Invoke the function synchronously 18 | Upload to AWS S3 | http://0.0.0.0:9000/upload_file | ---- 19 | Dynamodb Query | http://0.0.0.0:9000/dynamodb_example | Simple Dynamodb Query Example 20 | 21 | ### B) Road Map 22 | - ##### Integrate with Generally used Amazon Web Services ( more coming soon ) 23 | - ##### Integrate with Elastic Search 24 | - ##### Graphql 25 | 26 | - ##### Better Error handling 27 | 28 | ### C) Features : 29 | - ##### Actix Web framework 30 | - ##### Serde serialization framework 31 | - ##### Bcrypt for password hashing 32 | - ##### JWT token Based Authontication 33 | - ##### Postgres databse 34 | - ##### Use of Diesel ORM 35 | - ##### AWS Lambda, Dynamodb, S3 examples 36 | 37 | 38 | 39 | ### D) Dependencies: 40 | 41 | Here's what does what: 42 | Crate | Description 43 | :---------|:-------- 44 | [actix-web](https://github.com/actix/actix-web)|Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust. 45 | [Diesel](https://diesel.rs)|Diesel is a Safe, Extensible ORM and Query Builder for Rust 46 | [Serde](https://crates.io/crates/serde)|Serde is a framework for serializing and deserializing Rust data structures 47 | [rusoto](https://www.rusoto.org/)| Rusoto is an AWS SDK for Rust. 48 | [dotenv](https://crates.io/crates/dotenv)|Required for loading environment variables from .env file 49 | [env_logger](https://crates.io/crates/env_logger)|Implements a logger that can be configured via environment variables. 50 | [jsonwebtoken](https://crates.io/crates/jsonwebtoken)|To Create and parses JWT (JSON Web Tokens) 51 | [http](https://crates.io/crates/http)|A general purpose library of common HTTP types 52 | [easy_password](https://crates.io/crates/easy_password)| Simple crate for password hashing 53 | 54 | 55 | 56 | ### E) Run locally 57 | 58 | > Before you get started, make sure that you have [PostgreSQL](https://postgresql.org), [Rust](https://rust-lang.org), [Cargo](https://doc.rust-lang.org/cargo/), and the [Diesel](https://diesel.rs) CLI installed and that you have Postgres running somewhere. 59 | 60 | ```bash 61 | # Fetch the repo 62 | git clone https://github.com/intelliconnect/rust-lang-apps.git 63 | 64 | 65 | # Add environment variables to .env file. 66 | nano .env 67 | 68 | diesel setup 69 | diesel migration run 70 | 71 | cargo check 72 | cargo run # could take a while! 73 | ``` 74 | 75 | 76 | ### F) Example requests 77 | #### 1) Register 78 | ``` 79 | curl -i --request POST \ 80 | --url http://0.0.0.0:9000/register_user \ 81 | --header 'content-type: application/json' \ 82 | --data '{ 83 | "firstname":"abc", 84 | "lastname":"bbq", 85 | "username":"admin", 86 | "email":"admin@gmail.com", 87 | "mobile":"123456789", 88 | "password":"1313n218u41", 89 | "ip_address":"124.245.55.124", 90 | "isactive":true 91 | }' 92 | ``` 93 | 94 | #### 2) Login 95 | ``` 96 | curl -i --request POST \ 97 | --url http://0.0.0.0:9000/login \ 98 | --header 'content-type: application/json' \ 99 | --data '{ "username":"admin","password":"1313n218u41"}' 100 | ``` 101 | 102 | #### 3) View Holidays 103 | ``` 104 | curl -i --request GET \ 105 | --url http://0.0.0.0:9000/view_holidays \ 106 | --header 'content-type: application/json' \ 107 | --header 'Authorization: Bearer ' \ 108 | --data '{ "year": "2020" }' 109 | ``` 110 | 111 | 112 | #### 4) AWS Lambda Invoke 113 | 114 | ``` 115 | curl -i --request GET \ 116 | --url http://0.0.0.0:9000/lambda_example \ 117 | --header 'content-type: application/json' 118 | ``` 119 | ##### make sure you have lambda function similar to this 120 | 121 | ![Screenshot](docs/images/lambda.png) 122 | 123 | 124 | #### 5) AWS S3 Upload 125 | ##### make sure you have a bucket on S3 "elastic-search-bucket-test" 126 | 127 | ``` 128 | curl -i POST \ 129 | --url http://0.0.0.0:9000/upload_file \ 130 | --header 'content-type: multipart/form-data' \ 131 | -F "file=@image_upload_test.jpg" 132 | ``` 133 | 134 | 135 | 136 | 137 | 138 | #### 6) AWS Dynamodb Query 139 | ##### make sure you have table with name "rusttest" and an item similar to this 140 | 141 | ![Screenshot](docs/images/ddb.png) 142 | 143 | 144 | 145 | 146 | 147 | ``` 148 | curl -i --request GET \ 149 | --url http://0.0.0.0:9000/dynamodb_example \ 150 | --header 'content-type: application/json' \ 151 | --data '{"id":"ICT"}' 152 | ``` 153 | -------------------------------------------------------------------------------- /diesel.toml: -------------------------------------------------------------------------------- 1 | # For documentation on how to configure this file, 2 | # see diesel.rs/guides/configuring-diesel-cli 3 | 4 | [print_schema] 5 | file = "src/schema.rs" 6 | -------------------------------------------------------------------------------- /docs/images/ddb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intelliconnect/rust-lang-apps/0f05426c8fe4e1b33083a12ae46a392a143d97ce/docs/images/ddb.png -------------------------------------------------------------------------------- /docs/images/lambda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intelliconnect/rust-lang-apps/0f05426c8fe4e1b33083a12ae46a392a143d97ce/docs/images/lambda.png -------------------------------------------------------------------------------- /migrations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intelliconnect/rust-lang-apps/0f05426c8fe4e1b33083a12ae46a392a143d97ce/migrations/.gitkeep -------------------------------------------------------------------------------- /migrations/00000000000000_diesel_initial_setup/down.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); 6 | DROP FUNCTION IF EXISTS diesel_set_updated_at(); 7 | -------------------------------------------------------------------------------- /migrations/00000000000000_diesel_initial_setup/up.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | 6 | 7 | 8 | -- Sets up a trigger for the given table to automatically set a column called 9 | -- `updated_at` whenever the row is modified (unless `updated_at` was included 10 | -- in the modified columns) 11 | -- 12 | -- # Example 13 | -- 14 | -- ```sql 15 | -- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); 16 | -- 17 | -- SELECT diesel_manage_updated_at('users'); 18 | -- ``` 19 | CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ 20 | BEGIN 21 | EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s 22 | FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); 23 | END; 24 | $$ LANGUAGE plpgsql; 25 | 26 | CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ 27 | BEGIN 28 | IF ( 29 | NEW IS DISTINCT FROM OLD AND 30 | NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at 31 | ) THEN 32 | NEW.updated_at := current_timestamp; 33 | END IF; 34 | RETURN NEW; 35 | END; 36 | $$ LANGUAGE plpgsql; 37 | -------------------------------------------------------------------------------- /migrations/2020-12-30-082829_rust_api/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE user_login; 2 | DROP TABLE config_holidays; -------------------------------------------------------------------------------- /migrations/2020-12-30-082829_rust_api/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE user_login( 2 | id bigserial PRIMARY KEY , 3 | firstname varchar(50) NOT NULL, 4 | lastname varchar(50) NOT NULL, 5 | username varchar(100) NOT NULL, 6 | email varchar(100) NOT NULL, 7 | mobile varchar(15) NOT NULL, 8 | facebookconnect varchar(200) NULL, 9 | googleconnect varchar(200) NULL, 10 | password varchar(500) NOT NULL, 11 | ip_address varchar(50) NOT NULL, 12 | isactive boolean NULL , 13 | sort_order bigint NULL, 14 | created_at TIMESTAMP default NOW() NULL, 15 | created_by varchar(20) NULL, 16 | updated_at TIMESTAMP default NOW() NULL, 17 | updated_by varchar(20) NULL 18 | ); 19 | 20 | 21 | CREATE TABLE config_holidays ( 22 | id bigserial PRIMARY KEY, 23 | holiday_date varchar(15) NOT NULL, 24 | holiday_desc varchar(50) NOT NULL, 25 | createdat timestamp DEFAULT now(), 26 | updatedat timestamp DEFAULT now() 27 | ); 28 | 29 | -------------------------------------------------------------------------------- /src/controller/awsc.rs: -------------------------------------------------------------------------------- 1 | use actix_multipart::Multipart; 2 | use actix_web::{web, HttpResponse}; 3 | use futures::{StreamExt, TryStreamExt}; 4 | use rusoto_core::ByteStream; 5 | use rusoto_core::Region; 6 | use rusoto_lambda::{InvocationRequest, Lambda, LambdaClient}; 7 | use serde_json::json; 8 | 9 | use crate::model::dbmethods; 10 | use crate::model::structs; 11 | 12 | #[tokio::main] 13 | pub async fn lambda_example_synchronus() -> HttpResponse { 14 | //create AWS Lambda client with Region same as region of Lambda funmction (This uses AWS credentials from .env file) 15 | let client = LambdaClient::new(Region::ApSoutheast1); 16 | //Create body of Invocation Request 17 | let request = InvocationRequest { 18 | function_name: "helloWorldForRust".to_string(), 19 | invocation_type: Some("RequestResponse".to_string()), 20 | ..Default::default() 21 | }; 22 | 23 | match client.invoke(request).await { 24 | Ok(response) => match response.payload { 25 | Some(b_body) => { 26 | let vec_body = b_body.to_vec(); 27 | match String::from_utf8(vec_body) { 28 | Ok(body) => { 29 | let body = serde_json::from_str::(&body); 30 | match body { 31 | Ok(rbody) => HttpResponse::Ok().json(rbody), 32 | Err(err) => { 33 | warn!("{:?}", err); 34 | let errs = err.to_string(); 35 | HttpResponse::InternalServerError().json(json!({ "Error": [errs] })) 36 | } 37 | } 38 | } 39 | Err(err) => { 40 | warn!("{:?}", err); 41 | let errs = err.to_string(); 42 | HttpResponse::InternalServerError().json(json!({ "Error": [errs] })) 43 | } 44 | } 45 | } 46 | None => { 47 | warn!("empty response from lambda"); 48 | HttpResponse::InternalServerError().json("No response from lambda".to_string()) 49 | } 50 | }, 51 | Err(err) => { 52 | warn!("{:?}", err); 53 | let errs = err.to_string(); 54 | HttpResponse::InternalServerError().json(json!({ "Error": [errs] })) 55 | } 56 | } 57 | } 58 | 59 | pub async fn upload_file(mut payload: Multipart) -> HttpResponse { 60 | //get field(Actix streaming body) from payload 61 | while let Ok(Some(mut field)) = payload.try_next().await { 62 | let content_disp = field.content_disposition().unwrap(); 63 | 64 | //get filename from field 65 | match content_disp.get_filename() { 66 | Some(filename) => { 67 | //create filename for file to be saved 68 | let filename_tobe_saved = format!("{} - {}", chrono::Local::now(), filename,); 69 | //get chunks from field and upload them to AWS S3 70 | while let Some(chunk) = field.next().await { 71 | //Convert from bytes to bytestream 72 | let data = chunk.unwrap().to_vec(); 73 | let bst = ByteStream::from(data); 74 | //upload file to AWS S3 75 | return match dbmethods::send_to_s3(bst, filename_tobe_saved.clone()) { 76 | Ok(response) => { 77 | HttpResponse::Ok().json(format!("Uploaded - {:?}", response)) 78 | } 79 | Err(err) => HttpResponse::InternalServerError() 80 | .json(format!("Failed to upload {:?}", err)), 81 | }; 82 | } 83 | } 84 | None => { 85 | warn!("no upload file found"); 86 | return HttpResponse::Ok().body("no upload file found"); 87 | } 88 | } 89 | } 90 | HttpResponse::Ok().body("Upload failed") 91 | } 92 | 93 | pub async fn dynamodb_example(jsondata: web::Json) -> HttpResponse { 94 | match dbmethods::list_data(jsondata.id.clone()) { 95 | Ok(response) => HttpResponse::Ok().json(response), 96 | Err(err) => HttpResponse::InternalServerError().json(format!("{:?}", err)), 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/controller/home.rs: -------------------------------------------------------------------------------- 1 | use crate::model::{dbmethods, structs}; 2 | use actix_web::{web, HttpResponse}; 3 | 4 | //METHOD=POST, PATH="/login" 5 | pub async fn login(jsondata: web::Json) -> HttpResponse { 6 | //Declare new Response Object 7 | let mut rbody = structs::Response::new(); 8 | if jsondata.username.to_string().is_empty() { 9 | rbody.message.code = "UA101".to_string(); 10 | rbody.message.description = "empty username".to_string(); 11 | return HttpResponse::NotAcceptable().json(&rbody); 12 | } 13 | 14 | //Query database for username 15 | let results = dbmethods::fetch_user(&jsondata.username); 16 | 17 | //Check if Query results are empty,if not validate password 18 | //Create token, modify response object and return Response object 19 | if results.is_empty() { 20 | rbody.message.code = "UA101".to_string(); 21 | rbody.message.description = "Invalid User/Password Combination".to_string(); 22 | return HttpResponse::NotFound().json(&rbody); 23 | } else if dbmethods::varify_pass(&jsondata.password, &results[0].password) { 24 | let token = dbmethods::create_jwt(&results[0].username.to_string()); 25 | 26 | rbody.success = true; 27 | rbody.message.code = "200".to_string(); 28 | rbody.message.description = "Success".to_string(); 29 | rbody.data.token = token; 30 | return HttpResponse::Ok().json(&rbody); 31 | } else { 32 | rbody.message.code = "UA101".to_string(); 33 | rbody.message.description = "Invalid User/Password Combination".to_string(); 34 | HttpResponse::NotFound().json(&rbody) 35 | } 36 | } 37 | 38 | pub async fn fetch_holidays(jsondata: web::Json) -> HttpResponse { 39 | if jsondata.year.is_empty() { 40 | let mut rbody = structs::Response::new(); 41 | rbody.success = false; 42 | rbody.message.description = "Year is empty".to_string(); 43 | return HttpResponse::Ok().json(&rbody); 44 | } 45 | 46 | //Query database for holidays 47 | let results = dbmethods::fetch_holidays(&jsondata.year); 48 | 49 | //check if results are empty 50 | if results.is_empty() { 51 | let mut rbody = structs::Response::new(); 52 | rbody.success = false; 53 | rbody.message.description = "No holidays for this year found".to_string(); 54 | return HttpResponse::Ok().json(&rbody); 55 | } 56 | 57 | //Respond with Results 58 | HttpResponse::Ok().json(&results) 59 | } 60 | 61 | pub async fn register_user(jsondata: web::Json) -> HttpResponse { 62 | //Declare Body of Response 63 | let mut rbody = structs::Response::new(); 64 | 65 | //Convert JSON Request type from "web::Json" to "Year" 66 | let new_user = jsondata.into_inner(); 67 | 68 | //fetch user details from database 69 | let temp_user = dbmethods::fetch_user(&new_user.username); 70 | 71 | //check if results are empty(user already exists in database), if yes then set response as user already exists 72 | if !temp_user.is_empty() { 73 | rbody.message.description = "User already exists".to_string(); 74 | return HttpResponse::Ok().json(&rbody); 75 | } 76 | 77 | //if not create new user 78 | let result = dbmethods::create_user(new_user); 79 | 80 | //check whether user created or not and respond 81 | if result.is_ok() { 82 | rbody.success = false; 83 | rbody.message.description = "User created successfully".to_string(); 84 | return HttpResponse::Ok().json(&rbody); 85 | } else { 86 | rbody.message.description = "User creation failed".to_string(); 87 | return HttpResponse::Ok().json(&rbody); 88 | } 89 | } 90 | 91 | pub async fn list_users() -> HttpResponse { 92 | let results = dbmethods::list_users(); 93 | 94 | HttpResponse::Ok().json(results) 95 | } 96 | -------------------------------------------------------------------------------- /src/controller/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod awsc; 2 | pub mod home; 3 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate diesel; 3 | #[macro_use] 4 | extern crate log; 5 | extern crate easy_password; 6 | 7 | mod controller; 8 | mod middleware; 9 | mod model; 10 | mod routes; 11 | mod schema; 12 | #[cfg(test)] 13 | mod test; 14 | mod vars; 15 | 16 | use actix_cors::Cors; 17 | use actix_web::{App, HttpServer}; 18 | use dotenv::dotenv; 19 | 20 | use middleware::auth; 21 | 22 | use crate::model::dbmethods::string_to_attr; 23 | 24 | #[actix_web::main] 25 | async fn main() -> std::io::Result<()> { 26 | // Load env variables 27 | dotenv().ok(); 28 | //Initiate Logger 29 | env_logger::init(); 30 | info!("starting server"); 31 | 32 | 33 | //Configure and Start New HTTP server 34 | HttpServer::new(move || { 35 | //Cors::permissive() should only be used development purposes, not to be used in production 36 | let cors = Cors::permissive(); 37 | 38 | App::new() 39 | .wrap(cors) 40 | .wrap(auth::Auth) 41 | .wrap(actix_web::middleware::Logger::default()) 42 | .configure(routes::dieselr::configure) 43 | .configure(routes::awsr::configure) 44 | }) 45 | .bind(format!("{}:{}", vars::domain(), vars::port()))? 46 | .run() 47 | .await 48 | } 49 | -------------------------------------------------------------------------------- /src/middleware/auth.rs: -------------------------------------------------------------------------------- 1 | use crate::model::{dbmethods, structs}; 2 | use actix_service::{Service, Transform}; 3 | use actix_web::{ 4 | dev::{ServiceRequest, ServiceResponse}, 5 | http::header, 6 | Error, HttpResponse, 7 | }; 8 | use futures::future::{ok, Ready}; 9 | use futures::Future; 10 | use std::pin::Pin; 11 | use std::task::{Context, Poll}; 12 | 13 | pub struct Auth; 14 | 15 | impl Transform for Auth 16 | where 17 | S: Service, Error = Error>, 18 | S::Future: 'static, 19 | B: 'static, 20 | { 21 | type Request = ServiceRequest; 22 | type Response = ServiceResponse; 23 | type Error = Error; 24 | type InitError = (); 25 | type Transform = Authmiddleware; 26 | type Future = Ready>; 27 | 28 | fn new_transform(&self, service: S) -> Self::Future { 29 | ok(Authmiddleware { service }) 30 | } 31 | } 32 | 33 | pub struct Authmiddleware { 34 | service: S, 35 | } 36 | 37 | impl Service for Authmiddleware 38 | where 39 | S: Service, Error = Error>, 40 | S::Future: 'static, 41 | B: 'static, 42 | { 43 | type Request = ServiceRequest; 44 | type Response = ServiceResponse; 45 | type Error = Error; 46 | type Future = Pin>>>; 47 | 48 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 49 | self.service.poll_ready(cx) 50 | } 51 | 52 | fn call(&mut self, mut req: ServiceRequest) -> Self::Future { 53 | //println!("{:?}",req.uri()); 54 | let mut token_verification = false; 55 | 56 | //Skip middleware for this login API 57 | if req.uri().path() == "/login" || req.uri().path() == "/register_user" { 58 | token_verification = true 59 | } 60 | 61 | //Check if token is present 62 | //If yes,Decode and Validate token 63 | //Insert username from token into request header 64 | if let Some(token) = req.headers().get("AUTHORIZATION") { 65 | if let Ok(token_str) = token.to_str() { 66 | debug!("Token converted to string"); 67 | if token_str.starts_with("bearer") || token_str.starts_with("Bearer") { 68 | let token = token_str[6..token_str.len()].trim(); 69 | let decode_response = dbmethods::decode_token(token.to_string()); 70 | if let Ok(token_data) = decode_response { 71 | let username = token_data.claims.username; 72 | req.headers_mut().insert( 73 | header::HeaderName::from_static("token_username"), 74 | header::HeaderValue::from_str(&username).unwrap(), 75 | ); 76 | // println!("{:?}",req.headers()); 77 | token_verification = true 78 | } 79 | /* 80 | if decode_response.is_ok() { 81 | let token_data = decode_response.unwrap(); 82 | let username = token_data.claims.username; 83 | req.headers_mut().insert( 84 | header::HeaderName::from_static("token_username"), 85 | header::HeaderValue::from_str(&username).unwrap(), 86 | ); 87 | // println!("{:?}",req.headers()); 88 | token_verification = true 89 | } 90 | */ 91 | } 92 | } 93 | } 94 | // Continue if Token Verification is complete 95 | //If not, Stop and respond to request 96 | if token_verification { 97 | let fut = self.service.call(req); 98 | Box::pin(async move { 99 | let res = fut.await?; 100 | Ok(res) 101 | }) 102 | } else { 103 | let mut rbody = structs::Response::new(); 104 | rbody.success = false; 105 | rbody.message.code = "UA101".to_string(); 106 | rbody.message.description = "Token Validation failed".to_string(); 107 | 108 | Box::pin(async move { 109 | Ok(req.into_response(HttpResponse::Unauthorized().json(rbody).into_body())) 110 | }) 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/middleware/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth; 2 | -------------------------------------------------------------------------------- /src/model/dbmethods.rs: -------------------------------------------------------------------------------- 1 | use crate::model::structs; 2 | use crate::vars; 3 | use chrono::prelude::*; 4 | use diesel::PgConnection; 5 | use diesel::{prelude::*, sql_query}; 6 | use dotenv::dotenv; 7 | use easy_password::bcrypt; 8 | use failure::{format_err, Error}; 9 | use jsonwebtoken::{encode, Algorithm, DecodingKey, EncodingKey, Header, TokenData, Validation}; 10 | use rusoto_core::{ByteStream, Region, RusotoError}; 11 | use rusoto_dynamodb::{AttributeValue, DynamoDb, DynamoDbClient, QueryInput}; 12 | use rusoto_s3::{PutObjectError, PutObjectOutput, S3Client, S3}; 13 | use std::collections::HashMap; 14 | 15 | //TODO; implement r2d2 for connection pool 16 | // Error handling 17 | //Create new database connection and return 18 | pub fn getdbconn() -> PgConnection { 19 | dotenv().ok(); 20 | let database_url = vars::db_url(); 21 | PgConnection::establish(&database_url).unwrap() 22 | } 23 | 24 | //Query database for username 25 | pub fn fetch_user(jusername: &str) -> std::vec::Vec { 26 | use crate::schema::user_login::dsl::*; 27 | 28 | //create new databse connection 29 | let connection = getdbconn(); 30 | 31 | user_login 32 | .filter(username.eq(jusername)) 33 | .load::(&connection) 34 | .unwrap() 35 | } 36 | 37 | //Create New JWT 38 | pub fn create_jwt(uid: &str) -> String { 39 | //Set token to expire after 60 days 40 | let expiration = Utc::now() 41 | .checked_add_signed(chrono::Duration::days(60)) 42 | .unwrap() 43 | .timestamp(); 44 | 45 | //Set claims for JWT 46 | let claims = structs::Claims { 47 | username: uid.to_owned(), 48 | exp: expiration as usize, 49 | }; 50 | 51 | //Set header(algorithm) for JWT 52 | let header = Header::new(Algorithm::HS512); 53 | 54 | //Encode Token by using header,claims and secret key(from env variables) and return token 55 | let token = encode( 56 | &header, 57 | &claims, 58 | &EncodingKey::from_secret(vars::jwt_sc().as_ref()), 59 | ) 60 | .expect("JWT creation failed"); 61 | token 62 | } 63 | 64 | pub fn decode_token(token: String) -> jsonwebtoken::errors::Result> { 65 | let sc_key = vars::jwt_sc(); 66 | jsonwebtoken::decode::( 67 | &token, 68 | &DecodingKey::from_secret(sc_key.as_ref()), 69 | &Validation::new(Algorithm::HS512), 70 | ) 71 | } 72 | 73 | pub fn fetch_holidays(year: &str) -> std::vec::Vec { 74 | //create new databse connection 75 | let connection: PgConnection = getdbconn(); 76 | 77 | //Query database 78 | match sql_query(format!( 79 | "SELECT * FROM config_holidays WHERE holiday_date LIKE '%{}%'", 80 | year 81 | )) 82 | .load(&connection) 83 | { 84 | Ok(result) => result, 85 | Err(err) => { 86 | error!("sql query failed"); 87 | panic!("sql query failed {:?}", err); 88 | } 89 | } 90 | } 91 | 92 | pub fn create_user(mut jsondata: structs::NewUser) -> Result { 93 | use crate::schema::user_login::dsl::user_login; 94 | let connection = getdbconn(); 95 | // let mut jsondata = jsondata; 96 | let encrypted_password = hash_pass(&jsondata.password); 97 | jsondata.password = encrypted_password; 98 | 99 | let results = diesel::insert_into(user_login) 100 | .values(&jsondata) 101 | .get_results::(&connection); 102 | Ok(results.is_ok()) 103 | } 104 | 105 | pub fn hash_pass(pass_string: &str) -> String { 106 | let pass_secretkey = vars::get_pass_sc(); 107 | 108 | // hash 109 | bcrypt::hash_password(pass_string, pass_secretkey.as_ref(), 12).unwrap() 110 | } 111 | 112 | pub fn varify_pass(login_pass: &str, hash_pass: &str) -> bool { 113 | let pass_secretkey = vars::get_pass_sc(); 114 | return bcrypt::verify_password(login_pass, hash_pass, pass_secretkey.as_ref()).unwrap(); 115 | } 116 | 117 | #[tokio::main] 118 | pub async fn send_to_s3( 119 | bst: ByteStream, 120 | filename: String, 121 | ) -> Result> { 122 | let put_request = rusoto_s3::PutObjectRequest { 123 | bucket: "elastic-search-bucket-test".to_owned(), 124 | key: filename.clone(), 125 | body: Some(bst), 126 | ..Default::default() 127 | }; 128 | 129 | let client = S3Client::new(Region::ApSoutheast1); 130 | 131 | return match client.put_object(put_request).await { 132 | Ok(response) => Ok(response), 133 | Err(err) => Err(err), 134 | }; 135 | } 136 | 137 | #[tokio::main] 138 | pub async fn list_data(user_id: String) -> Result, Error> { 139 | let client = DynamoDbClient::new(Region::UsEast2); 140 | let expression = "ID =:id".to_string(); 141 | let mut values = HashMap::new(); 142 | values.insert(":id".into(), string_to_attr(user_id.to_string())); 143 | let query = QueryInput { 144 | table_name: "rusttest".into(), 145 | key_condition_expression: Some(expression), 146 | expression_attribute_values: Some(values), 147 | ..Default::default() 148 | }; 149 | match client.query(query).await { 150 | Ok(data) => { 151 | let msg = data 152 | .items 153 | .ok_or_else(|| format_err!("No items found in dynamodb"))?; 154 | 155 | let mut messages = Vec::new(); 156 | 157 | let id1 = msg[0] 158 | .get("ID") 159 | .ok_or_else(|| format_err!("error")) 160 | .and_then(attr_to_string)?; 161 | let msg = msg[0] 162 | .get("message") 163 | .ok_or_else(|| format_err!("error")) 164 | .and_then(attr_to_string)?; 165 | let message = structs::Messagedynamo { 166 | id: id1, 167 | message: msg, 168 | }; 169 | messages.push(message); 170 | 171 | Ok(messages) 172 | } 173 | Err(err) => return Err(format_err!("{:?}", err)), 174 | } 175 | } 176 | 177 | pub fn string_to_attr(s: String) -> AttributeValue { 178 | AttributeValue { 179 | s: Some(s), 180 | ..Default::default() 181 | } 182 | } 183 | 184 | pub fn attr_to_string(attr: &AttributeValue) -> Result { 185 | if let Some(value) = &attr.s { 186 | Ok(value.to_string()) 187 | } else { 188 | Err(format_err!("no string value")) 189 | } 190 | } 191 | 192 | pub fn list_users() -> Vec { 193 | let conn = getdbconn(); 194 | 195 | // results 196 | sql_query("SELECT * FROM user_login".to_string()) 197 | .load::(&conn) 198 | .unwrap() 199 | } 200 | -------------------------------------------------------------------------------- /src/model/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod dbmethods; 2 | pub mod structs; 3 | -------------------------------------------------------------------------------- /src/model/structs.rs: -------------------------------------------------------------------------------- 1 | use crate::schema::{config_holidays, user_login}; 2 | use diesel::sql_types::*; 3 | use serde::{Deserialize, Serialize}; 4 | use std::default::Default; 5 | 6 | //Object structure for Querying and inserting users 7 | #[derive(Debug, Serialize, Deserialize, Queryable, Insertable)] 8 | #[table_name = "user_login"] 9 | pub struct Users { 10 | pub id: i64, 11 | pub firstname: String, 12 | pub lastname: String, 13 | pub username: String, 14 | pub email: String, 15 | pub mobile: String, 16 | pub facebookconnect: Option, 17 | pub googleconnect: Option, 18 | pub password: String, 19 | pub ip_address: String, 20 | pub isactive: Option, 21 | pub sort_order: Option, 22 | pub created_at: Option, 23 | pub created_by: Option, 24 | pub updated_at: Option, 25 | pub updated_by: Option, 26 | } 27 | 28 | #[derive(Debug, Serialize, Deserialize, QueryableByName)] 29 | pub struct NUsers { 30 | #[sql_type = "BigInt"] 31 | pub id: i64, 32 | #[sql_type = "Text"] 33 | pub firstname: String, 34 | #[sql_type = "Text"] 35 | pub lastname: String, 36 | #[sql_type = "Text"] 37 | pub username: String, 38 | #[sql_type = "Text"] 39 | pub email: String, 40 | #[sql_type = "Text"] 41 | pub mobile: String, 42 | #[sql_type = "Nullable"] 43 | pub facebookconnect: Option, 44 | #[sql_type = "Nullable"] 45 | pub googleconnect: Option, 46 | #[sql_type = "Text"] 47 | #[serde(skip_serializing)] 48 | pub password: String, 49 | #[sql_type = "Text"] 50 | pub ip_address: String, 51 | #[sql_type = "Nullable"] 52 | pub isactive: Option, 53 | #[sql_type = "Nullable"] 54 | pub sort_order: Option, 55 | #[sql_type = "Nullable"] 56 | pub created_at: Option, 57 | #[sql_type = "Nullable"] 58 | pub created_by: Option, 59 | #[sql_type = "Nullable"] 60 | pub updated_at: Option, 61 | #[sql_type = "Nullable"] 62 | pub updated_by: Option, 63 | } 64 | 65 | #[derive(Debug, Serialize, Deserialize, Insertable, Default)] 66 | #[table_name = "user_login"] 67 | pub struct NewUser { 68 | pub firstname: String, 69 | pub lastname: String, 70 | pub username: String, 71 | pub email: String, 72 | pub mobile: String, 73 | pub facebookconnect: Option, 74 | pub googleconnect: Option, 75 | pub password: String, 76 | pub ip_address: String, 77 | pub isactive: Option, 78 | pub sort_order: Option, 79 | pub created_at: Option, 80 | pub created_by: Option, 81 | pub updated_at: Option, 82 | pub updated_by: Option, 83 | } 84 | 85 | #[derive(Debug, Serialize, Deserialize)] 86 | pub struct Logindata { 87 | pub username: String, 88 | pub password: String, 89 | } 90 | 91 | #[derive(Debug, Serialize, Deserialize, Queryable, Insertable, QueryableByName, Clone)] 92 | #[table_name = "config_holidays"] 93 | pub struct Holidays { 94 | pub id: i64, 95 | pub holiday_date: String, 96 | pub holiday_desc: String, 97 | pub createdat: Option, 98 | pub updatedat: Option, 99 | } 100 | 101 | //Structure for Login Response 102 | #[derive(Debug, Serialize, Deserialize)] 103 | pub struct Response { 104 | pub success: bool, 105 | pub message: Msg, 106 | pub data: Tokendata, 107 | } 108 | 109 | #[derive(Debug, Serialize, Deserialize)] 110 | pub struct Msg { 111 | pub code: String, 112 | pub description: String, 113 | } 114 | 115 | #[derive(Debug, Serialize, Deserialize)] 116 | pub struct Tokendata { 117 | pub name: String, 118 | pub token: String, 119 | } 120 | 121 | //Methods for Response 122 | impl Response { 123 | pub fn new() -> Response { 124 | Response { 125 | success: false, 126 | message: Msg { 127 | code: "".to_string(), 128 | description: "".to_string(), 129 | }, 130 | data: Tokendata { 131 | name: "".to_string(), 132 | token: "".to_string(), 133 | }, 134 | } 135 | } 136 | } 137 | 138 | //Object structure for JWT claims 139 | #[derive(Debug, Deserialize, Serialize)] 140 | pub struct Claims { 141 | pub username: String, 142 | pub exp: usize, 143 | } 144 | 145 | #[derive(Debug, Deserialize, Serialize)] 146 | pub struct Year { 147 | pub year: String, 148 | } 149 | 150 | #[derive(Debug, Serialize, Deserialize)] 151 | pub struct Lambdastruct { 152 | pub success: bool, 153 | pub result: Vec>, 154 | } 155 | 156 | #[derive(Default, Debug, Serialize, Deserialize)] 157 | pub struct Dynamouserid { 158 | pub id: String, 159 | } 160 | 161 | #[derive(Default, Debug, PartialEq, Serialize, Deserialize)] 162 | pub struct Messagedynamo { 163 | #[serde(rename = "ID")] 164 | pub id: String, 165 | pub message: String, 166 | } 167 | 168 | #[derive(Debug, Serialize, Deserialize, QueryableByName)] 169 | #[table_name = "config_holidays"] 170 | 171 | pub struct Medium { 172 | #[sql_type = "BigInt"] 173 | pub id: i64, 174 | #[sql_type = "Text"] 175 | pub firstname: String, 176 | #[sql_type = "Text"] 177 | pub lastname: String, 178 | #[sql_type = "Nullable"] 179 | pub email: Option, 180 | #[sql_type = "Text"] 181 | pub mobile: String, 182 | #[sql_type = "Text"] 183 | #[serde(skip_serializing)] 184 | pub password: String, 185 | #[sql_type = "Nullable"] 186 | pub isactive: Option, 187 | #[sql_type = "Nullable"] 188 | pub sort_order: Option, 189 | #[sql_type = "Nullable"] 190 | pub created_at: Option, 191 | #[sql_type = "Nullable"] 192 | pub created_by: Option, 193 | } 194 | -------------------------------------------------------------------------------- /src/routes/awsr.rs: -------------------------------------------------------------------------------- 1 | use actix_web::web::{self, ServiceConfig}; 2 | 3 | use crate::controller::awsc; 4 | 5 | pub fn configure(app: &mut ServiceConfig) { 6 | app.route( 7 | "/lambda_example", 8 | web::get().to(awsc::lambda_example_synchronus), 9 | ) 10 | .route("/upload_file", web::post().to(awsc::upload_file)) 11 | .route("/dynamodb_example", web::get().to(awsc::dynamodb_example)); 12 | } 13 | -------------------------------------------------------------------------------- /src/routes/dieselr.rs: -------------------------------------------------------------------------------- 1 | use actix_web::web::{self, ServiceConfig}; 2 | 3 | use crate::controller::home; 4 | 5 | pub fn configure(app: &mut ServiceConfig) { 6 | app.route("/login", web::post().to(home::login)) 7 | .route("/view_holidays", web::get().to(home::fetch_holidays)) 8 | .route("/register_user", web::post().to(home::register_user)) 9 | .route("/list_users", web::get().to(home::list_users)); 10 | } 11 | -------------------------------------------------------------------------------- /src/routes/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod awsr; 2 | pub mod dieselr; 3 | -------------------------------------------------------------------------------- /src/schema.rs: -------------------------------------------------------------------------------- 1 | table! { 2 | config_holidays (id) { 3 | id -> Int8, 4 | holiday_date -> Varchar, 5 | holiday_desc -> Varchar, 6 | createdat -> Nullable, 7 | updatedat -> Nullable, 8 | } 9 | } 10 | 11 | table! { 12 | user_login (id) { 13 | id -> Int8, 14 | firstname -> Varchar, 15 | lastname -> Varchar, 16 | username -> Varchar, 17 | email -> Varchar, 18 | mobile -> Varchar, 19 | facebookconnect -> Nullable, 20 | googleconnect -> Nullable, 21 | password -> Varchar, 22 | ip_address -> Varchar, 23 | isactive -> Nullable, 24 | sort_order -> Nullable, 25 | created_at -> Nullable, 26 | created_by -> Nullable, 27 | updated_at -> Nullable, 28 | updated_by -> Nullable, 29 | } 30 | } 31 | 32 | allow_tables_to_appear_in_same_query!(config_holidays, user_login,); 33 | -------------------------------------------------------------------------------- /src/test/integration.rs: -------------------------------------------------------------------------------- 1 | use crate::controller::home; 2 | use crate::model::structs; 3 | use actix_web::{test, web, App}; 4 | 5 | #[actix_rt::test] 6 | async fn test_index_post() { 7 | let data = structs::Logindata { 8 | username: "kaustubhbabar".to_string(), 9 | password: "1313n218u41".to_string(), 10 | }; 11 | 12 | let mut app = test::init_service(App::new().route("/login", web::post().to(home::login))).await; 13 | 14 | let req = test::TestRequest::post() 15 | .uri("/login") 16 | .set_json(&data) 17 | .to_request(); 18 | let resp = test::call_service(&mut app, req).await; 19 | assert_eq!(resp.status(), http::StatusCode::OK); 20 | } 21 | 22 | #[actix_rt::test] 23 | async fn test_register_user() { 24 | let data = structs::NewUser { 25 | firstname: "Kaustubh".to_string(), 26 | lastname: "Babar".to_string(), 27 | username: "kaustubhbabar".to_string(), 28 | email: "kaustubhbabar@gmail.com".to_string(), 29 | mobile: "123456789".to_string(), 30 | password: "1313n218u41".to_string(), 31 | ip_address: "124.245.55.124".to_string(), 32 | ..Default::default() 33 | }; 34 | 35 | let mut app = 36 | test::init_service(App::new().route("/register_user", web::post().to(home::register_user))) 37 | .await; 38 | 39 | let req = test::TestRequest::post() 40 | .uri("/register_user") 41 | .set_json(&data) 42 | .to_request(); 43 | let resp = test::call_service(&mut app, req).await; 44 | assert_eq!(resp.status(), http::StatusCode::OK); 45 | } 46 | -------------------------------------------------------------------------------- /src/test/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod integration; 2 | pub mod unit; 3 | -------------------------------------------------------------------------------- /src/test/unit.rs: -------------------------------------------------------------------------------- 1 | use rusoto_dynamodb::AttributeValue; 2 | 3 | use crate::model::dbmethods::{attr_to_string, string_to_attr}; 4 | 5 | #[test] 6 | fn attr_test() { 7 | let sample_string = String::from("hello"); 8 | 9 | let actual_attribute = string_to_attr(sample_string); 10 | 11 | let expected_attribute = AttributeValue { 12 | s: Some(String::from("hello")), 13 | ..Default::default() 14 | }; 15 | 16 | assert_eq!(actual_attribute, expected_attribute) 17 | } 18 | 19 | #[test] 20 | #[should_panic] 21 | fn attr_test2() { 22 | let sample_attribute = AttributeValue { 23 | ..Default::default() 24 | }; 25 | let actual_string = attr_to_string(&sample_attribute).unwrap(); 26 | let expected_string = String::from("hello"); 27 | 28 | assert_eq!(actual_string, expected_string) 29 | } 30 | 31 | #[test] 32 | fn attr_test3() { 33 | let sample_string = String::from("world"); 34 | 35 | let actual_attribute = string_to_attr(sample_string); 36 | 37 | let expected_attribute = AttributeValue { 38 | s: Some(String::from("hello")), 39 | ..Default::default() 40 | }; 41 | 42 | assert_ne!(actual_attribute, expected_attribute) 43 | } 44 | -------------------------------------------------------------------------------- /src/vars.rs: -------------------------------------------------------------------------------- 1 | use dotenv::dotenv; 2 | use std::env::var; 3 | 4 | //Return database URL 5 | pub fn db_url() -> String { 6 | dotenv().ok(); 7 | var("DATABASE_URL").unwrap() 8 | } 9 | //Return domain 10 | pub fn domain() -> String { 11 | dotenv().ok(); 12 | var("DOMAIN").unwrap_or_else(|_| "0.0.0.0".to_string()) 13 | } 14 | //return PORT 15 | pub fn port() -> u16 { 16 | dotenv().ok(); 17 | var("PORT") 18 | .unwrap_or_else(|_| "9000".to_string()) 19 | .parse::() 20 | .unwrap() 21 | } 22 | 23 | //Return Secret key for JWT 24 | pub fn jwt_sc() -> String { 25 | dotenv().ok(); 26 | var("JWT_SECRET").unwrap() 27 | } 28 | 29 | pub fn get_pass_sc() -> String { 30 | dotenv().ok(); 31 | var("PASS_SECRET").unwrap() 32 | } 33 | --------------------------------------------------------------------------------