has/src-tauri/src/admin.rs

338 lines
12 KiB
Rust

use serde::{Deserialize, Serialize};
use crate::auth::{connect_to_db, get_db_config};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DeviceSecurityInfo {
pub current_device_name: String,
pub current_ip: String,
pub is_admin: bool,
pub device_binding_enabled: bool,
pub trusted_device_name: Option<String>,
pub ip_binding_enabled: bool,
pub trusted_ip: Option<String>,
pub is_device_allowed: bool,
pub is_ip_allowed: bool,
pub can_admin_manage: bool,
pub pin_protection_enabled: bool,
}
pub fn get_admin_master_pin() -> String {
crate::auth::extract_clean_env("ADMIN_MASTER_PIN", "92057856")
}
pub fn verify_admin_pin(input_pin: Option<&str>) -> Result<(), String> {
let expected = get_admin_master_pin();
let expected_clean = expected.trim();
if expected_clean.is_empty() {
return Ok(());
}
let input = input_pin.unwrap_or("").trim();
if input.is_empty() {
return Err("Введите 8-значный мастер-PIN администратора для подтверждения".to_string());
}
if input != expected_clean {
return Err("Неверный мастер-PIN администратора! Проверьте значение в файле .env (ADMIN_MASTER_PIN)".to_string());
}
Ok(())
}
pub fn get_current_device_name() -> String {
if let Ok(name) = std::env::var("COMPUTERNAME") {
if !name.trim().is_empty() {
return name.trim().to_string();
}
}
if let Ok(name) = std::env::var("HOSTNAME") {
if !name.trim().is_empty() {
return name.trim().to_string();
}
}
if let Ok(output) = std::process::Command::new("hostname").output() {
if output.status.success() {
let s = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !s.is_empty() {
return s;
}
}
}
if let Ok(user) = std::env::var("USER") {
return format!("{}-workstation", user.trim());
}
"main-workstation".to_string()
}
pub fn get_current_local_ip() -> String {
if let Ok(socket) = std::net::UdpSocket::bind("0.0.0.0:0") {
if socket.connect("8.8.8.8:80").is_ok() {
if let Ok(addr) = socket.local_addr() {
return addr.ip().to_string();
}
}
}
"127.0.0.1".to_string()
}
pub async fn init_admin_security_table(client: &tokio_postgres::Client) -> Result<(), String> {
let sql = "
CREATE TABLE IF NOT EXISTS admin_security_config (
id SERIAL PRIMARY KEY,
device_binding_enabled BOOLEAN DEFAULT FALSE,
trusted_device_name VARCHAR(255),
ip_binding_enabled BOOLEAN DEFAULT FALSE,
trusted_ip VARCHAR(100)
);
INSERT INTO admin_security_config (id, device_binding_enabled, ip_binding_enabled)
VALUES (1, false, false)
ON CONFLICT (id) DO NOTHING;
";
// If has_app lacks DDL permissions on public schema, do not fail admin updates
if let Err(e) = client.batch_execute(sql).await {
eprintln!("[admin] admin_security_config table creation skipped: {}", e);
}
Ok(())
}
pub async fn get_admin_security_info(admin_email: &str) -> Result<DeviceSecurityInfo, String> {
let config = get_db_config();
let client = connect_to_db(&config).await?;
let _ = init_admin_security_table(&client).await;
let clean_email = admin_email.trim().to_lowercase();
let user_row = client
.query_opt(
&format!(
"SELECT role, id FROM {} WHERE LOWER({}) = LOWER($1)",
config.users_table, config.email_column
),
&[&clean_email],
)
.await
.map_err(|e| format!("Ошибка проверки роли: {}", e))?;
let (is_admin, _user_id) = match user_row {
Some(row) => {
let role: Option<String> = row.try_get(0).ok().flatten();
let id: i32 = row.get(1);
let is_adm = id == 1 || matches!(role.as_deref(), Some("admin") | Some("Администратор"));
(is_adm, id)
}
None => (false, -1),
};
let cfg_row = client
.query_opt(
"SELECT device_binding_enabled, trusted_device_name, ip_binding_enabled, trusted_ip FROM admin_security_config WHERE id = 1",
&[],
)
.await
.ok()
.flatten();
let (device_binding_enabled, trusted_device_name, ip_binding_enabled, trusted_ip): (bool, Option<String>, bool, Option<String>) = match cfg_row {
Some(row) => (row.get(0), row.get(1), row.get(2), row.get(3)),
None => (false, None, false, None),
};
let current_device = get_current_device_name();
let current_ip = get_current_local_ip();
let is_device_allowed = if !device_binding_enabled {
true
} else {
match &trusted_device_name {
Some(t) => t.trim().eq_ignore_ascii_case(&current_device.trim()),
None => true,
}
};
let is_ip_allowed = if !ip_binding_enabled {
true
} else {
match &trusted_ip {
Some(t) => t.trim().eq_ignore_ascii_case(&current_ip.trim()),
None => true,
}
};
let can_admin_manage = is_admin;
Ok(DeviceSecurityInfo {
current_device_name: current_device,
current_ip,
is_admin,
device_binding_enabled,
trusted_device_name,
ip_binding_enabled,
trusted_ip,
is_device_allowed,
is_ip_allowed,
can_admin_manage,
pin_protection_enabled: !get_admin_master_pin().is_empty(),
})
}
pub async fn update_admin_security_policy(
admin_email: &str,
device_binding: bool,
trusted_device: Option<String>,
ip_binding: bool,
trusted_ip: Option<String>,
) -> Result<DeviceSecurityInfo, String> {
let sec_info = get_admin_security_info(admin_email).await?;
if !sec_info.is_admin {
return Err("Доступ запрещен: действие доступно только администратору".to_string());
}
let config = get_db_config();
let client = connect_to_db(&config).await?;
init_admin_security_table(&client).await?;
let trusted_dev_clean = trusted_device.map(|s| s.trim().to_string()).filter(|s| !s.is_empty());
let trusted_ip_clean = trusted_ip.map(|s| s.trim().to_string()).filter(|s| !s.is_empty());
client
.execute(
"UPDATE admin_security_config
SET device_binding_enabled = $1, trusted_device_name = $2, ip_binding_enabled = $3, trusted_ip = $4
WHERE id = 1",
&[&device_binding, &trusted_dev_clean, &ip_binding, &trusted_ip_clean],
)
.await
.map_err(|e| format!("Ошибка сохранения политики безопасности: {}", e))?;
get_admin_security_info(admin_email).await
}
pub async fn admin_update_user(
admin_email: &str,
target_id: i32,
new_username: Option<String>,
new_email: Option<String>,
new_role: Option<String>,
new_password: Option<String>,
admin_pin: Option<String>,
) -> Result<String, String> {
let sec_info = get_admin_security_info(admin_email).await?;
if !sec_info.is_admin {
return Err("Отказано в доступе: требуются права администратора".to_string());
}
verify_admin_pin(admin_pin.as_deref())?;
let config = get_db_config();
let client = connect_to_db(&config).await?;
let clean_username = new_username.map(|s| s.trim().to_string()).filter(|s| !s.is_empty());
let clean_role = new_role.map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).unwrap_or_else(|| "user".to_string());
let clean_email = new_email.map(|s| s.trim().to_string()).filter(|s| !s.is_empty());
if let Some(pwd) = new_password.map(|s| s.trim().to_string()).filter(|s| !s.is_empty()) {
let hashed = bcrypt::hash(&pwd, 10).map_err(|e| format!("Ошибка хеширования: {}", e))?;
if let Some(em) = clean_email {
let update_sql = format!(
"UPDATE {} SET username = $1, {} = $2, role = $3, {} = $4 WHERE id = $5",
config.users_table, config.email_column, config.password_column
);
client
.execute(&update_sql, &[&clean_username, &em, &clean_role, &hashed, &target_id])
.await
.map_err(|e| format!("Ошибка обновления пользователя: {}", e))?;
} else {
let update_sql = format!(
"UPDATE {} SET username = $1, role = $2, {} = $3 WHERE id = $4",
config.users_table, config.password_column
);
client
.execute(&update_sql, &[&clean_username, &clean_role, &hashed, &target_id])
.await
.map_err(|e| format!("Ошибка обновления пользователя: {}", e))?;
}
} else {
if let Some(em) = clean_email {
let update_sql = format!(
"UPDATE {} SET username = $1, {} = $2, role = $3 WHERE id = $4",
config.users_table, config.email_column
);
client
.execute(&update_sql, &[&clean_username, &em, &clean_role, &target_id])
.await
.map_err(|e| format!("Ошибка обновления пользователя: {}", e))?;
} else {
let update_sql = format!(
"UPDATE {} SET username = $1, role = $2 WHERE id = $3",
config.users_table
);
client
.execute(&update_sql, &[&clean_username, &clean_role, &target_id])
.await
.map_err(|e| format!("Ошибка обновления пользователя: {}", e))?;
}
}
Ok("Данные пользователя успешно обновлены".to_string())
}
pub async fn admin_delete_user(
admin_email: &str,
target_id: i32,
admin_pin: Option<String>,
) -> Result<String, String> {
let sec_info = get_admin_security_info(admin_email).await?;
if !sec_info.is_admin {
return Err("Отказано в доступе: требуются права администратора".to_string());
}
verify_admin_pin(admin_pin.as_deref())?;
if target_id == 1 {
return Err("Нельзя удалить основного системного администратора (ID: 1)".to_string());
}
let config = get_db_config();
let client = connect_to_db(&config).await?;
let self_row = client
.query_opt(
&format!("SELECT id FROM {} WHERE LOWER({}) = LOWER($1)", config.users_table, config.email_column),
&[&admin_email.trim().to_lowercase()],
)
.await
.map_err(|e| format!("Ошибка проверки: {}", e))?;
if let Some(r) = self_row {
let self_id: i32 = r.get(0);
if self_id == target_id {
return Err("Нельзя удалить собственную учетную запись администратора".to_string());
}
}
let delete_sql = format!("DELETE FROM {} WHERE id = $1", config.users_table);
client
.execute(&delete_sql, &[&target_id])
.await
.map_err(|e| format!("Ошибка удаления пользователя: {}", e))?;
Ok("Пользователь успешно удален".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_init_admin_security() {
let config = get_db_config();
if let Ok(client) = connect_to_db(&config).await {
let res = init_admin_security_table(&client).await;
assert!(res.is_ok());
}
}
#[tokio::test]
async fn test_get_admin_security_info() {
let config = get_db_config();
if let Ok(_) = connect_to_db(&config).await {
let res = get_admin_security_info("dimatyilickiy0511@gmail.com").await;
println!("get_admin_security_info result: {:?}", res);
assert!(res.is_ok());
}
}
}