A blog with the web framework Rocket - part 3
- 500 words
- 3 min
Users for our blog
Now we need we users and the have to store this information.
We represent our entity User like this:
| User |
|---|
| 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.