feat: complete Phase 0 Enterprise IAM

This commit is contained in:
thakares committed 2026-07-21 15:26:16 +05:30
1 parent 3d2d291006
commit c2f5ba3f54
202 files changed
+21371 -1360

No files matched your search

+110 -12
View File
@@ -1,31 +1,129 @@
use sqlx::SqlitePool;
use crate::{
db::{models::Application, repository::applications as repo},
error::AppError,
};
use crate::{db::models::Application, error::AppError};
pub async fn create(
pool: &SqlitePool,
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
tenant_id: &str,
name: &str,
slug: &str,
) -> Result<Application, AppError> {
let name = name.trim();
let slug = slug.trim();
if name.is_empty() || slug.is_empty() {
return Err(AppError::InvalidInput(
"name and slug cannot be empty".into(),
));
}
if provider
.applications()
.find_by_slug(slug)
.await
.map_err(AppError::Database)?
.is_some()
{
return Err(AppError::Conflict(format!("slug '{slug}' already exists")));
}
let id = uuid::Uuid::new_v4().to_string();
repo::create(pool, &id, tenant_id, name, slug)
provider
.applications()
.create(&id, tenant_id, name, slug)
.await
.map_err(AppError::Database)
}
pub async fn list(pool: &SqlitePool, tenant_id: &str) -> Result<Vec<Application>, AppError> {
repo::list(pool, tenant_id)
pub async fn list(
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
tenant_id: &str,
) -> Result<Vec<Application>, AppError> {
provider
.applications()
.list(tenant_id)
.await
.map_err(AppError::Database)
}
pub async fn find_by_slug(pool: &SqlitePool, slug: &str) -> Result<Application, AppError> {
repo::find_by_slug(pool, slug)
pub async fn get(
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
id: &str,
) -> Result<Application, AppError> {
provider
.applications()
.find_by_id(id)
.await
.map_err(AppError::Database)?
.ok_or(AppError::NotFound)
}
pub async fn find_by_slug(
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
slug: &str,
) -> Result<Application, AppError> {
provider
.applications()
.find_by_slug(slug)
.await
.map_err(AppError::Database)?
.ok_or(AppError::NotFound)
}
pub async fn update(
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
id: &str,
name: &str,
slug: &str,
enabled: bool,
) -> Result<Application, AppError> {
let _ = provider.applications().find_by_id(id).await?;
let name = name.trim();
let slug = slug.trim();
if name.is_empty() || slug.is_empty() {
return Err(AppError::InvalidInput(
"name and slug cannot be empty".into(),
));
}
if let Some(other) = provider
.applications()
.find_by_slug(slug)
.await
.map_err(AppError::Database)?
{
if other.id != id {
return Err(AppError::Conflict(format!("slug '{slug}' already exists")));
}
}
provider
.applications()
.update(id, name, slug, enabled)
.await
.map_err(AppError::Database)?;
provider
.applications()
.find_by_id(id)
.await
.map_err(crate::error::AppError::Database)?
.ok_or_else(|| crate::error::AppError::NotFound)
}
pub async fn set_enabled(
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
id: &str,
enabled: bool,
) -> Result<(), AppError> {
let _ = provider.applications().find_by_id(id).await?;
provider
.applications()
.set_enabled(id, enabled)
.await
.map_err(AppError::Database)
}
pub async fn delete(
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
id: &str,
) -> Result<(), AppError> {
let _ = provider.applications().find_by_id(id).await?;
provider
.applications()
.delete(id)
.await
.map_err(AppError::Database)
}
+90 -8
View File
@@ -1,35 +1,117 @@
use sqlx::SqlitePool;
use crate::db::repository::traits::AuditRepositoryExt;
use crate::{db::repository::permissions as repo, error::AppError};
use crate::{db::models::Permission, error::AppError};
/// Return all permission names held by a user.
pub async fn list_user_permissions(
pool: &SqlitePool,
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
user_id: &str,
) -> Result<Vec<String>, AppError> {
repo::list_for_user(pool, user_id)
provider
.permissions()
.list_for_user(user_id)
.await
.map_err(AppError::Database)
}
/// List all system permissions.
pub async fn list_permissions(
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
) -> Result<Vec<Permission>, AppError> {
provider
.permissions()
.list_all()
.await
.map_err(AppError::Database)
}
/// List permissions for a role.
pub async fn list_role_permissions(
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
role_id: &str,
) -> Result<Vec<Permission>, AppError> {
provider
.permissions()
.list_for_role(role_id)
.await
.map_err(AppError::Database)
}
/// Set the full permission set for a role (replace semantics).
pub async fn set_role_permissions(
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
role_id: &str,
permission_names: &[String],
audit_actor_id: Option<&str>,
audit_ip: Option<&str>,
audit_ua: Option<&str>,
) -> Result<Vec<Permission>, AppError> {
// Resolve names → ids
let mut permission_ids = Vec::with_capacity(permission_names.len());
for name in permission_names {
let perm = provider
.permissions()
.find_by_name(name)
.await
.map_err(AppError::Database)?
.ok_or_else(|| AppError::InvalidInput(format!("unknown permission: {name}")))?;
permission_ids.push(perm.id);
}
// Clear existing assignments
provider
.permissions()
.clear_for_role(role_id)
.await
.map_err(AppError::Database)?;
for pid in &permission_ids {
provider
.permissions()
.assign_to_role(role_id, pid)
.await
.map_err(AppError::Database)?;
}
let metadata = serde_json::json!({ "permissions": permission_names }).to_string();
provider
.audit()
.log(crate::audit::AuditEvent {
actor_id: audit_actor_id,
target_id: None,
action: "role_permissions_updated",
resource_type: "role",
resource_id: Some(role_id),
severity: crate::db::models::AuditSeverity::Info,
ip: audit_ip,
ua: audit_ua,
metadata: Some(&metadata),
})
.await?;
list_role_permissions(provider, role_id).await
}
/// Returns true if the user holds the given named permission.
pub async fn has_permission(
pool: &SqlitePool,
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
user_id: &str,
permission: &str,
) -> Result<bool, AppError> {
repo::user_has_permission(pool, user_id, permission)
provider
.permissions()
.user_has_permission(user_id, permission)
.await
.map_err(AppError::Database)
}
/// Enforce that a user holds a permission, returning `Forbidden` otherwise.
pub async fn require_permission(
pool: &SqlitePool,
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
user_id: &str,
permission: &str,
) -> Result<(), AppError> {
if has_permission(pool, user_id, permission).await? {
if has_permission(provider, user_id, permission).await? {
Ok(())
} else {
Err(AppError::Forbidden)
+219 -38
View File
@@ -1,34 +1,33 @@
use sqlx::SqlitePool;
use crate::db::repository::traits::AuditRepositoryExt;
use crate::{
db::{models::Role, repository::roles as repo},
error::AppError,
};
use crate::{db::models::Role, error::AppError};
/// Assign a named role to a user. No-ops if already assigned.
pub async fn assign_role(
pool: &SqlitePool,
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
user_id: &str,
role_name: &str,
audit_actor_id: Option<&str>,
audit_ip: Option<&str>,
audit_ua: Option<&str>,
) -> Result<(), AppError> {
let role = repo::find_by_name(pool, role_name)
let role = provider
.roles()
.find_by_name(role_name)
.await
.map_err(AppError::Database)?
.ok_or(AppError::NotFound)?;
let mut tx = pool.begin().await.map_err(AppError::Database)?;
repo::assign_to_user(&mut tx, user_id, &role.id)
provider
.roles()
.assign_to_user(user_id, &role.id)
.await
.map_err(AppError::Database)?;
let metadata = serde_json::json!({ "role": role_name }).to_string();
crate::audit::log(
&mut tx,
crate::audit::AuditEvent {
provider
.audit()
.log(crate::audit::AuditEvent {
actor_id: audit_actor_id,
target_id: Some(user_id),
action: "role_assigned",
@@ -38,11 +37,8 @@ pub async fn assign_role(
ip: audit_ip,
ua: audit_ua,
metadata: Some(&metadata),
},
)
.await?;
tx.commit().await.map_err(AppError::Database)?;
})
.await?;
tracing::info!(user_id = %user_id, role = %role_name, "role assigned");
Ok(())
@@ -50,28 +46,30 @@ pub async fn assign_role(
/// Remove a named role from a user. No-ops if not assigned.
pub async fn remove_role(
pool: &SqlitePool,
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
user_id: &str,
role_name: &str,
audit_actor_id: Option<&str>,
audit_ip: Option<&str>,
audit_ua: Option<&str>,
) -> Result<(), AppError> {
let role = repo::find_by_name(pool, role_name)
let role = provider
.roles()
.find_by_name(role_name)
.await
.map_err(AppError::Database)?
.ok_or(AppError::NotFound)?;
let mut tx = pool.begin().await.map_err(AppError::Database)?;
repo::remove_from_user(&mut tx, user_id, &role.id)
provider
.roles()
.remove_from_user(user_id, &role.id)
.await
.map_err(AppError::Database)?;
let metadata = serde_json::json!({ "role": role_name }).to_string();
crate::audit::log(
&mut tx,
crate::audit::AuditEvent {
provider
.audit()
.log(crate::audit::AuditEvent {
actor_id: audit_actor_id,
target_id: Some(user_id),
action: "role_removed",
@@ -81,24 +79,207 @@ pub async fn remove_role(
ip: audit_ip,
ua: audit_ua,
metadata: Some(&metadata),
},
)
.await?;
tx.commit().await.map_err(AppError::Database)?;
})
.await?;
tracing::info!(user_id = %user_id, role = %role_name, "role removed");
Ok(())
}
/// List all roles defined in the system.
pub async fn list_roles(pool: &SqlitePool) -> Result<Vec<Role>, AppError> {
repo::list_all(pool).await.map_err(AppError::Database)
}
/// List roles held by a specific user.
pub async fn list_user_roles(pool: &SqlitePool, user_id: &str) -> Result<Vec<Role>, AppError> {
repo::list_for_user(pool, user_id)
pub async fn list_roles(
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
) -> Result<Vec<Role>, AppError> {
provider
.roles()
.list_all()
.await
.map_err(AppError::Database)
}
/// List roles held by a specific user.
pub async fn list_user_roles(
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
user_id: &str,
) -> Result<Vec<Role>, AppError> {
provider
.roles()
.list_for_user(user_id)
.await
.map_err(AppError::Database)
}
/// Create a new role.
pub async fn create_role(
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
name: &str,
description: Option<&str>,
audit_actor_id: Option<&str>,
audit_ip: Option<&str>,
audit_ua: Option<&str>,
) -> Result<Role, AppError> {
let name = name.trim();
if name.is_empty() {
return Err(AppError::InvalidInput("role name cannot be empty".into()));
}
if provider
.roles()
.find_by_name(name)
.await
.map_err(AppError::Database)?
.is_some()
{
return Err(AppError::Conflict(format!("role '{name}' already exists")));
}
let id = uuid::Uuid::new_v4().to_string();
let role = provider
.roles()
.create(&id, name, description)
.await
.map_err(AppError::Database)?;
provider
.audit()
.log(crate::audit::AuditEvent {
actor_id: audit_actor_id,
target_id: None,
action: "role_created",
resource_type: "role",
resource_id: Some(&role.id),
severity: crate::db::models::AuditSeverity::Info,
ip: audit_ip,
ua: audit_ua,
metadata: Some(&format!(r#"{{"name":"{name}"}}"#)),
})
.await?;
Ok(role)
}
/// Update an existing role.
pub async fn update_role(
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
id: &str,
name: &str,
description: Option<&str>,
audit_actor_id: Option<&str>,
audit_ip: Option<&str>,
audit_ua: Option<&str>,
) -> Result<Role, AppError> {
let name = name.trim();
if name.is_empty() {
return Err(AppError::InvalidInput("role name cannot be empty".into()));
}
let existing = provider
.roles()
.find_by_id(id)
.await
.map_err(AppError::Database)?
.ok_or(AppError::NotFound)?;
// Protect built-in admin role rename
if existing.name == "admin" && name != "admin" {
return Err(AppError::InvalidInput(
"cannot rename the built-in admin role".into(),
));
}
if let Some(other) = provider
.roles()
.find_by_name(name)
.await
.map_err(AppError::Database)?
{
if other.id != id {
return Err(AppError::Conflict(format!("role '{name}' already exists")));
}
}
provider
.roles()
.update(id, name, description)
.await
.map_err(AppError::Database)?;
provider
.audit()
.log(crate::audit::AuditEvent {
actor_id: audit_actor_id,
target_id: None,
action: "role_updated",
resource_type: "role",
resource_id: Some(id),
severity: crate::db::models::AuditSeverity::Info,
ip: audit_ip,
ua: audit_ua,
metadata: None,
})
.await?;
provider
.roles()
.find_by_id(id)
.await
.map_err(AppError::Database)?
.ok_or(AppError::NotFound)
}
/// Delete a role (cannot delete admin).
pub async fn delete_role(
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
id: &str,
audit_actor_id: Option<&str>,
audit_ip: Option<&str>,
audit_ua: Option<&str>,
) -> Result<(), AppError> {
let role = provider
.roles()
.find_by_id(id)
.await
.map_err(AppError::Database)?
.ok_or(AppError::NotFound)?;
if role.name == "admin" {
return Err(AppError::InvalidInput(
"cannot delete the built-in admin role".into(),
));
}
provider
.roles()
.delete(id)
.await
.map_err(AppError::Database)?;
provider
.audit()
.log(crate::audit::AuditEvent {
actor_id: audit_actor_id,
target_id: None,
action: "role_deleted",
resource_type: "role",
resource_id: Some(id),
severity: crate::db::models::AuditSeverity::Warning,
ip: audit_ip,
ua: audit_ua,
metadata: Some(&format!(r#"{{"name":"{}"}}"#, role.name)),
})
.await?;
Ok(())
}
/// Get a role by id.
pub async fn get_role(
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
id: &str,
) -> Result<Role, AppError> {
provider
.roles()
.find_by_id(id)
.await
.map_err(AppError::Database)?
.ok_or(AppError::NotFound)
}
+110 -27
View File
@@ -1,12 +1,9 @@
use sqlx::SqlitePool;
use crate::db::repository::traits::AuditRepositoryExt;
use crate::{
db::{models::ServiceAccount, repository::service_accounts as repo},
error::AppError,
};
use crate::{db::models::ServiceAccount, error::AppError};
pub async fn create(
pool: &SqlitePool,
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
tenant_id: &str,
name: &str,
description: Option<&str>,
@@ -15,15 +12,16 @@ pub async fn create(
audit_ua: Option<&str>,
) -> Result<ServiceAccount, AppError> {
let id = uuid::Uuid::new_v4().to_string();
let mut tx = pool.begin().await.map_err(AppError::Database)?;
let sa = repo::create(&mut tx, &id, tenant_id, name, description)
let sa = provider
.service_accounts()
.create(&id, tenant_id, name, description)
.await
.map_err(AppError::Database)?;
crate::audit::log(
&mut tx,
crate::audit::AuditEvent {
provider
.audit()
.log(crate::audit::AuditEvent {
actor_id: audit_actor_id,
target_id: None,
action: "service_account_created",
@@ -33,31 +31,36 @@ pub async fn create(
ip: audit_ip,
ua: audit_ua,
metadata: None,
},
)
.await?;
})
.await?;
tx.commit().await.map_err(AppError::Database)?;
Ok(sa)
}
pub async fn list(pool: &SqlitePool, tenant_id: &str) -> Result<Vec<ServiceAccount>, AppError> {
repo::list(pool, tenant_id)
pub async fn list(
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
tenant_id: &str,
) -> Result<Vec<ServiceAccount>, AppError> {
provider
.service_accounts()
.list(tenant_id)
.await
.map_err(AppError::Database)
}
pub async fn set_enabled(
pool: &SqlitePool,
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
id: &str,
enabled: bool,
audit_actor_id: Option<&str>,
audit_ip: Option<&str>,
audit_ua: Option<&str>,
) -> Result<(), AppError> {
let mut tx = pool.begin().await.map_err(AppError::Database)?;
let _ = provider.service_accounts().find_by_id(id).await?;
repo::set_enabled(&mut tx, id, enabled)
provider
.service_accounts()
.set_enabled(id, enabled)
.await
.map_err(AppError::Database)?;
@@ -67,9 +70,9 @@ pub async fn set_enabled(
"service_account_disabled"
};
crate::audit::log(
&mut tx,
crate::audit::AuditEvent {
provider
.audit()
.log(crate::audit::AuditEvent {
actor_id: audit_actor_id,
target_id: None,
action,
@@ -79,10 +82,90 @@ pub async fn set_enabled(
ip: audit_ip,
ua: audit_ua,
metadata: None,
},
)
.await?;
})
.await?;
tx.commit().await.map_err(AppError::Database)?;
Ok(())
}
pub async fn get(
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
id: &str,
) -> Result<ServiceAccount, AppError> {
provider
.service_accounts()
.find_by_id(id)
.await
.map_err(AppError::Database)?
.ok_or(AppError::NotFound)
}
pub async fn delete(
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
id: &str,
audit_actor_id: Option<&str>,
audit_ip: Option<&str>,
audit_ua: Option<&str>,
) -> Result<(), AppError> {
let _ = provider.service_accounts().find_by_id(id).await?;
provider
.service_accounts()
.delete(id)
.await
.map_err(AppError::Database)?;
provider
.audit()
.log(crate::audit::AuditEvent {
actor_id: audit_actor_id,
target_id: None,
action: "service_account_deleted",
resource_type: "service_account",
resource_id: Some(id),
severity: crate::db::models::AuditSeverity::Warning,
ip: audit_ip,
ua: audit_ua,
metadata: None,
})
.await?;
Ok(())
}
/// Generate a one-time display secret for a service account.
///
/// The raw secret is returned once; only a BLAKE3 hash is stored in audit metadata
/// until a dedicated secrets table is introduced (OAuth2 milestone).
pub async fn generate_secret(
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
id: &str,
audit_actor_id: Option<&str>,
audit_ip: Option<&str>,
audit_ua: Option<&str>,
) -> Result<String, AppError> {
let _ = provider.service_accounts().find_by_id(id).await?;
let mut bytes = [0u8; 32];
rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut bytes);
let raw = format!("nx9sa_{}", hex::encode(bytes));
let hash = hex::encode(blake3::hash(raw.as_bytes()).as_bytes());
let metadata = serde_json::json!({ "secret_hash": hash }).to_string();
provider
.audit()
.log(crate::audit::AuditEvent {
actor_id: audit_actor_id,
target_id: None,
action: "service_account_secret_rotated",
resource_type: "service_account",
resource_id: Some(id),
severity: crate::db::models::AuditSeverity::Warning,
ip: audit_ip,
ua: audit_ua,
metadata: Some(&metadata),
})
.await?;
Ok(raw)
}
+58 -52
View File
@@ -1,18 +1,13 @@
use sqlx::SqlitePool;
use crate::db::repository::traits::AuditRepositoryExt;
use crate::{
config::SecurityConfig,
db::{models::User, repository::users as repo},
error::AppError,
security::passwords,
};
use crate::{config::SecurityConfig, db::models::User, error::AppError, security::passwords};
/// Create a new user account in the given tenant.
///
/// Fails with `Conflict` if the username is already taken.
#[allow(clippy::too_many_arguments)]
pub async fn create_user(
pool: &SqlitePool,
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
cfg: &SecurityConfig,
tenant_id: &str,
username: &str,
@@ -26,7 +21,9 @@ pub async fn create_user(
}
passwords::validate_password_strength(password, false)?;
if repo::username_exists(pool, tenant_id, username)
if provider
.users()
.username_exists(tenant_id, username)
.await
.map_err(AppError::Database)?
{
@@ -38,15 +35,15 @@ pub async fn create_user(
let id = uuid::Uuid::new_v4().to_string();
let hash = passwords::hash_password(password, cfg)?;
let mut tx = pool.begin().await.map_err(AppError::Database)?;
let user = repo::create(&mut tx, &id, tenant_id, username, &hash)
let user = provider
.users()
.create(&id, tenant_id, username, &hash)
.await
.map_err(AppError::Database)?;
crate::audit::log(
&mut tx,
crate::audit::AuditEvent {
provider
.audit()
.log(crate::audit::AuditEvent {
actor_id: audit_actor_id,
target_id: Some(&user.id),
action: "user_created",
@@ -56,42 +53,54 @@ pub async fn create_user(
ip: audit_ip,
ua: audit_ua,
metadata: None,
},
)
.await?;
tx.commit().await.map_err(AppError::Database)?;
})
.await?;
tracing::info!(user_id = %user.id, username = %username, "user created");
Ok(user)
}
/// Retrieve a user by ID.
pub async fn get_user(pool: &SqlitePool, id: &str) -> Result<User, AppError> {
repo::find_by_id(pool, id)
pub async fn get_user(
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
id: &str,
) -> Result<User, AppError> {
provider
.users()
.find_by_id(id)
.await
.map_err(AppError::Database)?
.ok_or(AppError::NotFound)
}
/// Retrieve a user by username.
pub async fn get_user_by_username(pool: &SqlitePool, username: &str) -> Result<User, AppError> {
repo::find_by_username(pool, username)
pub async fn get_user_by_username(
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
username: &str,
) -> Result<User, AppError> {
provider
.users()
.find_by_username(username)
.await
.map_err(AppError::Database)?
.ok_or(AppError::NotFound)
}
/// List all users in a tenant.
pub async fn list_users(pool: &SqlitePool, tenant_id: &str) -> Result<Vec<User>, AppError> {
repo::list(pool, tenant_id)
pub async fn list_users(
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
tenant_id: &str,
) -> Result<Vec<User>, AppError> {
provider
.users()
.list(tenant_id)
.await
.map_err(AppError::Database)
}
/// Set a user's status (Active=1, Disabled=2, Locked=3).
pub async fn update_status(
pool: &SqlitePool,
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
user_id: &str,
status: i32,
audit_actor_id: Option<&str>,
@@ -99,11 +108,11 @@ pub async fn update_status(
audit_ua: Option<&str>,
) -> Result<(), AppError> {
// Verify user exists first
let _user = get_user(pool, user_id).await?;
let _user = provider.users().find_by_id(user_id).await?;
let mut tx = pool.begin().await.map_err(AppError::Database)?;
repo::update_status(&mut tx, user_id, status)
provider
.users()
.update_status(user_id, status)
.await
.map_err(AppError::Database)?;
@@ -119,9 +128,9 @@ pub async fn update_status(
_ => crate::db::models::AuditSeverity::Warning,
};
crate::audit::log(
&mut tx,
crate::audit::AuditEvent {
provider
.audit()
.log(crate::audit::AuditEvent {
actor_id: audit_actor_id,
target_id: Some(user_id),
action,
@@ -131,18 +140,16 @@ pub async fn update_status(
ip: audit_ip,
ua: audit_ua,
metadata: None,
},
)
.await?;
})
.await?;
tx.commit().await.map_err(AppError::Database)?;
tracing::info!(user_id = %user_id, status = %status, "user status updated");
Ok(())
}
/// Reset a user's password.
pub async fn reset_password(
pool: &SqlitePool,
provider: &std::sync::Arc<dyn crate::db::provider::DatabaseProvider>,
cfg: &SecurityConfig,
user_id: &str,
new_password: &str,
@@ -150,23 +157,25 @@ pub async fn reset_password(
audit_ip: Option<&str>,
audit_ua: Option<&str>,
) -> Result<(), AppError> {
let user = get_user(pool, user_id).await?;
let user_roles = crate::db::repository::roles::list_for_user(pool, &user.id)
let user = provider.users().find_by_id(user_id).await?;
let user_roles = provider
.roles()
.list_for_user(&user.unwrap().id)
.await
.map_err(AppError::Database)?;
let is_admin = user_roles.iter().any(|r| r.name == "admin");
passwords::validate_password_strength(new_password, is_admin)?;
let hash = passwords::hash_password(new_password, cfg)?;
let mut tx = pool.begin().await.map_err(AppError::Database)?;
repo::update_password_hash(&mut tx, user_id, &hash)
provider
.users()
.update_password_hash(user_id, &hash)
.await
.map_err(AppError::Database)?;
crate::audit::log(
&mut tx,
crate::audit::AuditEvent {
provider
.audit()
.log(crate::audit::AuditEvent {
actor_id: audit_actor_id,
target_id: Some(user_id),
action: "password_reset",
@@ -176,11 +185,8 @@ pub async fn reset_password(
ip: audit_ip,
ua: audit_ua,
metadata: None,
},
)
.await?;
tx.commit().await.map_err(AppError::Database)?;
})
.await?;
tracing::info!(user_id = %user_id, "password reset");
Ok(())