Files
erp/plans/lively-tickling-engelbart.md
iven 97d3c9026b fix: resolve remaining clippy warnings and improve workflow frontend
- Collapse nested if-let in user_service.rs search filter
- Suppress dead_code warning on ApiDoc struct
- Refactor server routing: nest all routes under /api/v1 prefix
- Simplify health check route path
- Improve workflow ProcessDesigner with edit mode and loading states
- Update workflow pages with enhanced UX
- Add Phase 2 implementation plan document
2026-04-11 12:59:43 +08:00

270 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Phase 2: 身份与权限模块实施计划
## Context
Phase 1 基础设施已完成workspace、core types、EventBus、ErpModule trait、AppState、health check、graceful shutdown。现在需要构建 Phase 2 身份与权限模块,这是所有后续模块的基础——工作流、消息、配置都依赖认证和权限中间件。
**目标**实现完整的用户认证JWT、RBAC 权限模型、多租户中间件、用户/角色/组织管理 CRUD以及对应的前端页面。
**范围界定**Phase 2 仅实现用户名/密码认证 + RBAC。OAuth/SSO/TOTP/ABAC 延后到后续 Phase。
---
## 关键决策
1. **ErpModule trait 路由问题**:当前 trait `register_routes` 使用 `Router`(无状态泛型),但实际路由需要 `Router<AppState>`。Phase 2 采用**务实方案**:模块暴露独立 `routes() -> Router<AppState>` 函数server 手动 merge。不改动 trait避免核心层依赖 server 类型。
2. **Token 存储**JWT 的 SHA-256 哈希存入 `user_tokens` 表,支持吊销。前端用 localStorage 存储 access/refresh tokenhttpOnly cookie 为 Phase 6 优化项)。
3. **中间件方案**:使用 `axum::middleware::from_fn_with_state` 实现 JWT 认证中间件,将 `TenantContext` 注入 `req.extensions()`
---
## Task 1: 数据库迁移10 张表)
**目标**:创建所有 Auth 相关的数据库表。
**创建文件**`crates/erp-server/migration/src/`
- `m20260411_000002_create_users.rs`
- `m20260411_000003_create_user_credentials.rs`
- `m20260411_000004_create_user_tokens.rs`
- `m20260411_000005_create_roles.rs`
- `m20260411_000006_create_permissions.rs`
- `m20260411_000007_create_role_permissions.rs`
- `m20260411_000008_create_user_roles.rs`
- `m20260411_000009_create_organizations.rs`
- `m20260411_000010_create_departments.rs`
- `m20260411_000011_create_positions.rs`
**修改文件**`migration/src/lib.rs` 注册所有新迁移
**表结构要点**
- 所有表含标准字段id(UUID PK), tenant_id, created_at, updated_at, created_by, updated_by, deleted_at, version
- **users**: username(VARCHAR, 复合唯一 tenant_id+username), email, phone, display_name, avatar_url, status(CHECK active/disabled/locked), last_login_at
- **user_credentials**: user_id FK, credential_type(CHECK password/oauth/sso), credential_data(JSONB), verified(bool)
- **user_tokens**: user_id FK, token_hash(VARCHAR UNIQUE), token_type(CHECK access/refresh), expires_at, revoked_at, device_info
- **roles**: name, code(复合唯一 tenant_id+code), description, is_system(bool)
- **permissions**: code(复合唯一 tenant_id+code), name, resource, action, description
- **role_permissions**: role_id + permission_id 复合 PK
- **user_roles**: user_id + role_id 复合 PK
- **organizations**: name, code, parent_id(自引用 FK), path(TEXT), level, sort_order
- **departments**: org_id FK, name, code, parent_id(自引用), manager_id FK users, path, sort_order
- **positions**: dept_id FK, name, code, level, sort_order
**验证**`cargo run -p erp-server``\dt` 显示全部 11 张表
---
## Task 2: SeaORM Entity 定义
**目标**:为所有 Auth 表创建类型安全的 Entity/Model/Relation。
**创建文件**`crates/erp-auth/src/entity/`
- `mod.rs`, `user.rs`, `user_credential.rs`, `user_token.rs`, `role.rs`, `permission.rs`, `role_permission.rs`, `user_role.rs`, `organization.rs`, `department.rs`, `position.rs`
**修改文件**
- `crates/erp-auth/Cargo.toml` — 添加 workspace 依赖jsonwebtoken, argon2, validator, thiserror, utoipa, erp-common
**同时创建 DTO 文件**`crates/erp-auth/src/dto.rs`
- `LoginReq` { username, password } — #[derive(Validate)]
- `LoginResp` { access_token, refresh_token, expires_in, user }
- `RefreshReq` { refresh_token }
- `CreateUserReq` { username, password, email?, phone?, display_name? }
- `UpdateUserReq` { email?, phone?, display_name?, status? }
- `UserResp`, `RoleResp`, `PermissionResp`
- `CreateRoleReq`, `UpdateRoleReq`, `AssignPermissionsReq`, `AssignRolesReq`
- 所有 DTO 添加 `#[derive(utoipa::ToSchema)]`
**验证**`cargo check -p erp-auth` 通过
---
## Task 3: 核心服务层(密码 + JWT + 认证)
**目标**实现密码哈希、JWT 签发/验证、登录/刷新/登出逻辑。
**创建文件**`crates/erp-auth/src/`
- `error.rs` — AuthError 枚举 + From<AuthError> for AppError
- `service/mod.rs`
- `service/password.rs` — Argon2 hash/verify
- `service/token_service.rs` — JWT sign/validate, token DB CRUD
- `service/auth_service.rs` — login/refresh/logout
- `service/user_service.rs` — user CRUD
**修改文件**`crates/erp-auth/src/lib.rs` — 声明模块
**关键实现**
- `password.rs`: `hash_password(plain) -> Result<String>`, `verify_password(plain, hash) -> Result<bool>`
- `token_service.rs`:
- JWT Claims: { sub: Uuid, tid: Uuid, roles: Vec<String>, permissions: Vec<String>, exp, iat, token_type: String }
- `sign_access_token(user_id, tenant_id, roles, permissions, jwt_config) -> Result<String>`
- `sign_refresh_token(user_id, tenant_id, db, jwt_config) -> Result<(String, Uuid)>` — 存 SHA-256 哈希到 DB
- `validate_refresh_token(token, db, jwt_config) -> Result<(Uuid, Claims)>` — 检查吊销状态
- `revoke_all_user_tokens(user_id, tenant_id, db)`
- `auth_service.rs`:
- `login(tenant_id, username, password, device_info, db, token_svc, event_bus)` → 查用户 → 验证密码 → 签发双 token → 更新 last_login_at → 发 user.login 事件
- `refresh(refresh_token, db, token_svc)` → 验证 → 吊销旧 refresh → 签发新双 token
- `logout(user_id, tenant_id, db, token_svc)` → 吊销全部 token
- `user_service.rs`: CRUD查询始终带 tenant_id 过滤 + deleted_at IS NULL
**验证**`cargo check -p erp-auth` 通过password hash/verify 单元测试通过
---
## Task 4: Handler 路由 + AuthModule 注册
**目标**:创建 Axum handler注册到 server。
**创建文件**`crates/erp-auth/src/`
- `handler/mod.rs`
- `handler/auth_handler.rs` — login/refresh/logout
- `handler/user_handler.rs` — user CRUD + 角色分配
- `module.rs` — AuthModule struct + ErpModule impl
**修改文件**
- `crates/erp-server/Cargo.toml` — 添加 `erp-auth.workspace = true`
- `crates/erp-server/src/main.rs` — 注册 auth 模块路由
- `crates/erp-server/src/config.rs` — 添加 `AuthConfig { super_admin_password }`
- `crates/erp-server/config/default.toml` — 添加 `[auth]`
**路由设计**
- 公开(无需 JWT`POST /api/v1/auth/login`, `POST /api/v1/auth/refresh`
- 受保护(需 JWT`POST /api/v1/auth/logout`, 全部 users/roles/permissions/orgs 路由
**main.rs 路由结构**
```
Router::new()
.merge(public_routes) // health + login + refresh
.merge(protected_routes) // logout + user/role CRUD, 后续加 JWT 中间件
.with_state(state)
```
**验证**server 启动,`POST /api/v1/auth/login` 返回 400无 body而非 404
---
## Task 5: JWT 认证中间件 + RBAC 权限检查
**目标**JWT 验证中间件注入 TenantContextRBAC 权限检查辅助函数。
**创建文件**`crates/erp-auth/src/`
- `middleware/mod.rs`
- `middleware/jwt_auth.rs``jwt_auth_middleware(State, Request, Next)` → 从 Bearer token 解码 Claims → 注入 TenantContext 到 extensions
- `middleware/rbac.rs``require_permission(perm: &str, ctx: &TenantContext) -> AppResult<()>`
**修改文件**
- `crates/erp-server/src/main.rs` — public/protected 路由分离protected 路由层加 JWT 中间件
**关键实现**
- 中间件用 `axum::middleware::from_fn_with_state(state, jwt_auth_middleware)`
-`Authorization: Bearer xxx` 提取 token → decode → 检查过期 → 注入 `TenantContext { tenant_id, user_id, roles, permissions }`
- RBAC: 简单检查 `ctx.permissions.contains(&required_permission)`
- login/refresh/health 路由跳过 JWT 中间件
**验证**:无 token 访问 `/api/v1/users` 返回 401login/refresh 不受影响
---
## Task 6: 租户初始化钩子 + 种子数据
**目标**:实现 `on_tenant_created` 创建默认角色/权限/管理员。
**创建文件**`crates/erp-auth/src/service/seed.rs`
**修改文件**
- `crates/erp-auth/src/module.rs` — 实现 `on_tenant_created`
- `crates/erp-server/src/main.rs` — server 启动时自动创建默认租户(开发用)
**种子数据**
- 20 个默认权限user:crud, role:crud, permission:read, organization:crud, department:crud, position:crud
- "admin" 角色is_system=true绑定所有权限
- "viewer" 角色is_system=true绑定只读权限
- super admin 用户username="admin",密码从配置读取)
**验证**:启动后查询 DB 有 admin 用户、admin/viewer 角色、20 个权限
---
## Task 7: 前端登录页 + Auth Store + API 层
**目标**:登录 UI、token 管理、路由守卫。
**创建文件**`apps/web/src/`
- `api/client.ts` — axios 实例 + 请求/响应拦截器(附加 token、401 自动 refresh
- `api/auth.ts` — login/refresh/logout API 调用
- `api/users.ts` — 用户 CRUD API
- `stores/auth.ts` — auth Zustand storeuser, permissions, tokens, login/logout
- `pages/Login.tsx` — 登录表单页
- `pages/Home.tsx` — 首页(从 App.tsx 抽取)
**修改文件**
- `apps/web/src/App.tsx` — 添加 PrivateRoute 守卫、/login 路由
- `apps/web/src/stores/app.ts` — 移除 isLoggedIn stub由 auth store 接管)
**验证**:打开 `/#/` 自动跳转 `/login`,输入 admin/Admin@2026 登录后跳转到首页,刷新保持登录状态
---
## Task 8: 角色/权限管理
**目标**:角色 CRUD + 权限分配后端 + 前端页面。
**创建文件**
- Backend: `crates/erp-auth/src/service/role_service.rs`, `service/permission_service.rs`, `handler/role_handler.rs`, `handler/permission_handler.rs`
- Frontend: `apps/web/src/pages/Roles.tsx`, `apps/web/src/api/roles.ts`
**修改文件**`module.rs` 注册新路由, `App.tsx` 添加路由
**验证**admin 登录 → 角色管理 → 创建角色 → 分配权限 → DB 验证
---
## Task 9: 组织/部门/岗位管理
**目标**:树形组织架构管理。
**创建文件**
- Backend: `service/org_service.rs`, `service/dept_service.rs`, `service/position_service.rs`, `handler/org_handler.rs`, `handler/dept_handler.rs`, `handler/position_handler.rs`
- Frontend: `apps/web/src/pages/Organizations.tsx`, `apps/web/src/api/orgs.ts`
**关键**:树形结构用 parent_id + path 列实现祖先查询
**验证**:创建根组织 → 子组织 → 部门 → 岗位,验证 path 正确
---
## Task 10: 用户管理页面 + 整合
**目标**:完整的用户 CRUD 界面 + 角色分配 + 主布局用户信息。
**创建文件**
- `apps/web/src/pages/Users.tsx` — Ant Design Table + 创建/编辑/角色分配 Modal
- `apps/web/src/layouts/MainLayout.tsx` — 更新:显示当前用户名、登出菜单
**验证**admin 登录 → 用户管理 → 创建用户 → 分配角色 → 禁用/启用 → 所有操作即时反映
---
## 依赖关系
```
Task 1 (迁移) → Task 2 (Entity) → Task 3 (服务层) → Task 4 (Handler+注册)
Task 5 (JWT 中间件)
Task 6 (种子数据)
Task 7 (前端登录)
Task 8 (角色权限) Task 9 (组织部门)
Task 10 (用户管理 UI)
```
## 验证标准
- [ ] `cargo check --workspace` 零错误零警告
- [ ] `cargo test --workspace` 全部通过
- [ ] server 启动后可 login → 获得 JWT → 带 JWT 访问 /api/v1/users
- [ ] 无 JWT 访问受保护端点返回 401
- [ ] 前端登录 → 跳转首页 → 刷新保持 → 登出清除
- [ ] 用户/角色/组织 CRUD 页面功能完整
- [ ] 多租户隔离:每个查询自动带 tenant_id 过滤