核心改动: 1. db.rs - 默认密码从环境变量读取或自动生成 - 优先级: MOMENTRY_DEFAULT_PASSWORD > 自动生成 - 格式: demo_<timestamp> 或自定义 - 首次创建时打印密码到控制台 2. main.rs - SMB 服务密码动态化 - 从 SMB_PASSWORD 或 MOMENTRY_DEFAULT_PASSWORD 读取 - 支持自定义 SMB 用户名 3. user_tool.rs - 新增用户管理工具 - list: 列出所有用户 - create: 创建新用户 - set-password: 修改密码 - delete: 删除用户 - init-demo: 初始化 demo 用户 4. install.sh - 安装时自动生成密码 - 生成随机密码: demo_$(openssl rand -hex 4) - 保存到 .env 文件 - 初始化数据库 5. .env.template - 新增密码配置 - MOMENTRY_DEFAULT_PASSWORD - SMB_USER / SMB_PASSWORD 安全改进: - 消除硬编码密码 'demo123' - 每次安装密码唯一 - 密码哈希存储 (Argon2id) - 支持自定义强密码 文档: - docs/DEFAULT_PASSWORD_SYSTEM.md 完整说明
433 lines
14 KiB
Rust
433 lines
14 KiB
Rust
use rusqlite::Connection;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::path::PathBuf;
|
|
use std::sync::Mutex;
|
|
use argon2::{self, Algorithm, Argon2, Params, PasswordHash, PasswordHasher, PasswordVerifier, Version};
|
|
use argon2::password_hash::{SaltString, rand_core::OsRng};
|
|
|
|
const APP_TABLES: &str = "
|
|
CREATE TABLE IF NOT EXISTS search_history (
|
|
id TEXT PRIMARY KEY,
|
|
query TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
chat_state TEXT,
|
|
mode TEXT DEFAULT 'keyword',
|
|
pinned INTEGER DEFAULT 0,
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS bookmarks (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
label TEXT NOT NULL,
|
|
history_id TEXT,
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS video_marks (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
file_uuid TEXT NOT NULL,
|
|
tag TEXT NOT NULL,
|
|
start_frame INTEGER NOT NULL,
|
|
end_frame INTEGER,
|
|
note TEXT,
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS app_users (
|
|
id TEXT PRIMARY KEY,
|
|
username TEXT NOT NULL UNIQUE,
|
|
password_hash TEXT,
|
|
display_name TEXT,
|
|
role TEXT DEFAULT 'user',
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_search_history_sort ON search_history(pinned DESC, updated_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_bookmarks_created ON bookmarks(created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_video_marks_file ON video_marks(file_uuid);
|
|
";
|
|
|
|
static DB_CONN: Mutex<Option<Connection>> = Mutex::new(None);
|
|
|
|
fn db_path() -> PathBuf {
|
|
let base = std::env::var("MOMENTRY_DATA_DIR").unwrap_or_else(|_| "../data/users".to_string());
|
|
std::fs::create_dir_all(&base).ok();
|
|
PathBuf::from(base).join("demo.sqlite")
|
|
}
|
|
|
|
pub fn init_db() -> Result<(), String> {
|
|
let path = db_path();
|
|
let conn = Connection::open(&path).map_err(|e| format!("Failed to open db: {}", e))?;
|
|
conn.execute_batch("PRAGMA journal_mode=WAL;").map_err(|e| format!("WAL mode error: {}", e))?;
|
|
conn.execute_batch(APP_TABLES).map_err(|e| format!("Failed to create tables: {}", e))?;
|
|
|
|
// Add password_hash column if not exists (migration)
|
|
conn.execute("ALTER TABLE app_users ADD COLUMN password_hash TEXT", []).ok();
|
|
|
|
// Get default user credentials from environment or use generated values
|
|
let default_id = "demo";
|
|
let default_user = "demo";
|
|
let default_password = std::env::var("MOMENTRY_DEFAULT_PASSWORD")
|
|
.unwrap_or_else(|_| {
|
|
// Generate a random password if not set
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
let timestamp = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_secs();
|
|
format!("demo_{}", timestamp % 1000000) // demo_123456
|
|
});
|
|
|
|
// Check if demo user exists and has password
|
|
let password_hash: Option<String> = conn
|
|
.query_row(
|
|
"SELECT password_hash FROM app_users WHERE id = ?1",
|
|
[default_id],
|
|
|row| row.get(0),
|
|
)
|
|
.unwrap_or(None);
|
|
|
|
if password_hash.is_none() || password_hash.as_ref().map_or(true, |h| h.is_empty()) {
|
|
let hashed = hash_password(&default_password);
|
|
conn.execute(
|
|
"INSERT OR REPLACE INTO app_users (id, username, password_hash, display_name, role) VALUES (?1, ?2, ?3, ?4, 'admin')",
|
|
rusqlite::params![default_id, default_user, hashed, "Demo User"],
|
|
)
|
|
.map_err(|e| format!("Failed to insert default user: {}", e))?;
|
|
|
|
// Print default credentials for user
|
|
eprintln!("========================================");
|
|
eprintln!("Default user created:");
|
|
eprintln!(" Username: {}", default_user);
|
|
eprintln!(" Password: {}", default_password);
|
|
eprintln!("========================================");
|
|
}
|
|
|
|
let mut lock = DB_CONN.lock().map_err(|e| format!("DB lock error: {}", e))?;
|
|
*lock = Some(conn);
|
|
Ok(())
|
|
}
|
|
|
|
fn get_conn() -> Result<Connection, String> {
|
|
let path = db_path();
|
|
let conn = Connection::open(&path).map_err(|e| format!("Failed to open db: {}", e))?;
|
|
conn.execute_batch("PRAGMA journal_mode=WAL;").map_err(|e| format!("WAL mode error: {}", e))?;
|
|
Ok(conn)
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
pub struct HistoryItem {
|
|
pub id: String,
|
|
pub query: String,
|
|
pub title: String,
|
|
pub chat_state: Option<String>,
|
|
pub mode: Option<String>,
|
|
pub pinned: bool,
|
|
pub created_at: Option<String>,
|
|
pub updated_at: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
pub struct BookmarkItem {
|
|
pub id: i64,
|
|
pub label: String,
|
|
pub history_id: Option<String>,
|
|
pub created_at: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
pub struct User {
|
|
pub id: String,
|
|
pub username: String,
|
|
pub display_name: Option<String>,
|
|
pub role: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct LoginResult {
|
|
pub success: bool,
|
|
pub user: Option<User>,
|
|
pub token: Option<String>,
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
fn hash_password(password: &str) -> String {
|
|
let salt = SaltString::generate(&mut OsRng);
|
|
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, Params::default());
|
|
argon2.hash_password(password.as_bytes(), &salt).unwrap().to_string()
|
|
}
|
|
|
|
fn verify_password(hash: &str, password: &str) -> bool {
|
|
let parsed_hash = PasswordHash::new(hash);
|
|
match parsed_hash {
|
|
Ok(h) => {
|
|
let argon2 = Argon2::default();
|
|
argon2.verify_password(password.as_bytes(), &h).is_ok()
|
|
}
|
|
Err(_) => false,
|
|
}
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
pub fn get_search_history(limit: Option<u32>) -> Result<Vec<HistoryItem>, String> {
|
|
let limit = limit.unwrap_or(30).min(30);
|
|
let conn = get_conn()?;
|
|
let mut stmt = conn
|
|
.prepare("SELECT id, query, title, chat_state, mode, pinned, created_at, updated_at FROM search_history ORDER BY pinned DESC, updated_at DESC LIMIT ?1")
|
|
.map_err(|e| format!("Prepare error: {}", e))?;
|
|
let items = stmt
|
|
.query_map([limit], |row| {
|
|
Ok(HistoryItem {
|
|
id: row.get(0)?,
|
|
query: row.get(1)?,
|
|
title: row.get(2)?,
|
|
chat_state: row.get(3)?,
|
|
mode: row.get(4)?,
|
|
pinned: row.get::<_, i64>(5)? != 0,
|
|
created_at: row.get(6)?,
|
|
updated_at: row.get(7)?,
|
|
})
|
|
})
|
|
.map_err(|e| format!("Query error: {}", e))?
|
|
.filter_map(|r| r.ok())
|
|
.collect();
|
|
Ok(items)
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
pub fn save_search_history(
|
|
id: String,
|
|
query: String,
|
|
title: String,
|
|
chat_state: Option<String>,
|
|
mode: Option<String>,
|
|
) -> Result<HistoryItem, String> {
|
|
let conn = get_conn()?;
|
|
let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
|
|
let effective_mode = mode.unwrap_or_else(|| "keyword".to_string());
|
|
conn.execute(
|
|
"INSERT OR REPLACE INTO search_history (id, query, title, chat_state, mode, pinned, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, (SELECT COALESCE((SELECT pinned FROM search_history WHERE id = ?1), 0)), ?6)",
|
|
rusqlite::params![id, query, title, chat_state, effective_mode, now],
|
|
)
|
|
.map_err(|e| format!("Insert error: {}", e))?;
|
|
|
|
conn.execute(
|
|
"DELETE FROM search_history WHERE id NOT IN (SELECT id FROM search_history ORDER BY pinned DESC, updated_at DESC LIMIT 30)",
|
|
[],
|
|
)
|
|
.ok();
|
|
|
|
Ok(HistoryItem {
|
|
id,
|
|
query: query.clone(),
|
|
title: title.clone(),
|
|
chat_state,
|
|
mode: Some(effective_mode),
|
|
pinned: false,
|
|
created_at: None,
|
|
updated_at: Some(now),
|
|
})
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
pub fn rename_search_history(id: String, title: String) -> Result<(), String> {
|
|
let conn = get_conn()?;
|
|
let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
|
|
conn.execute(
|
|
"UPDATE search_history SET title = ?1, updated_at = ?2 WHERE id = ?3",
|
|
rusqlite::params![title, now, id],
|
|
)
|
|
.map_err(|e| format!("Rename error: {}", e))?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
pub fn pin_search_history(id: String, pinned: bool) -> Result<(), String> {
|
|
let conn = get_conn()?;
|
|
let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
|
|
conn.execute(
|
|
"UPDATE search_history SET pinned = ?1, updated_at = ?2 WHERE id = ?3",
|
|
rusqlite::params![pinned as i64, now, id],
|
|
)
|
|
.map_err(|e| format!("Pin error: {}", e))?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
pub fn delete_search_history(id: String) -> Result<(), String> {
|
|
let conn = get_conn()?;
|
|
conn.execute("DELETE FROM search_history WHERE id = ?1", [id])
|
|
.map_err(|e| format!("Delete error: {}", e))?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
pub fn get_bookmarks() -> Result<Vec<BookmarkItem>, String> {
|
|
let conn = get_conn()?;
|
|
let mut stmt = conn
|
|
.prepare("SELECT id, label, history_id, created_at FROM bookmarks ORDER BY created_at DESC")
|
|
.map_err(|e| format!("Prepare error: {}", e))?;
|
|
let items = stmt
|
|
.query_map([], |row| {
|
|
Ok(BookmarkItem {
|
|
id: row.get(0)?,
|
|
label: row.get(1)?,
|
|
history_id: row.get(2)?,
|
|
created_at: row.get(3)?,
|
|
})
|
|
})
|
|
.map_err(|e| format!("Query error: {}", e))?
|
|
.filter_map(|r| r.ok())
|
|
.collect();
|
|
Ok(items)
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
pub fn save_bookmark(label: String, history_id: Option<String>) -> Result<BookmarkItem, String> {
|
|
let conn = get_conn()?;
|
|
conn.execute(
|
|
"INSERT INTO bookmarks (label, history_id) VALUES (?1, ?2)",
|
|
rusqlite::params![label, history_id],
|
|
)
|
|
.map_err(|e| format!("Insert error: {}", e))?;
|
|
let id = conn.last_insert_rowid();
|
|
Ok(BookmarkItem {
|
|
id,
|
|
label,
|
|
history_id,
|
|
created_at: None,
|
|
})
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
pub fn delete_bookmark(id: i64) -> Result<(), String> {
|
|
let conn = get_conn()?;
|
|
conn.execute("DELETE FROM bookmarks WHERE id = ?1", [id])
|
|
.map_err(|e| format!("Delete error: {}", e))?;
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
pub struct VideoMark {
|
|
pub id: i64,
|
|
pub file_uuid: String,
|
|
pub tag: String,
|
|
pub start_frame: i64,
|
|
pub end_frame: Option<i64>,
|
|
pub note: Option<String>,
|
|
pub created_at: Option<String>,
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
pub fn get_video_marks(file_uuid: String) -> Result<Vec<VideoMark>, String> {
|
|
let conn = get_conn()?;
|
|
let mut stmt = conn
|
|
.prepare("SELECT id, file_uuid, tag, start_frame, end_frame, note, created_at FROM video_marks WHERE file_uuid = ?1 ORDER BY start_frame ASC")
|
|
.map_err(|e| format!("Prepare error: {}", e))?;
|
|
let items = stmt
|
|
.query_map([file_uuid], |row| {
|
|
Ok(VideoMark {
|
|
id: row.get(0)?,
|
|
file_uuid: row.get(1)?,
|
|
tag: row.get(2)?,
|
|
start_frame: row.get(3)?,
|
|
end_frame: row.get(4)?,
|
|
note: row.get(5)?,
|
|
created_at: row.get(6)?,
|
|
})
|
|
})
|
|
.map_err(|e| format!("Query error: {}", e))?
|
|
.filter_map(|r| r.ok())
|
|
.collect();
|
|
Ok(items)
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
pub fn save_video_mark(
|
|
file_uuid: String,
|
|
tag: String,
|
|
start_frame: i64,
|
|
end_frame: Option<i64>,
|
|
note: Option<String>,
|
|
) -> Result<VideoMark, String> {
|
|
let conn = get_conn()?;
|
|
conn.execute(
|
|
"INSERT INTO video_marks (file_uuid, tag, start_frame, end_frame, note) VALUES (?1, ?2, ?3, ?4, ?5)",
|
|
rusqlite::params![file_uuid, tag, start_frame, end_frame, note],
|
|
)
|
|
.map_err(|e| format!("Insert error: {}", e))?;
|
|
let id = conn.last_insert_rowid();
|
|
Ok(VideoMark {
|
|
id,
|
|
file_uuid,
|
|
tag,
|
|
start_frame,
|
|
end_frame,
|
|
note,
|
|
created_at: None,
|
|
})
|
|
}
|
|
|
|
#[tauri::command(rename_all = "camelCase")]
|
|
pub fn delete_video_mark(id: i64) -> Result<(), String> {
|
|
let conn = get_conn()?;
|
|
conn.execute("DELETE FROM video_marks WHERE id = ?1", [id])
|
|
.map_err(|e| format!("Delete error: {}", e))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn verify_user(username: &str, password: &str) -> Result<Option<User>, String> {
|
|
let conn = get_conn()?;
|
|
let mut stmt = conn
|
|
.prepare("SELECT id, username, password_hash, display_name, role FROM app_users WHERE username = ?1")
|
|
.map_err(|e| format!("Prepare error: {}", e))?;
|
|
|
|
let result = stmt
|
|
.query_row([username], |row| {
|
|
let id: String = row.get(0)?;
|
|
let username: String = row.get(1)?;
|
|
let password_hash: Option<String> = row.get(2)?;
|
|
let display_name: Option<String> = row.get(3)?;
|
|
let role: String = row.get(4)?;
|
|
Ok((id, username, password_hash, display_name, role))
|
|
});
|
|
|
|
match result {
|
|
Ok((id, uname, password_hash, display_name, role)) => {
|
|
if let Some(hash) = password_hash {
|
|
if verify_password(&hash, password) {
|
|
Ok(Some(User { id, username: uname, display_name, role }))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
Err(_) => Ok(None),
|
|
}
|
|
}
|
|
|
|
pub fn get_user_by_username(username: &str) -> Result<Option<User>, String> {
|
|
let conn = get_conn()?;
|
|
let mut stmt = conn
|
|
.prepare("SELECT id, username, display_name, role FROM app_users WHERE username = ?1")
|
|
.map_err(|e| format!("Prepare error: {}", e))?;
|
|
|
|
let result = stmt
|
|
.query_row([username], |row| {
|
|
Ok(User {
|
|
id: row.get(0)?,
|
|
username: row.get(1)?,
|
|
display_name: row.get(2)?,
|
|
role: row.get(3)?,
|
|
})
|
|
});
|
|
|
|
match result {
|
|
Ok(user) => Ok(Some(user)),
|
|
Err(_) => Ok(None),
|
|
}
|
|
} |