# 用户数据隔离方案评审报告

## 结论

**方案大方向正确，但存在 3 个关键设计缺陷必须修正**，修正后可用。

---

## 关键缺陷

### ❌ 缺陷 1：前端传 userId 到 Rust 命令 — 安全漏洞

**方案原文：**
> 方式 A（推荐）：命令函数签名增加 `user_id: String` 参数
> 前端调用时传入 `user.id`

**问题：** Tauri `#[tauri::command]` 的参数由前端传入，前端可以伪造任意 `userId`。

```typescript
// 恶意前端代码：A 用户传入 B 的 userId，即可读取 B 的本地数据
await invoke('load_voice_materials', { userId: 'B的userId' })
```

**这完全破坏了隔离的安全性。**

**修正方案（推荐方式 B）：**

Rust 命令从已认证的 `auth.json` 中读取当前 `user.id`，不接收前端传来的 `userId`：

```rust
use tauri::State;

// 全局状态存储当前用户
pub struct CurrentUser {
    pub id: std::sync::Mutex<Option<String>>,
}

#[tauri::command]
pub async fn load_voice_materials(
    app: tauri::AppHandle,
) -> ApiResponse<Vec<VoiceMaterial>> {
    // 从 auth.json 读取已认证的 user.id
    let auth: Option<AuthState> = crate::storage::auth::load_auth_state(&app)?;
    let user_id = match auth.and_then(|a| a.user).map(|u| u.id) {
        Some(id) => id,
        None => return ApiResponse::err(401, "未登录"),
    };
    
    let path = get_user_voices_json_path(&user_id)?;
    // ...
}
```

或更简洁：在 `lib.rs` 的 `setup` 中读取 `auth.json`，将 `user_id` 存入 `ManagedState`，所有命令通过 `app.state::<CurrentUser>()` 获取。

**前端完全不需要传 `userId`，Rust 层自治。**

---

### ❌ 缺陷 2：迁移用 `fs::rename`（move）— 不可逆且易崩溃

**方案原文：**
```rust
fs::rename(&legacy_voices, user_dir.join("voices.json"))?;
```

**问题：**
- `rename` 是原子操作，但**多个文件的迁移不是原子的**
- 如果迁移 `voices.json` 后、迁移 `projects/` 前进程崩溃，会留下半迁移状态
- 不可逆，回滚困难

**修正方案：**

```rust
pub fn migrate_legacy_data(data_dir: &Path, app: &AppHandle) -> Result<(), StorageError> {
    // 1. 检查迁移标记
    let flag = data_dir.join(".migration_v1_done");
    if flag.exists() { return Ok(()); }

    // 2. 检查是否有旧数据
    let legacy_items = vec![
        ("voices.json", false),
        ("cover_avatars.json", false),
        ("cover_avatars/", true),
        ("projects/", true),
    ];
    let has_legacy = legacy_items.iter().any(|(name, _)| data_dir.join(name).exists());
    if !has_legacy {
        // 无旧数据，直接写标记跳过
        fs::write(&flag, "none")?;
        return Ok(());
    }

    // 3. 获取当前登录用户
    let auth: Option<AuthState> = crate::storage::auth::load_auth_state(app)?;
    let user_id = match auth.and_then(|a| a.user).map(|u| u.id) {
        Some(id) => id,
        None => {
            // 未登录但有旧数据：复制到 anonymous/，登录后再手动迁移
            let anon_dir = data_dir.join("anonymous");
            ensure_dir(&anon_dir)?;
            for (name, is_dir) in &legacy_items {
                let src = data_dir.join(name);
                if src.exists() {
                    let dest = anon_dir.join(name);
                    if *is_dir {
                        copy_dir_all(&src, &dest)?;
                    } else {
                        fs::copy(&src, &dest)?;
                    }
                }
            }
            fs::write(&flag, "anonymous")?;
            return Ok(());
        }
    };

    // 4. 创建用户目录，用 copy 而非 move
    let user_dir = data_dir.join("users").join(&user_id);
    ensure_dir(&user_dir)?;

    for (name, is_dir) in &legacy_items {
        let src = data_dir.join(name);
        let dest = user_dir.join(name);
        if src.exists() {
            if *is_dir {
                copy_dir_all(&src, &dest)?;
            } else {
                fs::copy(&src, &dest)?;
            }
        }
    }

    // 5. copy 成功后，再写标记，然后延迟清理旧数据
    fs::write(&flag, user_id)?;
    
    // 可选：启动后台线程，延迟 7 天后删除旧数据
    // 给用户一个回滚窗口

    Ok(())
}
```

---

### ⚠️ 缺陷 3：缺少切换用户时的热切换设计

**当前问题：**
- 切换账号后，前端 Zustand store 中仍残留上一个用户的数据
- 需要页面刷新或重启应用才能加载新用户的数据

**修正方案：**

在 `authStore.logout()` 和 `authStore.login()` 中增加**数据刷新契约**：

```typescript
// authStore.ts
logout: async () => {
  // ... 原有逻辑 ...
  
  // 新增：清空所有用户相关的内存状态
  useVoiceStore.getState().reset();
  useProjectStore.getState().reset();
  useAvatarStore.getState().reset();
  
  // 通知 Rust 层清空缓存
  await invoke('switch_user', { userId: null });
},

login: async (phone, code) => {
  // ... 原有登录逻辑 ...
  
  // 新增：通知 Rust 层切换数据目录
  await invoke('switch_user', { userId: data.user.id });
  
  // 重新加载当前用户的数据
  await useVoiceStore.getState().loadMaterials();
  await useProjectStore.getState().loadProjects();
},
```

Rust 层 `switch_user` 命令：
```rust
#[tauri::command]
pub async fn switch_user(
    app: tauri::AppHandle,
    user_id: Option<String>,
) -> Result<(), String> {
    // 更新 ManagedState 中的当前 user_id
    let state = app.state::<CurrentUser>();
    let mut guard = state.id.lock().map_err(|e| e.to_string())?;
    *guard = user_id;
    Ok(())
}
```

---

## 次要问题

### ⚠️ `sanitize_id` 对 user_id 的兼容性

当前 `sanitize_id` 只允许 `a-zA-Z0-9_-`。测试确认：
- UUID 格式（`550e8400-e29b-41d4-a716-446655440000`）：✅ 通过
- 邮箱格式（`user@example.com`）：❌ 失败
- 含点号（`user.name`）：❌ 失败

**建议：** 确认后端 `user.id` 的格式。如果是 UUID，无需改动；如果是邮箱或其他格式，需要对 `user_id` 单独做路径净化（如用 `sanitize_filename` 或直接 base64/urlencode）。

### ⚠️ `temp/` 目录也应隔离

当前 `extract_audio_from_video` 写到 `app_local_data_dir/temp/`，如果多用户并发操作会冲突。建议：
```
{app_local_data_dir}/temp/{user_id}/
```

### ⚠️ 全局 `config.json` 的拆分

当前 `config.json` 存的是 API 地址和环境配置（`production`/`debug`），属于设备级配置，保持全局合理。

但如果以后增加用户偏好（主题、音量、快捷键），需要拆分为：
- `config.json` — 设备级（全局）
- `users/{user_id}/preferences.json` — 用户级

---

## 与主流规范对比

| 应用 | 隔离方式 | 说明 |
|---|---|---|
| **微信/QQ 桌面端** | ✅ 按账号隔离目录 | 每个账号独立数据目录，切换账号需重启 |
| **Discord/Slack** | ❌ 不隔离 | 完全云端，本地只有缓存 |
| **VS Code** | ⚠️ 按 workspace | 不严格按用户，但 settings.json 可放用户目录 |
| **Figma** | ❌ 不隔离 | 完全云端 |
| **Adobe Creative Cloud** | ✅ 按账号隔离 | 登录后数据按 Creative Cloud 账号隔离 |

**本项目定位：** 本地创作工具，数据以本地为主，且支持多设备登录（手机号+验证码）。

**结论：** 按 `user_id` 隔离是**合理且必要的**，尤其在涉及**声音复刻素材**这种高敏感隐私数据时。

---

## 修正后的工作量

| 模块 | 修正后工作量 | 说明 |
|---|---|---|
| `paths.rs` 改造 | 2h | 加 `users/{user_id}/` 前缀 |
| `ManagedState` + `switch_user` | 2h | 新增当前用户状态管理 |
| Rust 命令读 auth.json 获取 user_id | 4h | 替代前端传参方案 |
| 前端 Store 增加 `reset()` + 登录后重载 | 2h | 热切换体验 |
| 数据迁移（copy + 标记） | 3h | 安全迁移 |
| 测试验证 | 2h | |
| **总计** | **~15h** | 比原方案多 0.5h（主要在 State 管理） |

---

## 建议实施时机

**v1.8.0** 实施，趁数据量小、迁移成本低时做。
