creating an axum rest api app and integrating with authjs through adaptor

passwords? where we’re going, we dont need passwords.

toc

  1. create axum rest api app with loco
  2. creating pg-adaptor tables

create rest api axum app with loco:

Terminal window
cargo install loco-cli
cargo install sea-orm-cli
Terminal window
loco new
App name? · myapp
? ❯ What would you like to build?
lightweight-service (minimal, only controllers and views)
Rest API (with DB and user auth)
SaaS app (with DB and user auth)

creating pg-adaptor tables

https://authjs.dev/reference/pg-adapter

we need to following tables:

CREATE TABLE verification_token (
identifier TEXT NOT NULL,
expires TIMESTAMPTZ NOT NULL,
token TEXT NOT NULL,
PRIMARY KEY (identifier, token)
);
CREATE TABLE accounts (
id SERIAL,
"userId" INTEGER NOT NULL,
type VARCHAR(255) NOT NULL,
provider VARCHAR(255) NOT NULL,
"providerAccountId" VARCHAR(255) NOT NULL,
refresh_token TEXT,
access_token TEXT,
expires_at BIGINT,
id_token TEXT,
scope TEXT,
session_state TEXT,
token_type TEXT,
PRIMARY KEY (id)
);
CREATE TABLE sessions (
id SERIAL,
"userId" INTEGER NOT NULL,
expires TIMESTAMPTZ NOT NULL,
"sessionToken" VARCHAR(255) NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE users (
id SERIAL,
name VARCHAR(255),
email VARCHAR(255),
"emailVerified" TIMESTAMPTZ,
image TEXT,
PRIMARY KEY (id)
);

create migrations with loco:

the rest api template app from loco already created a users table. we need to update that, but first let’s create the verification_token, accounts and sessions tables. add the --migration-only flag to not migrate the db and create the rust entity because we need to do some modifications before migrating:

Terminal window
cargo loco generate model verification_token identifier:string! token:string! expires:tstz! --migration-only

by default loco creates created_at and updated_at timestamps for models

use sea_orm_migration::{prelude::*, schema::*};
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
// 👇 the table_auto command creates timestamps fields
table_auto(VerificationTokens::Table)
.col(pk_auto(VerificationTokens::Id))
.col(string(VerificationTokens::Identifier))
.col(string(VerificationTokens::Token))
.col(timestamp_with_time_zone(VerificationTokens::Expires))
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(VerificationTokens::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum VerificationTokens {
Table,
Id,
Identifier,
Token,
Expires,
}

lets update the default migration to remove the created_at and updated_at timestamps and create the composite primary key on primary_key and token:

diff --git a/backend/migration/src/m20240508_101645_verification_tokens.rs b/backend/migration/src/m20240508_101645_verification_tokens.rs
index ebaca79..850901e 100644
--- a/backend/migration/src/m20240508_101645_verification_tokens.rs
+++ b/backend/migration/src/m20240508_101645_verification_tokens.rs
@@ -8,8 +8,14 @@ impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
- table_auto(VerificationTokens::Table)
- .col(pk_auto(VerificationTokens::Id))
+ Table::create()
+ .table(VerificationTokens::Table)
+ .primary_key(
+ Index::create()
+ .name("verification_tokens_pkey")
+ .col(VerificationTokens::Identifier)
+ .col(VerificationTokens::Token),
+ )
.col(string(VerificationTokens::Identifier))
.col(string(VerificationTokens::Token))
.col(timestamp_with_time_zone(VerificationTokens::Expires))
@@ -28,7 +34,6 @@ impl MigrationTrait for Migration {
#[derive(DeriveIden)]
enum VerificationTokens {
Table,
- Id,
Identifier,
Token,
Expires,

run migration

Terminal window
cargo loco db migrate

update our rust models based on the schema

Terminal window
cargo loco db entities

or verification_token entity will now look like this:

src/models/_entities/verification_token.rs
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "verification_tokens")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub identifier: String,
#[sea_orm(primary_key, auto_increment = false)]
pub token: String,
pub expires: DateTimeWithTimeZone,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}

create migration for accounts

Terminal window
cargo loco generate model accounts userId:int! type:string! provider:string! providerAccountId:string! refresh_token:text access_token:text expires_at:big_int id_token:text scope:text session_state:text token_type:text --migration-only

remove created_at and updated_at fields:

migration/src/m20240508_114021_accounts.rs
use sea_orm_migration::{prelude::*, schema::*};
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
// table_auto(Accounts::Table)
Table::create()
.table(Accounts::Table)
.col(pk_auto(Accounts::Id))
.col(integer(Accounts::UserId))
.col(string(Accounts::Type))
.col(string(Accounts::Provider))
.col(string(Accounts::ProviderAccountId))
.col(text_null(Accounts::RefreshToken))
.col(text_null(Accounts::AccessToken))
.col(big_integer_null(Accounts::ExpiresAt))
.col(text_null(Accounts::IdToken))
.col(text_null(Accounts::Scope))
.col(text_null(Accounts::SessionState))
.col(text_null(Accounts::TokenType))
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Accounts::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Accounts {
Table,
Id,
UserId,
Type,
Provider,
ProviderAccountId,
RefreshToken,
AccessToken,
ExpiresAt,
IdToken,
Scope,
SessionState,
TokenType,
}

create migration for sessions.

Terminal window
cargo loco generate model sessions userId:int! expires:tstz! sessionToken:string! --migration-only

same drill: migration only, remove table_auto statement from default migration.

migration/src/m20240508_140410_sessions.rs
use sea_orm_migration::{prelude::*, schema::*};
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
// table_auto(Sessions::Table)
Table::create()
.table(Sessions::Table)
.col(pk_auto(Sessions::Id))
.col(integer(Sessions::UserId))
.col(timestamp_with_time_zone(Sessions::Expires))
.col(string(Sessions::SessionToken))
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Sessions::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Sessions {
Table,
Id,
UserId,
Expires,
SessionToken,
}

credits