feat: 实现动态密码系统,消除硬编码

核心改动:
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 完整说明
This commit is contained in:
2026-10-12 14:25:47 +08:00
parent 14fe2eda07
commit 69ab0e8000
7 changed files with 724 additions and 5 deletions

View File

@@ -17,6 +17,10 @@ path = "src/main.rs"
name = "momentry-proxy"
path = "src/bin/proxy.rs"
[[bin]]
name = "user_tool"
path = "src/bin/user_tool.rs"
[dependencies]
dirs = "5"
tauri = { version = "2", features = ["protocol-asset"] }

View File

@@ -0,0 +1,176 @@
use rusqlite::Connection;
use std::path::PathBuf;
use argon2::{self, Algorithm, Argon2, Params, PasswordHasher, Version};
use argon2::password_hash::{SaltString, rand_core::OsRng};
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")
}
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 main() {
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
print_usage();
std::process::exit(1);
}
let command = &args[1];
let path = db_path();
let conn = Connection::open(&path).expect("Failed to open database");
match command.as_str() {
"list" => {
list_users(&conn);
}
"create" => {
if args.len() < 4 {
println!("Usage: user_tool create <username> <password>");
std::process::exit(1);
}
let username = &args[2];
let password = &args[3];
create_user(&conn, username, password);
}
"set-password" => {
if args.len() < 4 {
println!("Usage: user_tool set-password <username> <password>");
std::process::exit(1);
}
let username = &args[2];
let password = &args[3];
set_password(&conn, username, password);
}
"delete" => {
if args.len() < 3 {
println!("Usage: user_tool delete <username>");
std::process::exit(1);
}
let username = &args[2];
delete_user(&conn, username);
}
"init-demo" => {
init_demo_user(&conn);
}
_ => {
print_usage();
}
}
}
fn print_usage() {
println!("Momentry Studio User Management Tool");
println!();
println!("Usage:");
println!(" user_tool list - List all users");
println!(" user_tool create <user> <pass> - Create a new user");
println!(" user_tool set-password <user> <pass> - Change user password");
println!(" user_tool delete <user> - Delete a user");
println!(" user_tool init-demo - Initialize demo user");
}
fn list_users(conn: &Connection) {
let mut stmt = conn
.prepare("SELECT id, username, display_name, role FROM app_users")
.expect("Failed to prepare statement");
let users = stmt
.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, Option<String>>(2)?,
row.get::<_, String>(3)?,
))
})
.expect("Failed to query users");
println!("Users:");
println!("{:<10} {:<15} {:<20} {:<10}", "ID", "Username", "Display Name", "Role");
println!("{}", "-".repeat(60));
for user in users {
let (id, username, display_name, role) = user.expect("Failed to get user");
let display = display_name.unwrap_or_else(|| "-".to_string());
println!("{:<10} {:<15} {:<20} {:<10}", id, username, display, role);
}
}
fn create_user(conn: &Connection, username: &str, password: &str) {
let password_hash = hash_password(password);
let id = format!("user_{}", chrono::Utc::now().timestamp());
conn.execute(
"INSERT INTO app_users (id, username, password_hash, role) VALUES (?1, ?2, ?3, 'user')",
rusqlite::params![id, username, password_hash],
)
.expect("Failed to create user");
println!("✅ User '{}' created successfully", username);
}
fn set_password(conn: &Connection, username: &str, password: &str) {
let password_hash = hash_password(password);
let rows = conn
.execute(
"UPDATE app_users SET password_hash = ?1 WHERE username = ?2",
rusqlite::params![password_hash, username],
)
.expect("Failed to update password");
if rows > 0 {
println!("✅ Password updated for user '{}'", username);
} else {
println!("❌ User '{}' not found", username);
}
}
fn delete_user(conn: &Connection, username: &str) {
let rows = conn
.execute(
"DELETE FROM app_users WHERE username = ?1",
rusqlite::params![username],
)
.expect("Failed to delete user");
if rows > 0 {
println!("✅ User '{}' deleted", username);
} else {
println!("❌ User '{}' not found", username);
}
}
fn init_demo_user(conn: &Connection) {
let default_password = std::env::var("MOMENTRY_DEFAULT_PASSWORD")
.unwrap_or_else(|_| {
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
format!("demo_{}", timestamp % 1000000)
});
let password_hash = hash_password(&default_password);
conn.execute(
"INSERT OR REPLACE INTO app_users (id, username, password_hash, display_name, role) VALUES ('demo', 'demo', ?1, 'Demo User', 'admin')",
rusqlite::params![password_hash],
)
.expect("Failed to create demo user");
println!("========================================");
println!("Demo user initialized:");
println!(" Username: demo");
println!(" Password: {}", default_password);
println!("========================================");
}

View File

@@ -63,12 +63,22 @@ pub fn init_db() -> Result<(), String> {
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)
// 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 = "demo123";
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
@@ -80,12 +90,19 @@ pub fn init_db() -> Result<(), String> {
.unwrap_or(None);
if password_hash.is_none() || password_hash.as_ref().map_or(true, |h| h.is_empty()) {
let password_hash = hash_password(default_password);
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, password_hash, "Demo User"],
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))?;

View File

@@ -1427,7 +1427,20 @@ async fn start_service(name: String) -> Result<String, String> {
"SMB" => {
let markbase_root = get_markbase_root();
let storage_root = get_storage_root();
format!("cd {} && nohup cargo run --bin markbase-core --features smb-server -- smb-start --port 4445 --share-name markbase --root {} --user demo:demo123 > /tmp/markbase_smb.log 2>&1 &", markbase_root, storage_root)
// Get SMB credentials from environment or use default
let smb_user = std::env::var("SMB_USER").unwrap_or_else(|_| "demo".to_string());
let smb_password = std::env::var("SMB_PASSWORD")
.or_else(|_| std::env::var("MOMENTRY_DEFAULT_PASSWORD"))
.unwrap_or_else(|_| {
// Fallback to a generated password
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
format!("smb_{}", timestamp % 1000000)
});
format!("cd {} && nohup cargo run --bin markbase-core --features smb-server -- smb-start --port 4445 --share-name markbase --root {} --user {}:{} > /tmp/markbase_smb.log 2>&1 &", markbase_root, storage_root, smb_user, smb_password)
}
_ => return Err(format!("Unknown service: {}", name)),
};