Initial public release v0.1.0

This commit is contained in:
thakares committed 2026-06-21 20:05:29 +05:30
commit 6c39e0bfbf
87 files changed
+10867

No files matched your search

+31
View File
@@ -0,0 +1,31 @@
use sqlx::SqlitePool;
use crate::{
db::{models::Application, repository::applications as repo},
error::AppError,
};
pub async fn create(
pool: &SqlitePool,
tenant_id: &str,
name: &str,
slug: &str,
) -> Result<Application, AppError> {
let id = uuid::Uuid::new_v4().to_string();
repo::create(pool, &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)
.await
.map_err(AppError::Database)
}
pub async fn find_by_slug(pool: &SqlitePool, slug: &str) -> Result<Application, AppError> {
repo::find_by_slug(pool, slug)
.await
.map_err(AppError::Database)?
.ok_or(AppError::NotFound)
}
+5
View File
@@ -0,0 +1,5 @@
pub mod applications;
pub mod permissions;
pub mod roles;
pub mod service_accounts;
pub mod users;
+37
View File
@@ -0,0 +1,37 @@
use sqlx::SqlitePool;
use crate::{db::repository::permissions as repo, error::AppError};
/// Return all permission names held by a user.
pub async fn list_user_permissions(
pool: &SqlitePool,
user_id: &str,
) -> Result<Vec<String>, AppError> {
repo::list_for_user(pool, user_id)
.await
.map_err(AppError::Database)
}
/// Returns true if the user holds the given named permission.
pub async fn has_permission(
pool: &SqlitePool,
user_id: &str,
permission: &str,
) -> Result<bool, AppError> {
repo::user_has_permission(pool, 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,
user_id: &str,
permission: &str,
) -> Result<(), AppError> {
if has_permission(pool, user_id, permission).await? {
Ok(())
} else {
Err(AppError::Forbidden)
}
}
+104
View File
@@ -0,0 +1,104 @@
use sqlx::SqlitePool;
use crate::{
db::{models::Role, repository::roles as repo},
error::AppError,
};
/// Assign a named role to a user. No-ops if already assigned.
pub async fn assign_role(
pool: &SqlitePool,
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)
.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)
.await
.map_err(AppError::Database)?;
let metadata = serde_json::json!({ "role": role_name }).to_string();
crate::audit::log(
&mut tx,
crate::audit::AuditEvent {
actor_id: audit_actor_id,
target_id: Some(user_id),
action: "role_assigned",
resource_type: "role",
resource_id: Some(&role.id),
severity: crate::db::models::AuditSeverity::Info,
ip: audit_ip,
ua: audit_ua,
metadata: Some(&metadata),
},
)
.await?;
tx.commit().await.map_err(AppError::Database)?;
tracing::info!(user_id = %user_id, role = %role_name, "role assigned");
Ok(())
}
/// Remove a named role from a user. No-ops if not assigned.
pub async fn remove_role(
pool: &SqlitePool,
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)
.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)
.await
.map_err(AppError::Database)?;
let metadata = serde_json::json!({ "role": role_name }).to_string();
crate::audit::log(
&mut tx,
crate::audit::AuditEvent {
actor_id: audit_actor_id,
target_id: Some(user_id),
action: "role_removed",
resource_type: "role",
resource_id: Some(&role.id),
severity: crate::db::models::AuditSeverity::Info,
ip: audit_ip,
ua: audit_ua,
metadata: Some(&metadata),
},
)
.await?;
tx.commit().await.map_err(AppError::Database)?;
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)
.await
.map_err(AppError::Database)
}
+88
View File
@@ -0,0 +1,88 @@
use sqlx::SqlitePool;
use crate::{
db::{models::ServiceAccount, repository::service_accounts as repo},
error::AppError,
};
pub async fn create(
pool: &SqlitePool,
tenant_id: &str,
name: &str,
description: Option<&str>,
audit_actor_id: Option<&str>,
audit_ip: Option<&str>,
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)
.await
.map_err(AppError::Database)?;
crate::audit::log(
&mut tx,
crate::audit::AuditEvent {
actor_id: audit_actor_id,
target_id: None,
action: "service_account_created",
resource_type: "service_account",
resource_id: Some(&sa.id),
severity: crate::db::models::AuditSeverity::Info,
ip: audit_ip,
ua: audit_ua,
metadata: None,
},
)
.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)
.await
.map_err(AppError::Database)
}
pub async fn set_enabled(
pool: &SqlitePool,
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)?;
repo::set_enabled(&mut tx, id, enabled)
.await
.map_err(AppError::Database)?;
let action = if enabled {
"service_account_enabled"
} else {
"service_account_disabled"
};
crate::audit::log(
&mut tx,
crate::audit::AuditEvent {
actor_id: audit_actor_id,
target_id: None,
action,
resource_type: "service_account",
resource_id: Some(id),
severity: crate::db::models::AuditSeverity::Warning,
ip: audit_ip,
ua: audit_ua,
metadata: None,
},
)
.await?;
tx.commit().await.map_err(AppError::Database)?;
Ok(())
}
+187
View File
@@ -0,0 +1,187 @@
use sqlx::SqlitePool;
use crate::{
config::SecurityConfig,
db::{models::User, repository::users as repo},
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,
cfg: &SecurityConfig,
tenant_id: &str,
username: &str,
password: &str,
audit_actor_id: Option<&str>,
audit_ip: Option<&str>,
audit_ua: Option<&str>,
) -> Result<User, AppError> {
if username.trim().is_empty() {
return Err(AppError::InvalidInput("username cannot be empty".into()));
}
passwords::validate_password_strength(password, false)?;
if repo::username_exists(pool, tenant_id, username)
.await
.map_err(AppError::Database)?
{
return Err(AppError::Conflict(format!(
"username '{username}' is already taken"
)));
}
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)
.await
.map_err(AppError::Database)?;
crate::audit::log(
&mut tx,
crate::audit::AuditEvent {
actor_id: audit_actor_id,
target_id: Some(&user.id),
action: "user_created",
resource_type: "user",
resource_id: Some(&user.id),
severity: crate::db::models::AuditSeverity::Info,
ip: audit_ip,
ua: audit_ua,
metadata: None,
},
)
.await?;
tx.commit().await.map_err(AppError::Database)?;
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)
.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)
.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)
.await
.map_err(AppError::Database)
}
/// Set a user's status (Active=1, Disabled=2, Locked=3).
pub async fn update_status(
pool: &SqlitePool,
user_id: &str,
status: i32,
audit_actor_id: Option<&str>,
audit_ip: Option<&str>,
audit_ua: Option<&str>,
) -> Result<(), AppError> {
// Verify user exists first
let _user = get_user(pool, user_id).await?;
let mut tx = pool.begin().await.map_err(AppError::Database)?;
repo::update_status(&mut tx, user_id, status)
.await
.map_err(AppError::Database)?;
let action = match status {
1 => "user_enabled",
2 => "user_disabled",
3 => "user_locked",
_ => "user_updated",
};
let severity = match status {
1 => crate::db::models::AuditSeverity::Info,
_ => crate::db::models::AuditSeverity::Warning,
};
crate::audit::log(
&mut tx,
crate::audit::AuditEvent {
actor_id: audit_actor_id,
target_id: Some(user_id),
action,
resource_type: "user",
resource_id: Some(user_id),
severity,
ip: audit_ip,
ua: audit_ua,
metadata: None,
},
)
.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,
cfg: &SecurityConfig,
user_id: &str,
new_password: &str,
audit_actor_id: Option<&str>,
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)
.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)
.await
.map_err(AppError::Database)?;
crate::audit::log(
&mut tx,
crate::audit::AuditEvent {
actor_id: audit_actor_id,
target_id: Some(user_id),
action: "password_reset",
resource_type: "user",
resource_id: Some(user_id),
severity: crate::db::models::AuditSeverity::Warning,
ip: audit_ip,
ua: audit_ua,
metadata: None,
},
)
.await?;
tx.commit().await.map_err(AppError::Database)?;
tracing::info!(user_id = %user_id, "password reset");
Ok(())
}