pub use crate::auth::user_trait::RuniqueUser;
use crate::utils::aliases::ADb;
use crate::utils::config::TraceResult;
use crate::utils::pk::Pk;
use crate::{impl_objects, search};
use sea_orm::{ActiveModelTrait, ActiveValue::Set, EntityTrait, entity::prelude::*};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, serde::Serialize, serde::Deserialize)]
#[sea_orm(table_name = "eihwaz_users")]
pub struct Model {
#[cfg_attr(feature = "pk-uuid", sea_orm(primary_key, auto_increment = false))]
#[cfg_attr(not(feature = "pk-uuid"), sea_orm(primary_key, auto_increment = true))]
pub id: Pk,
pub username: String,
pub email: String,
pub password: String,
pub is_active: bool,
pub is_staff: bool,
pub is_superuser: bool,
pub created_at: Option<chrono::NaiveDateTime>,
pub updated_at: Option<chrono::NaiveDateTime>,
pub activated_at: Option<chrono::NaiveDateTime>,
}
impl_objects!(Entity);
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "crate::auth::permissions::users_groupes::Entity")]
UsersGroupes,
#[sea_orm(has_many = "crate::middleware::session::session_db::Entity")]
Sessions,
}
impl Related<crate::auth::permissions::users_groupes::Entity> for Entity {
fn to() -> RelationDef {
Relation::UsersGroupes.def()
}
}
impl Related<crate::middleware::session::session_db::Entity> for Entity {
fn to() -> RelationDef {
Relation::Sessions.def()
}
}
#[async_trait::async_trait]
impl ActiveModelBehavior for ActiveModel {
async fn before_save<C>(mut self, _db: &C, insert: bool) -> Result<Self, DbErr>
where
C: ConnectionTrait,
{
#[cfg(feature = "pk-uuid")]
if insert && self.id.is_not_set() {
self.id = Set(uuid::Uuid::now_v7());
}
#[cfg(not(feature = "pk-uuid"))]
let _ = insert;
Ok(self)
}
}
impl RuniqueUser for Model {
fn user_id(&self) -> Pk {
self.id
}
fn username(&self) -> &str {
&self.username
}
fn email(&self) -> &str {
&self.email
}
fn is_active(&self) -> bool {
self.is_active
}
fn can_sign_in(&self) -> bool {
self.is_active && self.activated_at.is_some()
}
fn is_staff(&self) -> bool {
self.is_staff
}
fn is_superuser(&self) -> bool {
self.is_superuser
}
}
pub struct BuiltinUserEntity;
impl BuiltinUserEntity {
pub async fn find_by_id(db: &ADb, id: Pk) -> Option<Model> {
Entity::find_by_id(id)
.one(db)
.await
.trace(
crate::utils::runique_log::get_log()
.db
.as_ref()
.and_then(|d| d.query),
"find user by id",
)
.flatten()
}
pub async fn find_by_username(db: &ADb, username: &str) -> Option<Model> {
search!(Entity => Username eq username)
.first(db)
.await
.trace(
crate::utils::runique_log::get_log()
.db
.as_ref()
.and_then(|d| d.query),
"find user by username",
)
.flatten()
}
pub async fn find_by_email(db: &ADb, email: &str) -> Option<Model> {
search!(Entity => Email eq email)
.first(db)
.await
.trace(
crate::utils::runique_log::get_log()
.db
.as_ref()
.and_then(|d| d.query),
"find user by email",
)
.flatten()
}
pub async fn update_password_by_id(
db: &ADb,
id: Pk,
new_hash: &str,
) -> Result<(), sea_orm::DbErr> {
let user = Entity::find_by_id(id)
.one(db)
.await?
.ok_or_else(|| sea_orm::DbErr::RecordNotFound("User not found".into()))?;
let mut active: ActiveModel = user.into();
active.password = Set(new_hash.to_string());
active.update(db).await?;
Ok(())
}
pub async fn activate_account(db: &ADb, id: Pk) -> Result<Option<Model>, sea_orm::DbErr> {
let user = Entity::find_by_id(id)
.one(db)
.await?
.ok_or_else(|| sea_orm::DbErr::RecordNotFound("User not found".into()))?;
if user.activated_at.is_some() {
return Ok(None);
}
let mut active: ActiveModel = user.into();
active.is_active = Set(true);
active.activated_at = Set(Some(chrono::Utc::now().naive_utc()));
active.update(db).await.map(Some)
}
pub async fn set_password_and_activate(
db: &ADb,
id: Pk,
new_hash: &str,
) -> Result<(), sea_orm::DbErr> {
Self::update_password_by_id(db, id, new_hash).await?;
Self::activate_account(db, id).await?;
Ok(())
}
}
pub async fn authenticate_user(db: &ADb, username: &str, password: &str) -> Option<Model> {
let user_opt = BuiltinUserEntity::find_by_username(db, username).await;
let hash = user_opt
.as_ref()
.map(|u| u.password.as_str())
.unwrap_or(crate::utils::password::dummy_hash());
let password_ok = crate::utils::password::verify(password, hash);
if password_ok && let Some(user) = user_opt.filter(RuniqueUser::can_sign_in) {
Some(rehash_if_outdated(db, user, password).await)
} else {
None
}
}
async fn rehash_if_outdated(db: &ADb, mut user: Model, password: &str) -> Model {
if crate::utils::password::is_algorithm_current(&user.password) {
return user;
}
let rehashed = match crate::utils::password::hash(password) {
Ok(rehashed) => rehashed,
Err(e) => {
tracing::error!(user_id = %user.id, error = %e, "password rehash failed");
return user;
}
};
match BuiltinUserEntity::update_password_by_id(db, user.id, &rehashed).await {
Ok(()) => user.password = rehashed,
Err(e) => {
tracing::error!(user_id = %user.id, error = %e, "saving the rehashed password failed")
}
}
user
}
pub async fn authenticate_admin(db: &ADb, username: &str, password: &str) -> Option<Model> {
authenticate_user(db, username, password)
.await
.filter(RuniqueUser::can_access_admin)
}
pub fn schema() -> crate::migration::schema::ModelSchema {
#[cfg(feature = "pk-uuid")]
let pk = crate::migration::PrimaryKeyDef::new("id")
.uuid()
.no_auto_increment();
#[cfg(all(feature = "big-pk", not(feature = "pk-uuid")))]
let pk = crate::migration::PrimaryKeyDef::new("id")
.i64()
.auto_increment();
#[cfg(not(any(feature = "big-pk", feature = "pk-uuid")))]
let pk = crate::migration::PrimaryKeyDef::new("id")
.i32()
.auto_increment();
crate::migration::ModelSchema::new("EihwazUsers")
.table_name("eihwaz_users")
.primary_key(pk)
.column(
crate::migration::ColumnDef::new("username")
.varchar(150)
.required()
.unique(),
)
.column(
crate::migration::ColumnDef::new("email")
.varchar(254)
.required()
.unique(),
)
.column(
crate::migration::ColumnDef::new("password")
.string()
.required(),
)
.column(
crate::migration::ColumnDef::new("is_active")
.boolean()
.required(),
)
.column(
crate::migration::ColumnDef::new("is_staff")
.boolean()
.required(),
)
.column(
crate::migration::ColumnDef::new("is_superuser")
.boolean()
.required(),
)
.column(
crate::migration::ColumnDef::new("created_at")
.datetime()
.nullable(),
)
.column(
crate::migration::ColumnDef::new("updated_at")
.datetime()
.nullable(),
)
.column(
crate::migration::ColumnDef::new("activated_at")
.datetime()
.nullable()
.ignore(),
)
.build()
.unwrap()
}