refactor: 收敛单用户模型并优化可访问性与响应式体验

- 后端移除 is_admin 权限字段与相关逻辑,统一为单用户系统模型
- 修复会话过期清理的时间比较方式(改为 RFC3339 参数比较)
- /api/config 聚合配置增加敏感字段脱敏,避免暴露 TURN/RustDesk 密钥与密码
- 配置更新日志改为摘要,避免打印完整配置内容
- 前端修复可点击卡片语义与键盘可达,补齐图标按钮可访问名称
- 调整弹窗与抽屉的响应式尺寸,优化多端显示与交互
This commit is contained in:
mofeng-git
2026-02-10 22:30:52 +08:00
parent 394baca938
commit 261deb1303
13 changed files with 81 additions and 60 deletions

View File

@@ -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())

View File

@@ -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?;

View File

@@ -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'))
)

View File

@@ -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)
}

View File

@@ -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!({

View File

@@ -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)))?;