mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-03-15 23:46:51 +08:00
refactor: 收敛单用户模型并优化可访问性与响应式体验
- 后端移除 is_admin 权限字段与相关逻辑,统一为单用户系统模型 - 修复会话过期清理的时间比较方式(改为 RFC3339 参数比较) - /api/config 聚合配置增加敏感字段脱敏,避免暴露 TURN/RustDesk 密钥与密码 - 配置更新日志改为摘要,避免打印完整配置内容 - 前端修复可点击卡片语义与键盘可达,补齐图标按钮可访问名称 - 调整弹窗与抽屉的响应式尺寸,优化多端显示与交互
This commit is contained in:
@@ -110,7 +110,9 @@ impl SessionStore {
|
||||
|
||||
/// Delete all expired sessions
|
||||
pub async fn cleanup_expired(&self) -> Result<u64> {
|
||||
let result = sqlx::query("DELETE FROM sessions WHERE expires_at < datetime('now')")
|
||||
let now = Utc::now().to_rfc3339();
|
||||
let result = sqlx::query("DELETE FROM sessions WHERE expires_at < ?1")
|
||||
.bind(now)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
Ok(result.rows_affected())
|
||||
|
||||
@@ -7,7 +7,7 @@ use super::password::{hash_password, verify_password};
|
||||
use crate::error::{AppError, Result};
|
||||
|
||||
/// User row type from database
|
||||
type UserRow = (String, String, String, i32, String, String);
|
||||
type UserRow = (String, String, String, String, String);
|
||||
|
||||
/// User data
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -16,7 +16,6 @@ pub struct User {
|
||||
pub username: String,
|
||||
#[serde(skip_serializing)]
|
||||
pub password_hash: String,
|
||||
pub is_admin: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
@@ -24,12 +23,11 @@ pub struct User {
|
||||
impl User {
|
||||
/// Convert from database row to User
|
||||
fn from_row(row: UserRow) -> Self {
|
||||
let (id, username, password_hash, is_admin, created_at, updated_at) = row;
|
||||
let (id, username, password_hash, created_at, updated_at) = row;
|
||||
Self {
|
||||
id,
|
||||
username,
|
||||
password_hash,
|
||||
is_admin: is_admin != 0,
|
||||
created_at: DateTime::parse_from_rfc3339(&created_at)
|
||||
.map(|dt| dt.with_timezone(&Utc))
|
||||
.unwrap_or_else(|_| Utc::now()),
|
||||
@@ -53,7 +51,7 @@ impl UserStore {
|
||||
}
|
||||
|
||||
/// Create a new user
|
||||
pub async fn create(&self, username: &str, password: &str, is_admin: bool) -> Result<User> {
|
||||
pub async fn create(&self, username: &str, password: &str) -> Result<User> {
|
||||
// Check if username already exists
|
||||
if self.get_by_username(username).await?.is_some() {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
@@ -68,21 +66,19 @@ impl UserStore {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
username: username.to_string(),
|
||||
password_hash,
|
||||
is_admin,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO users (id, username, password_hash, is_admin, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
||||
INSERT INTO users (id, username, password_hash, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)
|
||||
"#,
|
||||
)
|
||||
.bind(&user.id)
|
||||
.bind(&user.username)
|
||||
.bind(&user.password_hash)
|
||||
.bind(user.is_admin as i32)
|
||||
.bind(user.created_at.to_rfc3339())
|
||||
.bind(user.updated_at.to_rfc3339())
|
||||
.execute(&self.pool)
|
||||
@@ -94,7 +90,7 @@ impl UserStore {
|
||||
/// Get user by ID
|
||||
pub async fn get(&self, user_id: &str) -> Result<Option<User>> {
|
||||
let row: Option<UserRow> = sqlx::query_as(
|
||||
"SELECT id, username, password_hash, is_admin, created_at, updated_at FROM users WHERE id = ?1",
|
||||
"SELECT id, username, password_hash, created_at, updated_at FROM users WHERE id = ?1",
|
||||
)
|
||||
.bind(user_id)
|
||||
.fetch_optional(&self.pool)
|
||||
@@ -106,7 +102,7 @@ impl UserStore {
|
||||
/// Get user by username
|
||||
pub async fn get_by_username(&self, username: &str) -> Result<Option<User>> {
|
||||
let row: Option<UserRow> = sqlx::query_as(
|
||||
"SELECT id, username, password_hash, is_admin, created_at, updated_at FROM users WHERE username = ?1",
|
||||
"SELECT id, username, password_hash, created_at, updated_at FROM users WHERE username = ?1",
|
||||
)
|
||||
.bind(username)
|
||||
.fetch_optional(&self.pool)
|
||||
@@ -178,7 +174,7 @@ impl UserStore {
|
||||
/// List all users
|
||||
pub async fn list(&self) -> Result<Vec<User>> {
|
||||
let rows: Vec<UserRow> = sqlx::query_as(
|
||||
"SELECT id, username, password_hash, is_admin, created_at, updated_at FROM users ORDER BY created_at",
|
||||
"SELECT id, username, password_hash, created_at, updated_at FROM users ORDER BY created_at",
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
@@ -82,7 +82,6 @@ impl ConfigStore {
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
is_admin INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
|
||||
@@ -50,10 +50,26 @@ use std::sync::Arc;
|
||||
use crate::config::AppConfig;
|
||||
use crate::state::AppState;
|
||||
|
||||
fn sanitize_config_for_api(config: &mut AppConfig) {
|
||||
// Auth secrets
|
||||
config.auth.totp_secret = None;
|
||||
|
||||
// Stream secrets
|
||||
config.stream.turn_password = None;
|
||||
|
||||
// RustDesk secrets
|
||||
config.rustdesk.device_password.clear();
|
||||
config.rustdesk.relay_key = None;
|
||||
config.rustdesk.public_key = None;
|
||||
config.rustdesk.private_key = None;
|
||||
config.rustdesk.signing_public_key = None;
|
||||
config.rustdesk.signing_private_key = None;
|
||||
}
|
||||
|
||||
/// 获取完整配置
|
||||
pub async fn get_all_config(State(state): State<Arc<AppState>>) -> Json<AppConfig> {
|
||||
let mut config = (*state.config.get()).clone();
|
||||
// 不暴露敏感信息
|
||||
config.auth.totp_secret = None;
|
||||
sanitize_config_for_api(&mut config);
|
||||
Json(config)
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ pub async fn regenerate_device_password(
|
||||
Ok(Json(RustDeskConfigResponse::from(&new_config)))
|
||||
}
|
||||
|
||||
/// 获取设备密码(管理员专用)
|
||||
/// 获取设备密码(已认证用户)
|
||||
pub async fn get_device_password(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> {
|
||||
let config = state.config.get().rustdesk.clone();
|
||||
Json(serde_json::json!({
|
||||
|
||||
@@ -589,11 +589,8 @@ pub async fn setup_init(
|
||||
));
|
||||
}
|
||||
|
||||
// Create admin user
|
||||
state
|
||||
.users
|
||||
.create(&req.username, &req.password, true)
|
||||
.await?;
|
||||
// Create single system user
|
||||
state.users.create(&req.username, &req.password).await?;
|
||||
|
||||
// Update config
|
||||
state
|
||||
@@ -771,10 +768,7 @@ pub async fn setup_init(
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"System initialized successfully with admin user: {}",
|
||||
req.username
|
||||
);
|
||||
tracing::info!("System initialized successfully");
|
||||
|
||||
Ok(Json(LoginResponse {
|
||||
success: true,
|
||||
@@ -799,7 +793,7 @@ pub async fn update_config(
|
||||
// Keep old config for rollback
|
||||
let old_config = state.config.get();
|
||||
|
||||
tracing::info!("Received config update: {:?}", req.updates);
|
||||
tracing::info!("Received config update request");
|
||||
|
||||
// Validate and merge config first (outside the update closure)
|
||||
let config_json = serde_json::to_value(&old_config)
|
||||
@@ -808,8 +802,6 @@ pub async fn update_config(
|
||||
let merged = merge_json(config_json, req.updates.clone())
|
||||
.map_err(|_| AppError::Internal("Failed to merge config".to_string()))?;
|
||||
|
||||
tracing::debug!("Merged config: {:?}", merged);
|
||||
|
||||
let new_config: AppConfig = serde_json::from_value(merged)
|
||||
.map_err(|e| AppError::BadRequest(format!("Invalid config format: {}", e)))?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user