rustpbx 0.3.18

A SIP PBX implementation in Rust
Documentation
use anyhow::Context;
use anyhow::{Result, ensure};
use argon2::Argon2;
use argon2::PasswordHasher;
use argon2::password_hash::SaltString;
use argon2::password_hash::rand_core::OsRng;
use chrono::Utc;
use sea_orm::ActiveValue::Set;
use sea_orm::entity::prelude::*;
use sea_orm_migration::prelude::*;
use sea_orm_migration::schema::{
    boolean, string, string_null, string_uniq, timestamp, timestamp_null,
};
use sea_orm_migration::sea_query::ColumnDef;
use serde::Serialize;

#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize)]
#[sea_orm(table_name = "rustpbx_users")]
pub struct Model {
    #[sea_orm(primary_key, auto_increment = true)]
    pub id: i64,
    #[sea_orm(unique)]
    pub email: String,
    #[sea_orm(unique)]
    pub username: String,
    #[serde(skip_serializing)]
    pub password_hash: String,
    #[serde(skip_serializing)]
    pub reset_token: Option<String>,
    #[serde(skip_serializing)]
    pub reset_token_expires: Option<DateTimeUtc>,
    pub last_login_at: Option<DateTimeUtc>,
    pub last_login_ip: Option<String>,
    pub created_at: DateTimeUtc,
    pub updated_at: DateTimeUtc,
    pub is_active: bool,
    pub is_staff: bool,
    pub is_superuser: bool,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}

impl ActiveModelBehavior for ActiveModel {}

impl Model {
    pub fn token_expired(&self) -> bool {
        match (self.reset_token.as_ref(), self.reset_token_expires) {
            (Some(_), Some(expiry)) => expiry < Utc::now(),
            _ => true,
        }
    }

    pub async fn upsert_super_user(
        db: &DatabaseConnection,
        username: &str,
        email: &str,
        password: &str,
    ) -> Result<Model> {
        let username = username.trim();
        let email = email.trim().to_lowercase();
        ensure!(!username.is_empty(), "username is required");
        ensure!(!email.is_empty(), "email is required");
        ensure!(!password.is_empty(), "password is required");

        let salt = SaltString::generate(&mut OsRng);
        let hashed = Argon2::default()
            .hash_password(password.as_bytes(), &salt)
            .map_err(|e| anyhow::anyhow!("failed to hash password: {}", e))?
            .to_string();

        let now = Utc::now();

        let mut user = Entity::find()
            .filter(Column::Username.eq(username))
            .one(db)
            .await
            .with_context(|| format!("failed to lookup user by username: {}", username))?;

        if user.is_none() {
            if let Some(existing) = Entity::find()
                .filter(Column::Email.eq(email.clone()))
                .one(db)
                .await
                .with_context(|| format!("failed to lookup user by email: {}", email))?
            {
                user = Some(existing);
            }
        }

        if let Some(user) = user {
            let mut model: ActiveModel = user.into();
            model.username = Set(username.to_string());
            model.email = Set(email.clone());
            model.password_hash = Set(hashed);
            model.is_active = Set(true);
            model.is_staff = Set(true);
            model.is_superuser = Set(true);
            model.reset_token = Set(None);
            model.reset_token_expires = Set(None);
            model.updated_at = Set(now);
            model
                .update(db)
                .await
                .context("failed to update super user")
        } else {
            let mut model: ActiveModel = Default::default();
            model.username = Set(username.to_string());
            model.email = Set(email.clone());
            model.password_hash = Set(hashed);
            model.created_at = Set(now);
            model.updated_at = Set(now);
            model.is_active = Set(true);
            model.is_staff = Set(true);
            model.is_superuser = Set(true);
            model.reset_token = Set(None);
            model.reset_token_expires = Set(None);
            model
                .insert(db)
                .await
                .context("failed to create super user")
        }
    }
}

#[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::create()
                    .table(Entity)
                    .if_not_exists()
                    .col(
                        ColumnDef::new(Column::Id)
                            .big_integer()
                            .auto_increment()
                            .primary_key(),
                    )
                    .col(string_uniq(Column::Email).char_len(255))
                    .col(string_uniq(Column::Username).char_len(100))
                    .col(string(Column::PasswordHash).char_len(255))
                    .col(string_null(Column::ResetToken).char_len(128))
                    .col(timestamp_null(Column::ResetTokenExpires))
                    .col(timestamp_null(Column::LastLoginAt))
                    .col(string_null(Column::LastLoginIp).char_len(128))
                    .col(timestamp(Column::CreatedAt).default(Expr::current_timestamp()))
                    .col(timestamp(Column::UpdatedAt).default(Expr::current_timestamp()))
                    .col(boolean(Column::IsActive).default(true))
                    .col(boolean(Column::IsStaff).default(false))
                    .col(boolean(Column::IsSuperuser).default(false))
                    .to_owned(),
            )
            .await?;
        Ok(())
    }

    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager
            .drop_table(Table::drop().table(Entity).to_owned())
            .await
    }
}