关键修正: - AuditLog::new() 签名与代码库一致 (tenant_id, user_id, action, resource_type) - with_changes() 参数为 Option<Value>,调用时需 Some() 包装 - 限流 fail-closed 使用 (StatusCode, Json) 元组模式 - 添加缺失的 Task 7.5: 登录租户解析 (X-Tenant-ID) - Task 7 添加 assign_roles 到修复列表 - Task 10 明确所有 auth 函数签名变更需求 - Task 12 添加 import 和参数说明 - Task 14 添加 docker-compose.override.yml 开发端口恢复 - 统一环境变量名为 ERP__SUPER_ADMIN_PASSWORD
872 lines
24 KiB
Markdown
872 lines
24 KiB
Markdown
# Q2 安全地基 + CI/CD 实施计划
|
||
|
||
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 消除所有 CRITICAL/HIGH 安全风险,建立 CI/CD 自动化质量门,完成审计日志补全和 Docker 生产化。
|
||
|
||
**Architecture:** 密钥外部化通过环境变量强制注入 + 启动检查拒绝默认值;CI/CD 使用 Gitea Actions 四 job 并行;限流改为 fail-closed;审计日志补全 IP/UA 和变更值。
|
||
|
||
**Tech Stack:** Rust (Axum, SeaORM), Gitea Actions, Docker Compose, PostgreSQL 16, Redis 7
|
||
|
||
**Spec:** `docs/superpowers/specs/2026-04-17-platform-maturity-roadmap-design.md` §2
|
||
|
||
---
|
||
|
||
## File Structure
|
||
|
||
| 操作 | 文件 | 职责 |
|
||
|------|------|------|
|
||
| Modify | `crates/erp-server/config/default.toml` | 敏感值改为占位符 |
|
||
| Modify | `crates/erp-server/src/main.rs` | 启动时拒绝默认密钥 |
|
||
| Modify | `crates/erp-auth/src/module.rs:149-150` | 移除密码 fallback |
|
||
| Modify | `crates/erp-auth/src/error.rs:46-53` | 移除 `From<AppError> for AuthError` 反向映射 |
|
||
| Modify | `crates/erp-auth/src/service/auth_service.rs:177-181` | refresh 添加 tenant_id 过滤 |
|
||
| Modify | `crates/erp-auth/src/service/user_service.rs` | get_by_id/update/delete 改为 DB 级 tenant 过滤 |
|
||
| Modify | `crates/erp-server/src/middleware/rate_limit.rs:122-124,135-137` | fail-closed |
|
||
| Modify | `crates/erp-core/src/audit.rs` | `with_request_info` 类型扩展 |
|
||
| Modify | `crates/erp-auth/src/service/auth_service.rs` | login/logout/change_password 添加审计 |
|
||
| Modify | `crates/erp-plugin/src/data_service.rs` | CRUD 操作添加审计 |
|
||
| Modify | `docker/docker-compose.yml` | 端口不暴露、Redis 密码、资源限制 |
|
||
| Modify | `.gitignore` | 添加 `.test_token` |
|
||
| Create | `.gitea/workflows/ci.yml` | CI/CD 流水线 |
|
||
|
||
---
|
||
|
||
## Chunk 1: 密钥外部化与启动强制检查
|
||
|
||
### Task 1: 清理 `.test_token` 和 `.gitignore`
|
||
|
||
**Files:**
|
||
- Modify: `.gitignore`
|
||
- Delete: `.test_token`(仅本地文件)
|
||
|
||
- [ ] **Step 1: 验证 `.test_token` 是否曾提交到 git 历史**
|
||
|
||
```bash
|
||
git log --all --oneline -- .test_token
|
||
```
|
||
|
||
Expected: 无输出(从未提交)。如果有输出,需额外执行 BFG 清理。
|
||
|
||
- [ ] **Step 2: 添加 `.test_token` 到 `.gitignore`**
|
||
|
||
在 `.gitignore` 末尾添加:
|
||
|
||
```
|
||
# Test artifacts
|
||
.test_token
|
||
*.heapsnapshot
|
||
perf-trace-*.json
|
||
```
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add .gitignore
|
||
git commit -m "chore: 添加 .test_token 和测试产物到 .gitignore"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2: `default.toml` 敏感值改为占位符
|
||
|
||
**Files:**
|
||
- Modify: `crates/erp-server/config/default.toml`
|
||
|
||
- [ ] **Step 1: 替换敏感值**
|
||
|
||
将 `crates/erp-server/config/default.toml` 中的:
|
||
|
||
```toml
|
||
url = "postgres://erp:erp_dev_2024@localhost:5432/erp"
|
||
```
|
||
改为:
|
||
```toml
|
||
url = "__MUST_SET_VIA_ENV__"
|
||
```
|
||
|
||
将:
|
||
```toml
|
||
secret = "change-me-in-production"
|
||
```
|
||
改为:
|
||
```toml
|
||
secret = "__MUST_SET_VIA_ENV__"
|
||
```
|
||
|
||
将:
|
||
```toml
|
||
super_admin_password = "Admin@2026"
|
||
```
|
||
改为:
|
||
```toml
|
||
super_admin_password = "__MUST_SET_VIA_ENV__"
|
||
```
|
||
|
||
- [ ] **Step 2: 创建 `.env.development` 供本地开发使用**
|
||
|
||
在项目根目录创建 `.env.development`(已被 `.gitignore` 中 `*.env.local` 覆盖,但需显式添加 `.env.development`):
|
||
|
||
```
|
||
# .env.development — 本地开发用,不提交到仓库
|
||
# 注意:此文件需要手动 source 或通过 dotenv 工具加载,config crate 不会自动读取
|
||
ERP__DATABASE__URL=postgres://erp:erp_dev_2024@localhost:5432/erp
|
||
ERP__JWT__SECRET=dev-local-secret-change-me
|
||
ERP__SUPER_ADMIN_PASSWORD=Admin@2026
|
||
```
|
||
|
||
更新 `.gitignore`,添加 `.env.development`。
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add crates/erp-server/config/default.toml .gitignore
|
||
git commit -m "fix(security): default.toml 敏感值改为占位符,强制通过环境变量注入"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3: 启动检查 — 拒绝默认密钥
|
||
|
||
**Files:**
|
||
- Modify: `crates/erp-server/src/main.rs`(在服务启动前添加检查)
|
||
|
||
- [ ] **Step 1: 在 `main.rs` 的配置加载后、服务启动前添加安全检查**
|
||
|
||
在配置加载完成后(`let config = ...` 之后),添加:
|
||
|
||
```rust
|
||
// ── 安全检查:拒绝默认密钥 ──────────────────────────
|
||
if config.jwt.secret == "__MUST_SET_VIA_ENV__" || config.jwt.secret == "change-me-in-production" {
|
||
tracing::error!(
|
||
"JWT 密钥为默认值,拒绝启动。请设置环境变量 ERP__JWT__SECRET"
|
||
);
|
||
std::process::exit(1);
|
||
}
|
||
if config.database.url == "__MUST_SET_VIA_ENV__" {
|
||
tracing::error!(
|
||
"数据库 URL 为默认占位值,拒绝启动。请设置环境变量 ERP__DATABASE__URL"
|
||
);
|
||
std::process::exit(1);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 验证默认配置启动被拒绝**
|
||
|
||
```bash
|
||
ERP__JWT__SECRET="__MUST_SET_VIA_ENV__" cargo run -p erp-server
|
||
```
|
||
|
||
Expected: 进程退出,输出包含 "JWT 密钥为默认值,拒绝启动"
|
||
|
||
- [ ] **Step 3: 验证环境变量设置后正常启动**
|
||
|
||
```bash
|
||
ERP__JWT__SECRET="my-real-secret" ERP__DATABASE__URL="postgres://erp:erp_dev_2024@localhost:5432/erp" ERP__AUTH__SUPER_ADMIN_PASSWORD="TestPass123" cargo run -p erp-server
|
||
```
|
||
|
||
Expected: 服务正常启动(或因数据库未运行而失败,但不应因安全检查退出)
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add crates/erp-server/src/main.rs
|
||
git commit -m "fix(security): 启动时拒绝默认 JWT 密钥和数据库 URL"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 4: 移除密码 fallback 硬编码
|
||
|
||
**Files:**
|
||
- Modify: `crates/erp-auth/src/module.rs:149-150`
|
||
|
||
- [ ] **Step 1: 将 `unwrap_or_else` 改为显式错误处理**
|
||
|
||
将:
|
||
```rust
|
||
let password = std::env::var("ERP__SUPER_ADMIN_PASSWORD")
|
||
.unwrap_or_else(|_| "Admin@2026".to_string());
|
||
```
|
||
|
||
改为:
|
||
```rust
|
||
let password = std::env::var("ERP__SUPER_ADMIN_PASSWORD")
|
||
.map_err(|_| {
|
||
tracing::error!("环境变量 ERP__SUPER_ADMIN_PASSWORD 未设置,无法初始化租户认证");
|
||
erp_core::error::AppError::Internal(
|
||
"ERP__SUPER_ADMIN_PASSWORD 未设置".to_string(),
|
||
)
|
||
})?;
|
||
```
|
||
|
||
- [ ] **Step 2: 验证编译通过**
|
||
|
||
```bash
|
||
cargo check -p erp-auth
|
||
```
|
||
|
||
Expected: 编译成功
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add crates/erp-auth/src/module.rs
|
||
git commit -m "fix(security): 移除 super_admin_password 硬编码 fallback"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 5: 移除 `From<AppError> for AuthError` 反向映射
|
||
|
||
**Files:**
|
||
- Modify: `crates/erp-auth/src/error.rs:46-53`
|
||
|
||
- [ ] **Step 1: 删除反向映射 impl**
|
||
|
||
删除 `crates/erp-auth/src/error.rs` 中的整个 impl 块:
|
||
|
||
```rust
|
||
// 删除以下代码
|
||
impl From<AppError> for AuthError {
|
||
fn from(err: AppError) -> Self {
|
||
match err {
|
||
AppError::VersionMismatch => AuthError::VersionMismatch,
|
||
other => AuthError::Validation(other.to_string()),
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 修复所有依赖此反向映射的调用点**
|
||
|
||
反向映射主要用于 `on_tenant_created` / `on_tenant_deleted` 中。检查这两个函数 — 它们已经返回 `AppResult<()>`(不是 `AuthResult`),所以不会直接受影响。
|
||
|
||
真正受影响的是 `auth_service.rs` 中可能从其他 crate 传入 `AppError` 并隐式转为 `AuthError` 的路径。逐一检查:
|
||
- `auth_service.rs` — 所有 `.map_err()` 调用是否仍能编译
|
||
- `user_service.rs` — 同上
|
||
- 如果有编译错误,在调用点使用显式 `.map_err(|e| AuthError::Validation(e.to_string()))` 而非依赖隐式转换
|
||
|
||
- [ ] **Step 3: 删除反向映射的测试**
|
||
|
||
删除 `crates/erp-auth/src/error.rs` 测试中的 `app_error_version_mismatch_roundtrip` 和 `app_error_other_maps_to_auth_validation` 测试。
|
||
|
||
- [ ] **Step 4: 验证编译和测试**
|
||
|
||
```bash
|
||
cargo check -p erp-auth && cargo test -p erp-auth
|
||
```
|
||
|
||
Expected: 编译成功,所有测试通过
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add crates/erp-auth/src/
|
||
git commit -m "refactor(auth): 移除 From<AppError> for AuthError 反向映射"
|
||
```
|
||
|
||
---
|
||
|
||
## Chunk 2: 多租户安全加固 + 限流 fail-closed
|
||
|
||
### Task 6: `auth_service::refresh()` 添加 tenant_id 过滤
|
||
|
||
**Files:**
|
||
- Modify: `crates/erp-auth/src/service/auth_service.rs:177-181`
|
||
|
||
- [ ] **Step 1: 修改 refresh 中的用户查询**
|
||
|
||
将:
|
||
```rust
|
||
let user_model = user::Entity::find_by_id(claims.sub)
|
||
.one(db)
|
||
.await
|
||
.map_err(|e| AuthError::Validation(e.to_string()))?
|
||
.ok_or(AuthError::TokenRevoked)?;
|
||
```
|
||
|
||
改为:
|
||
```rust
|
||
let user_model = user::Entity::find_by_id(claims.sub)
|
||
.one(db)
|
||
.await
|
||
.map_err(|e| AuthError::Validation(e.to_string()))?
|
||
.ok_or(AuthError::TokenRevoked)?;
|
||
|
||
// 验证用户属于 JWT 中声明的租户
|
||
// 注意:JWT claims 中租户 ID 字段名为 `tid`(与 TokenService 签发时一致)
|
||
if user_model.tenant_id != claims.tid {
|
||
tracing::warn!(
|
||
user_id = %claims.sub,
|
||
jwt_tenant = %claims.tid,
|
||
actual_tenant = %user_model.tenant_id,
|
||
"Token tenant_id 与用户实际租户不匹配"
|
||
);
|
||
return Err(AuthError::TokenRevoked);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 验证编译**
|
||
|
||
```bash
|
||
cargo check -p erp-auth
|
||
```
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add crates/erp-auth/src/service/auth_service.rs
|
||
git commit -m "fix(auth): refresh token 流程添加 tenant_id 校验"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 7: `user_service` 改为 DB 级 tenant_id 过滤
|
||
|
||
**Files:**
|
||
- Modify: `crates/erp-auth/src/service/user_service.rs`(`get_by_id`、`update`、`delete`、`assign_roles` 四个函数)
|
||
|
||
- [ ] **Step 1: 修改 `get_by_id`(约第 129-134 行)**
|
||
|
||
将 `find_by_id` + 内存 `.filter()` 模式改为数据库级查询:
|
||
|
||
```rust
|
||
pub async fn get_by_id(id: Uuid, tenant_id: Uuid, db: &DatabaseConnection) -> AuthResult<user::Model> {
|
||
user::Entity::find()
|
||
.filter(user::Column::Id.eq(id))
|
||
.filter(user::Column::TenantId.eq(tenant_id))
|
||
.filter(user::Column::DeletedAt.is_null())
|
||
.one(db)
|
||
.await
|
||
.map_err(|e| AuthError::Validation(e.to_string()))?
|
||
.ok_or(AuthError::Validation("用户不存在".to_string()))
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 同样修改 `update`、`delete` 和 `assign_roles` 函数**
|
||
|
||
将这三个函数中的 `find_by_id` + 内存 `.filter()` 改为相同的 DB 级过滤模式。注意:`login`、`list` 函数已正确使用数据库级过滤,无需修改。
|
||
|
||
- [ ] **Step 3: 验证编译和测试**
|
||
|
||
```bash
|
||
cargo check -p erp-auth && cargo test -p erp-auth
|
||
```
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add crates/erp-auth/src/service/user_service.rs
|
||
git commit -m "fix(auth): get_by_id/update/delete 改为数据库级 tenant_id 过滤"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 7.5: 登录租户解析
|
||
|
||
**Files:**
|
||
- Modify: `crates/erp-auth/src/handler/auth_handler.rs`(登录 handler 提取租户信息)
|
||
- Modify: `crates/erp-auth/src/service/auth_service.rs`(login 函数签名调整)
|
||
|
||
- [ ] **Step 1: 在 `auth_handler.rs` 的 `login` handler 中提取租户 ID**
|
||
|
||
从请求头 `X-Tenant-ID` 提取租户 ID,若无此头则使用默认租户(向后兼容):
|
||
|
||
```rust
|
||
let tenant_id = headers
|
||
.get("X-Tenant-ID")
|
||
.and_then(|v| v.to_str().ok())
|
||
.and_then(|v| Uuid::parse_str(v).ok())
|
||
.unwrap_or(state.default_tenant_id);
|
||
```
|
||
|
||
将 `tenant_id` 传入 `AuthService::login`。
|
||
|
||
- [ ] **Step 2: 更新 `AuthService::login` 签名**
|
||
|
||
如果当前签名不含 `tenant_id` 参数,添加 `tenant_id: Uuid` 参数,替换函数内部对 `state.default_tenant_id` 的使用。
|
||
|
||
- [ ] **Step 3: 验证编译**
|
||
|
||
```bash
|
||
cargo check -p erp-auth
|
||
```
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add crates/erp-auth/src/handler/auth_handler.rs crates/erp-auth/src/service/auth_service.rs
|
||
git commit -m "feat(auth): 登录接口支持 X-Tenant-ID 请求头租户解析"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 8: 限流 fail-closed
|
||
|
||
**Files:**
|
||
- Modify: `crates/erp-server/src/middleware/rate_limit.rs:122-137`
|
||
|
||
- [ ] **Step 1: 将 Redis 不可达时的放行改为拒绝**
|
||
|
||
在 `apply_rate_limit` 函数中,将三处 `return next.run(req).await;` 改为返回 429:
|
||
|
||
```rust
|
||
// 第一处:Redis 不可达快速检查(约第 122-124 行)
|
||
if !avail.should_try().await {
|
||
tracing::warn!("Redis 不可达,启用 fail-closed 限流保护");
|
||
let body = RateLimitResponse {
|
||
error: "Too Many Requests".to_string(),
|
||
message: "服务暂时不可用,请稍后重试".to_string(),
|
||
};
|
||
return (StatusCode::TOO_MANY_REQUESTS, axum::Json(body)).into_response();
|
||
}
|
||
|
||
// 第二处:连接失败(约第 135-137 行)
|
||
Err(e) => {
|
||
tracing::warn!(error = %e, "Redis 连接失败,fail-closed 限流保护");
|
||
avail.mark_failed().await;
|
||
let body = RateLimitResponse {
|
||
error: "Too Many Requests".to_string(),
|
||
message: "服务暂时不可用,请稍后重试".to_string(),
|
||
};
|
||
return (StatusCode::TOO_MANY_REQUESTS, axum::Json(body)).into_response();
|
||
}
|
||
|
||
// 第三处:INCR 失败(约第 143-145 行)
|
||
Err(e) => {
|
||
tracing::warn!(error = %e, "Redis INCR 失败,fail-closed 限流保护");
|
||
let body = RateLimitResponse {
|
||
error: "Too Many Requests".to_string(),
|
||
message: "服务暂时不可用,请稍后重试".to_string(),
|
||
};
|
||
return (StatusCode::TOO_MANY_REQUESTS, axum::Json(body)).into_response();
|
||
}
|
||
```
|
||
|
||
注意:`RateLimitResponse` 已在模块级别定义(第 17-20 行),无需移动。使用 `(StatusCode, Json)` 元组模式与现有代码一致。
|
||
|
||
- [ ] **Step 2: 验证编译**
|
||
|
||
```bash
|
||
cargo check -p erp-server
|
||
```
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add crates/erp-server/src/middleware/rate_limit.rs
|
||
git commit -m "fix(server): 限流改为 fail-closed — Redis 不可达时拒绝请求"
|
||
```
|
||
|
||
---
|
||
|
||
## Chunk 3: 审计日志补全
|
||
|
||
### Task 9: 登录/登出/密码修改添加审计日志
|
||
|
||
**Files:**
|
||
- Modify: `crates/erp-auth/src/service/auth_service.rs`
|
||
- Reference: `crates/erp-core/src/audit.rs`
|
||
|
||
- [ ] **Step 1: 在 `login` 函数成功路径添加审计**
|
||
|
||
在登录成功后(签发 token 之后)添加:
|
||
|
||
```rust
|
||
// 审计日志:登录成功
|
||
// AuditLog::new 签名:(tenant_id: Uuid, user_id: Option<Uuid>, action: &str, resource_type: &str)
|
||
audit_service::record(
|
||
audit::AuditLog::new(user_model.tenant_id, Some(user_model.id), "user.login", "user"),
|
||
db,
|
||
).await;
|
||
```
|
||
|
||
- [ ] **Step 2: 在 `login` 函数失败路径添加审计**
|
||
|
||
失败审计需区分两种情况:
|
||
|
||
a) **用户不存在**(`find_by_username` 返回 None)— 此时无 `user_model`,使用 `Uuid::nil()` 作为 user_id:
|
||
```rust
|
||
// 在 Ok(None) => return Err(AuthError::InvalidCredentials) 之前添加
|
||
audit_service::record(
|
||
audit::AuditLog::new(tenant_id, None, "user.login_failed", "user")
|
||
.with_resource_id("username", &req.username),
|
||
db,
|
||
).await;
|
||
```
|
||
|
||
b) **密码错误** — 此时已有 `user_model`:
|
||
```rust
|
||
// 在密码验证失败返回 InvalidCredentials 之前添加
|
||
audit_service::record(
|
||
audit::AuditLog::new(tenant_id, Some(user_model.id), "user.login_failed", "user"),
|
||
db,
|
||
).await;
|
||
```
|
||
|
||
- [ ] **Step 3: 在 `logout` 函数添加审计**
|
||
|
||
- [ ] **Step 4: 在 `change_password` 函数添加审计**
|
||
|
||
- [ ] **Step 5: 验证编译**
|
||
|
||
```bash
|
||
cargo check -p erp-auth
|
||
```
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add crates/erp-auth/src/service/auth_service.rs
|
||
git commit -m "feat(auth): 登录/登出/密码修改添加审计日志"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 10: 审计日志添加 IP 和 User-Agent
|
||
|
||
**Files:**
|
||
- Modify: `crates/erp-core/src/audit.rs`(确保 `with_request_info` 接受 IP + UA)
|
||
- Modify: `crates/erp-auth/src/handler/auth_handler.rs`(从请求提取信息传入 service)
|
||
|
||
- [ ] **Step 1: 确认 `audit.rs` 中 `with_request_info` 的签名**
|
||
|
||
确认 `AuditLogBuilder::with_request_info(ip: String, user_agent: String)` 存在且类型正确。如果不存在则添加。
|
||
|
||
- [ ] **Step 2: 在 auth handler 中提取 IP 和 UA 并传给 service**
|
||
|
||
**必须修改**以下函数签名(不仅仅是 `login`):
|
||
|
||
- `AuthService::login` — 添加 `client_info: Option<ClientInfo>` 参数
|
||
- `AuthService::logout` — 同上
|
||
- `AuthService::change_password` — 同上
|
||
|
||
在 `auth_handler.rs` 中创建辅助函数提取请求信息:
|
||
|
||
```rust
|
||
struct ClientInfo {
|
||
ip: Option<String>,
|
||
user_agent: Option<String>,
|
||
}
|
||
|
||
fn extract_client_info(req: &Request) -> ClientInfo {
|
||
let ip = req.headers()
|
||
.get("X-Forwarded-For")
|
||
.or_else(|| req.headers().get("X-Real-IP"))
|
||
.and_then(|v| v.to_str().ok())
|
||
.map(|s| s.split(',').next().unwrap_or(s).trim().to_string());
|
||
let user_agent = req.headers()
|
||
.get("user-agent")
|
||
.and_then(|v| v.to_str().ok())
|
||
.map(|s| s.to_string());
|
||
ClientInfo { ip, user_agent }
|
||
}
|
||
```
|
||
|
||
在每个 auth handler 函数中调用 `extract_client_info` 并传给 service。
|
||
|
||
- [ ] **Step 3: 在审计日志记录时调用 `.with_request_info(ip, user_agent)`**
|
||
|
||
- [ ] **Step 4: 验证编译**
|
||
|
||
```bash
|
||
cargo check -p erp-auth && cargo check -p erp-core
|
||
```
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add crates/erp-auth/ crates/erp-core/src/audit.rs
|
||
git commit -m "feat(audit): 审计日志添加 IP 地址和 User-Agent"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 11: 关键实体 update 添加变更前后值
|
||
|
||
**Files:**
|
||
- Modify: `crates/erp-auth/src/service/user_service.rs`(`update` 函数)
|
||
- Modify: `crates/erp-auth/src/service/role_service.rs`(`update` 函数)
|
||
|
||
- [ ] **Step 1: 在 `user_service::update` 中,先查询旧值再更新**
|
||
|
||
在 update 函数中,获取旧模型后、执行更新前,记录:
|
||
|
||
```rust
|
||
let old_json = serde_json::to_value(&old_user)
|
||
.unwrap_or(serde_json::Value::Null);
|
||
// ... 执行更新 ...
|
||
let new_json = serde_json::to_value(&updated_user)
|
||
.unwrap_or(serde_json::Value::Null);
|
||
|
||
// AuditLog::new 签名:(tenant_id, user_id, action, resource_type)
|
||
// with_changes 签名:(Option<Value>, Option<Value>)
|
||
audit_service::record(
|
||
audit::AuditLog::new(tenant_id, Some(operator_id), "user.update", "user")
|
||
.with_resource_id("user_id", &old_user.id.to_string())
|
||
.with_changes(Some(old_json), Some(new_json)),
|
||
db,
|
||
).await;
|
||
```
|
||
|
||
- [ ] **Step 2: 同样修改 `role_service::update`**
|
||
|
||
- [ ] **Step 3: 确认 `with_changes` 方法签名**
|
||
|
||
实际签名为 `with_changes(mut self, old: Option<Value>, new: Option<Value>) -> Self`,已在 `audit.rs` 第 51-59 行定义。调用时用 `Some()` 包装值。
|
||
|
||
- [ ] **Step 4: 验证编译**
|
||
|
||
```bash
|
||
cargo check -p erp-auth
|
||
```
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add crates/erp-auth/ crates/erp-core/src/audit.rs
|
||
git commit -m "feat(audit): 用户/角色更新记录变更前后值"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 12: 插件 CRUD 添加审计日志
|
||
|
||
**Files:**
|
||
- Modify: `crates/erp-plugin/src/data_service.rs`
|
||
|
||
- [ ] **Step 1: 添加审计日志 import 和调用**
|
||
|
||
首先在 `data_service.rs` 顶部添加 import:
|
||
```rust
|
||
use erp_core::{audit, audit_service};
|
||
```
|
||
|
||
然后在 `create_record`、`update_record`(含 `partial_update`)、`delete_record` 中添加审计日志。审计调用需要 `tenant_id` 和 `operator_id`:
|
||
- `tenant_id` 从函数参数获取
|
||
- `operator_id` 从函数参数获取(若函数缺少此参数则需补充)
|
||
|
||
示例:
|
||
```rust
|
||
// create_record 审计
|
||
audit_service::record(
|
||
audit::AuditLog::new(tenant_id, Some(operator_id), "plugin.data.create", entity_name),
|
||
db,
|
||
).await;
|
||
```
|
||
|
||
- [ ] **Step 2: 验证编译**
|
||
|
||
```bash
|
||
cargo check -p erp-plugin
|
||
```
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add crates/erp-plugin/src/data_service.rs
|
||
git commit -m "feat(plugin): 数据 CRUD 操作添加审计日志"
|
||
```
|
||
|
||
---
|
||
|
||
## Chunk 4: CI/CD + Docker 生产化
|
||
|
||
### Task 13: 创建 Gitea Actions CI/CD 流水线
|
||
|
||
**Files:**
|
||
- Create: `.gitea/workflows/ci.yml`
|
||
|
||
- [ ] **Step 1: 创建工作流目录**
|
||
|
||
```bash
|
||
mkdir -p .gitea/workflows
|
||
```
|
||
|
||
- [ ] **Step 2: 创建 CI 流水线文件**
|
||
|
||
创建 `.gitea/workflows/ci.yml`:
|
||
|
||
```yaml
|
||
name: CI
|
||
|
||
on:
|
||
push:
|
||
branches: [main]
|
||
pull_request:
|
||
branches: [main]
|
||
|
||
jobs:
|
||
rust-check:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- uses: dtolnay/rust-toolchain@stable
|
||
- uses: Swatinem/rust-cache@v2
|
||
with:
|
||
workspaces: ". -> target"
|
||
- run: cargo fmt --check --all
|
||
- run: cargo clippy -- -D warnings
|
||
|
||
rust-test:
|
||
runs-on: ubuntu-latest
|
||
services:
|
||
postgres:
|
||
image: postgres:16
|
||
env:
|
||
POSTGRES_DB: erp_test
|
||
POSTGRES_USER: test
|
||
POSTGRES_PASSWORD: test
|
||
ports:
|
||
- 5432:5432
|
||
options: >-
|
||
--health-cmd pg_isready
|
||
--health-interval 10s
|
||
--health-timeout 5s
|
||
--health-retries 5
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- uses: dtolnay/rust-toolchain@stable
|
||
- uses: Swatinem/rust-cache@v2
|
||
with:
|
||
workspaces: ". -> target"
|
||
- run: cargo test --workspace
|
||
env:
|
||
ERP__DATABASE__URL: postgres://test:test@localhost:5432/erp_test
|
||
ERP__JWT__SECRET: ci-test-secret
|
||
ERP__AUTH__SUPER_ADMIN_PASSWORD: CI_Test_Pass_2026
|
||
|
||
frontend-build:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- uses: actions/setup-node@v4
|
||
with:
|
||
node-version: "20"
|
||
- run: cd apps/web && corepack enable && pnpm install --frozen-lockfile
|
||
- run: cd apps/web && pnpm build
|
||
|
||
security-audit:
|
||
runs-on: ubuntu-latest
|
||
continue-on-error: true
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- uses: dtolnay/rust-toolchain@stable
|
||
- run: cargo install cargo-audit && cargo audit
|
||
- uses: actions/setup-node@v4
|
||
with:
|
||
node-version: "20"
|
||
- run: cd apps/web && corepack enable && pnpm install --frozen-lockfile && pnpm audit
|
||
```
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add .gitea/
|
||
git commit -m "ci: 添加 Gitea Actions CI/CD 流水线"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 14: Docker 生产化
|
||
|
||
**Files:**
|
||
- Modify: `docker/docker-compose.yml`
|
||
|
||
- [ ] **Step 1: 移除端口暴露,添加 Redis 密码和资源限制**
|
||
|
||
将 PostgreSQL 的 `ports: "5432:5432"` 改为 `expose: ["5432"]`(仅容器网络内部可访问)。
|
||
将 Redis 的 `ports: "6379:6379"` 改为 `expose: ["6379"]`,并添加命令 `--requirepass ${REDIS_PASSWORD:-erp_redis_dev}`。
|
||
为两个服务添加资源限制:
|
||
|
||
```yaml
|
||
deploy:
|
||
resources:
|
||
limits:
|
||
cpus: "1.0"
|
||
memory: 512M
|
||
```
|
||
|
||
- [ ] **Step 2: 创建开发用 `docker-compose.override.yml`**
|
||
|
||
由于生产配置移除了端口暴露,本地开发需要 override 文件恢复端口访问:
|
||
|
||
```yaml
|
||
# docker/docker-compose.override.yml — 本地开发用,不提交到仓库
|
||
services:
|
||
postgres:
|
||
ports:
|
||
- "5432:5432"
|
||
redis:
|
||
ports:
|
||
- "6379:6379"
|
||
```
|
||
|
||
将 `docker-compose.override.yml` 添加到 `.gitignore`。Docker Compose 会自动合并 `docker-compose.yml` 和 `docker-compose.override.yml`。
|
||
|
||
- [ ] **Step 3: 更新 `.env.example`**
|
||
|
||
添加 `REDIS_PASSWORD` 变量说明。
|
||
|
||
- [ ] **Step 3: 更新 `default.toml` 的 Redis URL 格式**
|
||
|
||
如果 Redis 需要密码,URL 格式改为 `redis://:password@localhost:6379`。
|
||
|
||
- [ ] **Step 4: 验证 Docker Compose 配置有效**
|
||
|
||
```bash
|
||
cd docker && docker compose config
|
||
```
|
||
|
||
Expected: 无语法错误
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add docker/
|
||
git commit -m "fix(docker): 生产化配置 — 端口不暴露、Redis 密码、资源限制"
|
||
```
|
||
|
||
---
|
||
|
||
## 验证清单
|
||
|
||
完成所有 Task 后,执行以下验证:
|
||
|
||
- [ ] **V1: 默认配置拒绝启动**
|
||
```bash
|
||
cargo run -p erp-server
|
||
```
|
||
Expected: 进程退出,日志包含 "JWT 密钥为默认值,拒绝启动"
|
||
|
||
- [ ] **V2: 环境变量设置后正常启动**
|
||
```bash
|
||
ERP__JWT__SECRET="test-secret" ERP__DATABASE__URL="postgres://erp:erp_dev_2024@localhost:5432/erp" ERP__AUTH__SUPER_ADMIN_PASSWORD="TestPass123" cargo run -p erp-server
|
||
```
|
||
Expected: 服务正常启动
|
||
|
||
- [ ] **V3: 全量编译和测试**
|
||
```bash
|
||
cargo check && cargo test --workspace
|
||
```
|
||
Expected: 全部通过
|
||
|
||
- [ ] **V4: 前端构建**
|
||
```bash
|
||
cd apps/web && pnpm build
|
||
```
|
||
Expected: 构建成功
|
||
|
||
- [ ] **V5: Docker Compose 正常启动**
|
||
```bash
|
||
cd docker && docker compose up -d && docker compose ps
|
||
```
|
||
Expected: PostgreSQL 和 Redis 状态 healthy
|
||
|
||
- [ ] **V6: Push 到远程仓库**
|
||
```bash
|
||
git push origin main
|
||
```
|
||
Expected: Gitea Actions 触发 CI 流水线
|