This is the second part of our exploratory series on creating a web service with Rust.  Our exploration is broken down into distinct steps to make it easier for you to follow along:

In this article, we'll be looking at sending a confirmation link to the user's email address during the registration process.

Let's attend to the API case first.

Create src/register_handler.rs and paste in the following:

// src/register_handler.rs
use actix_web::{error::BlockingError, http, web, HttpResponse};
use actix_session::Session;
use diesel::prelude::*;
use serde::Deserialize;
use yarte::Template;

use crate::{
    email_service::send_confirmation_mail, 
    errors::AuthError, 
    models::{Confirmation, Pool},
    utils::is_signed_in
};


#[derive(Deserialize)]
pub struct RegisterData {
    pub email: String,
}

pub async fn send_confirmation(session: Session,
                              data: web::Json<RegisterData>,
                              pool: web::Data<Pool>) -> Result<HttpResponse, AuthError> {
    if (is_signed_in(&session)) {
      return Ok(HttpResponse::BadRequest().finish());
    }
            
    let result = web::block(move || create_confirmation(data.into_inner().email, &pool)).await;

    match result {
        Ok(_) => Ok(HttpResponse::Ok().finish()),
        Err(err) => match err {
            BlockingError::Error(auth_error) => Err(auth_error),
            BlockingError::Canceled => Err(AuthError::GenericError(String::from("Could not complete the process"))),
        },
    }
}


fn create_confirmation(email: String, pool: &web::Data<Pool>) -> Result<(), AuthError> {
    let confirmation = insert_record(email, pool)?;

    send_confirmation_mail(&confirmation)
}

fn insert_record(email: String, pool: &web::Data<Pool>) -> Result<Confirmation, AuthError> {
    use crate::schema::confirmations::dsl::confirmations;

    let new_record : Confirmation = email.into();

    let inserted_record = diesel::insert_into(confirmations)
                                .values(&new_record)
                                .get_result(&pool.get().unwrap())?;

    Ok(inserted_record)
}

send_confirmation is our route handler which we'll register in main.rs. It first checks if the user is signed in and if not, then calls create_confirmation which creates the database record and passes the record to the email service to send the confirmation email.

The email service is contained in src/email_service.rs:

// src/email_service.rs
use lettre::{
  Email, 
  SmtpClient, 
  ClientSecurity, 
  ClientTlsParameters,
  Transport, 
  smtp::{
      ConnectionReuseParameters, 
      authentication::{Credentials, Mechanism}
  }
};
use native_tls::{Protocol, TlsConnector};

use crate::{models::Confirmation, errors::AuthError, vars};


pub fn send_confirmation_mail(confirmation: &Confirmation) -> Result<(), AuthError> {
  let domain_url = vars::domain_url();
  let expires = confirmation.expires_at.format("%I:%M %p %A, %-d %B, %C%y").to_string();
  let html_text = format!(
      "Please click on the link below to complete registration. <br/>
       <a href=\"{domain}/register?id={id}&email={email}\">Complete registration</a> <br/>
      This link expires on <strong>{expires}</strong>",
      domain=domain_url,
      id=confirmation.id,
      email=confirmation.email,
      expires=expires
  );
  let plain_text = format!(
      "Please visit the link below to complete registration:\n
      {domain}/register.html?id={id}&email={email}\n
      This link expires on {expires}.",
      domain=domain_url,
      id=confirmation.id,
      email=confirmation.email,
      expires=expires
  );

  let email = Email::builder()
                    .to(confirmation.email.clone())
                    .from(("noreply@auth-service.com", vars::smtp_sender_name()))
                    .subject("Complete your registration on our one-of-a-kind Auth Service")
                    .text(plain_text)
                    .html(html_text)
                    .build()
                    .unwrap();

  let smtp_host = vars::smtp_host();
  let mut tls_builder = TlsConnector::builder();
  tls_builder.min_protocol_version(Some(Protocol::Tlsv10));
  let tls_parameters = ClientTlsParameters::new(smtp_host.clone(), tls_builder.build().unwrap());

  let mut mailer = SmtpClient::new((smtp_host.as_str(), vars::smtp_port()), ClientSecurity::Required(tls_parameters))
                              .unwrap()
                              .authentication_mechanism(Mechanism::Login)
                              .credentials(Credentials::new(vars::smtp_username(), vars::smtp_password()))
                              .connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
                              .transport();

  let result = mailer.send(email);

  if result.is_ok() {
      println!("Email sent");
      
      Ok(())
  } else {
      println!("Could not send email: {:?}", result);

      Err(AuthError::ProcessError(String::from("Could not send confirmation email")))
  }
}

Let's also create src/utils.rs to house is_signed_in and other authentication utility functions:

// src/utils.rs
use argonautica::{Hasher, Verifier};
use actix_session::Session;
use actix_web::{http::header::CONTENT_TYPE, HttpRequest};

use crate::{errors::AuthError, vars, models::SessionUser};


pub fn hash_password(password: &str) -> Result<String, AuthError> {
  Hasher::default()
      .with_password(password)
      .with_secret_key(vars::secret_key().as_str())
      .hash()
      .map_err(|_| AuthError::AuthenticationError(String::from("Could not hash password")))
}

pub fn verify(hash: &str, password: &str) -> Result<bool, AuthError> {
  Verifier::default()
      .with_hash(hash)
      .with_password(password)
      .with_secret_key(vars::secret_key().as_str())
      .verify()
      .map_err(|_| AuthError::AuthenticationError(String::from("Could not verify password")))
}

pub fn is_json_request(req: &HttpRequest) -> bool {
    req
      .headers()
      .get(CONTENT_TYPE)
      .map_or(
        false,
        |header| header.to_str().map_or(false, |content_type| "application/json" == content_type)
      )
}

pub fn is_signed_in(session: &Session) -> bool {
  match get_current_user(session) {
      Ok(_) => true,
      _ => false,
  }
}

pub fn set_current_user(session: &Session, user: &SessionUser) -> () {
    // serializing to string is alright for this case, 
    // but binary would be preferred in production use-cases.
    session.set("user", serde_json::to_string(user).unwrap()).unwrap();
}

pub fn get_current_user(session: &Session) -> Result<SessionUser, AuthError> {
    let err = AuthError::AuthenticationError(String::from("Could not retrieve user from session"));
    let session_result = session.get::<String>("user"); // Returns Result<Option<String>, Error>

    if session_result.is_err() {
        return Err(err);
    }

    session_result
        .unwrap()
        .map_or(
          Err(err.clone()),
          |user_str| serde_json::from_str(&user_str).or_else(|_| Err(err)) 
        ) 
}

Then in src/errors.rs we make AuthError clonable, and add 2 more error types:

// src/errors.rs
// ...

#[derive(Clone, Debug, Display)]
pub enum AuthError {
    // ...

    #[display(fmt = "ProcessFailed: {}", _0)]
    ProcessError(String),

    #[display(fmt = "AuthenticationError: {}", _0)]
    AuthenticationError(String),
}


impl ResponseError for AuthError {
    fn error_response(&self) -> HttpResponse {
        match self { 
            // ...

            AuthError::ProcessError(ref message) => HttpResponse::InternalServerError().json(message),

            AuthError::AuthenticationError(ref message) => HttpResponse::Unauthorized().json(message),
        }
    }
}


// ...

To tie everything together, we need to register our new modules and route handler in main.rs:

// src/main.rs

// ...

mod email_service;
mod register_handler;
mod utils;


#[actix_rt::main]
async fn main() -> std::io::Result<()> {
    // ...

    // Start http server
    HttpServer::new(move || {
        App::new()
            // ...
            .service(Files::new("/assets", "./templates/assets"))
            // Routes
            .service(
                web::scope("/")
                    .service(
                        web::resource("/register")
                            .route(web::post().to(register_handler::send_confirmation)),
                    ),
            )
    })
    // ...
}

With the above changes, we can now send confirmation links to new email addresses.

Before writing our handlers, we need to setup the templates we'll be working with.

First, create yarte.toml in the root directory:

# yarte.toml
[main]
dir = "templates"

[partials]
layouts = "./layouts"
includes = "./includes"

In the templates directory, create 3 subdirectories: includes, layouts and pages. The contents of the supporting files are given below:

<!-- includes/head.hbs -->
<head>
  <meta charset="utf-8"/>
  <title>{{ title }}</title>
  <link rel="stylesheet" href="/assets/css/tailwind-1.2.0.min.css">
</head>


<!-- includes/message.hbs -->
{{#if success }}
<div class="bg-teal-100 border-t-4 border-teal-100 rounded-b text-teal-900 px-4 py-3 shadow-md" role="alert">
  <div class="flex">
    <div class="py-1"><svg class="fill-current h-6 w-6 text-teal-500 mr-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M2.93 17.07A10 10 0 1 1 17.07 2.93 10 10 0 0 1 2.93 17.07zm12.73-1.41A8 8 0 1 0 4.34 4.34a8 8 0 0 0 11.32 11.32zM9 11V9h2v6H9v-4zm0-6h2v2H9V5z"/></svg></div>
    <div>
      <p class="font-bold">{{ message }}</p>
    </div>
  </div>
</div>
{{else}}
<div class="bg-red-100 border-t-4 border-red-100 rounded-b text-red-900 px-4 py-3 shadow-md" role="alert">
  <div class="flex">
    <div class="py-1"><svg class="fill-current h-6 w-6 text-red-500 mr-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M2.93 17.07A10 10 0 1 1 17.07 2.93 10 10 0 0 1 2.93 17.07zm12.73-1.41A8 8 0 1 0 4.34 4.34a8 8 0 0 0 11.32 11.32zM9 11V9h2v6H9v-4zm0-6h2v2H9V5z"/></svg></div>
    <div>
      <p class="font-bold">{{ message }}</p>
    </div>
  </div>
</div>
{{/if}}


<!-- layouts/base.hbs -->
<!DOCTYPE html>
<html>
  {{> includes/head }}
  <body>
    <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
      <div class="max-w-md w-full">
        {{> @partial-block }}
      </div>
    </div>
  </body>
</html>

Our first page, templates/pages/register.hbs contains the following template:


{{#> layouts/base title = "Auth Service | Register" }}

  {{#if sent }}
  {{> includes/message success = sent, message = "A confirmation email has been sent to your mailbox" }}
  {{else if error.is_some() }}
  {{> includes/message success = sent, message = error.as_ref().unwrap() }}
  {{/if}}

  <div>
    <h2 class="mt-6 text-center text-3xl leading-9 font-extrabold text-gray-900">
      Create an account
    </h2>
  </div>

  <form class="mt-8" action="/register2" method="POST">
    <div class="rounded-md shadow-sm">
      <div>
        <input 
          aria-label="Email address" 
          name="email" 
          type="email" 
          required 
          class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm sm:leading-5" 
          placeholder="Email address" />
      </div>
    </div>

    <div class="mt-6">
      <button type="submit" class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:shadow-outline-indigo active:bg-indigo-700 transition duration-150 ease-in-out">
        <span class="absolute left-0 inset-y-0 flex items-center pl-3">
          <svg class="h-5 w-5 text-indigo-500 group-hover:text-indigo-400 transition ease-in-out duration-150" fill="currentColor" viewBox="0 0 20 20">
            <path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd" />
          </svg>
        </span>
        Send confirmation link
      </button>
    </div>
  </form>
{{~/layouts/base }}

We're POSTing to /register2 as I do not know how to use the same route for handling web::Form<RegisterData> and web::Json<RegisterData> or if it is even possible. Now we've created the HTML templates we'll need, let's write our handlers.

Create src/templates.rs and paste in:

use yarte::Template;

#[derive(Template)]
#[template(path = "pages/register.hbs")]
pub struct Register {
    pub sent: bool,
    pub error: Option<String>
}

When a user visits /register in the browser, we want to show the registration form and send a confirmation code when the form is submitted.

Let's start from main.rs and work down.

// src/main.rs
// ...
mod templates;


#[actix_rt::main]
async fn main() -> std::io::Result<()> {
    // ...

    // Start http server
    HttpServer::new(move || {
        App::new()
            // ...
            // Routes
            .service(
                web::scope("/")
                    .service(
                        web::resource("/register")
                            .route(web::get().to(register_handler::show_confirmation_form))
                            .route(web::post().to(register_handler::send_confirmation)),
                    )
                    .route("/register2", web::post().to(register_handler::send_confirmation_for_browser)),
            )
    })
    // ...
}

We then update src/register_handler.rs with the new handlers:

// src/register_handler.rs
// ...

use crate::{
    email_service::send_confirmation_mail, 
    errors::AuthError, 
    models::{Confirmation, Pool},
    templates::Register,
    utils::{is_signed_in, to_home}
};
// ...

pub async fn show_confirmation_form(session: Session) -> Result<HttpResponse, AuthError> {
    if is_signed_in(&session) {
        Ok(to_home())
    } else {
        let template = Register { sent: false, error: None };

        Ok(HttpResponse::Ok().content_type("text/html; charset=utf-8").body(template.call().unwrap()))
    }
}

pub async fn send_confirmation_for_browser(data: web::Form<RegisterData>,
                                          pool: web::Data<Pool>) -> Result<HttpResponse, AuthError> {
    let result = web::block(move || create_confirmation(data.into_inner().email, &pool)).await;
    let template = match result {
        Ok(_) => Register { sent: true, error: None },
        Err(err) => match err {
            BlockingError::Error(auth_error) => Register { sent: false, error: Some(auth_error.to_string()) },
            BlockingError::Canceled => {
                Register { sent: false, error: Some(String::from("Could not complete the process")) }
            }
        },
    };

    Ok(HttpResponse::Ok().content_type("text/html; charset=utf-8").body(template.call().unwrap()))
}


// ...

When a user visits /register, we first check if the user is signed in in show_confirmation_form() and redirect them to their home page if they are. Otherwise, we render the form.

When the form is submitted to send_confirmation_for_browser(), we do the same thing we did in the API handler. The difference is we re-render the form with the result of the process displayed in an alert instead of returning a JSON response.

To complete this part, we need to implement the to_home() utility function for redirecting users to their home page.

// src/utils.rs
// ...
use actix_web::{
  http::header::{CONTENT_TYPE, LOCATION}, 
  HttpRequest, 
  HttpResponse
};

// ...

pub fn to_home() -> HttpResponse {
  HttpResponse::Found().header(LOCATION, "/me").finish()
}

Now we've successfully sent a confirmation link to the user. When they follow the link (browser) or send the id to the assigned route (API), we need to complete the creation of the user acount.