Skip to content

Latest commit

 

History

History
269 lines (222 loc) · 8.7 KB

File metadata and controls

269 lines (222 loc) · 8.7 KB

Adding Admin fields to User

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_fields

This 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.rs

Opening 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 up

This 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/src

and 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.