Authentication system using rust (actix-web) and sveltekit - DB and Redis Config
Tags: rust, tutorial, actix, webdev
Published: 2023-04-23 22:08:07

## Introduction

Moving on from where we stopped, we'll be focusing on setting up our database (PostgreSQL) and redis connections here. We'll leverage on the awesome rust ecosystem and previous setup to make this seamless. Redis is needed to help us forcefully destroy some tokens and to efficiently store our session cookies later on.

## Source code

The source code for this series is hosted on GitHub via:

{% github Sirneij/rust-auth %}

## Implementation

> You can get the overview of the code for this article on [github][1].

### Step 1: Create a `users` submodule in the `routes` module

This step is not really that relevant here but so that we can still feel focused, I decided to add it here. Let's prepare our application for the task ahead. In the `src/routes` folder, create a subfolder and name it `users`. Make the new folder a module:

```shell
~/rust-auth/backend$ mkdir src/routes/users && touch src/routes/users/mod.rs src/routes/users/register.rs
```

Open up `src/routes/users/mod.rs`:

```rust
// src/routes/users/mod.rs

mod register;
```

Then link the newly created module to its parent module:

```rust
// src/routes/mod.rs
...
mod users;
```

Let's leave that there and focus on this article's main business &mdash; linking our application up with a database and redis.

## Step 2: Install database connection dependencies and set them up

As discussed in the previous article, we'll be using [SQLx][2] to asynchronously interact with the database. It's not an ORM so we'll be using raw SQL which, though maybe tedious, will allow us to learn the good old SQL alongside. If you need ORM, you can check out [diesel][3]. Let's install [SQLx][2]:

```shell
~/rust-auth/backend$ cargo add sqlx --features runtime-actix-rustls,postgres,uuid,chrono,migrate
```

We are activating `runtime-actix-rustls` (since we use actix-web), `postgres` (our Database of choice), `uuid` (for IDs), `chrono` (to support rust `chrono` crate used for date and time), and `migrate` (to allow automatic migration) features. What do we want to migrate? We'll be migrating some SQL tables. To start out, let's create a `migrations` folder at the root of our application. That's the default path it should be located. However, you can change this. Whenever you run a migration, this folder will be checked to know whether or not some database stuff has been altered.

```shell
~/rust-auth/backend$ mkdir migrations
```

Now, using [SQLx CLI][4], we can generate `.sql` files for our tables in the `migrations` folder:

```shell
~/rust-auth/backend$ sqlx migrate add -r users_table
```

You should now see two `.sql` files generated. The flag `-r` makes creating *reversible* migrations with corresponding "up" and "down" scripts possible. If the above command didn't work, don't worry, we'll make it work.

Let's add some credentials to `settings/base.yaml` file for our DB and redis connections:

```yaml
# settings/base.yaml
...
database:
  username: "<your_db_username>"
  password: "<your_db_password>"
  port: <your_db_port>
  host: "<your_db_host>"
  database_name: "<your_db_name>"
  require_ssl: false

redis:
  uri: "<your_redis_uri>"
  pool_max_open: 16
  pool_max_idle: 8
  pool_timeout_seconds: 1
  pool_expire_seconds: 60
```

Some basic settings which you need to supply for your local setup of PostgreSQL and Redis. Proceeding to `src/settings.rs`:

```rust
// src/settings.rs
use sqlx::ConnectOptions;

/// Global settings for exposing all preconfigured variables
#[derive(serde::Deserialize, Clone)]
pub struct Settings {
    pub application: ApplicationSettings,
    pub debug: bool,
    pub database: DatabaseSettings,
    pub redis: RedisSettings,
}
...
/// Redis settings for the entire app
#[derive(serde::Deserialize, Clone, Debug)]
pub struct RedisSettings {
    pub uri: String,
    pub pool_max_open: u64,
    pub pool_max_idle: u64,
    pub pool_timeout_seconds: u64,
    pub pool_expire_seconds: u64,
}

/// Database settings for the entire app
#[derive(serde::Deserialize, Clone)]
pub struct DatabaseSettings {
    pub username: String,
    pub password: String,
    pub port: u16,
    pub host: String,
    pub database_name: String,
    pub require_ssl: bool,
}

impl DatabaseSettings {
    pub fn connect_to_db(&self) -> sqlx::postgres::PgConnectOptions {
        let ssl_mode = if self.require_ssl {
            sqlx::postgres::PgSslMode::Require
        } else {
            sqlx::postgres::PgSslMode::Prefer
        };
        let mut options = sqlx::postgres::PgConnectOptions::new()
            .host(&self.host)
            .username(&self.username)
            .password(&self.password)
            .port(self.port)
            .ssl_mode(ssl_mode)
            .database(&self.database_name);
        options.log_statements(tracing::log::LevelFilter::Trace);
        options
    }
}
```

We added `RedisSettings` and `DatabaseSettings` to our settings file and effected the change in the global `Settings` struct. We also implemented a method, `connect_to_db`, for `DatabaseSettings` so that we can easily connect to our database using the credentials provided. It's time to integrate these settings with our application. Open up `src/startup.rs`:

```diff
// src/startup.rs
...
    pub async fn build(
        settings: crate::settings::Settings,
+     test_pool: Option<sqlx::postgres::PgPool>,
    ) -> Result<Self, std::io::Error> {
+     let connection_pool = if let Some(pool) = test_pool {
+         pool
+      } else {
+          get_connection_pool(&settings.database).await
+     };

+     sqlx::migrate!()
+          .run(&connection_pool)
+          .await
+          .expect("Failed to migrate the database.");

        let address = format!(
            "{}:{}",
            settings.application.host, settings.application.port
        );

        let listener = std::net::TcpListener::bind(&address)?;
        let port = listener.local_addr().unwrap().port();
-      let server = run(listener).await?;
+      let server = run(listener, connection_pool, settings).await?;

        Ok(Self { port, server })
    }
...

+ pub async fn get_connection_pool(
+   settings: &crate::settings::DatabaseSettings,
+ ) -> sqlx::postgres::PgPool {
+   sqlx::postgres::PgPoolOptions::new()
+       .acquire_timeout(std::time::Duration::from_secs(2))
+      .connect_lazy_with(settings.connect_to_db())
+ }

async fn run(
    listener: std::net::TcpListener,
+   db_pool: sqlx::postgres::PgPool,
+   settings: crate::settings::Settings,
) -> Result<actix_web::dev::Server, std::io::Error> {
+  // Database connection pool application state
+  let pool = actix_web::web::Data::new(db_pool);

+  // Redis connection pool
+  let cfg = deadpool_redis::Config::from_url(settings.clone().redis.uri);
+  let redis_pool = cfg
+      .create_pool(Some(deadpool_redis::Runtime::Tokio1))
+     .expect("Cannot create deadpool redis.");
+  let redis_pool_data = actix_web::web::Data::new(redis_pool);

    let server = actix_web::HttpServer::new(move || {
        actix_web::App::new().service(crate::routes::health_check)
        actix_web::App::new()
            .service(crate::routes::health_check)
+         // Add database pool to application state
+         .app_data(pool.clone())
+          // Add redis pool to application state
+          .app_data(redis_pool_data.clone())
    })
    .listen(listener)?
    .run();
    Ok(server)
}
```

We created a new function, `get_connection_pool`, that really and lazily connects our application to the DB and then returns such connection for the app's use. Our `run` function was extended to take more parameters, such as the pool returned by the previously explained function. Since many endpoints will be needing access to the DB (and redis &mdash; to be created) pool created, we need to make it available app-wide. To do this, actix-web provides an extractor, `actix_web::web::Data<T>`, to help share the application state "with all routes and resources within the same scope". We then used this API to create `pool` and `redis_pool_data` which were then attached to the application via `App::pp_data()`. For the `build` method, we also extended it to allow an optional argument, `test_pool`, which will be used when tests are being run. We also allowed automatic migration of the DB using the `sqlx::migrate` macro. If you created your `migrations` folder in a different place other than the root directory, you must pass the path to such a folder in this macro. Before we install [deadpool-redis][5], let's update our `src/main.rs` one last time:

```diff
#[tokio::main]
async fn main() -> std::io::Result<()> {
    dotenv::dotenv().ok();
    let settings = backend::settings::get_settings().expect("Failed to read settings.");
    let subscriber = backend::telemetry::get_subscriber(settings.clone().debug);
    backend::telemetry::init_subscriber(subscriber);

-   let application = backend::startup::Application::build(settings).await?;
+  let application = backend::startup::Application::build(settings, None).await?;

    tracing::event!(target: "backend", tracing::Level::INFO, "Listening on http://127.0.0.1:{}/", application.port());

    application.run_until_stopped().await?;
    Ok(())
}
```

Since this is the real app, we set `test_pool` to `None`. Now, install [deadpool-redis][5]:

```shell
~/rust-auth/backend$ cargo add deadpool-redis
```

### Step 3: Create some DB tables for users

If this:

```shell
~/rust-auth/backend$ sqlx migrate add -r users_table
```

failed before, you can rerun it now. It's time to write some SQL. Open up `migrations/*_users_table.up.sql`:

```sql
-- migrations/*_users_table.up.sql
-- Add up migration script here
-- User table
CREATE TABLE IF NOT EXISTS users(
    id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
    email TEXT NOT NULL UNIQUE,
    password TEXT NOT NULL,
    first_name TEXT NOT NULL,
    last_name TEXT NOT NULL,
    is_active BOOLEAN DEFAULT FALSE,
    is_staff BOOLEAN DEFAULT FALSE,
    is_superuser BOOLEAN DEFAULT FALSE,
    thumbnail TEXT NULL,
    date_joined TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS users_id_email_is_active_indx ON users (id, email, is_active);
-- Create a domain for phone data type
CREATE DOMAIN phone AS TEXT CHECK(
    octet_length(VALUE) BETWEEN 1
    /*+*/
    + 8 AND 1
    /*+*/
    + 15 + 3
    AND VALUE ~ '^\+\d+$'
);
-- User details table (One-to-one relationship)
CREATE TABLE user_profile (
    id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL UNIQUE,
    phone_number phone NULL,
    birth_date DATE NULL,
    github_link TEXT NULL,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS users_detail_id_user_id ON user_profile (id, user_id);
```

As a person who's some Django experience, I tend to like its default User model and the `users` table models that except the replacement of `username` by `email`. I also like using `UUID` for my primary key.

From the SQL codes, we have two simple tables &mdash; `users` and `user_profile`. `user_profile` has a `one-to-one` relationship with the `users` table since a user can only have one profile. We also created a custom datatype, `phone`, using SQL's `DOMAIN`. This allows us to give some constraints to any text that will be stored as phone number &mdash; [E.164][6] standard was used. Database indexes were also created.

That's it for this article. See you soon.

## Outro

Enjoyed this article? Consider [contacting me for a job, something worthwhile or buying a coffee ☕](mailto:sirneij@gmail.com). You can also connect with/follow me on [LinkedIn](https://www.linkedin.com/in/idogun-john-nelson/) and [Twitter](https://twitter.com/Sirneij). It isn't bad if you help share this article for wider coverage. I will appreciate it...

[1]: https://github.com/Sirneij/rust-auth/pull/3/files "Database and redis configurations added"
[2]: https://crates.io/crates/sqlx "The Rust SQL Toolkit. An async, pure Rust SQL crate featuring compile-time checked queries without a DSL. Supports PostgreSQL, MySQL, and SQLite."
[3]: https://crates.io/crates/diesel "A safe, extensible ORM and Query Builder for PostgreSQL, SQLite, and MySQL"
[4]: https://crates.io/crates/sqlx-cli "Command-line utility for SQLx, the Rust SQL toolkit."
[5]: https://crates.io/crates/deadpool-redis "Dead simple async pool for redis"
[6]: https://en.wikipedia.org/wiki/E.164 "ITU-T Recommendation E.164"