mirror of
https://github.com/KwiTsukasa/kt-template-online-api.git
synced 2026-05-27 15:44:54 +08:00
fix(api): 优化WordPress降级和后台权限
This commit is contained in:
parent
755103f403
commit
74dad7d70d
@ -15,6 +15,8 @@ WORDPRESS_BASE_URL=http://localhost
|
|||||||
WORDPRESS_ADMIN_USERNAME=admin
|
WORDPRESS_ADMIN_USERNAME=admin
|
||||||
WORDPRESS_ADMIN_PASSWORD=
|
WORDPRESS_ADMIN_PASSWORD=
|
||||||
WORDPRESS_TIMEOUT_MS=15000
|
WORDPRESS_TIMEOUT_MS=15000
|
||||||
|
WORDPRESS_LOGIN_TIMEOUT_MS=3000
|
||||||
|
WORDPRESS_AVAILABILITY_TTL_MS=60000
|
||||||
|
|
||||||
ADMIN_TOKEN_SECRET=change-me
|
ADMIN_TOKEN_SECRET=change-me
|
||||||
ADMIN_COOKIE_SECURE=false
|
ADMIN_COOKIE_SECURE=false
|
||||||
|
|||||||
24
API.md
24
API.md
@ -25,7 +25,7 @@
|
|||||||
## 功能模块
|
## 功能模块
|
||||||
|
|
||||||
| 模块 | 说明 |
|
| 模块 | 说明 |
|
||||||
| --------- | --------------------------------------------------------------------- |
|
| --------- | ----------------------------------------------------------------------------------------- |
|
||||||
| Component | Admin 下受保护的组件/图表模板列表、详情、新增、编辑、逻辑删除,数据表为 `admin_component` |
|
| Component | Admin 下受保护的组件/图表模板列表、详情、新增、编辑、逻辑删除,数据表为 `admin_component` |
|
||||||
| Dict | 基于新 `admin_dict` 表的数据库字典查询,以及组件一级类型到二级类型的数据库关系映射 |
|
| Dict | 基于新 `admin_dict` 表的数据库字典查询,以及组件一级类型到二级类型的数据库关系映射 |
|
||||||
| Admin | Vben Admin 真实接口,包含认证、用户、菜单、角色、部门、时区和上传适配 |
|
| Admin | Vben Admin 真实接口,包含认证、用户、菜单、角色、部门、时区和上传适配 |
|
||||||
@ -62,16 +62,18 @@ WordPress 侧只使用客户端登录态,后端不走 BasicAuth。当前 WordP
|
|||||||
环境变量:
|
环境变量:
|
||||||
|
|
||||||
| 变量 | 说明 |
|
| 变量 | 说明 |
|
||||||
| --- | --- |
|
| ------------------------------- | ------------------------------------------------------------------------------------------ |
|
||||||
| `WORDPRESS_BASE_URL` | WordPress 站点根地址,例如 `http://192.168.31.224:8080` |
|
| `WORDPRESS_BASE_URL` | WordPress 站点根地址,例如 `http://192.168.31.224:8080` |
|
||||||
| `WORDPRESS_ADMIN_USERNAME` | WordPress 单管理员账号用户名 |
|
| `WORDPRESS_ADMIN_USERNAME` | WordPress 单管理员账号用户名 |
|
||||||
| `WORDPRESS_ADMIN_PASSWORD` | WordPress 单管理员账号密码,仅放真实 env,不提交到仓库 |
|
| `WORDPRESS_ADMIN_PASSWORD` | WordPress 单管理员账号密码,仅放真实 env,不提交到仓库 |
|
||||||
| `WORDPRESS_TIMEOUT_MS` | WordPress REST API 请求超时时间,默认 `15000` |
|
| `WORDPRESS_TIMEOUT_MS` | WordPress REST API 请求超时时间,默认 `15000` |
|
||||||
|
| `WORDPRESS_LOGIN_TIMEOUT_MS` | Admin 登录链路里 WordPress 自动认证的短超时时间,默认 `3000`,避免远程不可用阻塞主系统登录 |
|
||||||
|
| `WORDPRESS_AVAILABILITY_TTL_MS` | WordPress 可用性缓存时间,默认 `60000`;远程不可用时用于过滤博客菜单和按钮权限 |
|
||||||
|
|
||||||
支持的 WordPress 登录态来源:
|
支持的 WordPress 登录态来源:
|
||||||
|
|
||||||
| Header/Cookie | 说明 |
|
| Header/Cookie | 说明 |
|
||||||
| --- | --- |
|
| --------------------------- | --------------------------------------------------------------------------------------------------- |
|
||||||
| `X-WordPress-Authorization` | 优先透传的 WordPress 授权头,例如客户端登录拿到的 `Bearer <token>` |
|
| `X-WordPress-Authorization` | 优先透传的 WordPress 授权头,例如客户端登录拿到的 `Bearer <token>` |
|
||||||
| `Authorization` | 仅当它不是本系统 Admin access token 时才会透传,避免和后台认证冲突 |
|
| `Authorization` | 仅当它不是本系统 Admin access token 时才会透传,避免和后台认证冲突 |
|
||||||
| `X-WP-Nonce` | WordPress REST cookie 认证 nonce |
|
| `X-WP-Nonce` | WordPress REST cookie 认证 nonce |
|
||||||
@ -81,6 +83,8 @@ WordPress 侧只使用客户端登录态,后端不走 BasicAuth。当前 WordP
|
|||||||
|
|
||||||
如果 WordPress 所在 Apache/Nginx 未开启 rewrite,`/wp-json/*` 可能返回 404。后端会自动回退到 WordPress 原生 `?rest_route=/...` 形式,避免因为固定链接配置阻断文章、标签和分类管理接口。
|
如果 WordPress 所在 Apache/Nginx 未开启 rewrite,`/wp-json/*` 可能返回 404。后端会自动回退到 WordPress 原生 `?rest_route=/...` 形式,避免因为固定链接配置阻断文章、标签和分类管理接口。
|
||||||
|
|
||||||
|
Admin 主登录不依赖 WordPress 可用性:本系统账号验证通过后会先写入 Admin token;WordPress 自动认证失败时登录仍返回成功,`wordpressAuth` 为 `null`、`wordpressAvailable=false`,并清理旧 WordPress cookie。随后 `/menu/all` 与 `/auth/codes` 会基于最近一次 WordPress 可用性状态过滤 `Blog*` 菜单和 `Blog:*` 按钮权限码,避免前端展示不可用的文章、分类、标签管理入口。
|
||||||
|
|
||||||
### 数据库字典翻译
|
### 数据库字典翻译
|
||||||
|
|
||||||
组件数据维护在 `admin_component` 表中,字典数据维护在新的 `admin_dict` 表中。`Component.typeMsg`、`Component.componentTypeMsg` 会在 TypeORM `AfterLoad` 阶段根据字典缓存自动映射;旧 `/dict/*` 接口路径保持兼容,但仍需要登录态。
|
组件数据维护在 `admin_component` 表中,字典数据维护在新的 `admin_dict` 表中。`Component.typeMsg`、`Component.componentTypeMsg` 会在 TypeORM `AfterLoad` 阶段根据字典缓存自动映射;旧 `/dict/*` 接口路径保持兼容,但仍需要登录态。
|
||||||
@ -88,7 +92,7 @@ WordPress 侧只使用客户端登录态,后端不走 BasicAuth。当前 WordP
|
|||||||
`admin_dict` 表核心字段:
|
`admin_dict` 表核心字段:
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
| 字段 | 类型 | 说明 |
|
||||||
| ----------- | ------- | ------------------------------------------------------ |
|
| ------------ | ------- | ------------------------------------------------------ |
|
||||||
| id | string | 字典数字 ID |
|
| id | string | 字典数字 ID |
|
||||||
| dictCode | string | 字典分组,例如 `COMPONENT_TYPE`、`CHART`、`COMPONENT` |
|
| dictCode | string | 字典分组,例如 `COMPONENT_TYPE`、`CHART`、`COMPONENT` |
|
||||||
| label | string | 展示文本 |
|
| label | string | 展示文本 |
|
||||||
@ -101,7 +105,7 @@ WordPress 侧只使用客户端登录态,后端不走 BasicAuth。当前 WordP
|
|||||||
当前数据库示例关系:
|
当前数据库示例关系:
|
||||||
|
|
||||||
| dictCode | value | label | childrenCode |
|
| dictCode | value | label | childrenCode |
|
||||||
| -------------- | ----- | ----- | ----------- |
|
| -------------- | ----- | ----- | ------------ |
|
||||||
| COMPONENT_TYPE | 1 | 图表 | CHART |
|
| COMPONENT_TYPE | 1 | 图表 | CHART |
|
||||||
| COMPONENT_TYPE | 2 | 组件 | COMPONENT |
|
| COMPONENT_TYPE | 2 | 组件 | COMPONENT |
|
||||||
|
|
||||||
@ -324,7 +328,7 @@ Query:
|
|||||||
核心接口:
|
核心接口:
|
||||||
|
|
||||||
| 方法 | 路径 | 说明 |
|
| 方法 | 路径 | 说明 |
|
||||||
| --- | --- | --- |
|
| ------ | ------------------------------ | ----------------------------------------------------------------------------------------------------- |
|
||||||
| POST | `/auth/login` | 登录,返回 `accessToken` 与 `wordpressAuth`,并写入 access token、刷新 token 和 WordPress 授权 cookie |
|
| POST | `/auth/login` | 登录,返回 `accessToken` 与 `wordpressAuth`,并写入 access token、刷新 token 和 WordPress 授权 cookie |
|
||||||
| POST | `/auth/refresh` | 通过刷新 token cookie 刷新 accessToken,并更新 token cookie |
|
| POST | `/auth/refresh` | 通过刷新 token cookie 刷新 accessToken,并更新 token cookie |
|
||||||
| POST | `/auth/logout` | 退出登录并清理 access token、刷新 token 与 WordPress 授权 cookie |
|
| POST | `/auth/logout` | 退出登录并清理 access token、刷新 token 与 WordPress 授权 cookie |
|
||||||
@ -396,7 +400,7 @@ Query:
|
|||||||
### WordPress Article
|
### WordPress Article
|
||||||
|
|
||||||
| 方法 | 路径 | 说明 |
|
| 方法 | 路径 | 说明 |
|
||||||
| --- | --- | --- |
|
| ---- | ------------------------------------------- | ---------------- |
|
||||||
| GET | `/wordpress/article/list` | 获取文章分页列表 |
|
| GET | `/wordpress/article/list` | 获取文章分页列表 |
|
||||||
| GET | `/wordpress/article/detail?id=1` | 获取文章详情 |
|
| GET | `/wordpress/article/detail?id=1` | 获取文章详情 |
|
||||||
| POST | `/wordpress/article/save` | 新增文章 |
|
| POST | `/wordpress/article/save` | 新增文章 |
|
||||||
@ -406,7 +410,7 @@ Query:
|
|||||||
列表 Query:
|
列表 Query:
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
| --- | --- | --- | --- |
|
| ---------- | ------ | ---- | ----------------------- |
|
||||||
| pageNo | number | 否 | 页码,默认 `1` |
|
| pageNo | number | 否 | 页码,默认 `1` |
|
||||||
| pageSize | number | 否 | 每页条数,默认 `10` |
|
| pageSize | number | 否 | 每页条数,默认 `10` |
|
||||||
| search | string | 否 | 关键词搜索 |
|
| search | string | 否 | 关键词搜索 |
|
||||||
@ -434,7 +438,7 @@ Query:
|
|||||||
### WordPress Tag
|
### WordPress Tag
|
||||||
|
|
||||||
| 方法 | 路径 | 说明 |
|
| 方法 | 路径 | 说明 |
|
||||||
| --- | --- | --- |
|
| ---- | --------------------------------------- | ---------------- |
|
||||||
| GET | `/wordpress/tag/list` | 获取标签分页列表 |
|
| GET | `/wordpress/tag/list` | 获取标签分页列表 |
|
||||||
| GET | `/wordpress/tag/detail?id=1` | 获取标签详情 |
|
| GET | `/wordpress/tag/detail?id=1` | 获取标签详情 |
|
||||||
| POST | `/wordpress/tag/save` | 新增标签 |
|
| POST | `/wordpress/tag/save` | 新增标签 |
|
||||||
@ -455,7 +459,7 @@ Query:
|
|||||||
### WordPress Category
|
### WordPress Category
|
||||||
|
|
||||||
| 方法 | 路径 | 说明 |
|
| 方法 | 路径 | 说明 |
|
||||||
| --- | --- | --- |
|
| ---- | -------------------------------------------- | ---------------- |
|
||||||
| GET | `/wordpress/category/list` | 获取分类分页列表 |
|
| GET | `/wordpress/category/list` | 获取分类分页列表 |
|
||||||
| GET | `/wordpress/category/detail?id=1` | 获取分类详情 |
|
| GET | `/wordpress/category/detail?id=1` | 获取分类详情 |
|
||||||
| POST | `/wordpress/category/save` | 新增分类 |
|
| POST | `/wordpress/category/save` | 新增分类 |
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
## 功能模块
|
## 功能模块
|
||||||
|
|
||||||
| 模块 | 说明 |
|
| 模块 | 说明 |
|
||||||
| --- | --- |
|
| ----------- | ----------------------------------------------------------------------------------------- |
|
||||||
| `component` | Admin 下受保护的组件/图表模板列表、详情、新增、编辑、逻辑删除,数据表为 `admin_component` |
|
| `component` | Admin 下受保护的组件/图表模板列表、详情、新增、编辑、逻辑删除,数据表为 `admin_component` |
|
||||||
| `dict` | 基于新 `admin_dict` 表的字典查询,维护组件一级类型和二级类型关系 |
|
| `dict` | 基于新 `admin_dict` 表的字典查询,维护组件一级类型和二级类型关系 |
|
||||||
| `admin` | Vben Admin 真实接口,包含登录、用户、菜单、角色、部门、时区、上传和示例表格 |
|
| `admin` | Vben Admin 真实接口,包含登录、用户、菜单、角色、部门、时区、上传和示例表格 |
|
||||||
@ -57,6 +57,8 @@ WORDPRESS_BASE_URL=http://localhost
|
|||||||
WORDPRESS_ADMIN_USERNAME=admin
|
WORDPRESS_ADMIN_USERNAME=admin
|
||||||
WORDPRESS_ADMIN_PASSWORD=
|
WORDPRESS_ADMIN_PASSWORD=
|
||||||
WORDPRESS_TIMEOUT_MS=15000
|
WORDPRESS_TIMEOUT_MS=15000
|
||||||
|
WORDPRESS_LOGIN_TIMEOUT_MS=3000
|
||||||
|
WORDPRESS_AVAILABILITY_TTL_MS=60000
|
||||||
|
|
||||||
ADMIN_TOKEN_SECRET=change-me
|
ADMIN_TOKEN_SECRET=change-me
|
||||||
ADMIN_COOKIE_SECURE=false
|
ADMIN_COOKIE_SECURE=false
|
||||||
@ -113,7 +115,7 @@ pnpm test:e2e # e2e 测试
|
|||||||
- 如果旧版本曾写入 `admin_user.id=0`,先执行 `sql/fix-admin-user-zero-id.sql` 修复脏数据,再重启服务。
|
- 如果旧版本曾写入 `admin_user.id=0`,先执行 `sql/fix-admin-user-zero-id.sql` 修复脏数据,再重启服务。
|
||||||
- Admin、Component、Dict 与 MinIO 业务接口统一走 `JwtAuthGuard`;登录、刷新 token、退出登录和部分示例状态测试接口通过 `@Public()` 放行。
|
- Admin、Component、Dict 与 MinIO 业务接口统一走 `JwtAuthGuard`;登录、刷新 token、退出登录和部分示例状态测试接口通过 `@Public()` 放行。
|
||||||
- WordPress 管理接口同样先走本系统 `JwtAuthGuard`,再透传客户端 WordPress 登录态访问 WordPress REST API;当前 WordPress 只有单管理员账号且不开放注册,账号配置放在 env 中,但不作为 BasicAuth 发送。
|
- WordPress 管理接口同样先走本系统 `JwtAuthGuard`,再透传客户端 WordPress 登录态访问 WordPress REST API;当前 WordPress 只有单管理员账号且不开放注册,账号配置放在 env 中,但不作为 BasicAuth 发送。
|
||||||
- Admin 前端只调用现有 `/auth/login`;后端会在登录流程里自动登录 WordPress,把 WordPress cookie 写入本系统 httpOnly cookie,前端只持久化 REST nonce 和用户信息。
|
- Admin 前端只调用现有 `/auth/login`;后端会在登录流程里自动尝试登录 WordPress,把 WordPress cookie 写入本系统 httpOnly cookie,前端只持久化 REST nonce 和用户信息。WordPress 远程不可用时不会阻塞 Admin 主登录,后端会返回 `wordpressAuth=null` 并在菜单和按钮权限接口中过滤博客管理相关入口。
|
||||||
- WordPress 客户端登录态优先通过 `X-WordPress-Authorization` 透传,也支持 `X-WP-Nonce` 加 WordPress 登录 cookie 的 REST cookie 认证。
|
- WordPress 客户端登录态优先通过 `X-WordPress-Authorization` 透传,也支持 `X-WP-Nonce` 加 WordPress 登录 cookie 的 REST cookie 认证。
|
||||||
- 如果 WordPress 服务器未开启 rewrite 导致 `/wp-json/*` 返回 404,后端会自动回退到 `?rest_route=/...` 形式继续访问 REST API。
|
- 如果 WordPress 服务器未开启 rewrite 导致 `/wp-json/*` 返回 404,后端会自动回退到 `?rest_route=/...` 形式继续访问 REST API。
|
||||||
- `kt-template-admin` 登录会写入 access token 与刷新 token cookie,`kt-template-online-web` 和 `kt-template-online-playground` 可在回跳后通过刷新 token 重新持久化登录态。
|
- `kt-template-admin` 登录会写入 access token 与刷新 token cookie,`kt-template-online-web` 和 `kt-template-online-playground` 可在回跳后通过刷新 token 重新持久化登录态。
|
||||||
|
|||||||
@ -137,6 +137,19 @@ VALUES
|
|||||||
(2041700000000120201, 2041700000000100202, 'SystemDeptCreate', NULL, NULL, NULL, 'System:Dept:Create', 'button', '{"title":"common.create"}', 1, 0),
|
(2041700000000120201, 2041700000000100202, 'SystemDeptCreate', NULL, NULL, NULL, 'System:Dept:Create', 'button', '{"title":"common.create"}', 1, 0),
|
||||||
(2041700000000120202, 2041700000000100202, 'SystemDeptEdit', NULL, NULL, NULL, 'System:Dept:Edit', 'button', '{"title":"common.edit"}', 1, 0),
|
(2041700000000120202, 2041700000000100202, 'SystemDeptEdit', NULL, NULL, NULL, 'System:Dept:Edit', 'button', '{"title":"common.edit"}', 1, 0),
|
||||||
(2041700000000120203, 2041700000000100202, 'SystemDeptDelete', NULL, NULL, NULL, 'System:Dept:Delete', 'button', '{"title":"common.delete"}', 1, 0),
|
(2041700000000120203, 2041700000000100202, 'SystemDeptDelete', NULL, NULL, NULL, 'System:Dept:Delete', 'button', '{"title":"common.delete"}', 1, 0),
|
||||||
|
(2041700000000100300, 0, 'Blog', '/blog', NULL, '/blog/article', NULL, 'catalog', '{"icon":"lucide:newspaper","order":100,"title":"博客管理"}', 1, 100),
|
||||||
|
(2041700000000100301, 2041700000000100300, 'BlogArticle', '/blog/article', '/blog/article/list', NULL, 'Blog:Article:List', 'menu', '{"icon":"lucide:file-text","title":"文章管理"}', 1, 0),
|
||||||
|
(2041700000000120301, 2041700000000100301, 'BlogArticleCreate', NULL, NULL, NULL, 'Blog:Article:Create', 'button', '{"title":"common.create"}', 1, 0),
|
||||||
|
(2041700000000120302, 2041700000000100301, 'BlogArticleEdit', NULL, NULL, NULL, 'Blog:Article:Edit', 'button', '{"title":"common.edit"}', 1, 0),
|
||||||
|
(2041700000000120303, 2041700000000100301, 'BlogArticleDelete', NULL, NULL, NULL, 'Blog:Article:Delete', 'button', '{"title":"common.delete"}', 1, 0),
|
||||||
|
(2041700000000100302, 2041700000000100300, 'BlogCategory', '/blog/category', '/blog/category/list', NULL, 'Blog:Category:List', 'menu', '{"icon":"lucide:folder-tree","title":"分类管理"}', 1, 1),
|
||||||
|
(2041700000000120311, 2041700000000100302, 'BlogCategoryCreate', NULL, NULL, NULL, 'Blog:Category:Create', 'button', '{"title":"common.create"}', 1, 0),
|
||||||
|
(2041700000000120312, 2041700000000100302, 'BlogCategoryEdit', NULL, NULL, NULL, 'Blog:Category:Edit', 'button', '{"title":"common.edit"}', 1, 0),
|
||||||
|
(2041700000000120313, 2041700000000100302, 'BlogCategoryDelete', NULL, NULL, NULL, 'Blog:Category:Delete', 'button', '{"title":"common.delete"}', 1, 0),
|
||||||
|
(2041700000000100303, 2041700000000100300, 'BlogTag', '/blog/tag', '/blog/tag/list', NULL, 'Blog:Tag:List', 'menu', '{"icon":"lucide:tags","title":"标签管理"}', 1, 2),
|
||||||
|
(2041700000000120321, 2041700000000100303, 'BlogTagCreate', NULL, NULL, NULL, 'Blog:Tag:Create', 'button', '{"title":"common.create"}', 1, 0),
|
||||||
|
(2041700000000120322, 2041700000000100303, 'BlogTagEdit', NULL, NULL, NULL, 'Blog:Tag:Edit', 'button', '{"title":"common.edit"}', 1, 0),
|
||||||
|
(2041700000000120323, 2041700000000100303, 'BlogTagDelete', NULL, NULL, NULL, 'Blog:Tag:Delete', 'button', '{"title":"common.delete"}', 1, 0),
|
||||||
(2041700000000100009, 0, 'Project', '/vben-admin', NULL, NULL, NULL, 'catalog', '{"badgeType":"dot","icon":"carbon:data-center","order":9998,"title":"demos.vben.title"}', 1, 9998),
|
(2041700000000100009, 0, 'Project', '/vben-admin', NULL, NULL, NULL, 'catalog', '{"badgeType":"dot","icon":"carbon:data-center","order":9998,"title":"demos.vben.title"}', 1, 9998),
|
||||||
(2041700000000100901, 2041700000000100009, 'VbenDocument', '/vben-admin/document', 'IFrameView', NULL, NULL, 'embedded', '{"icon":"carbon:book","iframeSrc":"https://doc.vben.pro","title":"demos.vben.document"}', 1, 0),
|
(2041700000000100901, 2041700000000100009, 'VbenDocument', '/vben-admin/document', 'IFrameView', NULL, NULL, 'embedded', '{"icon":"carbon:book","iframeSrc":"https://doc.vben.pro","title":"demos.vben.document"}', 1, 0),
|
||||||
(2041700000000100902, 2041700000000100009, 'VbenGithub', '/vben-admin/github', 'IFrameView', NULL, NULL, 'link', '{"icon":"carbon:logo-github","link":"https://github.com/vbenjs/vue-vben-admin","title":"Github"}', 1, 0),
|
(2041700000000100902, 2041700000000100009, 'VbenGithub', '/vben-admin/github', 'IFrameView', NULL, NULL, 'link', '{"icon":"carbon:logo-github","link":"https://github.com/vbenjs/vue-vben-admin","title":"Github"}', 1, 0),
|
||||||
|
|||||||
@ -39,18 +39,26 @@ export class AdminAuthController {
|
|||||||
body.password,
|
body.password,
|
||||||
);
|
);
|
||||||
const wordpressLogin =
|
const wordpressLogin =
|
||||||
await this.wordpressService.loginWithConfiguredAdmin();
|
await this.wordpressService.tryLoginWithConfiguredAdmin();
|
||||||
this.authService.setAccessTokenCookie(res, accessToken);
|
this.authService.setAccessTokenCookie(res, accessToken);
|
||||||
this.authService.setRefreshTokenCookie(res, refreshToken);
|
this.authService.setRefreshTokenCookie(res, refreshToken);
|
||||||
this.wordpressService.setAuthCookie(res, wordpressLogin.cookie);
|
if (wordpressLogin.available) {
|
||||||
|
this.wordpressService.setAuthCookie(res, wordpressLogin.result.cookie);
|
||||||
|
} else {
|
||||||
|
this.wordpressService.clearAuthCookie(res);
|
||||||
|
}
|
||||||
|
|
||||||
return vbenSuccess({
|
return vbenSuccess({
|
||||||
...this.userService.serializeUser(user),
|
...this.userService.serializeUser(user),
|
||||||
accessToken,
|
accessToken,
|
||||||
wordpressAuth: {
|
wordpressAuth: wordpressLogin.available
|
||||||
...wordpressLogin.auth,
|
? {
|
||||||
user: wordpressLogin.user,
|
...wordpressLogin.result.auth,
|
||||||
},
|
user: wordpressLogin.result.user,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
wordpressAvailable: wordpressLogin.available,
|
||||||
|
wordpressError: wordpressLogin.error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { In, Repository } from 'typeorm';
|
import { In, Repository } from 'typeorm';
|
||||||
import { toTree } from '@/common';
|
import { toTree } from '@/common';
|
||||||
|
import { WordpressService } from '@/wordpress/wordpress.service';
|
||||||
import { AdminUser } from '../user/admin-user.entity';
|
import { AdminUser } from '../user/admin-user.entity';
|
||||||
import { AdminMenu, AdminMenuMeta } from './admin-menu.entity';
|
import { AdminMenu, AdminMenuMeta } from './admin-menu.entity';
|
||||||
|
|
||||||
@ -15,17 +16,20 @@ export class AdminMenuService {
|
|||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(AdminMenu)
|
@InjectRepository(AdminMenu)
|
||||||
private readonly menuRepository: Repository<AdminMenu>,
|
private readonly menuRepository: Repository<AdminMenu>,
|
||||||
|
private readonly wordpressService: WordpressService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getAccessCodes(user: AdminUser) {
|
async getAccessCodes(user: AdminUser) {
|
||||||
const menus = await this.getAllowedMenus(user);
|
const menus = this.filterUnavailableFeatureMenus(
|
||||||
return menus
|
await this.getAllowedMenus(user),
|
||||||
.map((menu) => menu.authCode)
|
);
|
||||||
.filter((authCode) => !!authCode);
|
return menus.map((menu) => menu.authCode).filter((authCode) => !!authCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRouteMenus(user: AdminUser) {
|
async getRouteMenus(user: AdminUser) {
|
||||||
const menus = await this.getAllowedMenus(user);
|
const menus = this.filterUnavailableFeatureMenus(
|
||||||
|
await this.getAllowedMenus(user),
|
||||||
|
);
|
||||||
return this.buildMenuTree(menus.filter((menu) => menu.type !== 'button'));
|
return this.buildMenuTree(menus.filter((menu) => menu.type !== 'button'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,6 +118,22 @@ export class AdminMenuService {
|
|||||||
return [...menuMap.values()];
|
return [...menuMap.values()];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private filterUnavailableFeatureMenus(menus: AdminMenu[]) {
|
||||||
|
if (!menus.some((menu) => this.isWordpressMenu(menu))) return menus;
|
||||||
|
if (this.wordpressService.isAdminIntegrationAvailable()) return menus;
|
||||||
|
|
||||||
|
// WordPress 是外部增强能力,远程不可用时不影响 Admin 主系统登录和系统管理菜单。
|
||||||
|
return menus.filter((menu) => !this.isWordpressMenu(menu));
|
||||||
|
}
|
||||||
|
|
||||||
|
private isWordpressMenu(menu: AdminMenu) {
|
||||||
|
return (
|
||||||
|
menu.name?.startsWith('Blog') ||
|
||||||
|
menu.path?.startsWith('/blog') ||
|
||||||
|
menu.authCode?.startsWith('Blog:')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private normalizeMenuInput(
|
private normalizeMenuInput(
|
||||||
data: MenuInput,
|
data: MenuInput,
|
||||||
includeEmptyMeta: boolean,
|
includeEmptyMeta: boolean,
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import {
|
|||||||
import { ApiHeader, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
import { ApiHeader, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||||
import type { Request } from 'express';
|
import type { Request } from 'express';
|
||||||
import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard';
|
import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard';
|
||||||
import { ToolsService } from '@/common';
|
import { vbenSuccess } from '@/common';
|
||||||
import {
|
import {
|
||||||
WordpressArticleBodyDto,
|
WordpressArticleBodyDto,
|
||||||
WordpressArticleListQueryDto,
|
WordpressArticleListQueryDto,
|
||||||
@ -35,10 +35,7 @@ import { WordpressService } from './wordpress.service';
|
|||||||
@Controller('wordpress/article')
|
@Controller('wordpress/article')
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
export class WordpressArticleController {
|
export class WordpressArticleController {
|
||||||
constructor(
|
constructor(private readonly wordpressService: WordpressService) {}
|
||||||
private readonly toolsService: ToolsService,
|
|
||||||
private readonly wordpressService: WordpressService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Get('list')
|
@Get('list')
|
||||||
@ApiOperation({ summary: '获取 WordPress 文章分页列表' })
|
@ApiOperation({ summary: '获取 WordPress 文章分页列表' })
|
||||||
@ -50,7 +47,7 @@ export class WordpressArticleController {
|
|||||||
const auth = this.wordpressService.getAuthContext(req);
|
const auth = this.wordpressService.getAuthContext(req);
|
||||||
const list = await this.wordpressService.articleList(query, auth);
|
const list = await this.wordpressService.articleList(query, auth);
|
||||||
|
|
||||||
return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', list));
|
return res.send(vbenSuccess(list));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('detail')
|
@Get('detail')
|
||||||
@ -60,7 +57,7 @@ export class WordpressArticleController {
|
|||||||
const auth = this.wordpressService.getAuthContext(req);
|
const auth = this.wordpressService.getAuthContext(req);
|
||||||
const detail = await this.wordpressService.articleDetail(id, auth);
|
const detail = await this.wordpressService.articleDetail(id, auth);
|
||||||
|
|
||||||
return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', detail));
|
return res.send(vbenSuccess(detail));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('save')
|
@Post('save')
|
||||||
@ -74,7 +71,7 @@ export class WordpressArticleController {
|
|||||||
const auth = this.wordpressService.getAuthContext(req);
|
const auth = this.wordpressService.getAuthContext(req);
|
||||||
const result = await this.wordpressService.articleSave(body, auth);
|
const result = await this.wordpressService.articleSave(body, auth);
|
||||||
|
|
||||||
return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', result));
|
return res.send(vbenSuccess(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('update')
|
@Post('update')
|
||||||
@ -88,7 +85,7 @@ export class WordpressArticleController {
|
|||||||
const auth = this.wordpressService.getAuthContext(req);
|
const auth = this.wordpressService.getAuthContext(req);
|
||||||
const result = await this.wordpressService.articleUpdate(body, auth);
|
const result = await this.wordpressService.articleUpdate(body, auth);
|
||||||
|
|
||||||
return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', result));
|
return res.send(vbenSuccess(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('remove')
|
@Post('remove')
|
||||||
@ -109,6 +106,6 @@ export class WordpressArticleController {
|
|||||||
auth,
|
auth,
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', result));
|
return res.send(vbenSuccess(result));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
HttpStatus,
|
|
||||||
Post,
|
Post,
|
||||||
Req,
|
Req,
|
||||||
Res,
|
Res,
|
||||||
@ -10,7 +9,7 @@ import {
|
|||||||
import { ApiHeader, ApiOperation, ApiTags } from '@nestjs/swagger';
|
import { ApiHeader, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard';
|
import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard';
|
||||||
import { Public, ToolsService } from '@/common';
|
import { Public, vbenSuccess } from '@/common';
|
||||||
import { WordpressService } from './wordpress.service';
|
import { WordpressService } from './wordpress.service';
|
||||||
|
|
||||||
@ApiTags('wordpress-auth')
|
@ApiTags('wordpress-auth')
|
||||||
@ -27,10 +26,7 @@ import { WordpressService } from './wordpress.service';
|
|||||||
@Controller('wordpress/auth')
|
@Controller('wordpress/auth')
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
export class WordpressAuthController {
|
export class WordpressAuthController {
|
||||||
constructor(
|
constructor(private readonly wordpressService: WordpressService) {}
|
||||||
private readonly toolsService: ToolsService,
|
|
||||||
private readonly wordpressService: WordpressService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Post('login')
|
@Post('login')
|
||||||
@ApiOperation({ summary: '使用环境变量中的 WordPress 管理员账号自动认证' })
|
@ApiOperation({ summary: '使用环境变量中的 WordPress 管理员账号自动认证' })
|
||||||
@ -39,7 +35,7 @@ export class WordpressAuthController {
|
|||||||
await this.wordpressService.loginWithConfiguredAdmin();
|
await this.wordpressService.loginWithConfiguredAdmin();
|
||||||
this.wordpressService.setAuthCookie(res, cookie);
|
this.wordpressService.setAuthCookie(res, cookie);
|
||||||
|
|
||||||
return this.toolsService.res(HttpStatus.OK, '操作成功', {
|
return vbenSuccess({
|
||||||
auth,
|
auth,
|
||||||
user,
|
user,
|
||||||
});
|
});
|
||||||
@ -51,7 +47,7 @@ export class WordpressAuthController {
|
|||||||
logout(@Res({ passthrough: true }) res: Response) {
|
logout(@Res({ passthrough: true }) res: Response) {
|
||||||
this.wordpressService.clearAuthCookie(res);
|
this.wordpressService.clearAuthCookie(res);
|
||||||
|
|
||||||
return this.toolsService.res(HttpStatus.OK, '操作成功', true);
|
return vbenSuccess(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('check')
|
@Get('check')
|
||||||
@ -60,6 +56,6 @@ export class WordpressAuthController {
|
|||||||
const auth = this.wordpressService.getAuthContext(req);
|
const auth = this.wordpressService.getAuthContext(req);
|
||||||
const user = await this.wordpressService.checkAuth(auth);
|
const user = await this.wordpressService.checkAuth(auth);
|
||||||
|
|
||||||
return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', user));
|
return res.send(vbenSuccess(user));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import {
|
|||||||
import { ApiHeader, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
import { ApiHeader, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||||
import type { Request } from 'express';
|
import type { Request } from 'express';
|
||||||
import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard';
|
import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard';
|
||||||
import { ToolsService } from '@/common';
|
import { vbenSuccess } from '@/common';
|
||||||
import {
|
import {
|
||||||
WordpressTermBodyDto,
|
WordpressTermBodyDto,
|
||||||
WordpressTermListQueryDto,
|
WordpressTermListQueryDto,
|
||||||
@ -35,10 +35,7 @@ import { WordpressService } from './wordpress.service';
|
|||||||
@Controller('wordpress/category')
|
@Controller('wordpress/category')
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
export class WordpressCategoryController {
|
export class WordpressCategoryController {
|
||||||
constructor(
|
constructor(private readonly wordpressService: WordpressService) {}
|
||||||
private readonly toolsService: ToolsService,
|
|
||||||
private readonly wordpressService: WordpressService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Get('list')
|
@Get('list')
|
||||||
@ApiOperation({ summary: '获取 WordPress 分类分页列表' })
|
@ApiOperation({ summary: '获取 WordPress 分类分页列表' })
|
||||||
@ -50,7 +47,7 @@ export class WordpressCategoryController {
|
|||||||
const auth = this.wordpressService.getAuthContext(req);
|
const auth = this.wordpressService.getAuthContext(req);
|
||||||
const list = await this.wordpressService.categoryList(query, auth);
|
const list = await this.wordpressService.categoryList(query, auth);
|
||||||
|
|
||||||
return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', list));
|
return res.send(vbenSuccess(list));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('detail')
|
@Get('detail')
|
||||||
@ -60,7 +57,7 @@ export class WordpressCategoryController {
|
|||||||
const auth = this.wordpressService.getAuthContext(req);
|
const auth = this.wordpressService.getAuthContext(req);
|
||||||
const detail = await this.wordpressService.categoryDetail(id, auth);
|
const detail = await this.wordpressService.categoryDetail(id, auth);
|
||||||
|
|
||||||
return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', detail));
|
return res.send(vbenSuccess(detail));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('save')
|
@Post('save')
|
||||||
@ -74,7 +71,7 @@ export class WordpressCategoryController {
|
|||||||
const auth = this.wordpressService.getAuthContext(req);
|
const auth = this.wordpressService.getAuthContext(req);
|
||||||
const result = await this.wordpressService.categorySave(body, auth);
|
const result = await this.wordpressService.categorySave(body, auth);
|
||||||
|
|
||||||
return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', result));
|
return res.send(vbenSuccess(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('update')
|
@Post('update')
|
||||||
@ -88,7 +85,7 @@ export class WordpressCategoryController {
|
|||||||
const auth = this.wordpressService.getAuthContext(req);
|
const auth = this.wordpressService.getAuthContext(req);
|
||||||
const result = await this.wordpressService.categoryUpdate(body, auth);
|
const result = await this.wordpressService.categoryUpdate(body, auth);
|
||||||
|
|
||||||
return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', result));
|
return res.send(vbenSuccess(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('remove')
|
@Post('remove')
|
||||||
@ -109,6 +106,6 @@ export class WordpressCategoryController {
|
|||||||
auth,
|
auth,
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', result));
|
return res.send(vbenSuccess(result));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import {
|
|||||||
import { ApiHeader, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
import { ApiHeader, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||||
import type { Request } from 'express';
|
import type { Request } from 'express';
|
||||||
import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard';
|
import { JwtAuthGuard } from '@/admin/auth/jwt-auth.guard';
|
||||||
import { ToolsService } from '@/common';
|
import { vbenSuccess } from '@/common';
|
||||||
import {
|
import {
|
||||||
WordpressTermBodyDto,
|
WordpressTermBodyDto,
|
||||||
WordpressTermListQueryDto,
|
WordpressTermListQueryDto,
|
||||||
@ -35,10 +35,7 @@ import { WordpressService } from './wordpress.service';
|
|||||||
@Controller('wordpress/tag')
|
@Controller('wordpress/tag')
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
export class WordpressTagController {
|
export class WordpressTagController {
|
||||||
constructor(
|
constructor(private readonly wordpressService: WordpressService) {}
|
||||||
private readonly toolsService: ToolsService,
|
|
||||||
private readonly wordpressService: WordpressService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Get('list')
|
@Get('list')
|
||||||
@ApiOperation({ summary: '获取 WordPress 标签分页列表' })
|
@ApiOperation({ summary: '获取 WordPress 标签分页列表' })
|
||||||
@ -50,7 +47,7 @@ export class WordpressTagController {
|
|||||||
const auth = this.wordpressService.getAuthContext(req);
|
const auth = this.wordpressService.getAuthContext(req);
|
||||||
const list = await this.wordpressService.tagList(query, auth);
|
const list = await this.wordpressService.tagList(query, auth);
|
||||||
|
|
||||||
return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', list));
|
return res.send(vbenSuccess(list));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('detail')
|
@Get('detail')
|
||||||
@ -60,7 +57,7 @@ export class WordpressTagController {
|
|||||||
const auth = this.wordpressService.getAuthContext(req);
|
const auth = this.wordpressService.getAuthContext(req);
|
||||||
const detail = await this.wordpressService.tagDetail(id, auth);
|
const detail = await this.wordpressService.tagDetail(id, auth);
|
||||||
|
|
||||||
return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', detail));
|
return res.send(vbenSuccess(detail));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('save')
|
@Post('save')
|
||||||
@ -74,7 +71,7 @@ export class WordpressTagController {
|
|||||||
const auth = this.wordpressService.getAuthContext(req);
|
const auth = this.wordpressService.getAuthContext(req);
|
||||||
const result = await this.wordpressService.tagSave(body, auth);
|
const result = await this.wordpressService.tagSave(body, auth);
|
||||||
|
|
||||||
return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', result));
|
return res.send(vbenSuccess(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('update')
|
@Post('update')
|
||||||
@ -88,7 +85,7 @@ export class WordpressTagController {
|
|||||||
const auth = this.wordpressService.getAuthContext(req);
|
const auth = this.wordpressService.getAuthContext(req);
|
||||||
const result = await this.wordpressService.tagUpdate(body, auth);
|
const result = await this.wordpressService.tagUpdate(body, auth);
|
||||||
|
|
||||||
return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', result));
|
return res.send(vbenSuccess(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('remove')
|
@Post('remove')
|
||||||
@ -109,6 +106,6 @@ export class WordpressTagController {
|
|||||||
auth,
|
auth,
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', result));
|
return res.send(vbenSuccess(result));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { AdminAuthGuardModule } from '@/admin/auth/admin-auth-guard.module';
|
import { AdminAuthGuardModule } from '@/admin/auth/admin-auth-guard.module';
|
||||||
import { ToolsService } from '@/common';
|
|
||||||
import { WordpressArticleController } from './wordpress-article.controller';
|
import { WordpressArticleController } from './wordpress-article.controller';
|
||||||
import { WordpressAuthController } from './wordpress-auth.controller';
|
import { WordpressAuthController } from './wordpress-auth.controller';
|
||||||
import { WordpressCategoryController } from './wordpress-category.controller';
|
import { WordpressCategoryController } from './wordpress-category.controller';
|
||||||
@ -15,7 +14,7 @@ import { WordpressTagController } from './wordpress-tag.controller';
|
|||||||
WordpressTagController,
|
WordpressTagController,
|
||||||
WordpressCategoryController,
|
WordpressCategoryController,
|
||||||
],
|
],
|
||||||
providers: [ToolsService, WordpressService],
|
providers: [WordpressService],
|
||||||
exports: [WordpressService],
|
exports: [WordpressService],
|
||||||
})
|
})
|
||||||
export class WordpressModule {}
|
export class WordpressModule {}
|
||||||
|
|||||||
@ -23,6 +23,30 @@ export type WordpressLoginResult = {
|
|||||||
user: any;
|
user: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WordpressOptionalLoginResult =
|
||||||
|
| {
|
||||||
|
available: false;
|
||||||
|
error: WordpressAvailabilityError;
|
||||||
|
result: null;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
available: true;
|
||||||
|
error: null;
|
||||||
|
result: WordpressLoginResult & { cookie: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WordpressAvailabilityError = {
|
||||||
|
error: any;
|
||||||
|
message: string;
|
||||||
|
status: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WordpressAvailabilityCache = {
|
||||||
|
available: boolean;
|
||||||
|
checkedAt: number;
|
||||||
|
error?: WordpressAvailabilityError;
|
||||||
|
};
|
||||||
|
|
||||||
type WordpressRequestOptions = {
|
type WordpressRequestOptions = {
|
||||||
auth: WordpressAuthContext;
|
auth: WordpressAuthContext;
|
||||||
body?: Record<string, unknown>;
|
body?: Record<string, unknown>;
|
||||||
@ -46,6 +70,8 @@ const WORDPRESS_AUTH_COOKIE = 'kt_wordpress_auth';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WordpressService {
|
export class WordpressService {
|
||||||
|
private availabilityCache: null | WordpressAvailabilityCache = null;
|
||||||
|
|
||||||
constructor(private readonly configService: ConfigService) {}
|
constructor(private readonly configService: ConfigService) {}
|
||||||
|
|
||||||
getAuthContext(request: Request): WordpressAuthContext {
|
getAuthContext(request: Request): WordpressAuthContext {
|
||||||
@ -79,9 +105,45 @@ export class WordpressService {
|
|||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async loginWithConfiguredAdmin(): Promise<
|
async tryLoginWithConfiguredAdmin(): Promise<WordpressOptionalLoginResult> {
|
||||||
WordpressLoginResult & { cookie: string }
|
try {
|
||||||
> {
|
const result = await this.loginWithConfiguredAdmin({
|
||||||
|
timeoutMs: this.getLoginTimeout(),
|
||||||
|
});
|
||||||
|
this.rememberAvailability(true);
|
||||||
|
|
||||||
|
return {
|
||||||
|
available: true,
|
||||||
|
error: null,
|
||||||
|
result,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
const error = this.normalizeAvailabilityError(err);
|
||||||
|
this.rememberAvailability(false, error);
|
||||||
|
|
||||||
|
return {
|
||||||
|
available: false,
|
||||||
|
error,
|
||||||
|
result: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdminIntegrationAvailable() {
|
||||||
|
if (!this.availabilityCache) return true;
|
||||||
|
if (
|
||||||
|
Date.now() - this.availabilityCache.checkedAt >
|
||||||
|
this.getAvailabilityTtl()
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.availabilityCache.available;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loginWithConfiguredAdmin(
|
||||||
|
options: { timeoutMs?: number } = {},
|
||||||
|
): Promise<WordpressLoginResult & { cookie: string }> {
|
||||||
const username = this.configService.get<string>('WORDPRESS_ADMIN_USERNAME');
|
const username = this.configService.get<string>('WORDPRESS_ADMIN_USERNAME');
|
||||||
const password = this.configService.get<string>('WORDPRESS_ADMIN_PASSWORD');
|
const password = this.configService.get<string>('WORDPRESS_ADMIN_PASSWORD');
|
||||||
|
|
||||||
@ -93,8 +155,12 @@ export class WordpressService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cookie = await this.loginByPassword(username, password);
|
const cookie = await this.loginByPassword(
|
||||||
const nonce = await this.fetchRestNonce(cookie);
|
username,
|
||||||
|
password,
|
||||||
|
options.timeoutMs,
|
||||||
|
);
|
||||||
|
const nonce = await this.fetchRestNonce(cookie, options.timeoutMs);
|
||||||
|
|
||||||
if (!nonce) {
|
if (!nonce) {
|
||||||
throwVbenError(
|
throwVbenError(
|
||||||
@ -138,7 +204,10 @@ export class WordpressService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async articleList(query: WordpressArticleListQueryDto, auth: WordpressAuthContext) {
|
async articleList(
|
||||||
|
query: WordpressArticleListQueryDto,
|
||||||
|
auth: WordpressAuthContext,
|
||||||
|
) {
|
||||||
const response = await this.request<any[]>('/wp-json/wp/v2/posts', {
|
const response = await this.request<any[]>('/wp-json/wp/v2/posts', {
|
||||||
auth,
|
auth,
|
||||||
query: {
|
query: {
|
||||||
@ -179,7 +248,10 @@ export class WordpressService {
|
|||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async articleUpdate(body: WordpressArticleBodyDto & { id: number }, auth: WordpressAuthContext) {
|
async articleUpdate(
|
||||||
|
body: WordpressArticleBodyDto & { id: number },
|
||||||
|
auth: WordpressAuthContext,
|
||||||
|
) {
|
||||||
const response = await this.request(`/wp-json/wp/v2/posts/${body.id}`, {
|
const response = await this.request(`/wp-json/wp/v2/posts/${body.id}`, {
|
||||||
auth,
|
auth,
|
||||||
body: this.getArticleBody(body),
|
body: this.getArticleBody(body),
|
||||||
@ -189,7 +261,11 @@ export class WordpressService {
|
|||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async articleRemove(id: string | number, force: boolean, auth: WordpressAuthContext) {
|
async articleRemove(
|
||||||
|
id: string | number,
|
||||||
|
force: boolean,
|
||||||
|
auth: WordpressAuthContext,
|
||||||
|
) {
|
||||||
const response = await this.request(`/wp-json/wp/v2/posts/${id}`, {
|
const response = await this.request(`/wp-json/wp/v2/posts/${id}`, {
|
||||||
auth,
|
auth,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
@ -213,15 +289,25 @@ export class WordpressService {
|
|||||||
return this.termSave('/wp-json/wp/v2/tags', body, auth);
|
return this.termSave('/wp-json/wp/v2/tags', body, auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
async tagUpdate(body: WordpressTermBodyDto & { id: number }, auth: WordpressAuthContext) {
|
async tagUpdate(
|
||||||
|
body: WordpressTermBodyDto & { id: number },
|
||||||
|
auth: WordpressAuthContext,
|
||||||
|
) {
|
||||||
return this.termUpdate('/wp-json/wp/v2/tags', body, auth);
|
return this.termUpdate('/wp-json/wp/v2/tags', body, auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
async tagRemove(id: string | number, force: boolean, auth: WordpressAuthContext) {
|
async tagRemove(
|
||||||
|
id: string | number,
|
||||||
|
force: boolean,
|
||||||
|
auth: WordpressAuthContext,
|
||||||
|
) {
|
||||||
return this.termRemove('/wp-json/wp/v2/tags', id, force, auth);
|
return this.termRemove('/wp-json/wp/v2/tags', id, force, auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
async categoryList(query: WordpressTermListQueryDto, auth: WordpressAuthContext) {
|
async categoryList(
|
||||||
|
query: WordpressTermListQueryDto,
|
||||||
|
auth: WordpressAuthContext,
|
||||||
|
) {
|
||||||
return this.termList('/wp-json/wp/v2/categories', query, auth);
|
return this.termList('/wp-json/wp/v2/categories', query, auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,11 +319,18 @@ export class WordpressService {
|
|||||||
return this.termSave('/wp-json/wp/v2/categories', body, auth);
|
return this.termSave('/wp-json/wp/v2/categories', body, auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
async categoryUpdate(body: WordpressTermBodyDto & { id: number }, auth: WordpressAuthContext) {
|
async categoryUpdate(
|
||||||
|
body: WordpressTermBodyDto & { id: number },
|
||||||
|
auth: WordpressAuthContext,
|
||||||
|
) {
|
||||||
return this.termUpdate('/wp-json/wp/v2/categories', body, auth);
|
return this.termUpdate('/wp-json/wp/v2/categories', body, auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
async categoryRemove(id: string | number, force: boolean, auth: WordpressAuthContext) {
|
async categoryRemove(
|
||||||
|
id: string | number,
|
||||||
|
force: boolean,
|
||||||
|
auth: WordpressAuthContext,
|
||||||
|
) {
|
||||||
return this.termRemove('/wp-json/wp/v2/categories', id, force, auth);
|
return this.termRemove('/wp-json/wp/v2/categories', id, force, auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -346,7 +439,11 @@ export class WordpressService {
|
|||||||
const data = await this.parseResponse(response);
|
const data = await this.parseResponse(response);
|
||||||
|
|
||||||
// 兼容未开启 Apache rewrite 的 WordPress:/wp-json 404 时自动回退到 ?rest_route=。
|
// 兼容未开启 Apache rewrite 的 WordPress:/wp-json 404 时自动回退到 ?rest_route=。
|
||||||
if (!response.ok && response.status === 404 && index < urls.length - 1) {
|
if (
|
||||||
|
!response.ok &&
|
||||||
|
response.status === 404 &&
|
||||||
|
index < urls.length - 1
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -380,14 +477,22 @@ export class WordpressService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loginByPassword(username: string, password: string) {
|
private async loginByPassword(
|
||||||
const response = await this.formRequest('/wp-login.php', {
|
username: string,
|
||||||
|
password: string,
|
||||||
|
timeoutMs?: number,
|
||||||
|
) {
|
||||||
|
const response = await this.formRequest(
|
||||||
|
'/wp-login.php',
|
||||||
|
{
|
||||||
log: username,
|
log: username,
|
||||||
pwd: password,
|
pwd: password,
|
||||||
redirect_to: this.getUrl('/wp-admin/'),
|
redirect_to: this.getUrl('/wp-admin/'),
|
||||||
testcookie: '1',
|
testcookie: '1',
|
||||||
'wp-submit': 'Log In',
|
'wp-submit': 'Log In',
|
||||||
});
|
},
|
||||||
|
timeoutMs,
|
||||||
|
);
|
||||||
const setCookies = this.getSetCookieHeaders(response.headers);
|
const setCookies = this.getSetCookieHeaders(response.headers);
|
||||||
const cookie = this.toCookieHeader(setCookies);
|
const cookie = this.toCookieHeader(setCookies);
|
||||||
|
|
||||||
@ -403,15 +508,23 @@ export class WordpressService {
|
|||||||
return cookie;
|
return cookie;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchRestNonce(cookie: string) {
|
private async fetchRestNonce(cookie: string, timeoutMs?: number) {
|
||||||
const adminPaths = ['/wp-admin/', '/wp-admin/post-new.php', '/wp-admin/edit.php'];
|
const adminPaths = [
|
||||||
|
'/wp-admin/',
|
||||||
|
'/wp-admin/post-new.php',
|
||||||
|
'/wp-admin/edit.php',
|
||||||
|
];
|
||||||
|
|
||||||
for (const path of adminPaths) {
|
for (const path of adminPaths) {
|
||||||
const response = await this.rawRequest(path, {
|
const response = await this.rawRequest(
|
||||||
|
path,
|
||||||
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: cookie,
|
Cookie: cookie,
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
timeoutMs,
|
||||||
|
);
|
||||||
const html = await response.text().catch(() => '');
|
const html = await response.text().catch(() => '');
|
||||||
const nonce = this.extractRestNonce(html);
|
const nonce = this.extractRestNonce(html);
|
||||||
|
|
||||||
@ -421,10 +534,16 @@ export class WordpressService {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
private async formRequest(path: string, body: Record<string, string>) {
|
private async formRequest(
|
||||||
|
path: string,
|
||||||
|
body: Record<string, string>,
|
||||||
|
timeoutMs?: number,
|
||||||
|
) {
|
||||||
const form = new URLSearchParams(body);
|
const form = new URLSearchParams(body);
|
||||||
|
|
||||||
return this.rawRequest(path, {
|
return this.rawRequest(
|
||||||
|
path,
|
||||||
|
{
|
||||||
body: form,
|
body: form,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
@ -432,12 +551,21 @@ export class WordpressService {
|
|||||||
},
|
},
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
redirect: 'manual',
|
redirect: 'manual',
|
||||||
});
|
},
|
||||||
|
timeoutMs,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async rawRequest(path: string, init: RequestInit = {}) {
|
private async rawRequest(
|
||||||
|
path: string,
|
||||||
|
init: RequestInit = {},
|
||||||
|
timeoutMs?: number,
|
||||||
|
) {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timer = setTimeout(() => controller.abort(), this.getTimeout());
|
const timer = setTimeout(
|
||||||
|
() => controller.abort(),
|
||||||
|
timeoutMs || this.getTimeout(),
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await fetch(this.getUrl(path), {
|
return await fetch(this.getUrl(path), {
|
||||||
@ -497,7 +625,8 @@ export class WordpressService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getCookieOptions() {
|
private getCookieOptions() {
|
||||||
const secure = this.configService.get<string>('ADMIN_COOKIE_SECURE') === 'true';
|
const secure =
|
||||||
|
this.configService.get<string>('ADMIN_COOKIE_SECURE') === 'true';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
@ -551,6 +680,31 @@ export class WordpressService {
|
|||||||
return Number(this.configService.get('WORDPRESS_TIMEOUT_MS') || 15000);
|
return Number(this.configService.get('WORDPRESS_TIMEOUT_MS') || 15000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getLoginTimeout() {
|
||||||
|
return Number(
|
||||||
|
this.configService.get('WORDPRESS_LOGIN_TIMEOUT_MS') ||
|
||||||
|
this.configService.get('WORDPRESS_TIMEOUT_MS') ||
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAvailabilityTtl() {
|
||||||
|
return Number(
|
||||||
|
this.configService.get('WORDPRESS_AVAILABILITY_TTL_MS') || 60_000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private rememberAvailability(
|
||||||
|
available: boolean,
|
||||||
|
error?: WordpressAvailabilityError,
|
||||||
|
) {
|
||||||
|
this.availabilityCache = {
|
||||||
|
available,
|
||||||
|
checkedAt: Date.now(),
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private getPageQuery(query: WordpressPagedQueryDto) {
|
private getPageQuery(query: WordpressPagedQueryDto) {
|
||||||
return {
|
return {
|
||||||
order: query.order,
|
order: query.order,
|
||||||
@ -642,6 +796,30 @@ export class WordpressService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private normalizeAvailabilityError(err: unknown): WordpressAvailabilityError {
|
||||||
|
if (err instanceof HttpException) {
|
||||||
|
const response = err.getResponse();
|
||||||
|
const responseBody =
|
||||||
|
response && typeof response === 'object'
|
||||||
|
? (response as Record<string, any>)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: responseBody.error || response,
|
||||||
|
message:
|
||||||
|
responseBody.message ||
|
||||||
|
(typeof response === 'string' ? response : err.message),
|
||||||
|
status: err.getStatus(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: err instanceof Error ? err.name : 'WordPressUnavailable',
|
||||||
|
message: err instanceof Error ? err.message : 'WordPress 暂不可用',
|
||||||
|
status: HttpStatus.BAD_GATEWAY,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private getErrorCause(err: unknown) {
|
private getErrorCause(err: unknown) {
|
||||||
const cause = (err as { cause?: { code?: string; message?: string } })
|
const cause = (err as { cause?: { code?: string; message?: string } })
|
||||||
?.cause;
|
?.cause;
|
||||||
@ -650,11 +828,16 @@ export class WordpressService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getLoginErrorMessage(html: string) {
|
private getLoginErrorMessage(html: string) {
|
||||||
const match = html.match(/<div[^>]*id=["']login_error["'][^>]*>([\s\S]*?)<\/div>/i);
|
const match = html.match(
|
||||||
|
/<div[^>]*id=["']login_error["'][^>]*>([\s\S]*?)<\/div>/i,
|
||||||
|
);
|
||||||
|
|
||||||
if (!match?.[1]) return 'WordPress 管理员登录失败';
|
if (!match?.[1]) return 'WordPress 管理员登录失败';
|
||||||
|
|
||||||
return match[1].replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
|
return match[1]
|
||||||
|
.replace(/<[^>]+>/g, '')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSetCookieHeaders(headers: Headers) {
|
private getSetCookieHeaders(headers: Headers) {
|
||||||
@ -778,4 +961,6 @@ export class WordpressService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type WordpressPagedQueryDto = WordpressArticleListQueryDto | WordpressTermListQueryDto;
|
type WordpressPagedQueryDto =
|
||||||
|
| WordpressArticleListQueryDto
|
||||||
|
| WordpressTermListQueryDto;
|
||||||
|
|||||||
@ -563,9 +563,8 @@ const routeTestCases: Record<string, RouteTestCase> = {
|
|||||||
expect(wordpressServiceMock.checkAuth).toHaveBeenCalledWith(
|
expect(wordpressServiceMock.checkAuth).toHaveBeenCalledWith(
|
||||||
wordpressAuthContext,
|
wordpressAuthContext,
|
||||||
);
|
);
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toMatchObject({
|
||||||
code: 200,
|
code: 0,
|
||||||
msg: '操作成功',
|
|
||||||
data: wordpressUser,
|
data: wordpressUser,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -584,9 +583,8 @@ const routeTestCases: Record<string, RouteTestCase> = {
|
|||||||
expect.anything(),
|
expect.anything(),
|
||||||
wordpressLoginResult.cookie,
|
wordpressLoginResult.cookie,
|
||||||
);
|
);
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toMatchObject({
|
||||||
code: 200,
|
code: 0,
|
||||||
msg: '操作成功',
|
|
||||||
data: {
|
data: {
|
||||||
auth: wordpressLoginResult.auth,
|
auth: wordpressLoginResult.auth,
|
||||||
user: wordpressUser,
|
user: wordpressUser,
|
||||||
@ -602,9 +600,8 @@ const routeTestCases: Record<string, RouteTestCase> = {
|
|||||||
expect(wordpressServiceMock.clearAuthCookie).toHaveBeenCalledWith(
|
expect(wordpressServiceMock.clearAuthCookie).toHaveBeenCalledWith(
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
);
|
);
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toMatchObject({
|
||||||
code: 200,
|
code: 0,
|
||||||
msg: '操作成功',
|
|
||||||
data: true,
|
data: true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -632,9 +629,8 @@ const routeTestCases: Record<string, RouteTestCase> = {
|
|||||||
},
|
},
|
||||||
wordpressAuthContext,
|
wordpressAuthContext,
|
||||||
);
|
);
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toMatchObject({
|
||||||
code: 200,
|
code: 0,
|
||||||
msg: '操作成功',
|
|
||||||
data: {
|
data: {
|
||||||
list: [wordpressArticle],
|
list: [wordpressArticle],
|
||||||
total: 1,
|
total: 1,
|
||||||
@ -654,9 +650,8 @@ const routeTestCases: Record<string, RouteTestCase> = {
|
|||||||
'1',
|
'1',
|
||||||
wordpressAuthContext,
|
wordpressAuthContext,
|
||||||
);
|
);
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toMatchObject({
|
||||||
code: 200,
|
code: 0,
|
||||||
msg: '操作成功',
|
|
||||||
data: wordpressArticle,
|
data: wordpressArticle,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -680,9 +675,8 @@ const routeTestCases: Record<string, RouteTestCase> = {
|
|||||||
},
|
},
|
||||||
wordpressAuthContext,
|
wordpressAuthContext,
|
||||||
);
|
);
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toMatchObject({
|
||||||
code: 200,
|
code: 0,
|
||||||
msg: '操作成功',
|
|
||||||
data: wordpressArticle,
|
data: wordpressArticle,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -705,9 +699,8 @@ const routeTestCases: Record<string, RouteTestCase> = {
|
|||||||
},
|
},
|
||||||
wordpressAuthContext,
|
wordpressAuthContext,
|
||||||
);
|
);
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toMatchObject({
|
||||||
code: 200,
|
code: 0,
|
||||||
msg: '操作成功',
|
|
||||||
data: wordpressArticle,
|
data: wordpressArticle,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -728,9 +721,8 @@ const routeTestCases: Record<string, RouteTestCase> = {
|
|||||||
false,
|
false,
|
||||||
wordpressAuthContext,
|
wordpressAuthContext,
|
||||||
);
|
);
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toMatchObject({
|
||||||
code: 200,
|
code: 0,
|
||||||
msg: '操作成功',
|
|
||||||
data: true,
|
data: true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -758,9 +750,8 @@ const routeTestCases: Record<string, RouteTestCase> = {
|
|||||||
},
|
},
|
||||||
wordpressAuthContext,
|
wordpressAuthContext,
|
||||||
);
|
);
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toMatchObject({
|
||||||
code: 200,
|
code: 0,
|
||||||
msg: '操作成功',
|
|
||||||
data: {
|
data: {
|
||||||
list: [wordpressTerm],
|
list: [wordpressTerm],
|
||||||
total: 1,
|
total: 1,
|
||||||
@ -780,9 +771,8 @@ const routeTestCases: Record<string, RouteTestCase> = {
|
|||||||
'1',
|
'1',
|
||||||
wordpressAuthContext,
|
wordpressAuthContext,
|
||||||
);
|
);
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toMatchObject({
|
||||||
code: 200,
|
code: 0,
|
||||||
msg: '操作成功',
|
|
||||||
data: wordpressTerm,
|
data: wordpressTerm,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -804,9 +794,8 @@ const routeTestCases: Record<string, RouteTestCase> = {
|
|||||||
},
|
},
|
||||||
wordpressAuthContext,
|
wordpressAuthContext,
|
||||||
);
|
);
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toMatchObject({
|
||||||
code: 200,
|
code: 0,
|
||||||
msg: '操作成功',
|
|
||||||
data: wordpressTerm,
|
data: wordpressTerm,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -829,9 +818,8 @@ const routeTestCases: Record<string, RouteTestCase> = {
|
|||||||
},
|
},
|
||||||
wordpressAuthContext,
|
wordpressAuthContext,
|
||||||
);
|
);
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toMatchObject({
|
||||||
code: 200,
|
code: 0,
|
||||||
msg: '操作成功',
|
|
||||||
data: wordpressTerm,
|
data: wordpressTerm,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -849,9 +837,8 @@ const routeTestCases: Record<string, RouteTestCase> = {
|
|||||||
true,
|
true,
|
||||||
wordpressAuthContext,
|
wordpressAuthContext,
|
||||||
);
|
);
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toMatchObject({
|
||||||
code: 200,
|
code: 0,
|
||||||
msg: '操作成功',
|
|
||||||
data: true,
|
data: true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -879,9 +866,8 @@ const routeTestCases: Record<string, RouteTestCase> = {
|
|||||||
},
|
},
|
||||||
wordpressAuthContext,
|
wordpressAuthContext,
|
||||||
);
|
);
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toMatchObject({
|
||||||
code: 200,
|
code: 0,
|
||||||
msg: '操作成功',
|
|
||||||
data: {
|
data: {
|
||||||
list: [wordpressTerm],
|
list: [wordpressTerm],
|
||||||
total: 1,
|
total: 1,
|
||||||
@ -901,9 +887,8 @@ const routeTestCases: Record<string, RouteTestCase> = {
|
|||||||
'1',
|
'1',
|
||||||
wordpressAuthContext,
|
wordpressAuthContext,
|
||||||
);
|
);
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toMatchObject({
|
||||||
code: 200,
|
code: 0,
|
||||||
msg: '操作成功',
|
|
||||||
data: wordpressTerm,
|
data: wordpressTerm,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -927,9 +912,8 @@ const routeTestCases: Record<string, RouteTestCase> = {
|
|||||||
},
|
},
|
||||||
wordpressAuthContext,
|
wordpressAuthContext,
|
||||||
);
|
);
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toMatchObject({
|
||||||
code: 200,
|
code: 0,
|
||||||
msg: '操作成功',
|
|
||||||
data: wordpressTerm,
|
data: wordpressTerm,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -954,9 +938,8 @@ const routeTestCases: Record<string, RouteTestCase> = {
|
|||||||
},
|
},
|
||||||
wordpressAuthContext,
|
wordpressAuthContext,
|
||||||
);
|
);
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toMatchObject({
|
||||||
code: 200,
|
code: 0,
|
||||||
msg: '操作成功',
|
|
||||||
data: wordpressTerm,
|
data: wordpressTerm,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -974,9 +957,8 @@ const routeTestCases: Record<string, RouteTestCase> = {
|
|||||||
true,
|
true,
|
||||||
wordpressAuthContext,
|
wordpressAuthContext,
|
||||||
);
|
);
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toMatchObject({
|
||||||
code: 200,
|
code: 0,
|
||||||
msg: '操作成功',
|
|
||||||
data: true,
|
data: true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user