A blog with the web framework Rocket - part 3

Users for our blog

Now we need we users and the have to store this information.

We represent our entity User like this:

User
email
username
password
role

We will use a postgreSQL database and the ORM Diesel

Set up Diesel

In the Cargo.toml:

# ...
[dependencies]
rocket = "0.4.0"
diesel = { version = "1.0.0", features = ["postgres"] }
dotenv = "0.9.0"

[dependencies.rocket_contrib]
version = "*"
default-features = false
features = ["tera_templates", "serve", "diesel_postgres_pool"]

Then we use Rocket configuration file, Rocket.toml and place these lines inside it:

[global.databases]
postgres_logs = { url = "postgres://username:password@localhost/rocket_blog" }

We also install Diesel CLI tool, diesel_cli but just for postgreSQL:

cargo install diesel_cli --no-default-features --features postgres

Then, we tell Diesel where to find our database using a .env file:

echo DATABASE_URL=postgres://username:password@localhost/rocket_blog > .env

Next, we create our database:

diesel setup

Then, we create our User table. First we create the migration:

diesel migration generate create_users

Creating migrations/2019-02-04-135651_create_users/up.sql
Creating migrations/2019-02-04-135651_create_users/down.sql

Two files have been created by Diesel in migrations/2019-02-04-135651_create_users/ one to apply apply the migration and one to revert it. We write then the SQL for both migrations.

up.sql:

CREATE TABLE users (
  email VARCHAR(100) PRIMARY KEY,
  username VARCHAR(50) NOT NULL,
  password TEXT NOT NULL,
  role VARCHAR(50) NOT NULL
);

down.sql:

DROP TABLE users;

Then, run the migration:

diesel migration run

Running migration 2019-02-04-135651_create_users

For our development we will need to populate our tables:

diesel migration generate populate_users

Once again two files have been generated: up.sql and down.sql. We write the SQL scripts in up.sql:

INSERT INTO users (email, username, password, role)
  VALUES ('deb@ian.com', 'Red', '$2b$08$918zaVlrq5ukEv1q5ZWyteszg8vEBKcauySEXvrhJK/OBbAiapZXS', 'admin');
INSERT INTO users (email, username, password, role)
  VALUES ('fed@ora.com', 'Blue', '$2b$08$NpJK3JG8G7qqlo/7q8JsHuadFKNX7ZBKMryTkDe4rDv6bDJqZz.06', 'user');
INSERT INTO users (email, username, password, role)
  VALUES ('ar@ch.com', 'Black', '$2b$08$ERqMurM4rYvu.7jdp1crmu5DW5NBeLU.OEWfPlj.MsrUegOKsyrBC', 'user');
INSERT INTO users (email, username, password, role)
  VALUES ('su@se.com', 'Green', '$2b$08$8qdQNIK3ykiUhrIvxLEBmuzxq3kuWeWSrIfs8wbcvl81XKXiNo02C', 'user');
INSERT INTO users (email, username, password, role)
  VALUES ('ub@untu.com', 'Purple', '$2b$08$jWxR6cT4PVA76QMAyOrFEuPe1wofUEC.Ahmee3FnT1xR/fjyhkimu', 'user');

and in down.sql

TRUNCATE users CASCADE;

Concerning the password column, I use a little CLI called cipher_password I made.

Let's pour some diesel on our rusty blog

A module containing the connection to our database

Let's create a folder src/db and a file src/db/mod.rs into it. Edit this file like this:

use diesel::PgConnection;

#[database("pg_db")]
pub struct DbConn(PgConnection);

PgConnection is the struct we use each time we want to connect to our database.

Diesel powered users

Create a folder src/users/ and a file src/users/mod.rs into it. Edit this file:

pub mod schema;
pub mod models;
pub mod router;

What about these files?

The first one, src/users/schema.rs, thanks to the macro table! generate a bunch of useful code. This code allow to do specific queries to the table users. The content of this file is generated by Diesel with the command:

diesel print-schema

So just copy the output into src/users/schema.rs.

table! {
    users (email) {
        email -> Varchar,
        username -> Varchar,
        password -> Text,
        role -> Varchar,
    }
}

The second one, src/users/models.rs implement the User struct and the methods associated:

use diesel::{self, prelude::*};

use super::schema::users;
use super::schema::users::dsl::users as all_users;

#[table_name = "users"]
#[derive(Serialize, Queryable, Insertable, Debug, Clone)]
pub struct User {
    pub email: String,
    pub username: String,
    pub password: String,
    pub role: String,
}

impl User {
    pub fn all(conn: &PgConnection) -> Vec<User> {
        all_users
            .order(users::email.desc())
            .load::<User>(conn)
            .unwrap()
    }
}

Here we created a Struct User that will be map to our database. Hence #[table_name=users]. Then we derive Insertable and Queryable. This generate all the necessary code to load our users and to insert new one. Finally we implement the function to load our users, the all function.

And finally router.rs contains the actions matching with routes linked to our users:

use super::models::User;
use crate::db;
use rocket::request::FlashMessage;
use rocket_contrib::templates::Template;

#[derive(Debug, Serialize)]
struct UserContext<'a, 'b> {
    title: String,
    static_path: String,
    msg: Option<(&'a str, &'b str)>,
    users: Vec<User>,
}

impl<'a, 'b> UserContext<'a, 'b> {
    pub fn err(conn: &db::DbConn, msg: &'a str) -> UserContext<'static, 'a> {
        UserContext {
            msg: Some(("error", msg)),
            users: User::all(conn),
            title: String::from("Users list"),
            static_path: String::from("../"),
        }
    }

    pub fn raw(conn: &db::DbConn, msg: Option<(&'a str, &'b str)>) -> UserContext<'a, 'b> {
        UserContext {
            msg: msg,
            users: User::all(conn),
            title: String::from("Users list"),
            static_path: String::from("../"),
        }
    }
}

#[get("/users")]
pub fn show_users(msg: Option<FlashMessage>, conn: db::DbConn) -> Template {
    Template::render(
        "users",
        &match msg {
            Some(ref msg) => UserContext::raw(&conn, Some((msg.name(), msg.msg()))),
            None => UserContext::raw(&conn, None),
        },
    )
}

So, we created a User specific context to be used inside a Template. We defined the route /users to display information about our users.

Now we write the template associated to this route, templates/users.html.tera:

{% extends "base" %}

{% block title %}{{ title }}{% endblock %}

{% block content %}
<div class="row">
  <div class="col s12">
    <table>
      <tr>
        <th>Username</th>
        <th>Email</th>
        <th>Role</th>
      </tr>
      {% for user in users %}
      <tr>
        <td>{{ user.username }}</td>
        <td>{{ user.email }}</td>
        <td>{{ user.role }}</td>
      </tr>
      {% endfor %}
    </table>
  </div>
</div>
{% endblock %}

Fuelled our rocket with diesel

In src/main.rs, import diesel crate

// ...
#[macro_use]
extern crate diesel
// ...
mod db; // The module containing the database logic
mod users; // The module containing users logic

fn main() {
    rocket::ignite()
        .attach(db::DbConn::fairing())
        .mount("/public", StaticFiles::from("static/"))
        .mount("/", routes![home])
        .mount("/", routes![users::router::show_users])
        .attach(Template::fairing())
        .launch();
}

That's it, now run your app:

   Compiling blog-rs v0.1.0 (/home/airone/Workspace/blog-rs)
    Finished dev [unoptimized + debuginfo] target(s) in 9.98s
     Running `target/debug/blog-rs`
🔧 Configured for development.
    => address: localhost
    => port: 8000
    => log: normal
    => workers: 4
    => secret key: generated
    => limits: forms = 32KiB
    => keep-alive: 5s
    => tls: disabled
    => [extra] databases: { pg_db = { url = "postgres://username:password@localhost/rocket_blog" } }
🛰  Mounting /public:
    => GET /public [10]
    => GET /public/<path..> [10]
🛰  Mounting /:
    => GET / (home)
🛰  Mounting /:
    => GET /users (show_users)
📡 Fairings:
    => 1 request: Templates
🚀 Rocket has launched from http://localhost:8000

Pfiouu! We did a good job. We've been able to load our users and to display them in a browser, that's cool.

Next time we should implement a CRUD for our users.