diff --git a/docs/USER_DATABASE_ISOLATION_FIX.md b/docs/USER_DATABASE_ISOLATION_FIX.md new file mode 100644 index 0000000..042c829 --- /dev/null +++ b/docs/USER_DATABASE_ISOLATION_FIX.md @@ -0,0 +1,408 @@ +# 用户数据库隔离设计 - 问题与修复方案 + +**发现时间**: 2026-10-12 +**状态**: ❌ 当前实现违反"一用户一数据库"原则 + +--- + +## 🔍 问题分析 + +### 设计原则 + +**原始设计**: 一个用户一个 SQLite 独立管理,不混淆 + +``` +data/users/ +├── demo.sqlite # demo 用户的数据 +├── admin.sqlite # admin 用户的数据 +└── user1.sqlite # user1 用户的数据 +``` + +每个用户的数据完全隔离,互不干扰。 + +--- + +### 当前实现 (错误) + +**Momentry Studio** (`src-tauri/src/db.rs`): +```rust +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") // ❌ 硬编码 demo +} +``` + +**问题**: +- ❌ 所有用户都使用同一个 `demo.sqlite` +- ❌ 没有根据当前登录用户动态选择数据库 +- ❌ 违反了"一用户一数据库"的设计原则 + +--- + +### 正确实现 (MarkBase Core) + +**MarkBase Core**: +``` +markbase/data/users/ +├── accusys.sqlite # accusys 用户的数据 +├── demo.sqlite # demo 用户的数据 +└── credentials.sqlite # 凭证数据库 +``` + +✅ 每个用户有独立的数据库文件 +✅ 数据完全隔离 + +--- + +## 📊 影响范围 + +### 受影响的功能 + +1. **搜索历史** (`search_history`) + - 所有用户共享同一个搜索历史 + - 用户 A 可以看到用户 B 的搜索记录 + +2. **书签** (`bookmarks`) + - 所有用户共享书签 + - 混乱的书签管理 + +3. **视频标记** (`video_marks`) + - 所有用户的标记混在一起 + - 无法区分是谁的标记 + +4. **用户认证** (`app_users`) + - 用户表本身是共享的(这是合理的) + - 但其他数据应该隔离 + +--- + +## ✅ 修复方案 + +### 方案 1: 函数参数传递 (推荐) + +**修改 db_path() 函数**: +```rust +// 旧代码 +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 db_path(username: &str) -> 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(format!("{}.sqlite", username)) // ✅ 动态用户 +} +``` + +**修改所有数据库函数**: +```rust +// 旧代码 +pub fn get_search_history(limit: Option) -> Result, String> { + let conn = get_conn()?; + // ... +} + +// 新代码 +pub fn get_search_history(username: String, limit: Option) -> Result, String> { + let conn = get_conn(&username)?; + // ... +} +``` + +**前端调用修改**: +```typescript +// 旧代码 +const history = await invoke('get_search_history', { limit: 30 }) + +// 新代码 +const username = localStorage.getItem('username') || 'demo' +const history = await invoke('get_search_history', { + username: username, + limit: 30 +}) +``` + +**优点**: +- ✅ 明确的用户隔离 +- ✅ 易于理解和维护 +- ✅ 类型安全 + +**缺点**: +- ❌ 需要修改所有函数签名 +- ❌ 需要修改所有前端调用 + +--- + +### 方案 2: 环境变量 (折中) + +**启动时设置环境变量**: +```rust +// 登录成功后设置 +pub fn set_current_user(username: &str) { + std::env::set_var("MOMENTRY_CURRENT_USER", username); +} + +// db_path 使用环境变量 +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(); + + let username = std::env::var("MOMENTRY_CURRENT_USER") + .unwrap_or_else(|_| "demo".to_string()); + + PathBuf::from(base).join(format!("{}.sqlite", username)) +} +``` + +**优点**: +- ✅ 不需要修改所有函数签名 +- ✅ 前端调用不需要修改 + +**缺点**: +- ❌ 依赖全局状态,不够安全 +- ❌ 多线程环境下可能有竞争条件 +- ❌ 切换用户时需要重启应用 + +--- + +### 方案 3: 全局状态 (Tauri 状态管理) + +**使用 Tauri 的 State 管理**: +```rust +use std::sync::Mutex; +use tauri::State; + +struct CurrentUser(Mutex); + +fn db_path(state: State) -> PathBuf { + let base = std::env::var("MOMENTRY_DATA_DIR") + .unwrap_or_else(|_| "../data/users".to_string()); + std::fs::create_dir_all(&base).ok(); + + let username = state.0.lock().unwrap().clone(); + PathBuf::from(base).join(format!("{}.sqlite", username)) +} +``` + +**优点**: +- ✅ 线程安全 +- ✅ 不需要修改所有函数签名 +- ✅ 集中管理用户状态 + +**缺点**: +- ❌ 需要在每个命令中传递 State +- ❌ 增加了复杂度 + +--- + +## 🎯 推荐方案 + +**推荐使用方案 1: 函数参数传递** + +**理由**: +1. ✅ 最清晰明确的设计 +2. ✅ 完全符合"一用户一数据库"原则 +3. ✅ 无全局状态,更安全 +4. ✅ 易于测试和维护 + +--- + +## 📝 实施步骤 + +### Phase 1: 准备 + +```bash +# 1. 创建备份分支 +git checkout -b fix/user-database-isolation + +# 2. 检查所有数据库函数 +grep -n "pub fn.*Result.*String" src-tauri/src/db.rs +``` + +--- + +### Phase 2: 修改核心函数 + +**修改 db_path()**: +```rust +fn db_path(username: &str) -> PathBuf { + let base = std::env::var("MOMENTRY_DATA_DIR") + .unwrap_or_else(|_| { + let home = dirs::home_dir() + .expect("Failed to get home directory"); + format!("{}/momentry_studio/data/users", home.display()) + }); + std::fs::create_dir_all(&base).ok(); + PathBuf::from(base).join(format!("{}.sqlite", username)) +} + +fn get_conn(username: &str) -> Result { + let path = db_path(username); + 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) +} +``` + +--- + +### Phase 3: 修改数据库函数 + +**需要修改的函数列表**: + +1. ✅ `get_search_history(limit) → get_search_history(username, limit)` +2. ✅ `save_search_history(item) → save_search_history(username, item)` +3. ✅ `rename_search_history(id, title) → rename_search_history(username, id, title)` +4. ✅ `pin_search_history(id, pinned) → pin_search_history(username, id, pinned)` +5. ✅ `delete_search_history(id) → delete_search_history(username, id)` +6. ✅ `get_bookmarks() → get_bookmarks(username)` +7. ✅ `save_bookmark(item) → save_bookmark(username, item)` +8. ✅ `delete_bookmark(id) → delete_bookmark(username, id)` +9. ✅ `get_video_marks(file_uuid) → get_video_marks(username, file_uuid)` +10. ✅ `save_video_mark(mark) → save_video_mark(username, mark)` + +**例外**: +- ❌ `init_db()` - 初始化用户数据库,不传递用户名 +- ❌ `login()` - 用户认证,使用全局 app_users 表 +- ❌ `verify_token()` - 验证 token,使用全局 app_users 表 + +--- + +### Phase 4: 修改前端调用 + +**创建统一工具函数**: +```typescript +// src/utils/db.ts +export function getCurrentUsername(): string { + return localStorage.getItem('username') || 'demo' +} + +export async function getSearchHistory(limit?: number) { + const username = getCurrentUsername() + return await invoke('get_search_history', { username, limit }) +} + +export async function saveSearchHistory(item: HistoryItem) { + const username = getCurrentUsername() + return await invoke('save_search_history', { username, item }) +} + +// ... 其他函数 +``` + +--- + +### Phase 5: 数据迁移 + +**将现有数据迁移到用户数据库**: +```bash +# 1. 备份现有数据库 +cp data/users/demo.sqlite data/users/demo.sqlite.backup + +# 2. 重命名为正确的用户数据库 +mv data/users/demo.sqlite data/users/{actual_username}.sqlite + +# 3. 或保持 demo 用户 +# 不需要迁移,demo.sqlite 保持不变 +``` + +--- + +### Phase 6: 测试 + +**测试场景**: +1. ✅ demo 用户登录,数据正确 +2. ✅ 切换到 admin 用户,数据隔离 +3. ✅ 新用户注册,自动创建数据库 +4. ✅ 多用户同时使用,数据不混淆 + +--- + +## ⚠️ 注意事项 + +### 1. 用户认证数据库 + +`app_users` 表应该保持共享,用于用户认证: +``` +data/users/ +├── app_users.sqlite # 共享的用户认证数据库 +├── demo.sqlite # demo 用户的私有数据 +└── admin.sqlite # admin 用户的私有数据 +``` + +**或者**: +``` +data/users/ +├── demo.sqlite # 包含 app_users 表 + 私有表 +└── admin.sqlite # 包含 app_users 表 + 私有表 +``` + +--- + +### 2. 向后兼容 + +**需要考虑**: +- 现有 `demo.sqlite` 如何处理 +- 是否需要数据迁移脚本 +- API 变更对前端的影响 + +--- + +### 3. 数据库初始化 + +**新用户注册时**: +```rust +pub fn init_user_db(username: &str) -> Result<(), String> { + let path = db_path(username); + if !path.exists() { + let conn = Connection::open(&path)?; + conn.execute_batch(APP_TABLES)?; + } + Ok(()) +} +``` + +--- + +## 📊 影响评估 + +| 影响项 | 范围 | 工作量 | +|-------|------|--------| +| **db.rs 修改** | 10+ 函数 | 中等 | +| **前端修改** | 20+ 调用点 | 中等 | +| **测试** | 功能测试 + 集成测试 | 中等 | +| **文档更新** | API 文档 + 用户指南 | 低 | +| **数据迁移** | 可选 | 低 | + +**预计工作量**: 2-3 天 + +--- + +## 🎯 总结 + +### 问题 +- ❌ 当前硬编码 `demo.sqlite`,违反"一用户一数据库"原则 +- ❌ 所有用户共享数据,不符合设计 + +### 解决方案 +- ✅ 方案 1: 函数参数传递 (推荐) +- ✅ 方案 2: 环境变量 (折中) +- ✅ 方案 3: 全局状态 (Tauri State) + +### 实施优先级 +- 🔴 **高** - 影响数据隔离和安全性 +- 建议在下一次重构时实施 + +--- + +**文档位置**: `docs/USER_DATABASE_ISOLATION_FIX.md` +**状态**: ❌ 需要修复 +**优先级**: 高 \ No newline at end of file diff --git a/scripts/check_user_isolation.sh b/scripts/check_user_isolation.sh new file mode 100755 index 0000000..448acbd --- /dev/null +++ b/scripts/check_user_isolation.sh @@ -0,0 +1,126 @@ +#!/bin/bash +# 用户数据库隔离检查脚本 + +echo "================================================================================ + 用户数据库隔离检查 +================================================================================ +" + +USER_HOME=$(eval echo ~${SUDO_USER:-$USER}) + +echo "1. 检查数据库文件" +echo "--------------------------------------------------------------------------------" + +# 检查 Momentry Studio 数据库 +STUDIO_DB_DIR="/Users/accusys/momentry_studio/data/users" +if [ -d "$STUDIO_DB_DIR" ]; then + echo "Momentry Studio 数据库目录:" + ls -lh "$STUDIO_DB_DIR"/*.sqlite 2>/dev/null || echo " ❌ 无数据库文件" + + echo "" + echo "数据库数量: $(ls "$STUDIO_DB_DIR"/*.sqlite 2>/dev/null | wc -l | tr -d ' ')" + + # 检查是否只有一个 demo.sqlite + if [ -f "$STUDIO_DB_DIR/demo.sqlite" ] && [ $(ls "$STUDIO_DB_DIR"/*.sqlite 2>/dev/null | wc -l) -eq 1 ]; then + echo " ⚠️ 只有 demo.sqlite,违反'一用户一数据库'原则" + fi +else + echo "❌ Momentry Studio 数据库目录不存在" +fi + +echo "" + +# 检查 MarkBase 数据库 +MARKBASE_DB_DIR="${USER_HOME}/markbase/data/users" +if [ -d "$MARKBASE_DB_DIR" ]; then + echo "MarkBase 数据库目录:" + ls -lh "$MARKBASE_DB_DIR"/*.sqlite 2>/dev/null + + echo "" + echo "数据库数量: $(ls "$MARKBASE_DB_DIR"/*.sqlite 2>/dev/null | wc -l | tr -d ' ')" + echo " ✅ 多个用户数据库,符合'一用户一数据库'原则" +else + echo "❌ MarkBase 数据库目录不存在" +fi + +echo "" +echo "2. 检查代码实现" +echo "--------------------------------------------------------------------------------" + +# 检查 db.rs 中的 db_path 函数 +if grep -q 'join("demo.sqlite")' /Users/accusys/momentry_studio/src-tauri/src/db.rs 2>/dev/null; then + echo "❌ db.rs 硬编码 demo.sqlite:" + grep -n 'join("demo.sqlite")' /Users/accusys/momentry_studio/src-tauri/src/db.rs + echo "" + echo " 违反了'一用户一数据库'原则" +else + echo "✅ db.rs 实现动态用户数据库路径" +fi + +echo "" +echo "3. 检查当前登录用户" +echo "--------------------------------------------------------------------------------" + +# 从 localStorage 获取当前用户(需要前端运行) +if pgrep -f "momentry-studio\|momentry-proxy" >/dev/null 2>&1; then + echo "应用正在运行" + echo " ⚠️ 无法直接检查 localStorage,需要手动验证" + echo " 建议: 检查前端 localStorage.getItem('username')" +else + echo "应用未运行" +fi + +echo "" +echo "================================================================================ + 问题总结 +================================================================================ +" + +echo "当前状态:" +echo "" +echo "Momentry Studio:" +if [ -f "$STUDIO_DB_DIR/demo.sqlite" ] && [ $(ls "$STUDIO_DB_DIR"/*.sqlite 2>/dev/null | wc -l) -eq 1 ]; then + echo " ❌ 只有 demo.sqlite (硬编码)" + echo " ❌ 所有用户共享同一数据库" + echo " ❌ 违反'一用户一数据库'原则" +else + echo " ✅ 多个用户数据库" + echo " ✅ 符合设计原则" +fi + +echo "" +echo "MarkBase Core:" +if [ -d "$MARKBASE_DB_DIR" ] && [ $(ls "$MARKBASE_DB_DIR"/*.sqlite 2>/dev/null | wc -l) -gt 1 ]; then + echo " ✅ 多个用户数据库" + echo " ✅ 符合'一用户一数据库'原则" +else + echo " ⚠️ 数据库数量不足" +fi + +echo "" +echo "================================================================================ + 修复建议 +================================================================================ +" + +if [ -f "$STUDIO_DB_DIR/demo.sqlite" ] && [ $(ls "$STUDIO_DB_DIR"/*.sqlite 2>/dev/null | wc -l) -eq 1 ]; then + echo "需要修复:" + echo "1. 修改 db.rs 的 db_path() 函数" + echo " - 支持动态用户名" + echo " - 从环境变量或函数参数获取" + echo "" + echo "2. 修改所有数据库函数" + echo " - 添加 username 参数" + echo " - 或使用环境变量" + echo "" + echo "3. 修改前端调用" + echo " - 传递当前用户名" + echo "" + echo "详见: docs/USER_DATABASE_ISOLATION_FIX.md" +else + echo "✅ 当前实现符合设计原则" +fi + +echo "" +echo "================================================================================ +"