mirror of
https://github.com/actix/examples
synced 2024-11-30 17:14:35 +01:00
Update to new actix 1.0 api, drop actix-rt (#158)
This commit is contained in:
parent
258eae3bb9
commit
7525903fe6
@ -6,23 +6,20 @@ edition = "2018"
|
|||||||
workspace = ".."
|
workspace = ".."
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix = "0.8.2"
|
actix-identity = "0.1.0"
|
||||||
actix-rt = "0.2.2"
|
actix-web = "1.0.3"
|
||||||
actix-web = "1.0.2"
|
argonautica = "0.2.0"
|
||||||
actix-files = "0.1.1"
|
|
||||||
actix-identity= "0.1.0"
|
|
||||||
|
|
||||||
bcrypt = "0.2.1"
|
|
||||||
chrono = { version = "0.4.6", features = ["serde"] }
|
chrono = { version = "0.4.6", features = ["serde"] }
|
||||||
diesel = { version = "1.3.3", features = ["postgres", "uuid", "r2d2", "chrono"] }
|
derive_more = "0.15.0"
|
||||||
dotenv = "0.13.0"
|
diesel = { version = "1.4.2", features = ["postgres","uuidv07", "r2d2", "chrono"] }
|
||||||
derive_more = "0.14"
|
dotenv = "0.14.1"
|
||||||
env_logger = "0.6.0"
|
env_logger = "0.6"
|
||||||
jsonwebtoken = "6.0.0"
|
futures = "0.1"
|
||||||
futures = "0.1.25"
|
r2d2 = "0.8"
|
||||||
r2d2 = "0.8.3"
|
lazy_static = "1.3.0"
|
||||||
serde_derive="1.0.80"
|
serde = "1.0"
|
||||||
serde_json="1.0"
|
serde_derive = "1.0"
|
||||||
serde="1.0"
|
serde_json = "1.0"
|
||||||
sparkpost = "0.5.2"
|
sparkpost = "0.5.2"
|
||||||
uuid = { version = "0.6.5", features = ["serde", "v4"] }
|
uuid = { version = "0.7", features = ["serde", "v4"] }
|
||||||
|
|
||||||
|
@ -1,21 +1,22 @@
|
|||||||
|
## Auth Web Microservice with rust using Actix-Web 1.0
|
||||||
|
|
||||||
##### Flow of the event would look like this:
|
##### Flow of the event would look like this:
|
||||||
|
|
||||||
- Registers with email address ➡ Receive an 📨 with a link to verify
|
- Registers with email address ➡ Receive an 📨 with a link to verify
|
||||||
- Follow the link ➡ register with same email and a password
|
- 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.
|
- [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.
|
- [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.
|
- [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.
|
- [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.
|
- [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.
|
- [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.
|
- [r2d2](https://crates.io/crates/r2d2) // A generic connection pool.
|
||||||
- [serde](https://crates.io/crates/serde) // A generic serialization/deserialization framework.
|
- [serde](https://crates.io/crates/serde) // A generic serialization/deserialization framework.
|
||||||
- [serde_json](https://crates.io/crates/serde_json) // A JSON serialization file format.
|
- [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.
|
- [uuid](https://crates.io/crates/uuid) // A library to generate and parse UUIDs.
|
||||||
|
|
||||||
|
|
||||||
Read the full tutorial series on [hgill.io](https://hgill.io)
|
Read the full tutorial series on [gill.net.in](https://gill.net.in)
|
||||||
|
|
||||||
- [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/)
|
|
||||||
|
|
||||||
|
- [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/)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
-- Your SQL goes here
|
-- Your SQL goes here
|
||||||
CREATE TABLE users (
|
CREATE TABLE users (
|
||||||
email VARCHAR(100) NOT NULL UNIQUE PRIMARY KEY,
|
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
|
created_at TIMESTAMP NOT NULL
|
||||||
);
|
);
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
use actix::{Handler, Message};
|
|
||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
use actix_web::{dev::Payload, Error, FromRequest, HttpRequest};
|
use actix_web::{
|
||||||
use bcrypt::verify;
|
dev::Payload, error::BlockingError, web, Error, FromRequest, HttpRequest, HttpResponse,
|
||||||
|
};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
use diesel::PgConnection;
|
||||||
|
use futures::Future;
|
||||||
|
|
||||||
use crate::errors::ServiceError;
|
use crate::errors::ServiceError;
|
||||||
use crate::models::{DbExecutor, SlimUser, User};
|
use crate::models::{Pool, SlimUser, User};
|
||||||
use crate::utils::decode_token;
|
use crate::utils::verify;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct AuthData {
|
pub struct AuthData {
|
||||||
@ -14,34 +16,6 @@ pub struct AuthData {
|
|||||||
pub password: String,
|
pub password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message for AuthData {
|
|
||||||
type Result = Result<SlimUser, ServiceError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Handler<AuthData> for DbExecutor {
|
|
||||||
type Result = Result<SlimUser, ServiceError>;
|
|
||||||
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::<User>(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
|
// we need the same data
|
||||||
// simple aliasing makes the intentions clear and its more readable
|
// simple aliasing makes the intentions clear and its more readable
|
||||||
pub type LoggedUser = SlimUser;
|
pub type LoggedUser = SlimUser;
|
||||||
@ -53,9 +27,55 @@ impl FromRequest for LoggedUser {
|
|||||||
|
|
||||||
fn from_request(req: &HttpRequest, pl: &mut Payload) -> Self::Future {
|
fn from_request(req: &HttpRequest, pl: &mut Payload) -> Self::Future {
|
||||||
if let Some(identity) = Identity::from_request(req, pl)?.identity() {
|
if let Some(identity) = Identity::from_request(req, pl)?.identity() {
|
||||||
let user: SlimUser = decode_token(&identity)?;
|
let user: LoggedUser = serde_json::from_str(&identity)?;
|
||||||
return Ok(user as LoggedUser);
|
return Ok(user);
|
||||||
}
|
}
|
||||||
Err(ServiceError::Unauthorized.into())
|
Err(ServiceError::Unauthorized.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn logout(id: Identity) -> HttpResponse {
|
||||||
|
id.forget();
|
||||||
|
HttpResponse::Ok().finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn login(
|
||||||
|
auth_data: web::Json<AuthData>,
|
||||||
|
id: Identity,
|
||||||
|
pool: web::Data<Pool>,
|
||||||
|
) -> impl Future<Item = HttpResponse, Error = ServiceError> {
|
||||||
|
web::block(move || query(auth_data.into_inner(), pool)).then(
|
||||||
|
move |res: Result<SlimUser, BlockingError<ServiceError>>| 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<Pool>) -> Result<SlimUser, ServiceError> {
|
||||||
|
use crate::schema::users::dsl::{email, users};
|
||||||
|
let conn: &PgConnection = &pool.get().unwrap();
|
||||||
|
let mut items = users
|
||||||
|
.filter(email.eq(&auth_data.email))
|
||||||
|
.load::<User>(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)
|
||||||
|
}
|
||||||
|
@ -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<AuthData>,
|
|
||||||
id: Identity,
|
|
||||||
db: web::Data<Addr<DbExecutor>>,
|
|
||||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
|
||||||
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)
|
|
||||||
}
|
|
@ -1,16 +1,18 @@
|
|||||||
|
// email_service.rs
|
||||||
|
use crate::errors::ServiceError;
|
||||||
use crate::models::Invitation;
|
use crate::models::Invitation;
|
||||||
use sparkpost::transmission::{
|
use sparkpost::transmission::{
|
||||||
EmailAddress, Message, Options, Recipient, Transmission, TransmissionResponse,
|
EmailAddress, Message, Options, Recipient, Transmission, TransmissionResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn get_api_key() -> String {
|
lazy_static::lazy_static! {
|
||||||
std::env::var("SPARKPOST_API_KEY").expect("SPARKPOST_API_KEY must be set")
|
static ref API_KEY: String = std::env::var("SPARKPOST_API_KEY").expect("SPARKPOST_API_KEY must be set");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_invitation(invitation: &Invitation) {
|
pub fn send_invitation(invitation: &Invitation) -> Result<(), ServiceError> {
|
||||||
let tm = Transmission::new_eu(get_api_key());
|
let tm = Transmission::new_eu(API_KEY.as_str());
|
||||||
let sending_email = std::env::var("SENDING_EMAIL_ADDRESS")
|
let sending_email =
|
||||||
.expect("SENDING_EMAIL_ADDRESS must be set");
|
std::env::var("SENDING_EMAIL_ADDRESS").expect("SENDING_EMAIL_ADDRESS must be set");
|
||||||
// new email message with sender name and email
|
// new email message with sender name and email
|
||||||
let mut email = Message::new(EmailAddress::new(sending_email, "Let's Organise"));
|
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 {
|
Ok(res) => match res {
|
||||||
TransmissionResponse::ApiResponse(api_res) => {
|
TransmissionResponse::ApiResponse(api_res) => {
|
||||||
println!("API Response: \n {:#?}", api_res);
|
println!("API Response: \n {:#?}", api_res);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
TransmissionResponse::ApiError(errors) => {
|
TransmissionResponse::ApiError(errors) => {
|
||||||
println!("Response Errors: \n {:#?}", &errors);
|
println!("Response Errors: \n {:#?}", &errors);
|
||||||
|
Err(ServiceError::InternalServerError)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
println!("error \n {:#?}", error);
|
println!("Send Email Error: \n {:#?}", error);
|
||||||
|
Err(ServiceError::InternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
use actix_web::{error::ResponseError, HttpResponse};
|
use actix_web::{error::ResponseError, HttpResponse};
|
||||||
use derive_more::Display;
|
use derive_more::Display;
|
||||||
use diesel::result::{DatabaseErrorKind, Error};
|
use diesel::result::{DatabaseErrorKind, Error as DBError};
|
||||||
use std::convert::From;
|
use std::convert::From;
|
||||||
use uuid::ParseError;
|
use uuid::parser::ParseError;
|
||||||
|
|
||||||
#[derive(Debug, Display)]
|
#[derive(Debug, Display)]
|
||||||
pub enum ServiceError {
|
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 trait allows to convert our errors into http responses with appropriate data
|
||||||
impl ResponseError for ServiceError {
|
impl ResponseError for ServiceError {
|
||||||
fn error_response(&self) -> HttpResponse {
|
fn error_response(&self) -> HttpResponse {
|
||||||
match *self {
|
match self {
|
||||||
ServiceError::InternalServerError => HttpResponse::InternalServerError()
|
ServiceError::InternalServerError => HttpResponse::InternalServerError()
|
||||||
.json("Internal Server Error, Please try later"),
|
.json("Internal Server Error, Please try later"),
|
||||||
ServiceError::BadRequest(ref message) => {
|
ServiceError::BadRequest(ref message) => HttpResponse::BadRequest()
|
||||||
HttpResponse::BadRequest().json(message)
|
.json(message),
|
||||||
}
|
ServiceError::Unauthorized => HttpResponse::Unauthorized()
|
||||||
ServiceError::Unauthorized => {
|
.json("Unauthorized"),
|
||||||
HttpResponse::Unauthorized().json("Unauthorized")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -40,15 +38,14 @@ impl From<ParseError> for ServiceError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Error> for ServiceError {
|
impl From<DBError> for ServiceError {
|
||||||
fn from(error: Error) -> ServiceError {
|
fn from(error: DBError) -> ServiceError {
|
||||||
// Right now we just care about UniqueViolation from diesel
|
// Right now we just care about UniqueViolation from diesel
|
||||||
// But this would be helpful to easily map errors as our app grows
|
// But this would be helpful to easily map errors as our app grows
|
||||||
match error {
|
match error {
|
||||||
Error::DatabaseError(kind, info) => {
|
DBError::DatabaseError(kind, info) => {
|
||||||
if let DatabaseErrorKind::UniqueViolation = kind {
|
if let DatabaseErrorKind::UniqueViolation = kind {
|
||||||
let message =
|
let message = info.details().unwrap_or_else(|| info.message()).to_string();
|
||||||
info.details().unwrap_or_else(|| info.message()).to_string();
|
|
||||||
return ServiceError::BadRequest(message);
|
return ServiceError::BadRequest(message);
|
||||||
}
|
}
|
||||||
ServiceError::InternalServerError
|
ServiceError::InternalServerError
|
||||||
|
@ -1,38 +1,50 @@
|
|||||||
use actix::{Handler, Message};
|
use actix_web::{error::BlockingError, web, HttpResponse};
|
||||||
use chrono::{Duration, Local};
|
use diesel::{prelude::*, PgConnection};
|
||||||
use diesel::{self, prelude::*};
|
use futures::Future;
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
|
use crate::email_service::send_invitation;
|
||||||
use crate::errors::ServiceError;
|
use crate::errors::ServiceError;
|
||||||
use crate::models::{DbExecutor, Invitation};
|
use crate::models::{Invitation, Pool};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct CreateInvitation {
|
pub struct InvitationData {
|
||||||
pub email: String,
|
pub email: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message for CreateInvitation {
|
pub fn post_invitation(
|
||||||
type Result = Result<Invitation, ServiceError>;
|
invitation_data: web::Json<InvitationData>,
|
||||||
|
pool: web::Data<Pool>,
|
||||||
|
) -> impl Future<Item = HttpResponse, Error = ServiceError> {
|
||||||
|
// 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<CreateInvitation> for DbExecutor {
|
fn create_invitation(
|
||||||
type Result = Result<Invitation, ServiceError>;
|
eml: String,
|
||||||
|
pool: web::Data<Pool>,
|
||||||
fn handle(&mut self, msg: CreateInvitation, _: &mut Self::Context) -> Self::Result {
|
) -> Result<(), crate::errors::ServiceError> {
|
||||||
use crate::schema::invitations::dsl::*;
|
let invitation = dbg!(query(eml, pool)?);
|
||||||
let conn: &PgConnection = &self.0.get().unwrap();
|
send_invitation(&invitation)
|
||||||
|
}
|
||||||
// creating a new Invitation object with expired at time that is 24 hours from now
|
|
||||||
let new_invitation = Invitation {
|
/// Diesel query
|
||||||
id: Uuid::new_v4(),
|
fn query(eml: String, pool: web::Data<Pool>) -> Result<Invitation, crate::errors::ServiceError> {
|
||||||
email: msg.email.clone(),
|
use crate::schema::invitations::dsl::invitations;
|
||||||
expires_at: Local::now().naive_local() + Duration::hours(24),
|
|
||||||
};
|
let new_invitation : Invitation = eml.into();
|
||||||
|
let conn: &PgConnection = &pool.get().unwrap();
|
||||||
let inserted_invitation = diesel::insert_into(invitations)
|
|
||||||
.values(&new_invitation)
|
let inserted_invitation = diesel::insert_into(invitations)
|
||||||
.get_result(conn)?;
|
.values(&new_invitation)
|
||||||
|
.get_result(conn)?;
|
||||||
Ok(inserted_invitation)
|
|
||||||
}
|
Ok(inserted_invitation)
|
||||||
}
|
}
|
||||||
|
@ -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<CreateInvitation>,
|
|
||||||
db: web::Data<Addr<DbExecutor>>,
|
|
||||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
|
||||||
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()),
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,98 +1,72 @@
|
|||||||
#![allow(unused_imports)]
|
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate diesel;
|
extern crate diesel;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde_derive;
|
extern crate serde_derive;
|
||||||
|
|
||||||
use actix::prelude::*;
|
|
||||||
use actix_files as fs;
|
|
||||||
use actix_identity::{CookieIdentityPolicy, IdentityService};
|
use actix_identity::{CookieIdentityPolicy, IdentityService};
|
||||||
use actix_web::middleware::Logger;
|
use actix_web::{middleware, web, App, HttpServer};
|
||||||
use actix_web::{web, App, HttpServer};
|
use diesel::prelude::*;
|
||||||
use chrono::Duration;
|
use diesel::r2d2::{self, ConnectionManager};
|
||||||
use diesel::{r2d2::ConnectionManager, PgConnection};
|
|
||||||
use dotenv::dotenv;
|
|
||||||
|
|
||||||
mod auth_handler;
|
mod auth_handler;
|
||||||
mod auth_routes;
|
|
||||||
mod email_service;
|
mod email_service;
|
||||||
mod errors;
|
mod errors;
|
||||||
mod invitation_handler;
|
mod invitation_handler;
|
||||||
mod invitation_routes;
|
|
||||||
mod models;
|
mod models;
|
||||||
mod register_handler;
|
mod register_handler;
|
||||||
mod register_routes;
|
|
||||||
mod schema;
|
mod schema;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use crate::models::DbExecutor;
|
|
||||||
|
|
||||||
fn main() -> std::io::Result<()> {
|
fn main() -> std::io::Result<()> {
|
||||||
dotenv().ok();
|
dotenv::dotenv().ok();
|
||||||
std::env::set_var(
|
std::env::set_var(
|
||||||
"RUST_LOG",
|
"RUST_LOG",
|
||||||
"simple-auth-server=debug,actix_web=info,actix_server=info",
|
"simple-auth-server=debug,actix_web=info,actix_server=info",
|
||||||
);
|
);
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
let sys = actix_rt::System::new("example");
|
|
||||||
|
|
||||||
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||||
|
|
||||||
// create db connection pool
|
// create db connection pool
|
||||||
let manager = ConnectionManager::<PgConnection>::new(database_url);
|
let manager = ConnectionManager::<PgConnection>::new(database_url);
|
||||||
let pool = r2d2::Pool::builder()
|
let pool: models::Pool = r2d2::Pool::builder()
|
||||||
.build(manager)
|
.build(manager)
|
||||||
.expect("Failed to create pool.");
|
.expect("Failed to create pool.");
|
||||||
|
let domain: String = std::env::var("DOMAIN").unwrap_or_else(|_| "localhost".to_string());
|
||||||
|
|
||||||
let address: Addr<DbExecutor> =
|
// Start http server
|
||||||
SyncArbiter::start(4, move || DbExecutor(pool.clone()));
|
|
||||||
|
|
||||||
HttpServer::new(move || {
|
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()
|
App::new()
|
||||||
.data(address.clone())
|
.data(pool.clone())
|
||||||
.wrap(Logger::default())
|
// enable logger
|
||||||
|
.wrap(middleware::Logger::default())
|
||||||
.wrap(IdentityService::new(
|
.wrap(IdentityService::new(
|
||||||
CookieIdentityPolicy::new(secret.as_bytes())
|
CookieIdentityPolicy::new(utils::SECRET_KEY.as_bytes())
|
||||||
.name("auth")
|
.name("auth")
|
||||||
.path("/")
|
.path("/")
|
||||||
.domain(domain.as_str())
|
.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
|
.secure(false), // this can only be true if you have https
|
||||||
))
|
))
|
||||||
|
.data(web::JsonConfig::default().limit(4096))
|
||||||
// everything under '/api/' route
|
// everything under '/api/' route
|
||||||
.service(
|
.service(
|
||||||
web::scope("/api")
|
web::scope("/api")
|
||||||
// routes for authentication
|
|
||||||
.service(
|
.service(
|
||||||
web::resource("/auth")
|
web::resource("/invitation")
|
||||||
.route(web::post().to_async(auth_routes::login))
|
.route(web::post().to_async(invitation_handler::post_invitation)),
|
||||||
.route(web::delete().to(auth_routes::logout))
|
|
||||||
.route(web::get().to_async(auth_routes::get_me)),
|
|
||||||
)
|
)
|
||||||
// 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(
|
.service(
|
||||||
web::resource("/register/{invitation_id}")
|
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")?
|
.bind("127.0.0.1:3000")?
|
||||||
.start();
|
.run()
|
||||||
|
|
||||||
sys.run()
|
|
||||||
}
|
}
|
||||||
|
@ -1,37 +1,23 @@
|
|||||||
use actix::{Actor, SyncContext};
|
use super::schema::*;
|
||||||
use chrono::{Local, NaiveDateTime};
|
use diesel::{r2d2::ConnectionManager, PgConnection};
|
||||||
use diesel::pg::PgConnection;
|
|
||||||
use diesel::r2d2::{ConnectionManager, Pool};
|
|
||||||
use std::convert::From;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::schema::{invitations, users};
|
// type alias to use in multiple places
|
||||||
|
pub type Pool = r2d2::Pool<ConnectionManager<PgConnection>>;
|
||||||
/// This is db executor actor. can be run in parallel
|
|
||||||
pub struct DbExecutor(pub Pool<ConnectionManager<PgConnection>>);
|
|
||||||
|
|
||||||
// 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<Self>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Queryable, Insertable)]
|
#[derive(Debug, Serialize, Deserialize, Queryable, Insertable)]
|
||||||
#[table_name = "users"]
|
#[table_name = "users"]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub password: String,
|
pub hash: String,
|
||||||
pub created_at: NaiveDateTime,
|
pub created_at: chrono::NaiveDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
pub fn with_details(email: String, password: String) -> Self {
|
pub fn from_details<S: Into<String>, T: Into<String>>(email: S, pwd: T) -> Self {
|
||||||
User {
|
User {
|
||||||
email,
|
email: email.into(),
|
||||||
password,
|
hash: pwd.into(),
|
||||||
created_at: Local::now().naive_local(),
|
created_at: chrono::Local::now().naive_local(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -39,9 +25,21 @@ impl User {
|
|||||||
#[derive(Debug, Serialize, Deserialize, Queryable, Insertable)]
|
#[derive(Debug, Serialize, Deserialize, Queryable, Insertable)]
|
||||||
#[table_name = "invitations"]
|
#[table_name = "invitations"]
|
||||||
pub struct Invitation {
|
pub struct Invitation {
|
||||||
pub id: Uuid,
|
pub id: uuid::Uuid,
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub expires_at: NaiveDateTime,
|
pub expires_at: chrono::NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
// any type that implements Into<String> can be used to create Invitation
|
||||||
|
impl<T> From<T> for Invitation where
|
||||||
|
T: Into<String> {
|
||||||
|
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)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
@ -1,58 +1,65 @@
|
|||||||
use actix::{Handler, Message};
|
use actix_web::{error::BlockingError, web, HttpResponse};
|
||||||
use chrono::Local;
|
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use uuid::Uuid;
|
use futures::Future;
|
||||||
|
|
||||||
use crate::errors::ServiceError;
|
use crate::errors::ServiceError;
|
||||||
use crate::models::{DbExecutor, Invitation, SlimUser, User};
|
use crate::models::{Invitation, Pool, SlimUser, User};
|
||||||
use crate::utils::hash_password;
|
use crate::utils::hash_password;
|
||||||
|
|
||||||
// UserData is used to extract data from a post request by the client
|
// UserData is used to extract data from a post request by the client
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct UserData {
|
pub struct UserData {
|
||||||
pub password: String,
|
pub password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// to be used to send data via the Actix actor system
|
pub fn register_user(
|
||||||
#[derive(Debug)]
|
invitation_id: web::Path<String>,
|
||||||
pub struct RegisterUser {
|
user_data: web::Json<UserData>,
|
||||||
pub invitation_id: String,
|
pool: web::Data<Pool>,
|
||||||
pub password: String,
|
) -> impl Future<Item = HttpResponse, Error = ServiceError> {
|
||||||
|
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 {
|
fn query(
|
||||||
type Result = Result<SlimUser, ServiceError>;
|
invitation_id: String,
|
||||||
}
|
password: String,
|
||||||
|
pool: web::Data<Pool>,
|
||||||
|
) -> Result<SlimUser, crate::errors::ServiceError> {
|
||||||
|
use crate::schema::invitations::dsl::{id, invitations};
|
||||||
|
use crate::schema::users::dsl::users;
|
||||||
|
let invitation_id = uuid::Uuid::parse_str(&invitation_id)?;
|
||||||
|
|
||||||
impl Handler<RegisterUser> for DbExecutor {
|
let conn: &PgConnection = &pool.get().unwrap();
|
||||||
type Result = Result<SlimUser, ServiceError>;
|
invitations
|
||||||
fn handle(&mut self, msg: RegisterUser, _: &mut Self::Context) -> Self::Result {
|
.filter(id.eq(invitation_id))
|
||||||
use crate::schema::invitations::dsl::{id, invitations};
|
.load::<Invitation>(conn)
|
||||||
use crate::schema::users::dsl::users;
|
.map_err(|_db_error| ServiceError::BadRequest("Invalid Invitation".into()))
|
||||||
let conn: &PgConnection = &self.0.get().unwrap();
|
.and_then(|mut result| {
|
||||||
|
if let Some(invitation) = result.pop() {
|
||||||
// try parsing the string provided by the user as url parameter
|
// if invitation is not expired
|
||||||
// return early with error that will be converted to ServiceError
|
if invitation.expires_at > chrono::Local::now().naive_local() {
|
||||||
let invitation_id = Uuid::parse_str(&msg.invitation_id)?;
|
// try hashing the password, else return the error that will be converted to ServiceError
|
||||||
|
let password: String = hash_password(&password)?;
|
||||||
invitations
|
dbg!(&password);
|
||||||
.filter(id.eq(invitation_id))
|
let user = User::from_details(invitation.email, password);
|
||||||
.load::<Invitation>(conn)
|
let inserted_user: User =
|
||||||
.map_err(|_db_error| ServiceError::BadRequest("Invalid Invitation".into()))
|
diesel::insert_into(users).values(&user).get_result(conn)?;
|
||||||
.and_then(|mut result| {
|
dbg!(&inserted_user);
|
||||||
if let Some(invitation) = result.pop() {
|
return Ok(inserted_user.into());
|
||||||
// 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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(ServiceError::BadRequest("Invalid Invitation".into()))
|
}
|
||||||
})
|
Err(ServiceError::BadRequest("Invalid Invitation".into()))
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
@ -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<String>,
|
|
||||||
user_data: web::Json<UserData>,
|
|
||||||
db: web::Data<Addr<DbExecutor>>,
|
|
||||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
|
||||||
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()),
|
|
||||||
})
|
|
||||||
}
|
|
@ -9,7 +9,7 @@ table! {
|
|||||||
table! {
|
table! {
|
||||||
users (email) {
|
users (email) {
|
||||||
email -> Varchar,
|
email -> Varchar,
|
||||||
password -> Varchar,
|
hash -> Varchar,
|
||||||
created_at -> Timestamp,
|
created_at -> Timestamp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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::errors::ServiceError;
|
||||||
use crate::models::SlimUser;
|
use argonautica::{Hasher, Verifier};
|
||||||
|
|
||||||
pub fn hash_password(plain: &str) -> Result<String, ServiceError> {
|
lazy_static::lazy_static! {
|
||||||
// get the hashing cost from the env variable or use default
|
pub static ref SECRET_KEY: String = std::env::var("SECRET_KEY").unwrap_or_else(|_| "0123".repeat(8));
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
// WARNING THIS IS ONLY FOR DEMO PLEASE DO MORE RESEARCH FOR PRODUCTION USE
|
||||||
struct Claims {
|
pub fn hash_password(password: &str) -> Result<String, ServiceError> {
|
||||||
// issuer
|
Hasher::default()
|
||||||
iss: String,
|
.with_password(password)
|
||||||
// subject
|
.with_secret_key(SECRET_KEY.as_str())
|
||||||
sub: String,
|
.hash()
|
||||||
//issued at
|
.map_err(|err| {
|
||||||
iat: i64,
|
dbg!(err);
|
||||||
// expiry
|
ServiceError::InternalServerError
|
||||||
exp: i64,
|
})
|
||||||
// user email
|
|
||||||
email: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// struct to get converted to token and back
|
pub fn verify(hash: &str, password: &str) -> Result<bool, ServiceError> {
|
||||||
impl Claims {
|
Verifier::default()
|
||||||
fn with_email(email: &str) -> Self {
|
.with_hash(hash)
|
||||||
Claims {
|
.with_password(password)
|
||||||
iss: "localhost".into(),
|
.with_secret_key(SECRET_KEY.as_str())
|
||||||
sub: "auth".into(),
|
.verify()
|
||||||
email: email.to_owned(),
|
.map_err(|err| {
|
||||||
iat: Local::now().timestamp(),
|
dbg!(err);
|
||||||
exp: (Local::now() + Duration::hours(24)).timestamp(),
|
ServiceError::Unauthorized
|
||||||
}
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Claims> for SlimUser {
|
|
||||||
fn from(claims: Claims) -> Self {
|
|
||||||
SlimUser {
|
|
||||||
email: claims.email,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_token(data: &SlimUser) -> Result<String, ServiceError> {
|
|
||||||
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<SlimUser, ServiceError> {
|
|
||||||
decode::<Claims>(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())
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user