From 7525903fe690b9b3c9a67ff252d02f70ff5ec65b Mon Sep 17 00:00:00 2001 From: Harry Gill Date: Thu, 18 Jul 2019 12:55:14 +0100 Subject: [PATCH] Update to new actix 1.0 api, drop actix-rt (#158) --- simple-auth-server/Cargo.toml | 33 +++---- simple-auth-server/README.md | 20 ++-- .../migrations/2018-10-09-101948_users/up.sql | 2 +- simple-auth-server/src/auth_handler.rs | 90 +++++++++++------- simple-auth-server/src/auth_routes.rs | 34 ------- simple-auth-server/src/email_service.rs | 19 ++-- simple-auth-server/src/errors.rs | 25 +++-- simple-auth-server/src/invitation_handler.rs | 68 ++++++++------ simple-auth-server/src/invitation_routes.rs | 22 ----- simple-auth-server/src/main.rs | 72 +++++--------- simple-auth-server/src/models.rs | 50 +++++----- simple-auth-server/src/register_handler.rs | 93 ++++++++++--------- simple-auth-server/src/register_routes.rs | 25 ----- simple-auth-server/src/schema.rs | 2 +- simple-auth-server/src/utils.rs | 83 +++++------------ 15 files changed, 264 insertions(+), 374 deletions(-) delete mode 100644 simple-auth-server/src/auth_routes.rs delete mode 100644 simple-auth-server/src/invitation_routes.rs delete mode 100644 simple-auth-server/src/register_routes.rs diff --git a/simple-auth-server/Cargo.toml b/simple-auth-server/Cargo.toml index 2edffbb3..9a96ced1 100644 --- a/simple-auth-server/Cargo.toml +++ b/simple-auth-server/Cargo.toml @@ -6,23 +6,20 @@ edition = "2018" workspace = ".." [dependencies] -actix = "0.8.2" -actix-rt = "0.2.2" -actix-web = "1.0.2" -actix-files = "0.1.1" -actix-identity= "0.1.0" - -bcrypt = "0.2.1" +actix-identity = "0.1.0" +actix-web = "1.0.3" +argonautica = "0.2.0" chrono = { version = "0.4.6", features = ["serde"] } -diesel = { version = "1.3.3", features = ["postgres", "uuid", "r2d2", "chrono"] } -dotenv = "0.13.0" -derive_more = "0.14" -env_logger = "0.6.0" -jsonwebtoken = "6.0.0" -futures = "0.1.25" -r2d2 = "0.8.3" -serde_derive="1.0.80" -serde_json="1.0" -serde="1.0" +derive_more = "0.15.0" +diesel = { version = "1.4.2", features = ["postgres","uuidv07", "r2d2", "chrono"] } +dotenv = "0.14.1" +env_logger = "0.6" +futures = "0.1" +r2d2 = "0.8" +lazy_static = "1.3.0" +serde = "1.0" +serde_derive = "1.0" +serde_json = "1.0" sparkpost = "0.5.2" -uuid = { version = "0.6.5", features = ["serde", "v4"] } +uuid = { version = "0.7", features = ["serde", "v4"] } + diff --git a/simple-auth-server/README.md b/simple-auth-server/README.md index 4c6048e1..dc25484a 100644 --- a/simple-auth-server/README.md +++ b/simple-auth-server/README.md @@ -1,21 +1,22 @@ +## Auth Web Microservice with rust using Actix-Web 1.0 + ##### Flow of the event would look like this: - Registers with email address ➡ Receive an 📨 with a link to verify - Follow the link ➡ register with same email and a password -- Login with email and password ➡ Get verified and receive jwt token +- Login with email and password ➡ Get verified and receive auth cookie -##### Crates we are going to use +##### Crates Used -- [actix](https://crates.io/crates/actix) // Actix is a Rust actors framework. - [actix-web](https://crates.io/crates/actix-web) // Actix web is a simple, pragmatic and extremely fast web framework for Rust. -- [bcrypt](https://crates.io/crates/bcrypt) // Easily hash and verify passwords using bcrypt. +- [argonautica](https://docs.rs/argonautica) // crate for hashing passwords using the cryptographically-secure Argon2 hashing algorithm. - [chrono](https://crates.io/crates/chrono) // Date and time library for Rust. - [diesel](https://crates.io/crates/diesel) // A safe, extensible ORM and Query Builder for PostgreSQL, SQLite, and MySQL. - [dotenv](https://crates.io/crates/dotenv) // A dotenv implementation for Rust. +- [derive_more](https://crates.io/crates/derive_more) // Convenience macros to derive tarits easily - [env_logger](https://crates.io/crates/env_logger) // A logging implementation for log which is configured via an environment variable. -- [failure](https://crates.io/crates/failure) // Experimental error handling abstraction. -- [jsonwebtoken](https://crates.io/crates/jsonwebtoken) // Create and parse JWT in a strongly typed way. - [futures](https://crates.io/crates/futures) // An implementation of futures and streams featuring zero allocations, composability, and iterator-like interfaces. +- [lazy_static](https://docs.rs/lazy_static) // A macro for declaring lazily evaluated statics. - [r2d2](https://crates.io/crates/r2d2) // A generic connection pool. - [serde](https://crates.io/crates/serde) // A generic serialization/deserialization framework. - [serde_json](https://crates.io/crates/serde_json) // A JSON serialization file format. @@ -24,9 +25,6 @@ - [uuid](https://crates.io/crates/uuid) // A library to generate and parse UUIDs. -Read the full tutorial series on [hgill.io](https://hgill.io) - -- [Auth Web Microservice with rust using Actix-Web - Complete Tutorial Part 1](https://hgill.io/posts/auth-microservice-rust-actix-web-diesel-complete-tutorial-part-1/) -- [Auth Web Microservice with rust using Actix-Web - Complete Tutorial Part 2](https://hgill.io/posts/auth-microservice-rust-actix-web-diesel-complete-tutorial-part-2/) -- [Auth Web Microservice with rust using Actix-Web - Complete Tutorial Part 3](https://hgill.io/posts/auth-microservice-rust-actix-web-diesel-complete-tutorial-part-3/) +Read the full tutorial series on [gill.net.in](https://gill.net.in) +- [Auth Web Microservice with rust using Actix-Web 1.0 - Complete Tutorial](https://gill.net.in/posts/auth-microservice-rust-actix-web1.0-diesel-complete-tutorial/) diff --git a/simple-auth-server/migrations/2018-10-09-101948_users/up.sql b/simple-auth-server/migrations/2018-10-09-101948_users/up.sql index 890b6676..daf25276 100644 --- a/simple-auth-server/migrations/2018-10-09-101948_users/up.sql +++ b/simple-auth-server/migrations/2018-10-09-101948_users/up.sql @@ -1,6 +1,6 @@ -- Your SQL goes here CREATE TABLE users ( email VARCHAR(100) NOT NULL UNIQUE PRIMARY KEY, - password VARCHAR(64) NOT NULL, --bcrypt hash + hash VARCHAR(122) NOT NULL, --argon hash created_at TIMESTAMP NOT NULL ); diff --git a/simple-auth-server/src/auth_handler.rs b/simple-auth-server/src/auth_handler.rs index 22112b7b..7f0f47d8 100644 --- a/simple-auth-server/src/auth_handler.rs +++ b/simple-auth-server/src/auth_handler.rs @@ -1,12 +1,14 @@ -use actix::{Handler, Message}; use actix_identity::Identity; -use actix_web::{dev::Payload, Error, FromRequest, HttpRequest}; -use bcrypt::verify; +use actix_web::{ + dev::Payload, error::BlockingError, web, Error, FromRequest, HttpRequest, HttpResponse, +}; use diesel::prelude::*; +use diesel::PgConnection; +use futures::Future; use crate::errors::ServiceError; -use crate::models::{DbExecutor, SlimUser, User}; -use crate::utils::decode_token; +use crate::models::{Pool, SlimUser, User}; +use crate::utils::verify; #[derive(Debug, Deserialize)] pub struct AuthData { @@ -14,34 +16,6 @@ pub struct AuthData { pub password: String, } -impl Message for AuthData { - type Result = Result; -} - -impl Handler for DbExecutor { - type Result = Result; - fn handle(&mut self, msg: AuthData, _: &mut Self::Context) -> Self::Result { - use crate::schema::users::dsl::{email, users}; - let conn: &PgConnection = &self.0.get().unwrap(); - - let mut items = users.filter(email.eq(&msg.email)).load::(conn)?; - - if let Some(user) = items.pop() { - match verify(&msg.password, &user.password) { - Ok(matching) => { - if matching { - return Ok(user.into()); - } - } - Err(_) => (), - } - } - Err(ServiceError::BadRequest( - "Username and Password don't match".into(), - )) - } -} - // we need the same data // simple aliasing makes the intentions clear and its more readable pub type LoggedUser = SlimUser; @@ -53,9 +27,55 @@ impl FromRequest for LoggedUser { fn from_request(req: &HttpRequest, pl: &mut Payload) -> Self::Future { if let Some(identity) = Identity::from_request(req, pl)?.identity() { - let user: SlimUser = decode_token(&identity)?; - return Ok(user as LoggedUser); + let user: LoggedUser = serde_json::from_str(&identity)?; + return Ok(user); } Err(ServiceError::Unauthorized.into()) } } + +pub fn logout(id: Identity) -> HttpResponse { + id.forget(); + HttpResponse::Ok().finish() +} + +pub fn login( + auth_data: web::Json, + id: Identity, + pool: web::Data, +) -> impl Future { + web::block(move || query(auth_data.into_inner(), pool)).then( + move |res: Result>| match res { + Ok(user) => { + let user_string = serde_json::to_string(&user).unwrap(); + id.remember(user_string); + Ok(HttpResponse::Ok().finish()) + } + Err(err) => match err { + BlockingError::Error(service_error) => Err(service_error), + BlockingError::Canceled => Err(ServiceError::InternalServerError), + }, + }, + ) +} + +pub fn get_me(logged_user: LoggedUser) -> HttpResponse { + HttpResponse::Ok().json(logged_user) +} +/// Diesel query +fn query(auth_data: AuthData, pool: web::Data) -> Result { + use crate::schema::users::dsl::{email, users}; + let conn: &PgConnection = &pool.get().unwrap(); + let mut items = users + .filter(email.eq(&auth_data.email)) + .load::(conn)?; + + if let Some(user) = items.pop() { + if let Ok(matching) = verify(&user.hash, &auth_data.password) { + if matching { + return Ok(user.into()); + } + } + } + Err(ServiceError::Unauthorized) +} diff --git a/simple-auth-server/src/auth_routes.rs b/simple-auth-server/src/auth_routes.rs deleted file mode 100644 index caed66ca..00000000 --- a/simple-auth-server/src/auth_routes.rs +++ /dev/null @@ -1,34 +0,0 @@ -use actix::Addr; -use actix_identity::Identity; -use actix_web::{web, Error, HttpRequest, HttpResponse, Responder, ResponseError}; -use futures::Future; - -use crate::auth_handler::{AuthData, LoggedUser}; -use crate::models::DbExecutor; -use crate::utils::create_token; - -pub fn login( - auth_data: web::Json, - id: Identity, - db: web::Data>, -) -> impl Future { - db.send(auth_data.into_inner()) - .from_err() - .and_then(move |res| match res { - Ok(user) => { - let token = create_token(&user)?; - id.remember(token); - Ok(HttpResponse::Ok().into()) - } - Err(err) => Ok(err.error_response()), - }) -} - -pub fn logout(id: Identity) -> impl Responder { - id.forget(); - HttpResponse::Ok() -} - -pub fn get_me(logged_user: LoggedUser) -> HttpResponse { - HttpResponse::Ok().json(logged_user) -} diff --git a/simple-auth-server/src/email_service.rs b/simple-auth-server/src/email_service.rs index 290d37ab..1583e042 100644 --- a/simple-auth-server/src/email_service.rs +++ b/simple-auth-server/src/email_service.rs @@ -1,16 +1,18 @@ +// email_service.rs +use crate::errors::ServiceError; use crate::models::Invitation; use sparkpost::transmission::{ EmailAddress, Message, Options, Recipient, Transmission, TransmissionResponse, }; -fn get_api_key() -> String { - std::env::var("SPARKPOST_API_KEY").expect("SPARKPOST_API_KEY must be set") +lazy_static::lazy_static! { +static ref API_KEY: String = std::env::var("SPARKPOST_API_KEY").expect("SPARKPOST_API_KEY must be set"); } -pub fn send_invitation(invitation: &Invitation) { - let tm = Transmission::new_eu(get_api_key()); - let sending_email = std::env::var("SENDING_EMAIL_ADDRESS") - .expect("SENDING_EMAIL_ADDRESS must be set"); +pub fn send_invitation(invitation: &Invitation) -> Result<(), ServiceError> { + let tm = Transmission::new_eu(API_KEY.as_str()); + let sending_email = + std::env::var("SENDING_EMAIL_ADDRESS").expect("SENDING_EMAIL_ADDRESS must be set"); // new email message with sender name and email let mut email = Message::new(EmailAddress::new(sending_email, "Let's Organise")); @@ -53,13 +55,16 @@ pub fn send_invitation(invitation: &Invitation) { Ok(res) => match res { TransmissionResponse::ApiResponse(api_res) => { println!("API Response: \n {:#?}", api_res); + Ok(()) } TransmissionResponse::ApiError(errors) => { println!("Response Errors: \n {:#?}", &errors); + Err(ServiceError::InternalServerError) } }, Err(error) => { - println!("error \n {:#?}", error); + println!("Send Email Error: \n {:#?}", error); + Err(ServiceError::InternalServerError) } } } diff --git a/simple-auth-server/src/errors.rs b/simple-auth-server/src/errors.rs index bac56094..c6ade0ff 100644 --- a/simple-auth-server/src/errors.rs +++ b/simple-auth-server/src/errors.rs @@ -1,8 +1,8 @@ use actix_web::{error::ResponseError, HttpResponse}; use derive_more::Display; -use diesel::result::{DatabaseErrorKind, Error}; +use diesel::result::{DatabaseErrorKind, Error as DBError}; use std::convert::From; -use uuid::ParseError; +use uuid::parser::ParseError; #[derive(Debug, Display)] pub enum ServiceError { @@ -19,15 +19,13 @@ pub enum ServiceError { // impl ResponseError trait allows to convert our errors into http responses with appropriate data impl ResponseError for ServiceError { fn error_response(&self) -> HttpResponse { - match *self { + match self { ServiceError::InternalServerError => HttpResponse::InternalServerError() .json("Internal Server Error, Please try later"), - ServiceError::BadRequest(ref message) => { - HttpResponse::BadRequest().json(message) - } - ServiceError::Unauthorized => { - HttpResponse::Unauthorized().json("Unauthorized") - } + ServiceError::BadRequest(ref message) => HttpResponse::BadRequest() + .json(message), + ServiceError::Unauthorized => HttpResponse::Unauthorized() + .json("Unauthorized"), } } } @@ -40,15 +38,14 @@ impl From for ServiceError { } } -impl From for ServiceError { - fn from(error: Error) -> ServiceError { +impl From for ServiceError { + fn from(error: DBError) -> ServiceError { // Right now we just care about UniqueViolation from diesel // But this would be helpful to easily map errors as our app grows match error { - Error::DatabaseError(kind, info) => { + DBError::DatabaseError(kind, info) => { if let DatabaseErrorKind::UniqueViolation = kind { - let message = - info.details().unwrap_or_else(|| info.message()).to_string(); + let message = info.details().unwrap_or_else(|| info.message()).to_string(); return ServiceError::BadRequest(message); } ServiceError::InternalServerError diff --git a/simple-auth-server/src/invitation_handler.rs b/simple-auth-server/src/invitation_handler.rs index b0a8ceb7..26246471 100644 --- a/simple-auth-server/src/invitation_handler.rs +++ b/simple-auth-server/src/invitation_handler.rs @@ -1,38 +1,50 @@ -use actix::{Handler, Message}; -use chrono::{Duration, Local}; -use diesel::{self, prelude::*}; -use uuid::Uuid; +use actix_web::{error::BlockingError, web, HttpResponse}; +use diesel::{prelude::*, PgConnection}; +use futures::Future; +use crate::email_service::send_invitation; use crate::errors::ServiceError; -use crate::models::{DbExecutor, Invitation}; +use crate::models::{Invitation, Pool}; #[derive(Deserialize)] -pub struct CreateInvitation { +pub struct InvitationData { pub email: String, } -impl Message for CreateInvitation { - type Result = Result; +pub fn post_invitation( + invitation_data: web::Json, + pool: web::Data, +) -> impl Future { + // run diesel blocking code + web::block(move || create_invitation(invitation_data.into_inner().email, pool)).then(|res| { + match res { + Ok(_) => Ok(HttpResponse::Ok().finish()), + Err(err) => match err { + BlockingError::Error(service_error) => Err(service_error), + BlockingError::Canceled => Err(ServiceError::InternalServerError), + }, + } + }) } -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: CreateInvitation, _: &mut Self::Context) -> Self::Result { - use crate::schema::invitations::dsl::*; - let conn: &PgConnection = &self.0.get().unwrap(); - - // creating a new Invitation object with expired at time that is 24 hours from now - let new_invitation = Invitation { - id: Uuid::new_v4(), - email: msg.email.clone(), - expires_at: Local::now().naive_local() + Duration::hours(24), - }; - - let inserted_invitation = diesel::insert_into(invitations) - .values(&new_invitation) - .get_result(conn)?; - - Ok(inserted_invitation) - } +fn create_invitation( + eml: String, + pool: web::Data, +) -> Result<(), crate::errors::ServiceError> { + let invitation = dbg!(query(eml, pool)?); + send_invitation(&invitation) +} + +/// Diesel query +fn query(eml: String, pool: web::Data) -> Result { + use crate::schema::invitations::dsl::invitations; + + let new_invitation : Invitation = eml.into(); + let conn: &PgConnection = &pool.get().unwrap(); + + let inserted_invitation = diesel::insert_into(invitations) + .values(&new_invitation) + .get_result(conn)?; + + Ok(inserted_invitation) } diff --git a/simple-auth-server/src/invitation_routes.rs b/simple-auth-server/src/invitation_routes.rs deleted file mode 100644 index ea76bca2..00000000 --- a/simple-auth-server/src/invitation_routes.rs +++ /dev/null @@ -1,22 +0,0 @@ -use actix::Addr; -use actix_web::{web, Error, HttpResponse, ResponseError}; -use futures::future::Future; - -use crate::email_service::send_invitation; -use crate::invitation_handler::CreateInvitation; -use crate::models::DbExecutor; - -pub fn register_email( - signup_invitation: web::Json, - db: web::Data>, -) -> impl Future { - db.send(signup_invitation.into_inner()) - .from_err() - .and_then(|db_response| match db_response { - Ok(invitation) => { - send_invitation(&invitation); - Ok(HttpResponse::Ok().into()) - } - Err(err) => Ok(err.error_response()), - }) -} diff --git a/simple-auth-server/src/main.rs b/simple-auth-server/src/main.rs index 45d1ab2d..aaccf080 100644 --- a/simple-auth-server/src/main.rs +++ b/simple-auth-server/src/main.rs @@ -1,98 +1,72 @@ -#![allow(unused_imports)] - #[macro_use] extern crate diesel; #[macro_use] extern crate serde_derive; -use actix::prelude::*; -use actix_files as fs; use actix_identity::{CookieIdentityPolicy, IdentityService}; -use actix_web::middleware::Logger; -use actix_web::{web, App, HttpServer}; -use chrono::Duration; -use diesel::{r2d2::ConnectionManager, PgConnection}; -use dotenv::dotenv; +use actix_web::{middleware, web, App, HttpServer}; +use diesel::prelude::*; +use diesel::r2d2::{self, ConnectionManager}; mod auth_handler; -mod auth_routes; mod email_service; mod errors; mod invitation_handler; -mod invitation_routes; mod models; mod register_handler; -mod register_routes; mod schema; mod utils; -use crate::models::DbExecutor; - fn main() -> std::io::Result<()> { - dotenv().ok(); + dotenv::dotenv().ok(); std::env::set_var( "RUST_LOG", "simple-auth-server=debug,actix_web=info,actix_server=info", ); env_logger::init(); - let sys = actix_rt::System::new("example"); - let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); // create db connection pool let manager = ConnectionManager::::new(database_url); - let pool = r2d2::Pool::builder() + let pool: models::Pool = r2d2::Pool::builder() .build(manager) .expect("Failed to create pool."); + let domain: String = std::env::var("DOMAIN").unwrap_or_else(|_| "localhost".to_string()); - let address: Addr = - SyncArbiter::start(4, move || DbExecutor(pool.clone())); - + // Start http server HttpServer::new(move || { - // secret is a random minimum 32 bytes long base 64 string - let secret: String = - std::env::var("SECRET_KEY").unwrap_or_else(|_| "0123".repeat(8)); - let domain: String = - std::env::var("DOMAIN").unwrap_or_else(|_| "localhost".to_string()); - App::new() - .data(address.clone()) - .wrap(Logger::default()) + .data(pool.clone()) + // enable logger + .wrap(middleware::Logger::default()) .wrap(IdentityService::new( - CookieIdentityPolicy::new(secret.as_bytes()) + CookieIdentityPolicy::new(utils::SECRET_KEY.as_bytes()) .name("auth") .path("/") .domain(domain.as_str()) - .max_age_time(Duration::days(1)) + .max_age_time(chrono::Duration::days(1)) .secure(false), // this can only be true if you have https )) + .data(web::JsonConfig::default().limit(4096)) // everything under '/api/' route .service( web::scope("/api") - // routes for authentication .service( - web::resource("/auth") - .route(web::post().to_async(auth_routes::login)) - .route(web::delete().to(auth_routes::logout)) - .route(web::get().to_async(auth_routes::get_me)), + web::resource("/invitation") + .route(web::post().to_async(invitation_handler::post_invitation)), ) - // routes to invitation - .service( - web::resource("/invitation").route( - web::post().to_async(invitation_routes::register_email), - ), - ) - // routes to register as a user after the .service( web::resource("/register/{invitation_id}") - .route(web::post().to_async(register_routes::register_user)), + .route(web::post().to_async(register_handler::register_user)), + ) + .service( + web::resource("/auth") + .route(web::post().to_async(auth_handler::login)) + .route(web::delete().to(auth_handler::logout)) + .route(web::get().to(auth_handler::get_me)), ), ) - // serve static files - .service(fs::Files::new("/", "./static/").index_file("index.html")) }) .bind("127.0.0.1:3000")? - .start(); - - sys.run() + .run() } diff --git a/simple-auth-server/src/models.rs b/simple-auth-server/src/models.rs index 76dcfac7..dc07509b 100644 --- a/simple-auth-server/src/models.rs +++ b/simple-auth-server/src/models.rs @@ -1,37 +1,23 @@ -use actix::{Actor, SyncContext}; -use chrono::{Local, NaiveDateTime}; -use diesel::pg::PgConnection; -use diesel::r2d2::{ConnectionManager, Pool}; -use std::convert::From; -use uuid::Uuid; +use super::schema::*; +use diesel::{r2d2::ConnectionManager, PgConnection}; -use crate::schema::{invitations, users}; - -/// This is db executor actor. can be run in parallel -pub struct DbExecutor(pub Pool>); - -// Actors communicate exclusively by exchanging messages. -// The sending actor can optionally wait for the response. -// Actors are not referenced directly, but by means of addresses. -// Any rust type can be an actor, it only needs to implement the Actor trait. -impl Actor for DbExecutor { - type Context = SyncContext; -} +// type alias to use in multiple places +pub type Pool = r2d2::Pool>; #[derive(Debug, Serialize, Deserialize, Queryable, Insertable)] #[table_name = "users"] pub struct User { pub email: String, - pub password: String, - pub created_at: NaiveDateTime, + pub hash: String, + pub created_at: chrono::NaiveDateTime, } impl User { - pub fn with_details(email: String, password: String) -> Self { + pub fn from_details, T: Into>(email: S, pwd: T) -> Self { User { - email, - password, - created_at: Local::now().naive_local(), + email: email.into(), + hash: pwd.into(), + created_at: chrono::Local::now().naive_local(), } } } @@ -39,9 +25,21 @@ impl User { #[derive(Debug, Serialize, Deserialize, Queryable, Insertable)] #[table_name = "invitations"] pub struct Invitation { - pub id: Uuid, + pub id: uuid::Uuid, pub email: String, - pub expires_at: NaiveDateTime, + pub expires_at: chrono::NaiveDateTime, +} + +// any type that implements Into can be used to create Invitation +impl From for Invitation where + T: Into { + fn from(email: T) -> Self { + Invitation { + id: uuid::Uuid::new_v4(), + email: email.into(), + expires_at: chrono::Local::now().naive_local() + chrono::Duration::hours(24), + } + } } #[derive(Debug, Serialize, Deserialize)] diff --git a/simple-auth-server/src/register_handler.rs b/simple-auth-server/src/register_handler.rs index 04fe164e..2b43a006 100644 --- a/simple-auth-server/src/register_handler.rs +++ b/simple-auth-server/src/register_handler.rs @@ -1,58 +1,65 @@ -use actix::{Handler, Message}; -use chrono::Local; +use actix_web::{error::BlockingError, web, HttpResponse}; use diesel::prelude::*; -use uuid::Uuid; +use futures::Future; use crate::errors::ServiceError; -use crate::models::{DbExecutor, Invitation, SlimUser, User}; +use crate::models::{Invitation, Pool, SlimUser, User}; use crate::utils::hash_password; - // UserData is used to extract data from a post request by the client #[derive(Debug, Deserialize)] pub struct UserData { pub password: String, } -// to be used to send data via the Actix actor system -#[derive(Debug)] -pub struct RegisterUser { - pub invitation_id: String, - pub password: String, +pub fn register_user( + invitation_id: web::Path, + user_data: web::Json, + pool: web::Data, +) -> impl Future { + web::block(move || { + query( + invitation_id.into_inner(), + user_data.into_inner().password, + pool, + ) + }) + .then(|res| match res { + Ok(user) => Ok(HttpResponse::Ok().json(&user)), + Err(err) => match err { + BlockingError::Error(service_error) => Err(service_error), + BlockingError::Canceled => Err(ServiceError::InternalServerError), + }, + }) } -impl Message for RegisterUser { - type Result = Result; -} +fn query( + invitation_id: String, + password: String, + pool: web::Data, +) -> Result { + use crate::schema::invitations::dsl::{id, invitations}; + use crate::schema::users::dsl::users; + let invitation_id = uuid::Uuid::parse_str(&invitation_id)?; -impl Handler for DbExecutor { - type Result = Result; - fn handle(&mut self, msg: RegisterUser, _: &mut Self::Context) -> Self::Result { - use crate::schema::invitations::dsl::{id, invitations}; - use crate::schema::users::dsl::users; - let conn: &PgConnection = &self.0.get().unwrap(); - - // try parsing the string provided by the user as url parameter - // return early with error that will be converted to ServiceError - let invitation_id = Uuid::parse_str(&msg.invitation_id)?; - - invitations - .filter(id.eq(invitation_id)) - .load::(conn) - .map_err(|_db_error| ServiceError::BadRequest("Invalid Invitation".into())) - .and_then(|mut result| { - if let Some(invitation) = result.pop() { - // if invitation is not expired - if invitation.expires_at > Local::now().naive_local() { - // try hashing the password, else return the error that will be converted to ServiceError - let password: String = hash_password(&msg.password)?; - let user = User::with_details(invitation.email, password); - let inserted_user: User = - diesel::insert_into(users).values(&user).get_result(conn)?; - - return Ok(inserted_user.into()); - } + let conn: &PgConnection = &pool.get().unwrap(); + invitations + .filter(id.eq(invitation_id)) + .load::(conn) + .map_err(|_db_error| ServiceError::BadRequest("Invalid Invitation".into())) + .and_then(|mut result| { + if let Some(invitation) = result.pop() { + // if invitation is not expired + if invitation.expires_at > chrono::Local::now().naive_local() { + // try hashing the password, else return the error that will be converted to ServiceError + let password: String = hash_password(&password)?; + dbg!(&password); + let user = User::from_details(invitation.email, password); + let inserted_user: User = + diesel::insert_into(users).values(&user).get_result(conn)?; + dbg!(&inserted_user); + return Ok(inserted_user.into()); } - Err(ServiceError::BadRequest("Invalid Invitation".into())) - }) - } + } + Err(ServiceError::BadRequest("Invalid Invitation".into())) + }) } diff --git a/simple-auth-server/src/register_routes.rs b/simple-auth-server/src/register_routes.rs deleted file mode 100644 index 19b3866f..00000000 --- a/simple-auth-server/src/register_routes.rs +++ /dev/null @@ -1,25 +0,0 @@ -use actix::Addr; -use actix_web::{web, Error, HttpResponse, ResponseError}; -use futures::Future; - -use crate::models::DbExecutor; -use crate::register_handler::{RegisterUser, UserData}; - -pub fn register_user( - invitation_id: web::Path, - user_data: web::Json, - db: web::Data>, -) -> impl Future { - let msg = RegisterUser { - // into_inner() returns the inner string value from Path - invitation_id: invitation_id.into_inner(), - password: user_data.password.clone(), - }; - - db.send(msg) - .from_err() - .and_then(|db_response| match db_response { - Ok(slim_user) => Ok(HttpResponse::Ok().json(slim_user)), - Err(service_error) => Ok(service_error.error_response()), - }) -} diff --git a/simple-auth-server/src/schema.rs b/simple-auth-server/src/schema.rs index 95c37038..8c48962f 100644 --- a/simple-auth-server/src/schema.rs +++ b/simple-auth-server/src/schema.rs @@ -9,7 +9,7 @@ table! { table! { users (email) { email -> Varchar, - password -> Varchar, + hash -> Varchar, created_at -> Timestamp, } } diff --git a/simple-auth-server/src/utils.rs b/simple-auth-server/src/utils.rs index 37035277..ce2e66a9 100644 --- a/simple-auth-server/src/utils.rs +++ b/simple-auth-server/src/utils.rs @@ -1,67 +1,30 @@ -use bcrypt::{hash, DEFAULT_COST}; -use chrono::{Duration, Local}; -use jsonwebtoken::{decode, encode, Header, Validation}; - use crate::errors::ServiceError; -use crate::models::SlimUser; +use argonautica::{Hasher, Verifier}; -pub fn hash_password(plain: &str) -> Result { - // get the hashing cost from the env variable or use default - let hashing_cost: u32 = match std::env::var("HASH_ROUNDS") { - Ok(cost) => cost.parse().unwrap_or(DEFAULT_COST), - _ => DEFAULT_COST, - }; - println!("{}", &hashing_cost); - hash(plain, hashing_cost).map_err(|_| ServiceError::InternalServerError) +lazy_static::lazy_static! { +pub static ref SECRET_KEY: String = std::env::var("SECRET_KEY").unwrap_or_else(|_| "0123".repeat(8)); } -#[derive(Debug, Serialize, Deserialize)] -struct Claims { - // issuer - iss: String, - // subject - sub: String, - //issued at - iat: i64, - // expiry - exp: i64, - // user email - email: String, +// WARNING THIS IS ONLY FOR DEMO PLEASE DO MORE RESEARCH FOR PRODUCTION USE +pub fn hash_password(password: &str) -> Result { + Hasher::default() + .with_password(password) + .with_secret_key(SECRET_KEY.as_str()) + .hash() + .map_err(|err| { + dbg!(err); + ServiceError::InternalServerError + }) } -// struct to get converted to token and back -impl Claims { - fn with_email(email: &str) -> Self { - Claims { - iss: "localhost".into(), - sub: "auth".into(), - email: email.to_owned(), - iat: Local::now().timestamp(), - exp: (Local::now() + Duration::hours(24)).timestamp(), - } - } -} - -impl From for SlimUser { - fn from(claims: Claims) -> Self { - SlimUser { - email: claims.email, - } - } -} - -pub fn create_token(data: &SlimUser) -> Result { - let claims = Claims::with_email(data.email.as_str()); - encode(&Header::default(), &claims, get_secret().as_ref()) - .map_err(|_err| ServiceError::InternalServerError) -} - -pub fn decode_token(token: &str) -> Result { - decode::(token, get_secret().as_ref(), &Validation::default()) - .map(|data| Ok(data.claims.into())) - .map_err(|_err| ServiceError::Unauthorized)? -} - -fn get_secret() -> String { - std::env::var("JWT_SECRET").unwrap_or_else(|_| "my secret".into()) +pub fn verify(hash: &str, password: &str) -> Result { + Verifier::default() + .with_hash(hash) + .with_password(password) + .with_secret_key(SECRET_KEY.as_str()) + .verify() + .map_err(|err| { + dbg!(err); + ServiceError::Unauthorized + }) }