Now we will add the ability for a User to have administrative privileges. Right now we do not have any way to tell a
normal User from an admin. We will add some boolean values to our User model where we can save the admin status for
it.
We can again drop the entire database from our PostgreSQL instance, but this time around let's take the ALTER TABLE
approach. This could serve as a great learning experience. Since in reality if we encountered similar problem, we
wouldn't want to delete all our user data.
Let's add a migration to alter the User model. We start by creating a migration file with sea-orm-cli,
sea-orm-cli migrate generate alter_user_table_add_admin_fieldsThis creates the migration file, m20240916_144220_alter_user_table_add_admin_fields.rs inside migration/src. Here's
what that directory looks like after running this command,
src
├── lib.rs
├── m20240913_193712_create_user_table.rs
├── m20240916_144220_alter_user_table_add_admin_fields.rs
└── main.rsOpening the m20240916_144220_alter_user_table_add_admin_fields.rs file we get the same default migration example *
Sea-ORM* creates for us. This is actually helpful. This serves as an example and also has few todo()s to hint you
where you need to start working.
We remove that and replace it with the following,
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table( // previously this was `.create_table()`
Table::alter()
.table(User::Table)
.add_column_if_not_exists(ColumnDef::new(User::IsAdmin).boolean().default(false))
.add_column_if_not_exists(ColumnDef::new(User::IsSuperadmin).boolean().default(false))
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(User::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum User {
Table,
IsAdmin,
IsSuperadmin,
}Then we add this migration to the list of migrations to run in migration/src/lib.rs,
pub use sea_orm_migration::prelude::*;
mod m20240913_193712_create_user_table;
mod m20240916_144220_alter_user_table_add_admin_fields; // just added
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m20240913_193712_create_user_table::Migration),
Box::new(m20240916_144220_alter_user_table_add_admin_fields::Migration), // just added
]
}
}After that, we rerun the migration,
sea-orm-cli migrate upThis should update the database and our User table should now contain the two additional fields we just added above.
Our entity and the rest of our code that uses this entity doesn't yet reflect that.
To update the entity, we can just run the entity creation command again,
sea-orm-cli generate entity -o entity/srcand this will update our entity with the newly added fields. Beware though, previously when I ran this command, it
deleted the entire entity directory and generated it again. Although the content seemed same to me, except for the
additional fields in the entity, Git for some reason saw the entire entity crate as new content and told me that I
had untracked changes in my working directory. This didn't affect the code, but it made my git commit history a bit
dirty. I am not sure that will not happen again, so fingers crossed.
Anyways, after the changes our User entity looks like this,
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0
use sea_orm::entity::prelude::*;
use serde::Serialize;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize)]
#[sea_orm(table_name = "user")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(unique)]
pub username: String,
pub firstname: Option<String>,
pub lastname: Option<String>,
#[sea_orm(unique)]
pub email: String,
pub password: String,
pub is_active: Option<bool>,
pub last_login: Option<DateTime>,
pub date_joined: Option<DateTime>,
pub created_at: Option<DateTime>,
pub updated_at: Option<DateTime>,
pub is_admin: Option<bool>, // new field
pub is_superadmin: Option<bool>, // new field
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}Now let's change our UserRequest model first. Open up src/users/models.rs and add the admin fields,
pub struct UserRequest {
pub username: String,
pub firstname: Option<String>,
pub lastname: Option<String>,
pub email: String,
pub password: String,
pub is_active: Option<bool>,
pub last_login: Option<NaiveDateTime>,
pub date_joined: Option<NaiveDateTime>,
pub created_at: Option<NaiveDateTime>,
pub updated_at: Option<NaiveDateTime>,
pub is_admin: Option<bool>, // new field
pub is_superadmin: Option<bool>, // new field
}Then we will update our UserSerializer. We will add the fields is_admin and is_superadmin to it and add utility
functions is_admin() and is_superadmin() accordingly. Here's the entire code block from serializers.rs,
use crate::users::models::UserRequest;
use actix_web::web::Json;
use entity::user::ActiveModel;
use sea_orm::ActiveValue;
pub struct UserSerializer {
pub data: Json<UserRequest>,
}
impl UserSerializer {
pub fn serialize(&self) -> ActiveModel {
let is_active = self.is_active();
let is_admin = self.is_admin(); // new field
let is_superadmin = self.is_superadmin(); // new field
let user = ActiveModel {
username: ActiveValue::Set(self.data.username.clone()),
firstname: ActiveValue::Set(self.data.firstname.clone()),
lastname: ActiveValue::Set(self.data.lastname.clone()),
email: ActiveValue::Set(String::from(self.data.email.clone())),
password: ActiveValue::Set(self.data.password.clone()),
is_active: ActiveValue::Set(Option::from(is_active)),
last_login: ActiveValue::Set(Option::from(self.data.last_login)),
date_joined: ActiveValue::Set(Option::from(self.data.date_joined)),
created_at: ActiveValue::Set(Option::from(self.data.created_at)),
updated_at: ActiveValue::Set(Option::from(self.data.updated_at)),
is_admin: ActiveValue::Set(Option::from(is_admin)), // new field
is_superadmin: ActiveValue::Set(Option::from(is_superadmin)), // new field
..Default::default()
};
user
}
fn is_active(&self) -> bool {
match self.data.is_active {
None => false,
Some(_) => true
}
}
// new utility function
fn is_admin(&self) -> bool {
match self.data.is_admin {
None => false,
Some(_) => true
}
}
// new utility function
fn is_superadmin(&self) -> bool {
match self.data.is_admin {
None => false,
Some(_) => true
}
}
}With this, we can just leave our create_user handler as is and run our server to test this. In the terminal hit,
cargo run from with the project root.
Head over to Postman, and send a POST request to http://localhost:8080/users/create with the following payload,
{
"username": "danny",
"firstname": "Daenerys",
"lastname": "Targaryen",
"email": "danny@not.danny",
"password": "123456",
"last_login": "2024-12-13T00:00:00",
"date_joined": "2024-11-15T00:00:00",
"created_at": "2024-10-17T00:00:00",
"updated_at": "2024-09-19T00:00:00"
}You should get a response like this,
{
"id": 29,
"username": "danny",
"firstname": "Daenerys",
"lastname": "Targaryen",
"email": "danny@not.danny",
"password": "123456",
"is_active": false,
"last_login": "2024-12-13T00:00:00",
"date_joined": "2024-11-15T00:00:00",
"created_at": "2024-10-17T00:00:00",
"updated_at": "2024-09-19T00:00:00",
"is_admin": false,
"is_superadmin": false
}Our response contains, is_admin and is_superadmin. So we can be sure that our code works. Now that I remember, we
actually set default values for these fields in our migration, adding the functions is_admin() and is_superadmin()
wasn't really necessary, we can get rid of them.