use secrecy::{ExposeSecret, SecretBox};
use tracing::info;
use uuid::Uuid;
use crate::errors::AppError;
use crate::models::{User, UserRole};
use crate::repositories::user_repository::UserRepository;
use crate::services::auth_service::AuthService;
pub struct CreateUserResult {
pub user: User,
pub master_key: SecretBox<Vec<u8>>,
}
pub struct BootstrapResult {
pub user_id: Uuid,
pub username: String,
pub master_key: SecretBox<Vec<u8>>,
}
#[trait_variant::make(AdminService: Send)]
pub trait LocalAdminService {
async fn create_user(
&self,
actor_id: Uuid,
username: &str,
password: SecretBox<Vec<u8>>,
role: UserRole,
) -> Result<CreateUserResult, AppError>;
async fn delete_user(&self, actor_id: Uuid, target_user_id: Uuid) -> Result<(), AppError>;
async fn update_user_role(
&self,
actor_id: Uuid,
target_user_id: Uuid,
new_role: UserRole,
) -> Result<(), AppError>;
async fn list_all_users(&self, actor_id: Uuid) -> Result<Vec<User>, AppError>;
async fn reset_user_password(
&self,
actor_id: Uuid,
target_user_id: Uuid,
new_password: SecretBox<Vec<u8>>,
) -> Result<SecretBox<Vec<u8>>, AppError>;
}
pub async fn bootstrap_first_admin(
user_repo: &impl UserRepository,
auth_service: &impl AuthService,
username: &str,
password: SecretBox<Vec<u8>>,
) -> Result<BootstrapResult, AppError> {
if !user_repo.list_all().await?.is_empty() {
return Err(AppError::Conflict(
"vault already initialized; use the login form to access your account".to_string(),
));
}
let trimmed = username.trim();
if trimmed.is_empty() {
return Err(AppError::Validation(
"username must not be empty".to_string(),
));
}
let password_bytes = password.expose_secret().clone();
auth_service
.create_user(trimmed, SecretBox::new(Box::new(password_bytes.clone())))
.await?;
let master_key = auth_service
.derive_key_if_valid(trimmed, SecretBox::new(Box::new(password_bytes)))
.await?
.ok_or(AppError::Internal)?;
let envelope = auth_service.get_password_envelope(trimmed).await?;
let user_id = Uuid::new_v4();
user_repo
.create_user_db(user_id, trimmed, &UserRole::Admin)
.await?;
user_repo
.update_password_envelope(user_id, envelope)
.await?;
info!(user_id = %user_id, username = trimmed, "bootstrap: first admin account created");
Ok(BootstrapResult {
user_id,
username: trimmed.to_string(),
master_key,
})
}
pub struct CommunityAdminService;
impl AdminService for CommunityAdminService {
async fn create_user(
&self,
_actor_id: Uuid,
_username: &str,
_password: SecretBox<Vec<u8>>,
_role: UserRole,
) -> Result<CreateUserResult, AppError> {
Err(AppError::FeatureNotAvailable(
"feature-name-user-management",
))
}
async fn delete_user(&self, _actor_id: Uuid, _target_user_id: Uuid) -> Result<(), AppError> {
Err(AppError::FeatureNotAvailable(
"feature-name-user-management",
))
}
async fn update_user_role(
&self,
_actor_id: Uuid,
_target_user_id: Uuid,
_new_role: UserRole,
) -> Result<(), AppError> {
Err(AppError::FeatureNotAvailable(
"feature-name-user-management",
))
}
async fn list_all_users(&self, _actor_id: Uuid) -> Result<Vec<User>, AppError> {
Err(AppError::FeatureNotAvailable(
"feature-name-user-management",
))
}
async fn reset_user_password(
&self,
_actor_id: Uuid,
_target_user_id: Uuid,
_new_password: SecretBox<Vec<u8>>,
) -> Result<SecretBox<Vec<u8>>, AppError> {
Err(AppError::FeatureNotAvailable(
"feature-name-user-management",
))
}
}
impl UserRole {
pub fn to_db_str(&self) -> &'static str {
match self {
UserRole::Admin => "admin",
UserRole::User => "user",
}
}
}