DEV Community

Cover image for Auth Web Microservice with rust using Actix-Web - Complete Tutorial Part 1
Harry Gill
Harry Gill

Posted on • Edited on

Auth Web Microservice with rust using Actix-Web - Complete Tutorial Part 1

This post is outdated now please read the updated version Auth Web Microservice with rust using Actix-Web 1.0 - Complete Tutorial

What?

We are going to create a web-server in rust that only deals with user registration and authentication. I will be explaining the steps in each file as we go. The complete project code is here repo. Please take all this with a pinch of salt as I'm a still a noob to rust 😉.

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
Crates we are going to use
  • actix // Actix is a Rust actors framework.
  • actix-web // Actix web is a simple, pragmatic and extremely fast web framework for Rust.
  • brcypt // Easily hash and verify passwords using bcrypt.
  • chrono // Date and time library for Rust.
  • diesel // A safe, extensible ORM and Query Builder for PostgreSQL, SQLite, and MySQL.
  • dotenv // A dotenv implementation for Rust.
  • env_logger // A logging implementation for log which is configured via an environment variable.
  • failure // Experimental error handling abstraction.
  • jsonwebtoken // Create and parse JWT in a strongly typed way.
  • futures // An implementation of futures and streams featuring zero allocations, composability, and iterator-like interfaces.
  • r2d2 // A generic connection pool.
  • serde // A generic serialization/deserialization framework.
  • serde_json // A JSON serialization file format.
  • serde_derive // Macros 1.1 implementation of #[derive(Serialize, Deserialize)].
  • sparkpost // Rust bindings for sparkpost email api v1.
  • uuid // A library to generate and parse UUIDs.

I have provided a brief info about the crates in use from their official description. If you want to know more about any of these crates please click on the name to go to crates.io. Shameless plug: sparkpost is my crate please leave feedback if you like/dislike it.

Prerequisite

I will assume here that you have some knowledge of programming, preferably some rust as well. A working setup of rust is required. Checkout https://rustup.rs for esasy rust setup. To know more about rust checkout
The Book
.

We will be using diesel to create models and deal with database, queries and migrations. Pleas head over to http://diesel.rs/guides/getting-started/ to get started and setup diesel_cli. In this tutorial we will be using postgresql so follow the instructions to setup for postgres. You need to have a running postgres server and can create a database to follow this tutorial through. Another nice to have tool is Cargo Watch that lets you watch the file system and re-compile and re-run the app when you make any changes.

Install Curl if don't have it already on your system for testing the api locally.

Let's Begin

After checking your rust and cargo version and creating a new project with

# at the time of writing this tutorial my setup is 
rustc --version && cargo --version
# rustc 1.29.1 (b801ae664 2018-09-20)
# cargo 1.29.0 (524a578d7 2018-08-05)

cargo new simple-auth-server
# Created binary (application) `simple-auth-server` project

cd simple-auth-server # and then 

# watch for changes re-compile and run
cargo watch -x run 

Fill in the cargo dependencies with the following, I will go through each of them as get used in the project. I am using explicit versions of the crates, as you know things get old and change.(in case you are reading this tutorial after a long time it was created). In part 1 of this tutorial we won't be using all of them but they will all become handy in the final app.

[dependencies]
actix = "0.7.4"
actix-web = "0.7.8"
bcrypt = "0.2.0"
chrono = { version = "0.4.6", features = ["serde"] }
diesel = { version = "1.3.3", features = ["postgres", "uuid", "r2d2", "chrono"] }
dotenv = "0.13.0"
env_logger = "0.5.13"
failure = "0.1.2"
frank_jwt = "3.0"
futures = "0.1"
r2d2 = "0.8.2"
serde_derive="1.0.79"
serde_json="1.0"
serde="1.0"
sparkpost = "0.4"
uuid = { version = "0.6.5", features = ["serde", "v4"] }
Setup The Base APP

Create new files src/models.rs src/app.rs.

// models.rs
use actix::{Actor, SyncContext};
use diesel::pg::PgConnection;
use diesel::r2d2::{ConnectionManager, Pool};

/// 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>;
}

To use this Actor we need to set up actix-web server. We have the following in src/app.rs. We are leaving the resource builders empty for now. This is where the meat of the routing is going to go.

// app.rs
use actix::prelude::*;
use actix_web::{http::Method, middleware, App};
use models::DbExecutor;

pub struct AppState {
    pub db: Addr<DbExecutor>,
}

// helper function to create and returns the app after mounting all routes/resources
pub fn create_app(db: Addr<DbExecutor>) -> App<AppState> {
    App::with_state(AppState { db })
        // setup builtin logger to get nice logging for each request
        .middleware(middleware::Logger::new("\"%r\" %s %b %Dms"))

         // routes for authentication
        .resource("/auth", |r| {
        })
        // routes to invitation
        .resource("/invitation/", |r| {
        })
        // routes to register as a user after the
        .resource("/register/", |r| {
        })
}
// main.rs
// to avoid the warning from diesel macros
#![allow(proc_macro_derive_resolution_fallback)]

extern crate actix;
extern crate actix_web;
extern crate serde;
extern crate chrono;
extern crate dotenv;
extern crate futures;
extern crate r2d2;
extern crate uuid;
#[macro_use] extern crate diesel;
#[macro_use] extern crate serde_derive;
#[macro_use] extern crate failure;

mod app;
mod models;
mod schema;
// mod errors;
// mod invitation_handler;
// mod invitation_routes;

use models::DbExecutor;
use actix::prelude::*;
use actix_web::server;
use diesel::{r2d2::ConnectionManager, PgConnection};
use dotenv::dotenv;
use std::env;


fn main() {
    dotenv().ok();
    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    let sys = actix::System::new("Actix_Tutorial");

    // create db connection pool
    let manager = ConnectionManager::<PgConnection>::new(database_url);
    let pool = r2d2::Pool::builder()
        .build(manager)
        .expect("Failed to create pool.");

    let address :Addr<DbExecutor>  = SyncArbiter::start(4, move || DbExecutor(pool.clone()));

    server::new(move || app::create_app(address.clone()))
        .bind("127.0.0.1:3000")
        .expect("Can not bind to '127.0.0.1:3000'")
        .start();

    sys.run();
}

At this stage your server should compile and run on 127.0.0.1:3000. It doesn't do anything useful for now. Let's create some Models.

Setting up Diesel and creating our user Model

We start with creating a model for the user. Assuming from the previous steps you have postgres and diesel-cli installed and working. In your terminal echo DATABASE_URL=postgres://username:password@localhost/database_name > .env replace database_name, username and password as you have setup. Then we run diesel setup in the terminal. This will create our database if didn't exist and setup a migration directory etc.

Let's write some SQL, shall we. Create migrations by diesel migration generate users and invitation diesel migration generate invitations. Open the up.sql and down.sql files in migrations folder and add with following sql respectively.


--migrations/TIMESTAMP_users/up.sql
CREATE TABLE users (
  email VARCHAR(100) NOT NULL PRIMARY KEY,
  password VARCHAR(64) NOT NULL, --bcrypt hash
  created_at TIMESTAMP NOT NULL
);

--migrations/TIMESTAMP_users/down.sql
DROP TABLE users;

--migrations/TIMESTAMP_invitations/up.sql
CREATE TABLE invitations (
  id UUID NOT NULL PRIMARY KEY,
  email VARCHAR(100) NOT NULL,
  expires_at TIMESTAMP NOT NULL
);

--migrations/TIMESTAMP_invitations/down.sql
DROP TABLE invitations;

Command diesel migration run will create the table in the DB and a file src/schema.rs. This is the extent I will go about diesel-cli and migrations. Please read their documentation to learn more.

At this stage we have created the tables in the db, let's write some code to create a representation of user and invitation in rust. In models.rs we add the following.

// models.rs
...
// --- snip
use chrono::NaiveDateTime;
use uuid::Uuid;
use schema::{users,invitations};

#[derive(Debug, Serialize, Deserialize, Queryable, Insertable)]
#[table_name = "users"]
pub struct User {
    pub email: String,
    pub password: String,
    pub created_at: NaiveDateTime, // only NaiveDateTime works here due to diesel limitations
}

impl User {
    // this is just a helper function to remove password from user just before we return the value out later
    pub fn remove_pwd(mut self) -> Self {
        self.password = "".to_string();
        self
    }
}

#[derive(Debug, Serialize, Deserialize, Queryable, Insertable)]
#[table_name = "invitations"]
pub struct Invitation {
    pub id: Uuid,
    pub email: String,
    pub expires_at: NaiveDateTime,
}

Check your implementation is free from errors/warnings and keep an eye on cargo watch -x run command in the terminal.

read more...

Top comments (3)

Collapse
 
maxdietrich profile image
Max Dietrich • Edited

Hey there, thanks for the guide, it's really helping me getting into web development with Rust! I'm following along and running into a compile error at the first part, where it says that it should compile, though.

The problem stems from the fact that the

mod schema;

in main.rs is not commented out, although the schema is only generated in the next step. Commenting this line out as well solves the issue.

Also at this point, the main thread panics because DATABASE_URL is not present, as the .env entry is also written afterwards.

Collapse
 
mygnu profile image
Harry Gill

Noted Thanks

Collapse
 
jeikabu profile image
jeikabu • Edited

Good stuff!
Seeing the cool work others do always inspires me.
Cargo watch is interesting. I'll have to check that out.