# 用户认证（Auth Callback）

> 文档：`vjcad/doc/version/user-auth.md`
> 示例：`vjcad/example/version/06userauthcallback.js`

## 1. 总体架构

```mermaid
flowchart LR
    Front[MainView<br/>userInfo + onAuthError] -->|API 请求<br/>+ userId/sessionId| Cad[vjcad 后端]
    Cad -->|auth_callback HTTP| Auth[业务系统鉴权]
    Auth -->|allow / deny + reason| Cad
    Cad -->|返回结果或 401/403| Front
```

vjcad 后端不内建权限模型，而是把 **当前用户 + 操作 + 资源** 转发给 **业务系统配置的 `auth_callback`**，由业务系统决定放行还是拒绝。

## 2. 前端：MainView 配置

```js
const cadView = new MainView({
    serviceUrl:  "http://127.0.0.1:27660/api/v1",
    accessToken: "your-access-token",

    userInfo: {
        userId:    "user_12345",
        userName:  "张三",
        sessionId: "sess_abc123",
        // 只认 userId / userName / sessionId 三个字段（存入 SystemConstants.USER_ID/USER_NAME/SESSION_ID）；
        // 自定义字段（如 role）会被丢弃，不会发给后端——角色等信息请由业务系统按 userId/sessionId 自行查
    },

    onAuthError: (error) => {
        // error: { errorCode, message, operation } —— 字段是 errorCode 不是 code
        // 注意：v1.0.16 库内不会自动调用此回调（见 §4 末尾警示），此处为预留接线
        console.error("权限不足：", error);
        if (error.errorCode === 'AUTH_EXPIRED') location.href = '/login';
        else message.error(error.message || '没有权限执行该操作');
    },
});

initCadContainer('cad', cadView);   // 第一参是容器（id 或元素），第二参是 cadView
await cadView.onLoad();
```

> 完整代码：`vjcad/example/version/06userauthcallback.js`

后续登录态变化（切换用户 / 续签）：

```js
cadView.updateUserInfo({
    userId:    "user_67890",
    userName:  "李四",
    sessionId: "sess_new_xyz"
});
```

## 3. 后端：config.json

```json
{
    "auth_callback": {
        "url":     "http://your-backend/api/cad/auth-check",
        "method":  "POST",
        "timeout": 5000,
        "fail_policy": "deny"        // "deny" | "allow"，回调失败时的兜底
    }
}
```

`fail_policy` 推荐 `deny`：业务系统不通时禁止操作，安全优先。

## 4. 回调请求示意

vjcad 后端会以以下结构 POST 到业务系统：

```json
{
    "userId":    "user_12345",
    "sessionId": "sess_abc123",
    "userName":  "张三",
    "operation": "saveWebcadPatch",
    "resource": {
        "type":        "imports",
        "mapId":       "test",
        "version":     "v1",
        "designPath":  "（仅 designs 类型）",
        "branch":      "main"
    }
}
```

⚠️ 字段名注意：`resource` 里是 **`mapId`**（驼峰，不是 `mapid`）和 **`branch`**（不是 `branchName`）；顶层还含 `userName`。`method: "GET"` 时参数改走 query string（`userId/sessionId/operation/mapId/version`）。

业务系统返回 JSON，后端只解析三个字段：

```json
{
    "allowed": true
}
```

或拒绝（`reason`/`message` 会被后端透传为 API 响应的 `errorCode`/`error` 字段）：

```json
{
    "allowed": false,
    "reason":  "no_permission",
    "message": "你没有该图纸的写权限"
}
```

拒绝后后端给前端的响应是 `{ status: 403, error: message, errorCode: reason }`——即 `reason` 落在各 API 结果的 `errorCode` 上，`message` 落在 `error` 上。

⚠️ 这里的 `status` 是 **数字 403（真值）**，而库里普遍用 `if (!res.status)` / `if (res.status)` 判成败（如 `listBranches`、`SaveToServerCommand`），被拒绝的结果会被当成成功（`listBranches` 拿到空数据，`SAVESERVER` 甚至提示保存成功）。自己调用时用 `res.status === true` 判成功，或先检查 `res.errorCode`：

```js
const res = await drawingManager.saveDrawing({ /* ... */ });
if (res.errorCode) {                       // 鉴权拒绝（status 为 403）
    cadView.getOnAuthError()?.({ errorCode: res.errorCode, message: res.error || '', operation: 'saveWebcadPatch' });
    return;
}
if (res.status !== true) { /* 冲突或其它失败：看 res.error / res.conflict */ return; }
```

各类失败对应的 `errorCode`：

- 200 + `allowed:false` → 响应 `errorCode` = `reason`（缺省 `"no_permission"`），`error` = `message`
- 非 200 响应 → `errorCode: "service_error"`
- 网络错误 / 超时 → `errorCode: "service_unavailable"`；响应非 JSON → `"service_error"`（`fail_policy: "allow"` 时以上三类失败全部放行）

> ⚠️ **`onAuthError` 在 vjcad@1.0.16 是死回调**：`MainViewConfig.onAuthError` 会被存到 `MainView._onAuthError` 并经 `getOnAuthError()` 暴露，但库内没有任何代码调用它（types.d.ts 有声明、UMD 中存在）。要拿到权限拒绝需自行检查各 API 结果的 `errorCode`/`status`，或在自己的拦截层里调用 `cadView.getOnAuthError()?.({ errorCode, message, operation })`。

## 5. 操作枚举（operation）

后端 `checkAuthCallback` 实际只拦截这 7 个操作（**不是蛇形命名**）：

| operation | 触发时机 |
|---|---|
| `listWebcadDraws` | 列图纸 |
| `getWebcadData` | 打开图纸——但 1.0.16 前端用二进制 GET 请求它、**不带 userId**，后端因此直接放行，打开图纸实际 **不经过** auth_callback（仅未指定 `patchId` 时前置的 `listWebcadDraws` 会被鉴权） |
| `saveWebcadPatch` | 保存补丁（`SAVESERVER` / `saveDrawing`） |
| `deleteWebcadDraw` | 删除图纸 / 补丁 |
| `createWebcadBranch` | 创建分支 |
| `deleteWebcadBranch` | 删除分支 |
| `mergeWebcadBranch` | 合并分支 |

> 符号库读写、本地 IndexedDB 操作**不经过** auth_callback；未配置 `auth_callback` 或请求未带 `userId` 时后端直接放行。前端只在走 `Service.execCommand` 的请求里自动注入 `userId/userName/sessionId`（且仅当配置了 `userInfo.userId`），`getWebcadData` 不走这条路——要保护"打开图纸"，需在网关 / 反向代理层另做校验。

业务系统按 operation + resource 决策。

## 6. Node.js 回调骨架

```js
// Express 示例
app.post('/api/cad/auth-check', async (req, res) => {
    const { userId, sessionId, operation, resource } = req.body;

    const session = await getSession(sessionId);
    if (!session || session.userId !== userId) {
        // 字段必须是 allowed / reason / message
        return res.json({ allowed: false, reason: 'session_expired', message: '会话失效' });
    }

    const ok = await checkPermission(userId, operation, resource);
    if (!ok) return res.json({ allowed: false, reason: 'no_permission', message: '没有权限' });

    res.json({ allowed: true });
});
```

## 7. 前端常见错误处理

`error.errorCode` 的值 = 业务返回的 `reason` 字段（自定义任意字符串）+ 后端内建值（若走 `onAuthError` 接线需自行在 API 结果检查处触发，见 §4）：

```js
onAuthError: (error) => {
    switch (error.errorCode) {
        case 'session_expired':          // 业务 reason（示例值，可自定义）
            location.href = '/login';
            break;
        case 'no_permission':            // reason 默认值
            message.error('权限不足');
            break;
        case 'service_unavailable':      // 后端内建：回调网络失败
        case 'service_error':            // 后端内建：回调非 200 / 响应非 JSON
            message.error('权限服务异常');
            break;
        default:
            message.error(error.message || '认证失败');
    }
}
```

## 红旗

- `userInfo.sessionId` 必须 **每次会话变更都更新**（登录、续签、切租户）
- 不要把 `accessToken` 直接当成用户身份；vjcad 用 `userInfo` 字段配合后端鉴权
- 后端 `auth_callback.timeout` 不要太长（默认 5s 已足够），否则操作明显卡顿
- 默认 `fail_policy` 必须是 `deny`；只有内网/POC 环境可放宽
- 回调要做幂等：vjcad 在网络重试时可能重复请求同一 operation

## 参考（官方文档 MCP）

- 文档：`vjcad/doc/version/user-auth.md`
- 示例：`vjcad/example/version/06userauthcallback.js`
