├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── .gitignore ├── LICENSE ├── README.md ├── app ├── api │ ├── cms │ │ ├── admin.js │ │ ├── file.js │ │ ├── log.js │ │ ├── test.js │ │ └── user.js │ └── v1 │ │ └── book.js ├── app.js ├── config │ ├── code-message.js │ ├── log.js │ ├── secure.js │ └── setting.js ├── dao │ ├── admin.js │ ├── book.js │ ├── log.js │ └── user.js ├── extension │ ├── file │ │ ├── config.js │ │ └── local-uploader.js │ └── socket │ │ ├── config.js │ │ └── socket.js ├── lib │ ├── captcha.js │ ├── db.js │ ├── exception.js │ ├── type.js │ └── util.js ├── middleware │ ├── jwt.js │ └── logger.js ├── model │ ├── book.js │ ├── file.js │ ├── group-permission.js │ ├── group.js │ ├── log.js │ ├── permission.js │ ├── user-group.js │ └── user.js ├── plugin │ └── poem │ │ ├── app │ │ ├── controller.js │ │ ├── index.js │ │ ├── model.js │ │ └── validator.js │ │ └── config.js ├── starter.js └── validator │ ├── admin.js │ ├── book.js │ ├── common.js │ ├── log.js │ └── user.js ├── code.md ├── index.js ├── jest.config.js ├── package.json ├── schema.sql └── test ├── api └── cms │ ├── admin.test.js │ ├── test1.test.js │ ├── user1.test.js │ └── user2.test.js └── helper ├── fake-book ├── fake-book.js └── index.js ├── fake ├── fake.js └── index.js ├── initial.js ├── poem ├── index.js └── poem.js ├── secure.js └── token.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", {"targets": {"node": "current"}}] 4 | ], 5 | "plugins": [ 6 | ["@babel/plugin-proposal-decorators", { "legacy": true }] 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'standard', 3 | plugins: ['jest'], 4 | rules: { 5 | semi: ['warn', 'always'], 6 | quotes: ['warn', 'single'], 7 | 'camelcase': 0, 8 | 'eol-last': 0, 9 | 'jest/no-disabled-tests': 'warn', 10 | 'jest/no-focused-tests': 'error', 11 | 'jest/no-identical-title': 'error', 12 | 'jest/prefer-to-have-length': 'warn', 13 | 'jest/valid-expect': 'error', 14 | }, 15 | env: { 16 | jest: true 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 提出一个bug 3 | about: 提出bug帮助我们完善项目 4 | --- 5 | 6 | **描述 bug** 7 | 8 | - 你是如何操作的? 9 | - 发生了什么? 10 | - 你觉得应该出现什么? 11 | 12 | **你使用哪个版本出现该问题?** 13 | 14 | 如果使用`master`,请表明是 master 分支,否则给出具体的版本号 15 | 16 | **如何再现** 17 | 18 | If your bug is deterministic, can you give a minimal reproducing code? 19 | Some bugs are not deterministic. Can you describe with precision in which context it happened? 20 | If this is possible, can you share your code? 21 | 22 | 如果你确定存在这个 bug,你能提供我们一个最小的实现代码吗? 23 | 一些 bug 是不确定,只会在某些条件下触发,你能详细描述一下具体的情况和提供复现的步骤吗? 24 | 当然如果你提供在线的 repo,那就再好不过了。 25 | 26 | 如果你发现了 bug,并修复了它,请用`git rebase`合并成一条标准的`fix: description`提交,然后向我们的 27 | 项目提 PR,我们会在第一时间审核,并感谢您的参与。 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 提出新特性 3 | about: 对项目的发展提出建议 4 | --- 5 | 6 | CMS 是一个颇为复杂的应用,它需要的东西太多。我们无法涉及到方方面面,因此关于新特性,我们会以讨论的形式来确定这个特性是否去实现,以什么形式实现。 7 | 我们鼓励所有对这个特性感兴趣的人来参与讨论,当然如果你想参与特性的开发那就更好了。 8 | 9 | 如果你实现了一个 feature,并通过了单元测试,请用`git rebase`合并成一条标准的`feat: description`提交,然后向我们的 10 | 项目提 PR,我们会在第一时间审核,并感谢您的参与。 11 | 12 | **请问这个特性跟什么问题相关? 有哪些应用场景?请详细描述。** 13 | 请清晰准确的描述问题的内容,以及真实的场景。 14 | 15 | **请描述一下你想怎么实现这个特性** 16 | 怎么样去实现这个特性?加入核心库?加入工程项目?还是其他方式。 17 | 当然你也可以描述它的具体实现. 18 | 19 | **讨论** 20 | 如果这个特性应用场景非常多,或者非常重要,我们会第一时间去处理。但更多的我们希望更多的人参与讨论,来斟酌它的可行性。 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 提出问题 3 | about: 关于项目的疑问 4 | --- 5 | 6 | 请详细描述您对本项目的任何问题,我们会在第一时间查阅和解决。 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | npm-debug.log 3 | yarn-error.log 4 | node_modules/ 5 | package-lock.json 6 | coverage/ 7 | .idea/ 8 | run/ 9 | suspect/ 10 | .DS_Store 11 | *.sw* 12 | *.un~ 13 | .vscode/ 14 | dist 15 | learn 16 | tokens.json 17 | assets -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 TaleLin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 |
5 | Lin-CMS-Koa 6 |

7 | 8 |

一个简单易用的CMS后端项目

9 | 10 |

11 | 12 | flask version 13 | lin-cms version 14 | LISENCE 15 |

16 | 17 |
18 | Lin-CMS 是林间有风团队经过大量项目实践所提炼出的一套内容管理系统框架
19 | Lin-CMS 可以有效的帮助开发者提高 CMS 的开发效率。 20 |
21 | 22 |

23 | 简介 | 版本日志 24 |

25 | 26 | ## 简介 27 | 28 | ### 什么是 Lin CMS? 29 | 30 | Lin-CMS 是林间有风团队经过大量项目实践所提炼出的一套**内容管理系统框架**。Lin-CMS 可以有效的帮助开发者提高 CMS 的开发效率。 31 | 32 | 本项目是 Lin CMS 后端的 koa 实现,需要前端?请访问[前端仓库](https://github.com/TaleLin/lin-cms-vue)。 33 | 34 | ### 当前最新版本 35 | 36 | lin-cms-koa(当前示例工程):0.3.10 37 | 38 | lin-mizar(核心库) :0.3.8 39 | 40 | ### 文档地址 41 | 42 | [http://doc.cms.talelin.com/](http://doc.cms.talelin.com/) 43 | 44 | ### 线上 demo 45 | 46 | [http://face.cms.7yue.pro/](http://face.cms.7yue.pro/) 47 | 48 | ### 免费入门视频教程 49 | 50 | [https://www.imooc.com/learn/1247](https://www.imooc.com/learn/1247) 51 | 52 | ### QQ 交流群 53 | 54 | QQ 群号:643205479 / 814597236 55 | 56 | 57 | 58 | ### 微信公众号 59 | 60 | 微信搜索:林间有风 61 | 62 | 63 | 64 | ## 版本日志 65 | 66 | 最新版本 `0.3.12` 67 | 68 | ### 0.3.12 69 | 70 | 1. `A` 新增验证码功能,默认关闭验证码 71 | 2. `U` assets 目录用作本地文件上传,移到项目根目录 72 | 73 | ### 0.3.11 74 | 75 | 1. `F` 修复消息中心 API 调用拼写错误 76 | 77 | ### 0.3.10 78 | 79 | 1. `A` 新增[消息中心](https://github.com/TaleLin/lin-cms-koa/tree/master/app/extension/socket)扩展 80 | 81 | ### 0.3.9 82 | 83 | 1. `F` 修复 logger 第二次模板解析错误的问题 84 | 2. `U` 更新 lin-mizar 到 0.3.8 85 | 86 | ### 0.3.8 87 | 88 | 1. `F` 修复 缺少 mysql2 89 | 90 | ### 0.3.7 91 | 92 | 1. `U` 优化 编辑用户至少选择一个分组 93 | 94 | ### 0.3.6 95 | 96 | 1. `A` 新增 yarn.lock 97 | 2. `U` 更新 lin-mizar 到 0.3.5 版本 98 | 3. `F` 修复 disableLoading 为 `undefined` 的问题 99 | 100 | ### 0.3.5 101 | 102 | 1. `U` 更新核心库 lin-mizar 到 0.3.4 版本 103 | 2. `F` 修复文件上传丢失 key 字段 104 | 105 | ### 0.3.4 106 | 107 | 1. `U` 更新路由视图权限挂载的方式 108 | 2. `U` HttpException 不允许直接修改 status,传入的参数由 errorCode 改为 code 109 | 3. `U` 新增 code-message 配置,返回的成功码和错误码都在这里配置 110 | 4. `U` 支持自定义工作目录 111 | 5. `U` 更新核心库 lin-mizar 到 0.3.3 版本 112 | 113 | ### 0.3.3 114 | 115 | 1. `F` `GET /cms/user/information` 返回完整的头像链接 116 | 2. `F` 文件名重命名为用 `-` 连接,并且使用单数 117 | 118 | ### 0.3.2 119 | 120 | 1. `F` 更改文件上传返回字段 121 | 2. `F` `GET admin/users` 和 `GET admin/group/all` 接口过滤 `root` 用户 122 | 3. `F` `PUT /admin/user/{id}` 接口不允许修改 `root` 用户的分组 123 | 124 | ### 0.3.1 125 | 126 | 1. `F` 更新 `lin-mizar` 到 `0.3.2` 版本,路由属性名由 `auth` --> `permission` 127 | 128 | ### 0.3.0 129 | 130 | 1. `A` 将模型层抽离核心库进行重构 131 | 132 | ## Lin CMS 的特点 133 | 134 | Lin CMS 的构筑思想是有其自身特点的。下面我们阐述一些 Lin 的主要特点。 135 | 136 | #### Lin CMS 是一个前后端分离的 CMS 解决方案 137 | 138 | 这意味着,Lin 既提供后台的支撑,也有一套对应的前端系统,当然双端分离的好处不仅仅 139 | 在于此,我们会在后续提供`NodeJS`和`PHP`版本的 Lin。如果你心仪 Lin,却又因为技术 140 | 栈的原因无法即可使用,没关系,我们会在后续提供更多的语言版本。为什么 Lin 要选择 141 | 前后端分离的单页面架构呢? 142 | 143 | 首先,传统的网站开发更多的是采用服务端渲染的方式,需用使用一种模板语言在服务端完 144 | 成页面渲染:比如 JinJa2、Jade 等。服务端渲染的好处在于可以比较好的支持 SEO,但作 145 | 为内部使用的 CMS 管理系统,SEO 并不重要。 146 | 147 | 但一个不可忽视的事实是,服务器渲染的页面到底是由前端开发者来完成,还是由服务器开 148 | 发者来完成?其实都不太合适。现在已经没有多少前端开发者是了解这些服务端模板语言的 149 | ,而服务器开发者本身是不太擅长开发页面的。那还是分开吧,前端用最熟悉的 Vue 写 JS 150 | 和 CSS,而服务器只关注自己的 API 即可。 151 | 152 | 其次,单页面应用程序的体验本身就要好于传统网站。 153 | 154 | #### 框架本身已内置了 CMS 常用的功能 155 | 156 | Lin 已经内置了 CMS 中最为常见的需求:用户管理、权限管理、日志系统等。开发者只需 157 | 要集中精力开发自己的 CMS 业务即可 158 | 159 | #### Lin CMS 本身也是一套开发规范 160 | 161 | Lin CMS 除了内置常见的功能外,还提供了一套开发规范与工具类。换句话说,开发者无需 162 | 再纠结如何验证参数?如何操作数据库?如何做全局的异常处理?API 的结构如何?前端结 163 | 构应该如何组织?这些问题 Lin CMS 已经给出了解决方案。当然,如果你不喜欢 Lin 给出 164 | 的架构,那么自己去实现自己的 CMS 架构也是可以的。但通常情况下,你确实无需再做出 165 | 架构上的改动,Lin 可以满足绝大多数中小型的 CMS 需求。 166 | 167 | 举例来说,每个 API 都需要校验客户端传递的参数。但校验的方法有很多种,不同的开发 168 | 者会有不同的构筑方案。但 Lin 提供了一套验证机制,开发者无需再纠结如何校验参数, 169 | 只需模仿 Lin 的校验方案去写自己的业务即可。 170 | 171 | 还是基于这样的一个原则:Lin CMS 只需要开发者关注自己的业务开发,它已经内置了很多 172 | 机制帮助开发者快速开发自己的业务。 173 | 174 | #### 基于插件的扩展 175 | 176 | 任何优秀的框架都需要考虑到扩展。而 Lin 的扩展支持是通过插件的思想来设计的。当你 177 | 需要新增一个功能时,你既可以直接在 Lin 的目录下编写代码,也可以将功能以插件的形 178 | 式封装。比如,你开发了一个文章管理功能,你可以选择以插件的形式来发布,这样其他开 179 | 发者通过安装你的插件就可以使用这个功能了。毫无疑问,以插件的形式封装功能将最大化 180 | 代码的可复用性。你甚至可以把自己开发的插件发布,以提供给其他开发者使用。这种机制 181 | 相当的棒。 182 | 183 | #### 前端组件库支持 184 | 185 | Lin 还将提供一套类似于 Vue Element 的前端组件库,以方便前端开发者快速开发。相比 186 | 于 Vue Element 或 iView 等成熟的组件库,Lin 所提供的组件库将针对 Lin CMS 的整体 187 | 设计风格、交互体验等作出大量的优化,使用 Lin 的组件库将更容易开发出体验更好的 188 | CMS 系统。当然,Lin 本身不限制开发者选用任何的组件库,你完全可以根据自己的喜好/ 189 | 习惯/熟悉度,去选择任意的一个基于 Vue 的组件库,比如前面提到的 Vue Element 和 190 | iView 等。你甚至可以混搭使用。当然,前提是这些组件库是基于 Vue 的。 191 | 192 | #### 完善的文档 193 | 194 | 我们将提供详尽的文档来帮助开发者使用 Lin 195 | -------------------------------------------------------------------------------- /app/api/cms/admin.js: -------------------------------------------------------------------------------- 1 | import { LinRouter, Failed, NotFound } from 'lin-mizar'; 2 | import { 3 | AdminUsersValidator, 4 | ResetPasswordValidator, 5 | UpdateUserInfoValidator, 6 | NewGroupValidator, 7 | UpdateGroupValidator, 8 | DispatchPermissionValidator, 9 | DispatchPermissionsValidator, 10 | RemovePermissionsValidator 11 | } from '../../validator/admin'; 12 | import { PositiveIdValidator, PaginateValidator } from '../../validator/common'; 13 | 14 | import { adminRequired } from '../../middleware/jwt'; 15 | import { AdminDao } from '../../dao/admin'; 16 | 17 | const admin = new LinRouter({ 18 | prefix: '/cms/admin', 19 | module: '管理员', 20 | // 管理员权限暂不支持分配,开启分配后也无实际作用 21 | mountPermission: false 22 | }); 23 | 24 | const adminDao = new AdminDao(); 25 | 26 | admin.linGet( 27 | 'getAllPermissions', 28 | '/permission', 29 | admin.permission('查询所有可分配的权限'), 30 | adminRequired, 31 | async ctx => { 32 | const permissions = await adminDao.getAllPermissions(); 33 | ctx.json(permissions); 34 | } 35 | ); 36 | 37 | admin.linGet( 38 | 'getAdminUsers', 39 | '/users', 40 | admin.permission('查询所有用户'), 41 | adminRequired, 42 | async ctx => { 43 | const v = await new AdminUsersValidator().validate(ctx); 44 | const { users, total } = await adminDao.getUsers( 45 | v.get('query.group_id'), 46 | v.get('query.page'), 47 | v.get('query.count') 48 | ); 49 | ctx.json({ 50 | items: users, 51 | total, 52 | count: v.get('query.count'), 53 | page: v.get('query.page') 54 | }); 55 | } 56 | ); 57 | 58 | admin.linPut( 59 | 'changeUserPassword', 60 | '/user/:id/password', 61 | admin.permission('修改用户密码'), 62 | adminRequired, 63 | async ctx => { 64 | const v = await new ResetPasswordValidator().validate(ctx); 65 | await adminDao.changeUserPassword(ctx, v); 66 | ctx.success({ 67 | code: 4 68 | }); 69 | } 70 | ); 71 | 72 | admin.linDelete( 73 | 'deleteUser', 74 | '/user/:id', 75 | admin.permission('删除用户'), 76 | adminRequired, 77 | async ctx => { 78 | const v = await new PositiveIdValidator().validate(ctx); 79 | const id = v.get('path.id'); 80 | await adminDao.deleteUser(ctx, id); 81 | ctx.success({ 82 | code: 5 83 | }); 84 | } 85 | ); 86 | 87 | admin.linPut( 88 | 'updateUser', 89 | '/user/:id', 90 | admin.permission('管理员更新用户信息'), 91 | adminRequired, 92 | async ctx => { 93 | const v = await new UpdateUserInfoValidator().validate(ctx); 94 | await adminDao.updateUserInfo(ctx, v); 95 | ctx.success({ 96 | code: 6 97 | }); 98 | } 99 | ); 100 | 101 | admin.linGet( 102 | 'getAdminGroups', 103 | '/group', 104 | admin.permission('查询所有权限组及其权限'), 105 | adminRequired, 106 | async ctx => { 107 | const v = await new PaginateValidator().validate(ctx); 108 | const { groups, total } = await adminDao.getGroups( 109 | ctx, 110 | v.get('query.page'), 111 | v.get('query.count') 112 | ); 113 | if (groups.length < 1) { 114 | throw new NotFound({ 115 | code: 10024 116 | }); 117 | } 118 | ctx.json({ 119 | items: groups, 120 | total: total, 121 | page: v.get('query.page'), 122 | count: v.get('query.count') 123 | }); 124 | } 125 | ); 126 | 127 | admin.linGet( 128 | 'getAllGroup', 129 | '/group/all', 130 | admin.permission('查询所有权限组'), 131 | adminRequired, 132 | async ctx => { 133 | const groups = await adminDao.getAllGroups(); 134 | if (!groups || groups.length < 1) { 135 | throw new NotFound({ 136 | code: 10024 137 | }); 138 | } 139 | ctx.json(groups); 140 | } 141 | ); 142 | 143 | admin.linGet( 144 | 'getGroup', 145 | '/group/:id', 146 | admin.permission('查询一个权限组及其权限'), 147 | adminRequired, 148 | async ctx => { 149 | const v = await new PositiveIdValidator().validate(ctx); 150 | const group = await adminDao.getGroup(ctx, v.get('path.id')); 151 | ctx.json(group); 152 | } 153 | ); 154 | 155 | admin.linPost( 156 | 'createGroup', 157 | '/group', 158 | admin.permission('新建权限组'), 159 | adminRequired, 160 | async ctx => { 161 | const v = await new NewGroupValidator().validate(ctx); 162 | const ok = await adminDao.createGroup(ctx, v); 163 | if (!ok) { 164 | throw new Failed({ 165 | code: 10027 166 | }); 167 | } 168 | ctx.success({ 169 | code: 15 170 | }); 171 | } 172 | ); 173 | 174 | admin.linPut( 175 | 'updateGroup', 176 | '/group/:id', 177 | admin.permission('更新一个权限组'), 178 | adminRequired, 179 | async ctx => { 180 | const v = await new UpdateGroupValidator().validate(ctx); 181 | await adminDao.updateGroup(ctx, v); 182 | ctx.success({ 183 | code: 7 184 | }); 185 | } 186 | ); 187 | 188 | admin.linDelete( 189 | 'deleteGroup', 190 | '/group/:id', 191 | admin.permission('删除一个权限组'), 192 | adminRequired, 193 | async ctx => { 194 | const v = await new PositiveIdValidator().validate(ctx); 195 | const id = v.get('path.id'); 196 | await adminDao.deleteGroup(ctx, id); 197 | ctx.success({ 198 | code: 8 199 | }); 200 | } 201 | ); 202 | 203 | admin.linPost( 204 | 'dispatchPermission', 205 | '/permission/dispatch', 206 | admin.permission('分配单个权限'), 207 | adminRequired, 208 | async ctx => { 209 | const v = await new DispatchPermissionValidator().validate(ctx); 210 | await adminDao.dispatchPermission(ctx, v); 211 | ctx.success({ 212 | code: 9 213 | }); 214 | } 215 | ); 216 | 217 | admin.linPost( 218 | 'dispatchPermissions', 219 | '/permission/dispatch/batch', 220 | admin.permission('分配多个权限'), 221 | adminRequired, 222 | async ctx => { 223 | const v = await new DispatchPermissionsValidator().validate(ctx); 224 | await adminDao.dispatchPermissions(ctx, v); 225 | ctx.success({ 226 | code: 9 227 | }); 228 | } 229 | ); 230 | 231 | admin.linPost( 232 | 'removePermissions', 233 | '/permission/remove', 234 | admin.permission('删除多个权限'), 235 | adminRequired, 236 | async ctx => { 237 | const v = await new RemovePermissionsValidator().validate(ctx); 238 | await adminDao.removePermissions(ctx, v); 239 | ctx.success({ 240 | code: 10 241 | }); 242 | } 243 | ); 244 | 245 | export { admin }; 246 | -------------------------------------------------------------------------------- /app/api/cms/file.js: -------------------------------------------------------------------------------- 1 | import { LinRouter, ParametersException } from 'lin-mizar'; 2 | 3 | import { loginRequired } from '../../middleware/jwt'; 4 | import { LocalUploader } from '../../extension/file/local-uploader'; 5 | 6 | const file = new LinRouter({ 7 | prefix: '/cms/file' 8 | }); 9 | 10 | file.linPost('upload', '/', loginRequired, async ctx => { 11 | const files = await ctx.multipart(); 12 | if (files.length < 1) { 13 | throw new ParametersException({ code: 10033 }); 14 | } 15 | const uploader = new LocalUploader('assets'); 16 | const arr = await uploader.upload(files); 17 | ctx.json(arr); 18 | }); 19 | 20 | export { file }; 21 | -------------------------------------------------------------------------------- /app/api/cms/log.js: -------------------------------------------------------------------------------- 1 | import { LinRouter, NotFound } from 'lin-mizar'; 2 | import { LogFindValidator } from '../../validator/log'; 3 | import { PaginateValidator } from '../../validator/common'; 4 | 5 | import { groupRequired } from '../../middleware/jwt'; 6 | import { LogDao } from '../../dao/log'; 7 | 8 | const log = new LinRouter({ 9 | prefix: '/cms/log', 10 | module: '日志' 11 | }); 12 | 13 | const logDao = new LogDao(); 14 | 15 | log.linGet( 16 | 'getLogs', 17 | '/', 18 | log.permission('查询所有日志'), 19 | groupRequired, 20 | async ctx => { 21 | const v = await new LogFindValidator().validate(ctx); 22 | const { rows, total } = await logDao.getLogs(v); 23 | if (!rows || rows.length < 1) { 24 | throw new NotFound({ 25 | code: 10220 26 | }); 27 | } 28 | ctx.json({ 29 | total: total, 30 | items: rows, 31 | page: v.get('query.page'), 32 | count: v.get('query.count') 33 | }); 34 | } 35 | ); 36 | 37 | log.linGet( 38 | 'getUserLogs', 39 | '/search', 40 | log.permission('搜索日志'), 41 | groupRequired, 42 | async ctx => { 43 | const v = await new LogFindValidator().validate(ctx); 44 | const keyword = v.get('query.keyword', false, ''); 45 | const { rows, total } = await logDao.searchLogs(v, keyword); 46 | ctx.json({ 47 | total: total, 48 | items: rows, 49 | page: v.get('query.page'), 50 | count: v.get('query.count') 51 | }); 52 | } 53 | ); 54 | 55 | log.linGet( 56 | 'getUsers', 57 | '/users', 58 | log.permission('查询日志记录的用户'), 59 | groupRequired, 60 | async ctx => { 61 | const v = await new PaginateValidator().validate(ctx); 62 | const arr = await logDao.getUserNames( 63 | v.get('query.page'), 64 | v.get('query.count') 65 | ); 66 | ctx.json({ 67 | total: arr.length, 68 | items: arr, 69 | page: v.get('query.page'), 70 | count: v.get('query.count') 71 | }); 72 | } 73 | ); 74 | 75 | export { log }; 76 | -------------------------------------------------------------------------------- /app/api/cms/test.js: -------------------------------------------------------------------------------- 1 | import { LinRouter } from 'lin-mizar'; 2 | import { loginRequired, groupRequired } from '../../middleware/jwt'; 3 | import { logger } from '../../middleware/logger'; 4 | 5 | const test = new LinRouter({ 6 | prefix: '/cms/test', 7 | module: '信息' 8 | }); 9 | 10 | test.get('/', async ctx => { 11 | ctx.type = 'html'; 12 | ctx.body = `

16 | Lin
心上无垢,林间有风。

`; 17 | }); 18 | 19 | test.linGet( 20 | 'getTestMessage', 21 | '/json', 22 | test.permission('测试日志记录'), 23 | loginRequired, 24 | logger('{user.username}就是皮了一波'), 25 | async ctx => { 26 | ctx.json({ 27 | message: '物质决定意识,经济基础决定上层建筑' 28 | }); 29 | } 30 | ); 31 | 32 | test.linGet( 33 | 'getTestInfo', 34 | '/info', 35 | test.permission('查看lin的信息'), 36 | groupRequired, 37 | async ctx => { 38 | ctx.json({ 39 | message: 40 | 'Lin 是一套基于 Python-Flask 的一整套开箱即用的后台管理系统(CMS)。Lin 遵循简洁、高效的原则,通过核心库加插件的方式来驱动整个系统高效的运行' 41 | }); 42 | } 43 | ); 44 | 45 | export { test }; 46 | -------------------------------------------------------------------------------- /app/api/cms/user.js: -------------------------------------------------------------------------------- 1 | import { LinRouter, getTokens, config } from 'lin-mizar'; 2 | import { 3 | RegisterValidator, 4 | LoginValidator, 5 | UpdateInfoValidator, 6 | ChangePasswordValidator 7 | } from '../../validator/user'; 8 | 9 | import { 10 | adminRequired, 11 | loginRequired, 12 | refreshTokenRequiredWithUnifyException 13 | } from '../../middleware/jwt'; 14 | import { UserIdentityModel } from '../../model/user'; 15 | import { logger } from '../../middleware/logger'; 16 | import { UserDao } from '../../dao/user'; 17 | import { generateCaptcha } from '../../lib/captcha'; 18 | 19 | const user = new LinRouter({ 20 | prefix: '/cms/user', 21 | module: '用户', 22 | // 用户权限暂不支持分配,开启分配后也无实际作用 23 | mountPermission: false 24 | }); 25 | 26 | const userDao = new UserDao(); 27 | 28 | user.linPost( 29 | 'userRegister', 30 | '/register', 31 | user.permission('注册'), 32 | adminRequired, 33 | logger('管理员新建了一个用户'), 34 | async ctx => { 35 | const v = await new RegisterValidator().validate(ctx); 36 | await userDao.createUser(v); 37 | if (config.getItem('socket.enable')) { 38 | const username = v.get('body.username'); 39 | ctx.websocket.broadCast( 40 | JSON.stringify({ 41 | name: username, 42 | content: `管理员${ctx.currentUser.getDataValue( 43 | 'username' 44 | )}新建了一个用户${username}`, 45 | time: new Date() 46 | }) 47 | ); 48 | } 49 | ctx.success({ 50 | code: 11 51 | }); 52 | } 53 | ); 54 | 55 | user.linPost('userLogin', '/login', user.permission('登录'), async ctx => { 56 | const v = await new LoginValidator().validate(ctx); 57 | const { accessToken, refreshToken } = await userDao.getTokens(v, ctx); 58 | ctx.json({ 59 | access_token: accessToken, 60 | refresh_token: refreshToken 61 | }); 62 | }); 63 | 64 | user.linPost('userCaptcha', '/captcha', async ctx => { 65 | let tag = null; 66 | let image = null; 67 | 68 | if (config.getItem('loginCaptchaEnabled', false)) { 69 | ({ tag, image } = await generateCaptcha()); 70 | } 71 | 72 | ctx.json({ 73 | tag, 74 | image 75 | }); 76 | }); 77 | 78 | user.linPut( 79 | 'userUpdate', 80 | '/', 81 | user.permission('更新用户信息'), 82 | loginRequired, 83 | async ctx => { 84 | const v = await new UpdateInfoValidator().validate(ctx); 85 | await userDao.updateUser(ctx, v); 86 | ctx.success({ 87 | code: 6 88 | }); 89 | } 90 | ); 91 | 92 | user.linPut( 93 | 'userUpdatePassword', 94 | '/change_password', 95 | user.permission('修改密码'), 96 | loginRequired, 97 | async ctx => { 98 | const user = ctx.currentUser; 99 | const v = await new ChangePasswordValidator().validate(ctx); 100 | await UserIdentityModel.changePassword( 101 | user, 102 | v.get('body.old_password'), 103 | v.get('body.new_password') 104 | ); 105 | ctx.success({ 106 | code: 4 107 | }); 108 | } 109 | ); 110 | 111 | user.linGet( 112 | 'userGetToken', 113 | '/refresh', 114 | user.permission('刷新令牌'), 115 | refreshTokenRequiredWithUnifyException, 116 | async ctx => { 117 | const user = ctx.currentUser; 118 | const { accessToken, refreshToken } = getTokens(user); 119 | ctx.json({ 120 | access_token: accessToken, 121 | refresh_token: refreshToken 122 | }); 123 | } 124 | ); 125 | 126 | user.linGet( 127 | 'userGetPermissions', 128 | '/permissions', 129 | user.permission('查询自己拥有的权限'), 130 | loginRequired, 131 | async ctx => { 132 | const user = await userDao.getPermissions(ctx); 133 | ctx.json(user); 134 | } 135 | ); 136 | 137 | user.linGet( 138 | 'getInformation', 139 | '/information', 140 | user.permission('查询自己信息'), 141 | loginRequired, 142 | async ctx => { 143 | const info = await userDao.getInformation(ctx); 144 | ctx.json(info); 145 | } 146 | ); 147 | 148 | export { user }; 149 | -------------------------------------------------------------------------------- /app/api/v1/book.js: -------------------------------------------------------------------------------- 1 | import { LinRouter, NotFound, disableLoading } from 'lin-mizar'; 2 | import { groupRequired } from '../../middleware/jwt'; 3 | import { 4 | BookSearchValidator, 5 | CreateOrUpdateBookValidator 6 | } from '../../validator/book'; 7 | import { PositiveIdValidator } from '../../validator/common'; 8 | 9 | import { getSafeParamId } from '../../lib/util'; 10 | import { BookNotFound } from '../../lib/exception'; 11 | import { BookDao } from '../../dao/book'; 12 | 13 | // book 的红图实例 14 | const bookApi = new LinRouter({ 15 | prefix: '/v1/book', 16 | module: '图书' 17 | }); 18 | 19 | // book 的dao 数据库访问层实例 20 | const bookDto = new BookDao(); 21 | 22 | bookApi.get('/:id', async ctx => { 23 | const v = await new PositiveIdValidator().validate(ctx); 24 | const id = v.get('path.id'); 25 | const book = await bookDto.getBook(id); 26 | if (!book) { 27 | throw new NotFound({ 28 | code: 10022 29 | }); 30 | } 31 | ctx.json(book); 32 | }); 33 | 34 | bookApi.get('/', async ctx => { 35 | const books = await bookDto.getBooks(); 36 | // if (!books || books.length < 1) { 37 | // throw new NotFound({ 38 | // message: '没有找到相关书籍' 39 | // }); 40 | // } 41 | ctx.json(books); 42 | }); 43 | 44 | bookApi.get('/search/one', async ctx => { 45 | const v = await new BookSearchValidator().validate(ctx); 46 | const book = await bookDto.getBookByKeyword(v.get('query.q')); 47 | if (!book) { 48 | throw new BookNotFound(); 49 | } 50 | ctx.json(book); 51 | }); 52 | 53 | bookApi.post('/', async ctx => { 54 | const v = await new CreateOrUpdateBookValidator().validate(ctx); 55 | await bookDto.createBook(v); 56 | ctx.success({ 57 | code: 12 58 | }); 59 | }); 60 | 61 | bookApi.put('/:id', async ctx => { 62 | const v = await new CreateOrUpdateBookValidator().validate(ctx); 63 | const id = getSafeParamId(ctx); 64 | await bookDto.updateBook(v, id); 65 | ctx.success({ 66 | code: 13 67 | }); 68 | }); 69 | 70 | bookApi.linDelete( 71 | 'deleteBook', 72 | '/:id', 73 | bookApi.permission('删除图书'), 74 | groupRequired, 75 | async ctx => { 76 | const v = await new PositiveIdValidator().validate(ctx); 77 | const id = v.get('path.id'); 78 | await bookDto.deleteBook(id); 79 | ctx.success({ 80 | code: 14 81 | }); 82 | } 83 | ); 84 | 85 | module.exports = { bookApi, [disableLoading]: false }; 86 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | import Koa from 'koa'; 2 | import KoaBodyParser from 'koa-bodyparser'; 3 | import cors from '@koa/cors'; 4 | import mount from 'koa-mount'; 5 | import serve from 'koa-static'; 6 | import { config, json, logging, success, jwt, Loader } from 'lin-mizar'; 7 | import { PermissionModel } from './model/permission'; 8 | import WebSocket from './extension/socket/socket'; 9 | 10 | /** 11 | * 首页 12 | */ 13 | function indexPage (app) { 14 | app.context.loader.mainRouter.get('/', async ctx => { 15 | ctx.type = 'html'; 16 | ctx.body = `

20 | Lin
心上无垢,林间有风。

`; 21 | }); 22 | } 23 | 24 | /** 25 | * 跨域支持 26 | * @param app koa实例 27 | */ 28 | function applyCors (app) { 29 | app.use(cors()); 30 | } 31 | 32 | /** 33 | * 解析Body参数 34 | * @param app koa实例 35 | */ 36 | function applyBodyParse (app) { 37 | // 参数解析 38 | app.use(KoaBodyParser()); 39 | } 40 | 41 | /** 42 | * 静态资源服务 43 | * @param app koa实例 44 | * @param prefix 静态资源存放相对路径 45 | */ 46 | function applyStatic (app, prefix = '/assets') { 47 | const assetsDir = config.getItem('file.storeDir', 'app/static'); 48 | app.use(mount(prefix, serve(assetsDir))); 49 | } 50 | 51 | /** 52 | * json logger 扩展 53 | * @param app koa实例 54 | */ 55 | function applyDefaultExtends (app) { 56 | json(app); 57 | logging(app); 58 | success(app); 59 | } 60 | 61 | /** 62 | * websocket 扩展 63 | * @param app koa实例 64 | */ 65 | function applyWebSocket (app) { 66 | if (config.getItem('socket.enable')) { 67 | const server = new WebSocket(app); 68 | return server.init(); 69 | } 70 | return app; 71 | } 72 | 73 | /** 74 | * loader 加载插件和路由文件 75 | * @param app koa实例 76 | */ 77 | function applyLoader (app) { 78 | const pluginPath = config.getItem('pluginPath'); 79 | const loader = new Loader(pluginPath, app); 80 | loader.initLoader(); 81 | } 82 | 83 | /** 84 | * jwt 85 | * @param app koa实例 86 | */ 87 | function applyJwt (app) { 88 | const secret = config.getItem('secret'); 89 | jwt.initApp(app, secret); 90 | } 91 | 92 | /** 93 | * 初始化Koa实例 94 | */ 95 | async function createApp () { 96 | const app = new Koa(); 97 | applyBodyParse(app); 98 | applyCors(app); 99 | applyStatic(app); 100 | const { log, error, Lin, multipart } = require('lin-mizar'); 101 | app.use(log); 102 | app.on('error', error); 103 | applyDefaultExtends(app); 104 | applyLoader(app); 105 | applyJwt(app); 106 | const lin = new Lin(); 107 | await lin.initApp(app, true); // 是否挂载插件路由,默认为true 108 | await PermissionModel.initPermission(); 109 | indexPage(app); 110 | multipart(app); 111 | return applyWebSocket(app); 112 | } 113 | 114 | module.exports = { createApp }; 115 | -------------------------------------------------------------------------------- /app/config/code-message.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | codeMessage: { 5 | getMessage (code) { 6 | return this[code] || ''; 7 | }, 8 | 0: '成功', 9 | 1: '创建成功', 10 | 2: '更新成功', 11 | 3: '删除成功', 12 | 4: '密码修改成功', 13 | 5: '删除用户成功', 14 | 6: '更新用户成功', 15 | 7: '更新分组成功', 16 | 8: '删除分组成功', 17 | 9: '添加权限成功', 18 | 10: '删除权限成功', 19 | 11: '注册成功', 20 | 12: '新建图书成功', 21 | 13: '更新图书成功', 22 | 14: '删除图书成功', 23 | 15: '新建分组成功', 24 | 9999: '服务器未知错误', 25 | 10000: '未携带令牌', 26 | 10001: '权限不足', 27 | 10010: '授权失败', 28 | 10011: '更新密码失败', 29 | 10012: '请传入认证头字段', 30 | 10013: '认证头字段解析失败', 31 | 10020: '资源不存在', 32 | 10021: '用户不存在', 33 | 10022: '未找到相关书籍', 34 | 10023: '分组不存在,无法新建用户', 35 | 10024: '分组不存在', 36 | 10025: '找不到相应的视图处理器', 37 | 10026: '未找到文件', 38 | 10027: '新建分组失败', 39 | 10030: '参数错误', 40 | 10031: '用户名或密码错误', 41 | 10032: '请输入正确的密码', 42 | 10033: '为找到符合条件的文件资源', 43 | 10040: '令牌失效', 44 | 10041: 'access token 损坏', 45 | 10042: 'refresh token 损坏', 46 | 10050: '令牌过期', 47 | 10051: 'access token 过期', 48 | 10052: 'refresh token 过期', 49 | 10060: '字段重复', 50 | 10070: '禁止操作', 51 | 10071: '已经有用户使用了该名称,请重新输入新的用户名', 52 | 10072: '分组名已被使用,请重新填入新的分组名', 53 | 10073: 'root分组不可添加用户', 54 | 10074: 'root分组不可删除', 55 | 10075: 'guest分组不可删除', 56 | 10076: '邮箱已被使用,请重新填入新的邮箱', 57 | 10077: '不可将用户分配给不存在的分组', 58 | 10078: '不可修改root用户的分组', 59 | 10079: 'root分组的用户不可删除', 60 | 10080: '请求方法不允许', 61 | 10100: '刷新令牌获取失败', 62 | 10110: '{name}大小不能超过{size}字节', 63 | 10111: '总文件体积不能超过{size}字节', 64 | 10120: '文件数量过多', 65 | 10121: '文件太多,文件总数不可超过{num}', 66 | 10130: '不支持类型为{ext}的文件', 67 | 10140: '请求过于频繁,请稍后重试', 68 | 10150: '丢失参数', 69 | 10160: '类型错误', 70 | 10170: '请求体不可为空', 71 | 10180: '全部文件大小不能超过{num}', 72 | 10190: '读取文件数据失败', 73 | 10200: '失败', 74 | 10210: '文件损坏,无法读取', 75 | 10220: '没有找到相关日志', 76 | 10230: '已有权限,不可重复添加', 77 | 10231: '无法分配不存在的权限', 78 | 10240: '书籍已存在', 79 | 10250: '请使用正确类型的令牌', 80 | 10251: '请使用正确作用域的令牌', 81 | 10260: '请输入正确的验证码' 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /app/config/log.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | log: { 5 | level: 'DEBUG', 6 | dir: 'logs', 7 | sizeLimit: 1024 * 1024 * 5, 8 | requestLog: true, 9 | file: true 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /app/config/secure.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | db: { 5 | database: 'lin-cms', 6 | host: 'localhost', 7 | dialect: 'mysql', 8 | port: 3306, 9 | username: 'root', 10 | password: '123456', 11 | logging: false, 12 | timezone: '+08:00', 13 | define: { 14 | charset: 'utf8mb4' 15 | } 16 | }, 17 | secret: 18 | '\x88W\xf09\x91\x07\x98\x89\x87\x96\xa0A\xc68\xf9\xecJJU\x17\xc5V\xbe\x8b\xef\xd7\xd8\xd3\xe6\x95*4' // 发布生产环境前,请务必修改此默认秘钥 19 | }; 20 | -------------------------------------------------------------------------------- /app/config/setting.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | port: 5000, 5 | siteDomain: 'http://localhost:5000', 6 | countDefault: 10, 7 | pageDefault: 0, 8 | apiDir: 'app/api', 9 | accessExp: 60 * 60, // 1h 单位秒 10 | // 指定工作目录,默认为 process.cwd() 路径 11 | baseDir: path.resolve(__dirname, '../../'), 12 | // debug 模式 13 | debug: true, 14 | // refreshExp 设置refresh_token的过期时间,默认一个月 15 | refreshExp: 60 * 60 * 24 * 30, 16 | // 暂不启用插件 17 | pluginPath: { 18 | // // plugin name 19 | // poem: { 20 | // // determine a plugin work or not 21 | // enable: true, 22 | // // path of the plugin 23 | // path: "app/plugin/poem", 24 | // // other config 25 | // limit: 2 26 | // }, 27 | }, 28 | // 是否开启登录验证码 29 | loginCaptchaEnabled: false 30 | }; 31 | -------------------------------------------------------------------------------- /app/dao/admin.js: -------------------------------------------------------------------------------- 1 | import { NotFound, Forbidden } from 'lin-mizar'; 2 | import { PermissionModel } from '../model/permission'; 3 | import { UserModel, UserIdentityModel } from '../model/user'; 4 | import { GroupModel } from '../model/group'; 5 | 6 | import { GroupPermissionModel } from '../model/group-permission'; 7 | import { UserGroupModel } from '../model/user-group'; 8 | 9 | import sequelize from '../lib/db'; 10 | import { MountType, GroupLevel } from '../lib/type'; 11 | import { Op } from 'sequelize'; 12 | import { has, set, get } from 'lodash'; 13 | 14 | class AdminDao { 15 | async getAllPermissions () { 16 | const permissions = await PermissionModel.findAll({ 17 | where: { 18 | mount: MountType.Mount 19 | } 20 | }); 21 | const result = Object.create(null); 22 | permissions.forEach(v => { 23 | const item = { 24 | id: get(v, 'id'), 25 | name: get(v, 'name'), 26 | module: get(v, 'module') 27 | }; 28 | if (has(result, item.module)) { 29 | result[item.module].push(item); 30 | } else { 31 | set(result, item.module, [item]); 32 | } 33 | }); 34 | return result; 35 | } 36 | 37 | async getUsers (groupId, page, count1) { 38 | let userIds = []; 39 | const condition = { 40 | where: { 41 | username: { 42 | [Op.ne]: 'root' 43 | } 44 | }, 45 | offset: page * count1, 46 | limit: count1 47 | }; 48 | if (groupId) { 49 | const userGroup = await UserGroupModel.findAll({ 50 | where: { 51 | group_id: groupId 52 | } 53 | }); 54 | userIds = userGroup.map(v => v.user_id); 55 | set(condition, 'where.id', { 56 | [Op.in]: userIds 57 | }); 58 | } 59 | const { rows, count } = await UserModel.findAndCountAll(condition); 60 | 61 | for (const user of rows) { 62 | const userGroup = await UserGroupModel.findAll({ 63 | where: { 64 | user_id: user.id 65 | } 66 | }); 67 | const groupIds = userGroup.map(v => v.group_id); 68 | const groups = await GroupModel.findAll({ 69 | where: { 70 | id: { 71 | [Op.in]: groupIds 72 | } 73 | } 74 | }); 75 | set(user, 'groups', groups); 76 | } 77 | 78 | return { 79 | users: rows, 80 | total: count 81 | }; 82 | } 83 | 84 | async changeUserPassword (ctx, v) { 85 | const user = await UserModel.findByPk(v.get('path.id')); 86 | if (!user) { 87 | throw new NotFound({ 88 | code: 10021 89 | }); 90 | } 91 | await UserIdentityModel.resetPassword(user, v.get('body.new_password')); 92 | } 93 | 94 | async deleteUser (ctx, id) { 95 | const user = await UserModel.findOne({ 96 | where: { 97 | id 98 | } 99 | }); 100 | if (!user) { 101 | throw new NotFound({ 102 | code: 10021 103 | }); 104 | } 105 | const root = await UserGroupModel.findOne({ 106 | where: { 107 | group_id: GroupLevel.Root, 108 | user_id: id 109 | } 110 | }); 111 | if (root) { 112 | throw new Forbidden({ 113 | code: 10079 114 | }); 115 | } 116 | let transaction; 117 | try { 118 | transaction = await sequelize.transaction(); 119 | await user.destroy({ 120 | transaction 121 | }); 122 | await UserGroupModel.destroy({ 123 | where: { 124 | user_id: id 125 | }, 126 | transaction 127 | }); 128 | await UserIdentityModel.destroy({ 129 | where: { 130 | user_id: id 131 | }, 132 | transaction 133 | }); 134 | await transaction.commit(); 135 | } catch (err) { 136 | if (transaction) await transaction.rollback(); 137 | } 138 | } 139 | 140 | async updateUserInfo (ctx, v) { 141 | const user = await UserModel.findByPk(v.get('path.id')); 142 | if (!user) { 143 | throw new NotFound({ 144 | code: 10021 145 | }); 146 | } 147 | 148 | const userGroup = await UserGroupModel.findAll({ 149 | where: { 150 | user_id: user.id 151 | } 152 | }); 153 | const groupIds = userGroup.map(v => v.group_id); 154 | const isAdmin = await GroupModel.findOne({ 155 | where: { 156 | level: GroupLevel.Root, 157 | id: { 158 | [Op.in]: groupIds 159 | } 160 | } 161 | }); 162 | 163 | if (isAdmin) { 164 | throw new Forbidden({ 165 | code: 10078 166 | }); 167 | } 168 | 169 | for (const id of v.get('body.group_ids') || []) { 170 | const group = await GroupModel.findByPk(id); 171 | if (group.level === GroupLevel.Root) { 172 | throw new Forbidden({ 173 | code: 10073 174 | }); 175 | } 176 | if (!group) { 177 | throw new NotFound({ 178 | code: 10077 179 | }); 180 | } 181 | } 182 | 183 | let transaction; 184 | try { 185 | transaction = await sequelize.transaction(); 186 | await UserGroupModel.destroy({ 187 | where: { 188 | user_id: v.get('path.id') 189 | }, 190 | transaction 191 | }); 192 | for (const id of v.get('body.group_ids') || []) { 193 | await UserGroupModel.create( 194 | { 195 | user_id: v.get('path.id'), 196 | group_id: id 197 | }, 198 | { 199 | transaction 200 | } 201 | ); 202 | } 203 | await transaction.commit(); 204 | } catch (err) { 205 | if (transaction) await transaction.rollback(); 206 | } 207 | } 208 | 209 | async getGroups (ctx, page, count1) { 210 | const { rows, count } = await GroupModel.findAndCountAll({ 211 | offset: page * count1, 212 | limit: count1 213 | }); 214 | 215 | return { 216 | groups: rows, 217 | total: count 218 | }; 219 | } 220 | 221 | async getAllGroups () { 222 | const allGroups = await GroupModel.findAll({ 223 | where: { 224 | level: { 225 | [Op.ne]: GroupLevel.Root 226 | } 227 | } 228 | }); 229 | return allGroups; 230 | } 231 | 232 | async getGroup (ctx, id) { 233 | const group = await GroupModel.findByPk(id); 234 | if (!group) { 235 | throw new NotFound({ 236 | code: 10024 237 | }); 238 | } 239 | 240 | const groupPermission = await GroupPermissionModel.findAll({ 241 | where: { 242 | group_id: id 243 | } 244 | }); 245 | const permissionIds = groupPermission.map(v => v.permission_id); 246 | 247 | const permissions = await PermissionModel.findAll({ 248 | where: { 249 | mount: MountType.Mount, 250 | id: { 251 | [Op.in]: permissionIds 252 | } 253 | } 254 | }); 255 | 256 | return set(group, 'permissions', permissions); 257 | } 258 | 259 | async createGroup (ctx, v) { 260 | const group = await GroupModel.findOne({ 261 | where: { 262 | name: v.get('body.name') 263 | } 264 | }); 265 | if (group) { 266 | throw new Forbidden({ 267 | code: 10072 268 | }); 269 | } 270 | 271 | for (const id of v.get('body.permission_ids') || []) { 272 | const permission = await PermissionModel.findOne({ 273 | where: { 274 | id, 275 | mount: MountType.Mount 276 | } 277 | }); 278 | if (!permission) { 279 | throw new NotFound({ 280 | code: 10231 281 | }); 282 | } 283 | } 284 | 285 | let transaction; 286 | try { 287 | transaction = await sequelize.transaction(); 288 | 289 | const group = await GroupModel.create( 290 | { 291 | name: v.get('body.name'), 292 | info: v.get('body.info') 293 | }, 294 | { 295 | transaction 296 | } 297 | ); 298 | 299 | for (const id of v.get('body.permission_ids') || []) { 300 | await GroupPermissionModel.create( 301 | { 302 | group_id: group.id, 303 | permission_id: id 304 | }, 305 | { 306 | transaction 307 | } 308 | ); 309 | } 310 | await transaction.commit(); 311 | } catch (err) { 312 | if (transaction) await transaction.rollback(); 313 | } 314 | return true; 315 | } 316 | 317 | async updateGroup (ctx, v) { 318 | const group = await GroupModel.findByPk(v.get('path.id')); 319 | if (!group) { 320 | throw new NotFound({ 321 | code: 10024 322 | }); 323 | } 324 | group.name = v.get('body.name'); 325 | group.info = v.get('body.info'); 326 | await group.save(); 327 | } 328 | 329 | async deleteGroup (ctx, id) { 330 | const group = await GroupModel.findByPk(id); 331 | if (!group) { 332 | throw new NotFound({ 333 | code: 10024 334 | }); 335 | } 336 | if (group.level === GroupLevel.Root) { 337 | throw new Forbidden({ 338 | code: 10074 339 | }); 340 | } else if (group.level === GroupLevel.Guest) { 341 | throw new Forbidden({ 342 | code: 10075 343 | }); 344 | } 345 | 346 | let transaction; 347 | try { 348 | transaction = await sequelize.transaction(); 349 | await group.destroy({ 350 | transaction 351 | }); 352 | await GroupPermissionModel.destroy({ 353 | where: { 354 | group_id: group.id 355 | }, 356 | transaction 357 | }); 358 | await UserGroupModel.destroy({ 359 | where: { 360 | group_id: group.id 361 | }, 362 | transaction 363 | }); 364 | await transaction.commit(); 365 | } catch (error) { 366 | if (transaction) await transaction.rollback(); 367 | } 368 | } 369 | 370 | async dispatchPermission (ctx, v) { 371 | const group = await GroupModel.findByPk(v.get('body.group_id')); 372 | if (!group) { 373 | throw new NotFound({ 374 | code: 10024 375 | }); 376 | } 377 | 378 | const permission = await PermissionModel.findOne({ 379 | where: { 380 | id: v.get('body.permission_id'), 381 | mount: MountType.Mount 382 | } 383 | }); 384 | if (!permission) { 385 | throw new NotFound({ 386 | code: 10231 387 | }); 388 | } 389 | 390 | const one = await GroupPermissionModel.findOne({ 391 | where: { 392 | group_id: v.get('body.group_id'), 393 | permission_id: v.get('body.permission_id') 394 | } 395 | }); 396 | if (one) { 397 | throw new Forbidden({ 398 | code: 10230 399 | }); 400 | } 401 | await GroupPermissionModel.create({ 402 | group_id: v.get('body.group_id'), 403 | permission_id: v.get('body.permission_id') 404 | }); 405 | } 406 | 407 | async dispatchPermissions (ctx, v) { 408 | const group = await GroupModel.findByPk(v.get('body.group_id')); 409 | if (!group) { 410 | throw new NotFound({ 411 | code: 10024 412 | }); 413 | } 414 | for (const id of v.get('body.permission_ids') || []) { 415 | const permission = await PermissionModel.findOne({ 416 | where: { 417 | id, 418 | mount: MountType.Mount 419 | } 420 | }); 421 | if (!permission) { 422 | throw new NotFound({ 423 | code: 10231 424 | }); 425 | } 426 | } 427 | 428 | let transaction; 429 | try { 430 | transaction = await sequelize.transaction(); 431 | for (const id of v.get('body.permission_ids')) { 432 | await GroupPermissionModel.create( 433 | { 434 | group_id: group.id, 435 | permission_id: id 436 | }, 437 | { 438 | transaction 439 | } 440 | ); 441 | } 442 | await transaction.commit(); 443 | } catch (err) { 444 | if (transaction) await transaction.rollback(); 445 | } 446 | } 447 | 448 | async removePermissions (ctx, v) { 449 | const group = await GroupModel.findByPk(v.get('body.group_id')); 450 | if (!group) { 451 | throw new NotFound({ 452 | code: 10024 453 | }); 454 | } 455 | for (const id of v.get('body.permission_ids') || []) { 456 | const permission = await PermissionModel.findOne({ 457 | where: { 458 | id, 459 | mount: MountType.Mount 460 | } 461 | }); 462 | if (!permission) { 463 | throw new NotFound({ 464 | code: 10231 465 | }); 466 | } 467 | } 468 | 469 | await GroupPermissionModel.destroy({ 470 | where: { 471 | group_id: v.get('body.group_id'), 472 | permission_id: { 473 | [Op.in]: v.get('body.permission_ids') 474 | } 475 | } 476 | }); 477 | } 478 | } 479 | 480 | export { AdminDao }; 481 | -------------------------------------------------------------------------------- /app/dao/book.js: -------------------------------------------------------------------------------- 1 | import { NotFound, Forbidden } from 'lin-mizar'; 2 | import Sequelize from 'sequelize'; 3 | import { Book } from '../model/book'; 4 | 5 | class BookDao { 6 | async getBook (id) { 7 | const book = await Book.findOne({ 8 | where: { 9 | id 10 | } 11 | }); 12 | return book; 13 | } 14 | 15 | async getBookByKeyword (q) { 16 | const book = await Book.findOne({ 17 | where: { 18 | title: { 19 | [Sequelize.Op.like]: `%${q}%` 20 | } 21 | } 22 | }); 23 | return book; 24 | } 25 | 26 | async getBooks () { 27 | const books = await Book.findAll(); 28 | return books; 29 | } 30 | 31 | async createBook (v) { 32 | const book = await Book.findOne({ 33 | where: { 34 | title: v.get('body.title') 35 | } 36 | }); 37 | if (book) { 38 | throw new Forbidden({ 39 | code: 10240 40 | }); 41 | } 42 | const bk = new Book(); 43 | bk.title = v.get('body.title'); 44 | bk.author = v.get('body.author'); 45 | bk.summary = v.get('body.summary'); 46 | bk.image = v.get('body.image'); 47 | await bk.save(); 48 | } 49 | 50 | async updateBook (v, id) { 51 | const book = await Book.findByPk(id); 52 | if (!book) { 53 | throw new NotFound({ 54 | code: 10022 55 | }); 56 | } 57 | book.title = v.get('body.title'); 58 | book.author = v.get('body.author'); 59 | book.summary = v.get('body.summary'); 60 | book.image = v.get('body.image'); 61 | await book.save(); 62 | } 63 | 64 | async deleteBook (id) { 65 | const book = await Book.findOne({ 66 | where: { 67 | id 68 | } 69 | }); 70 | if (!book) { 71 | throw new NotFound({ 72 | code: 10022 73 | }); 74 | } 75 | book.destroy(); 76 | } 77 | } 78 | 79 | export { BookDao }; 80 | -------------------------------------------------------------------------------- /app/dao/log.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize'; 2 | import { LogModel } from '../model/log'; 3 | import { set } from 'lodash'; 4 | import sequelize from '../lib/db'; 5 | 6 | class LogDao { 7 | async getLogs (v) { 8 | const start = v.get('query.page'); 9 | const count1 = v.get('query.count'); 10 | const condition = {}; 11 | v.get('query.name') && set(condition, 'user_name', v.get('query.name')); 12 | v.get('query.start') && 13 | v.get('query.end') && 14 | set(condition, 'create_time', { 15 | [Sequelize.Op.between]: [v.get('query.start'), v.get('query.end')] 16 | }); 17 | const { rows, count } = await LogModel.findAndCountAll({ 18 | where: Object.assign({}, condition), 19 | offset: start * count1, 20 | limit: count1, 21 | order: [['create_time', 'DESC']] 22 | }); 23 | return { 24 | rows, 25 | total: count 26 | }; 27 | } 28 | 29 | async searchLogs (v, keyword) { 30 | const start = v.get('query.page'); 31 | const count1 = v.get('query.count'); 32 | const condition = {}; 33 | v.get('query.name') && set(condition, 'username', v.get('query.name')); 34 | v.get('query.start') && 35 | v.get('query.end') && 36 | set(condition, 'create_time', { 37 | [Sequelize.Op.between]: [v.get('query.start'), v.get('query.end')] 38 | }); 39 | const { rows, count } = await LogModel.findAndCountAll({ 40 | where: Object.assign({}, condition, { 41 | message: { 42 | [Sequelize.Op.like]: `%${keyword}%` 43 | } 44 | }), 45 | offset: start * count1, 46 | limit: count1, 47 | order: [['create_time', 'DESC']] 48 | }); 49 | return { 50 | rows, 51 | total: count 52 | }; 53 | } 54 | 55 | async getUserNames (start, count) { 56 | const logs = await sequelize.query( 57 | 'SELECT lin_log.username AS names FROM lin_log GROUP BY lin_log.username HAVING COUNT(lin_log.username)>0 limit :count offset :start', 58 | { 59 | replacements: { 60 | start: start * count, 61 | count: count 62 | } 63 | } 64 | ); 65 | const arr = Array.from(logs[0].map(it => it.names)); 66 | return arr; 67 | } 68 | } 69 | 70 | export { LogDao }; 71 | -------------------------------------------------------------------------------- /app/dao/user.js: -------------------------------------------------------------------------------- 1 | import { 2 | RepeatException, 3 | generate, 4 | NotFound, 5 | Forbidden, 6 | config, 7 | getTokens 8 | } from 'lin-mizar'; 9 | import { UserModel, UserIdentityModel } from '../model/user'; 10 | import { UserGroupModel } from '../model/user-group'; 11 | import { GroupPermissionModel } from '../model/group-permission'; 12 | import { PermissionModel } from '../model/permission'; 13 | import { GroupModel } from '../model/group'; 14 | 15 | import sequelize from '../lib/db'; 16 | import { MountType, GroupLevel, IdentityType } from '../lib/type'; 17 | import { Op } from 'sequelize'; 18 | import { set, has, uniq } from 'lodash'; 19 | import { verifyCaptcha } from '../lib/captcha'; 20 | 21 | class UserDao { 22 | async createUser (v) { 23 | let user = await UserModel.findOne({ 24 | where: { 25 | username: v.get('body.username') 26 | } 27 | }); 28 | if (user) { 29 | throw new RepeatException({ 30 | code: 10071 31 | }); 32 | } 33 | if (v.get('body.email') && v.get('body.email').trim() !== '') { 34 | user = await UserModel.findOne({ 35 | where: { 36 | email: v.get('body.email') 37 | } 38 | }); 39 | if (user) { 40 | throw new RepeatException({ 41 | code: 10076 42 | }); 43 | } 44 | } 45 | for (const id of v.get('body.group_ids') || []) { 46 | const group = await GroupModel.findByPk(id); 47 | if (group.level === GroupLevel.Root) { 48 | throw new Forbidden({ 49 | code: 10073 50 | }); 51 | } 52 | if (!group) { 53 | throw new NotFound({ 54 | code: 10023 55 | }); 56 | } 57 | } 58 | await this.registerUser(v); 59 | } 60 | 61 | async getTokens (v, ctx) { 62 | if (config.getItem('loginCaptchaEnabled', false)) { 63 | const tag = ctx.req.headers.tag; 64 | const captcha = v.get('body.captcha'); 65 | 66 | if (!verifyCaptcha(captcha, tag)) { 67 | throw new Forbidden({ 68 | code: 10260 69 | }); 70 | } 71 | } 72 | const user = await UserIdentityModel.verify( 73 | v.get('body.username'), 74 | v.get('body.password') 75 | ); 76 | const { accessToken, refreshToken } = getTokens({ 77 | id: user.user_id 78 | }); 79 | 80 | return { 81 | accessToken, 82 | refreshToken 83 | }; 84 | } 85 | 86 | async updateUser (ctx, v) { 87 | const user = ctx.currentUser; 88 | if (v.get('body.username') && user.username !== v.get('body.username')) { 89 | const exit = await UserModel.findOne({ 90 | where: { 91 | username: v.get('body.username') 92 | } 93 | }); 94 | if (exit) { 95 | throw new RepeatException({ 96 | code: 10071 97 | }); 98 | } 99 | user.username = v.get('body.username'); 100 | } 101 | if (v.get('body.email') && user.email !== v.get('body.email')) { 102 | const exit = await UserModel.findOne({ 103 | where: { 104 | email: v.get('body.email') 105 | } 106 | }); 107 | if (exit) { 108 | throw new RepeatException({ 109 | code: 10076 110 | }); 111 | } 112 | user.email = v.get('body.email'); 113 | } 114 | if (v.get('body.nickname')) { 115 | user.nickname = v.get('body.nickname'); 116 | } 117 | if (v.get('body.avatar')) { 118 | user.avatar = v.get('body.avatar'); 119 | } 120 | await user.save(); 121 | } 122 | 123 | async getInformation (ctx) { 124 | const user = ctx.currentUser; 125 | 126 | const userGroup = await UserGroupModel.findAll({ 127 | where: { 128 | user_id: user.id 129 | } 130 | }); 131 | const groupIds = userGroup.map(v => v.group_id); 132 | const groups = await GroupModel.findAll({ 133 | where: { 134 | id: { 135 | [Op.in]: groupIds 136 | } 137 | } 138 | }); 139 | 140 | set(user, 'groups', groups); 141 | return user; 142 | } 143 | 144 | async getPermissions (ctx) { 145 | const user = ctx.currentUser; 146 | const userGroup = await UserGroupModel.findAll({ 147 | where: { 148 | user_id: user.id 149 | } 150 | }); 151 | const groupIds = userGroup.map(v => v.group_id); 152 | 153 | const root = await GroupModel.findOne({ 154 | where: { 155 | level: GroupLevel.Root, 156 | id: { 157 | [Op.in]: groupIds 158 | } 159 | } 160 | }); 161 | 162 | set(user, 'admin', !!root); 163 | 164 | let permissions = []; 165 | 166 | if (root) { 167 | permissions = await PermissionModel.findAll({ 168 | where: { 169 | mount: MountType.Mount 170 | } 171 | }); 172 | } else { 173 | const groupPermission = await GroupPermissionModel.findAll({ 174 | where: { 175 | group_id: { 176 | [Op.in]: groupIds 177 | } 178 | } 179 | }); 180 | 181 | const permissionIds = uniq(groupPermission.map(v => v.permission_id)); 182 | 183 | permissions = await PermissionModel.findAll({ 184 | where: { 185 | id: { 186 | [Op.in]: permissionIds 187 | }, 188 | mount: MountType.Mount 189 | } 190 | }); 191 | } 192 | 193 | set(user, 'permissions', this.formatPermissions(permissions)); 194 | 195 | return user; 196 | } 197 | 198 | async registerUser (v) { 199 | let transaction; 200 | try { 201 | transaction = await sequelize.transaction(); 202 | const user = { 203 | username: v.get('body.username') 204 | }; 205 | if (v.get('body.email') && v.get('body.email').trim() !== '') { 206 | user.email = v.get('body.email'); 207 | } 208 | const { id: user_id } = await UserModel.create(user, { 209 | transaction 210 | }); 211 | await UserIdentityModel.create( 212 | { 213 | user_id, 214 | identity_type: IdentityType.Password, 215 | identifier: user.username, 216 | credential: generate(v.get('body.password')) 217 | }, 218 | { 219 | transaction 220 | } 221 | ); 222 | 223 | const groupIds = v.get('body.group_ids'); 224 | if (groupIds && groupIds.length > 0) { 225 | for (const id of v.get('body.group_ids') || []) { 226 | await UserGroupModel.create( 227 | { 228 | user_id, 229 | group_id: id 230 | }, 231 | { 232 | transaction 233 | } 234 | ); 235 | } 236 | } else { 237 | // 未指定分组,默认加入游客分组 238 | const guest = await GroupModel.findOne({ 239 | where: { 240 | level: GroupLevel.Guest 241 | } 242 | }); 243 | await UserGroupModel.create({ 244 | user_id, 245 | group_id: guest.id 246 | }); 247 | } 248 | await transaction.commit(); 249 | } catch (error) { 250 | if (transaction) await transaction.rollback(); 251 | } 252 | return true; 253 | } 254 | 255 | formatPermissions (permissions) { 256 | const map = {}; 257 | permissions.forEach(v => { 258 | const module = v.module; 259 | if (has(map, module)) { 260 | map[module].push({ 261 | permission: v.name, 262 | module 263 | }); 264 | } else { 265 | set(map, module, [ 266 | { 267 | permission: v.name, 268 | module 269 | } 270 | ]); 271 | } 272 | }); 273 | return Object.keys(map).map(k => { 274 | const tmp = Object.create(null); 275 | set(tmp, k, map[k]); 276 | return tmp; 277 | }); 278 | } 279 | } 280 | 281 | export { UserDao }; 282 | -------------------------------------------------------------------------------- /app/extension/file/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | file: { 5 | storeDir: 'assets', 6 | singleLimit: 1024 * 1024 * 2, 7 | totalLimit: 1024 * 1024 * 20, 8 | nums: 10, 9 | exclude: [] 10 | // include:[] 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /app/extension/file/local-uploader.js: -------------------------------------------------------------------------------- 1 | import { Uploader, config } from 'lin-mizar'; 2 | import { FileModel } from '../../model/file'; 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | 6 | class LocalUploader extends Uploader { 7 | /** 8 | * 处理文件对象 9 | * { size, encoding, fieldname, filename, mimeType, data } 10 | */ 11 | async upload (files) { 12 | const arr = []; 13 | for (const file of files) { 14 | // 由于stream的特性,当读取其中的数据时,它的buffer会被消费 15 | // 所以此处深拷贝一份计算md5值 16 | const md5 = this.generateMd5(file); 17 | const siteDomain = config.getItem('siteDomain', 'http://localhost'); 18 | // 检查md5存在 19 | const exist = await FileModel.findOne({ 20 | where: { 21 | md5: md5 22 | } 23 | }); 24 | if (exist) { 25 | arr.push({ 26 | id: exist.id, 27 | key: file.fieldName, 28 | path: exist.path, 29 | url: `${siteDomain}/assets/${exist.path}`, 30 | type: exist.type, 31 | name: exist.name, 32 | extension: exist.extension, 33 | size: exist.size 34 | }); 35 | } else { 36 | const { absolutePath, relativePath, realName } = this.getStorePath( 37 | file.filename 38 | ); 39 | const target = fs.createWriteStream(absolutePath); 40 | await target.write(file.data); 41 | const ext = path.extname(realName); 42 | const saved = await FileModel.createRecord( 43 | { 44 | path: relativePath, 45 | // type: 1, 46 | name: realName, 47 | extension: ext, 48 | size: file.size, 49 | md5: md5 50 | }, 51 | true 52 | ); 53 | arr.push({ 54 | id: saved.id, 55 | key: file.fieldName, 56 | path: saved.path, 57 | url: `${siteDomain}/assets/${saved.path}`, 58 | type: saved.type, 59 | name: file.name, 60 | extension: saved.extension, 61 | size: saved.size 62 | }); 63 | } 64 | } 65 | return arr; 66 | } 67 | } 68 | 69 | module.exports = { LocalUploader }; 70 | -------------------------------------------------------------------------------- /app/extension/socket/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | socket: { 5 | path: '/ws/message', 6 | enable: false, // 是否开启 websocket 模块 7 | intercept: false // 是否开启 websocket 的鉴权拦截器 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /app/extension/socket/socket.js: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import Ws from 'ws'; 3 | import { config, jwt } from 'lin-mizar'; 4 | import { URLSearchParams } from 'url'; 5 | import { set, get } from 'lodash'; 6 | import { UserGroupModel } from '../../model/user-group'; 7 | 8 | const USER_KEY = Symbol('user'); 9 | 10 | const INTERCEPTORS = Symbol('WebSocket#interceptors'); 11 | 12 | const HANDLE_CLOSE = Symbol('WebSocket#close'); 13 | 14 | const HANDLE_ERROR = Symbol('WebSocket#error'); 15 | 16 | class WebSocket { 17 | constructor (app) { 18 | this.app = app; 19 | this.wss = null; 20 | this.sessions = new Set(); 21 | } 22 | 23 | /** 24 | * 初始化,挂载 socket 25 | */ 26 | init () { 27 | const server = http.createServer(this.app.callback()); 28 | this.wss = new Ws.Server({ 29 | path: config.getItem('socket.path', '/ws/message'), 30 | noServer: true 31 | }); 32 | 33 | server.on('upgrade', this[INTERCEPTORS].bind(this)); 34 | 35 | this.wss.on('connection', socket => { 36 | socket.on('close', this[HANDLE_CLOSE].bind(this)); 37 | socket.on('error', this[HANDLE_ERROR].bind(this)); 38 | }); 39 | 40 | this.app.context.websocket = this; 41 | return server; 42 | } 43 | 44 | [INTERCEPTORS] (request, socket, head) { 45 | // 是否开启 websocket 的鉴权拦截器 46 | if (config.getItem('socket.intercept')) { 47 | const params = new URLSearchParams( 48 | request.url.slice(request.url.indexOf('?')) 49 | ); 50 | const token = params.get('token'); 51 | try { 52 | const { identity } = jwt.verifyToken(token); 53 | this.wss.handleUpgrade(request, socket, head, ws => { 54 | set(ws, USER_KEY, identity); 55 | this.sessions.add(ws); 56 | this.wss.emit('connection', ws, request); 57 | }); 58 | } catch (error) { 59 | console.log(error.message); 60 | socket.destroy(); 61 | } 62 | return; 63 | } 64 | this.wss.handleUpgrade(request, socket, head, ws => { 65 | this.sessions.add(ws); 66 | this.wss.emit('connection', ws, request); 67 | }); 68 | } 69 | 70 | [HANDLE_CLOSE] () { 71 | for (const session of this.sessions) { 72 | if (session.readyState === Ws.CLOSED) { 73 | this.sessions.delete(session); 74 | } 75 | } 76 | } 77 | 78 | [HANDLE_ERROR] (session, error) { 79 | console.log(error); 80 | } 81 | 82 | /** 83 | * 发送消息 84 | * 85 | * @param {number} userId 用户id 86 | * @param {string} message 消息 87 | */ 88 | sendMessage (userId, message) { 89 | for (const session of this.sessions) { 90 | if (session.readyState === Ws.OPEN) { 91 | continue; 92 | } 93 | if (get(session, USER_KEY) === userId) { 94 | session.send(message); 95 | break; 96 | } 97 | } 98 | } 99 | 100 | /** 101 | * 发送消息 102 | * 103 | * @param {WebSocket} session 当前会话 104 | * @param {string} message 消息 105 | */ 106 | sendMessageToSession (session, message) { 107 | session.send(message); 108 | } 109 | 110 | /** 111 | * 广播 112 | * 113 | * @param {string} message 消息 114 | */ 115 | broadCast (message) { 116 | this.sessions.forEach(session => { 117 | if (session.readyState === Ws.OPEN) { 118 | session.send(message); 119 | } 120 | }); 121 | } 122 | 123 | /** 124 | * 对某个分组广播 125 | * 126 | * @param {number} 分组id 127 | * @param {string} 消息 128 | */ 129 | async broadCastToGroup (groupId, message) { 130 | const userGroup = await UserGroupModel.findAll({ 131 | where: { 132 | group_id: groupId 133 | } 134 | }); 135 | const userIds = userGroup.map(v => v.getDataValue('user_id')); 136 | for (const session of this.sessions) { 137 | if (session.readyState !== Ws.OPEN) { 138 | continue; 139 | } 140 | const userId = get(session, USER_KEY); 141 | if (!userId) { 142 | continue; 143 | } 144 | if (userIds.includes(userId)) { 145 | session.send(message); 146 | } 147 | } 148 | } 149 | 150 | /** 151 | * 获取所有会话 152 | */ 153 | getSessions () { 154 | return this.sessions; 155 | } 156 | 157 | /** 158 | * 获得当前连接数 159 | */ 160 | getConnectionCount () { 161 | return this.sessions.size; 162 | } 163 | } 164 | 165 | export default WebSocket; 166 | -------------------------------------------------------------------------------- /app/lib/captcha.js: -------------------------------------------------------------------------------- 1 | import sharp from 'sharp'; 2 | import svgCaptcha from 'svg-captcha'; 3 | import { 4 | createCipheriv, 5 | createDecipheriv, 6 | randomBytes, 7 | createHash 8 | } from 'crypto'; 9 | import { config } from 'lin-mizar'; 10 | 11 | const iv = Buffer.from(randomBytes(8)).toString('hex'); 12 | const secret = config.getItem('secret'); 13 | const key = createHash('sha256') 14 | .update(String(secret)) 15 | .digest('base64') 16 | .substr(0, 32); 17 | 18 | /** 19 | * 加密 20 | * 21 | * @param {string} value 需要加密的信息 22 | * @returns 加密后的值 23 | */ 24 | function aesEncrypt (value) { 25 | const cipher = createCipheriv('aes-256-ctr', key, iv); 26 | let encrypted = cipher.update(value, 'utf8', 'hex'); 27 | encrypted += cipher.final('hex'); 28 | return encrypted; 29 | } 30 | 31 | /** 32 | * 解密 33 | * 34 | * @param {string} encrypted 需要解密的信息 35 | * @returns 解密后的值 36 | */ 37 | function aesDecrypt (encrypted) { 38 | const cipher = createDecipheriv('aes-256-ctr', key, iv); 39 | let decrypted = cipher.update(encrypted, 'hex', 'utf8'); 40 | decrypted += cipher.final('utf8'); 41 | return decrypted; 42 | } 43 | 44 | /** 45 | * 给 tag 加密 46 | */ 47 | function getTag (captcha) { 48 | const date = new Date(); 49 | // 5 分钟后过期 50 | date.setMinutes(date.getMinutes() + 5); 51 | const info = { 52 | captcha, 53 | expired: date.getTime() 54 | }; 55 | 56 | return aesEncrypt(JSON.stringify(info)); 57 | } 58 | 59 | /** 60 | * 校验验证码是否正确 61 | */ 62 | function verifyCaptcha (loginCaptcha, tag) { 63 | if (!loginCaptcha || !tag) { 64 | return false; 65 | } 66 | const decrypted = aesDecrypt(tag); 67 | try { 68 | const { captcha, expired } = JSON.parse(decrypted); 69 | // 大小写不敏感 70 | if ( 71 | loginCaptcha.toLowerCase() !== captcha.toLowerCase() || 72 | new Date().getTime() > expired 73 | ) { 74 | return false; 75 | } 76 | } catch (error) { 77 | return false; 78 | } 79 | return true; 80 | } 81 | 82 | /** 83 | * 生成验证码图片及对称加密用到的 tag 84 | */ 85 | async function generateCaptcha () { 86 | const captcha = svgCaptcha.create({ 87 | size: 4, // 验证码长度 88 | fontSize: 45, // 验证码字号 89 | noise: Math.floor(Math.random() * 5), // 干扰线条数目 90 | width: 80, // 宽度 91 | height: 40, // 高度 92 | color: true, // 验证码字符是否有颜色,默认是没有,但是如果设置了背景颜色,那么默认就是有字符颜色 93 | background: '#fff' // 背景色 94 | }); 95 | 96 | const { data, text } = captcha; 97 | const str = await sharp(Buffer.from(data)) 98 | .png() 99 | .toBuffer(); 100 | const image = 'data:image/jpg;base64,' + str.toString('base64'); 101 | 102 | return { 103 | image, 104 | tag: getTag(text) 105 | }; 106 | } 107 | 108 | export { generateCaptcha, verifyCaptcha }; 109 | -------------------------------------------------------------------------------- /app/lib/db.js: -------------------------------------------------------------------------------- 1 | import { config } from 'lin-mizar'; 2 | import { Sequelize } from 'sequelize'; 3 | 4 | /** 5 | * 数据库名,默认lin-cms 6 | */ 7 | const database = config.getItem('db.database', 'lin-cms'); 8 | 9 | /** 10 | * 数据库用户名,默认root 11 | */ 12 | const username = config.getItem('db.username', 'root'); 13 | 14 | /** 15 | * 数据库密码,默认123456 16 | */ 17 | const password = config.getItem('db.password', '123456'); 18 | 19 | /** 20 | * 其它数据库配置项 21 | */ 22 | const options = config.getItem('db', {}); 23 | 24 | /** 25 | * 全局的 Sequelize 实例 26 | */ 27 | const sequelize = new Sequelize(database, username, password, { 28 | ...options 29 | }); 30 | 31 | sequelize.sync({ 32 | force: false 33 | }); 34 | 35 | export default sequelize; 36 | -------------------------------------------------------------------------------- /app/lib/exception.js: -------------------------------------------------------------------------------- 1 | import { HttpException, config } from 'lin-mizar'; 2 | 3 | const CodeMessage = config.getItem('codeMessage', {}); 4 | 5 | /** 6 | * 自定义异常类 7 | */ 8 | class BookNotFound extends HttpException { 9 | constructor (ex) { 10 | super(); 11 | this.status = 404; 12 | this.code = 10022; 13 | this.message = CodeMessage.getMessage(10022); 14 | this.exceptionHandler(ex); 15 | } 16 | } 17 | 18 | export { BookNotFound }; 19 | -------------------------------------------------------------------------------- /app/lib/type.js: -------------------------------------------------------------------------------- 1 | const MountType = { 2 | Mount: 1, // 挂载 3 | Unmount: 0 4 | }; 5 | 6 | const IdentityType = { 7 | Password: 'USERNAME_PASSWORD' 8 | }; 9 | 10 | const GroupLevel = { 11 | Root: 1, 12 | Guest: 2, 13 | User: 3 14 | }; 15 | 16 | export { MountType, IdentityType, GroupLevel }; 17 | -------------------------------------------------------------------------------- /app/lib/util.js: -------------------------------------------------------------------------------- 1 | import { toSafeInteger, get, isInteger } from 'lodash'; 2 | import { ParametersException } from 'lin-mizar'; 3 | 4 | function getSafeParamId (ctx) { 5 | const id = toSafeInteger(get(ctx.params, 'id')); 6 | if (!isInteger(id)) { 7 | throw new ParametersException({ 8 | code: 10030 9 | }); 10 | } 11 | return id; 12 | } 13 | 14 | function isOptional (val) { 15 | // undefined , null , "" , " ", 皆通过 16 | if (val === undefined) { 17 | return true; 18 | } 19 | if (val === null) { 20 | return true; 21 | } 22 | if (typeof val === 'string') { 23 | return val === '' || val.trim() === ''; 24 | } 25 | return false; 26 | } 27 | 28 | export { getSafeParamId, isOptional }; 29 | -------------------------------------------------------------------------------- /app/middleware/jwt.js: -------------------------------------------------------------------------------- 1 | import { 2 | NotFound, 3 | AuthFailed, 4 | parseHeader, 5 | RefreshException, 6 | TokenType, 7 | routeMetaInfo 8 | } from 'lin-mizar'; 9 | import { UserGroupModel } from '../model/user-group'; 10 | import { GroupModel } from '../model/group'; 11 | import { GroupPermissionModel } from '../model/group-permission'; 12 | import { PermissionModel } from '../model/permission'; 13 | import { UserModel } from '../model/user'; 14 | import { MountType, GroupLevel } from '../lib/type'; 15 | import { Op } from 'sequelize'; 16 | import { uniq } from 'lodash'; 17 | 18 | // 是否超级管理员 19 | async function isAdmin (ctx) { 20 | const userGroup = await UserGroupModel.findAll({ 21 | where: { 22 | user_id: ctx.currentUser.id 23 | } 24 | }); 25 | const groupIds = userGroup.map(v => v.group_id); 26 | const is = await GroupModel.findOne({ 27 | where: { 28 | level: GroupLevel.Root, 29 | id: { 30 | [Op.in]: groupIds 31 | } 32 | } 33 | }); 34 | return is; 35 | } 36 | 37 | /** 38 | * 将 user 挂在 ctx 上 39 | */ 40 | async function mountUser (ctx) { 41 | const { identity } = parseHeader(ctx); 42 | const user = await UserModel.findByPk(identity); 43 | if (!user) { 44 | throw new NotFound({ 45 | code: 10021 46 | }); 47 | } 48 | // 将user挂在ctx上 49 | ctx.currentUser = user; 50 | } 51 | 52 | /** 53 | * 守卫函数,非超级管理员不可访问 54 | */ 55 | async function adminRequired (ctx, next) { 56 | if (ctx.request.method !== 'OPTIONS') { 57 | await mountUser(ctx); 58 | 59 | if (await isAdmin(ctx)) { 60 | await next(); 61 | } else { 62 | throw new AuthFailed({ 63 | code: 10001 64 | }); 65 | } 66 | } else { 67 | await next(); 68 | } 69 | } 70 | 71 | /** 72 | * 守卫函数,用户登陆即可访问 73 | */ 74 | async function loginRequired (ctx, next) { 75 | if (ctx.request.method !== 'OPTIONS') { 76 | await mountUser(ctx); 77 | 78 | await next(); 79 | } else { 80 | await next(); 81 | } 82 | } 83 | 84 | /** 85 | * 守卫函数,用户刷新令牌,统一异常 86 | */ 87 | async function refreshTokenRequiredWithUnifyException (ctx, next) { 88 | if (ctx.request.method !== 'OPTIONS') { 89 | try { 90 | const { identity } = parseHeader(ctx, TokenType.REFRESH); 91 | const user = await UserModel.findByPk(identity); 92 | if (!user) { 93 | ctx.throw( 94 | new NotFound({ 95 | code: 10021 96 | }) 97 | ); 98 | } 99 | // 将user挂在ctx上 100 | ctx.currentUser = user; 101 | } catch (error) { 102 | throw new RefreshException(); 103 | } 104 | await next(); 105 | } else { 106 | await next(); 107 | } 108 | } 109 | 110 | /** 111 | * 守卫函数,用于权限组鉴权 112 | */ 113 | async function groupRequired (ctx, next) { 114 | if (ctx.request.method !== 'OPTIONS') { 115 | await mountUser(ctx); 116 | 117 | // 超级管理员 118 | if (await isAdmin(ctx)) { 119 | await next(); 120 | } else { 121 | if (ctx.matched) { 122 | const routeName = ctx._matchedRouteName || ctx.routerName; 123 | const endpoint = `${ctx.method} ${routeName}`; 124 | const { permission, module } = routeMetaInfo.get(endpoint); 125 | const userGroup = await UserGroupModel.findAll({ 126 | where: { 127 | user_id: ctx.currentUser.id 128 | } 129 | }); 130 | const groupIds = userGroup.map(v => v.group_id); 131 | const groupPermission = await GroupPermissionModel.findAll({ 132 | where: { 133 | group_id: { 134 | [Op.in]: groupIds 135 | } 136 | } 137 | }); 138 | const permissionIds = uniq(groupPermission.map(v => v.permission_id)); 139 | const item = await PermissionModel.findOne({ 140 | where: { 141 | name: permission, 142 | mount: MountType.Mount, 143 | module, 144 | id: { 145 | [Op.in]: permissionIds 146 | } 147 | } 148 | }); 149 | if (item) { 150 | await next(); 151 | } else { 152 | throw new AuthFailed({ 153 | code: 10001 154 | }); 155 | } 156 | } else { 157 | throw new AuthFailed({ 158 | code: 10001 159 | }); 160 | } 161 | } 162 | } else { 163 | await next(); 164 | } 165 | } 166 | 167 | export { 168 | adminRequired, 169 | loginRequired, 170 | groupRequired, 171 | refreshTokenRequiredWithUnifyException 172 | }; 173 | -------------------------------------------------------------------------------- /app/middleware/logger.js: -------------------------------------------------------------------------------- 1 | import { get } from 'lodash'; 2 | import { routeMetaInfo, assert } from 'lin-mizar'; 3 | import { LogModel } from '../model/log'; 4 | 5 | const REG_XP = /(?<=\{)[^}]*(?=\})/g; 6 | 7 | /** 8 | * 日志记录中间件 9 | * @param template 消息模板 10 | * 11 | * ```js 12 | * test.linGet( 13 | * "getTestMessage", 14 | * "/json", 15 | * { 16 | * permission: "hello", 17 | * module: "world", 18 | * mount: true 19 | * }, 20 | * loginRequired, 21 | * logger("{user.username}就是皮了一波"), 22 | * async ctx => { 23 | * ctx.json({ 24 | * message: "物质决定意识,经济基础决定上层建筑" 25 | * }); 26 | * } 27 | * ); 28 | * ``` 29 | */ 30 | export const logger = template => { 31 | return async (ctx, next) => { 32 | await next(); 33 | // 取数据,写入到日志中 34 | writeLog(template, ctx); 35 | }; 36 | }; 37 | 38 | function writeLog (template, ctx) { 39 | const message = parseTemplate( 40 | template, 41 | ctx.currentUser, 42 | ctx.response, 43 | ctx.request 44 | ); 45 | if (ctx.matched) { 46 | const info = findAuthAndModule(ctx); 47 | let permission = ''; 48 | if (info) { 49 | permission = get(info, 'permission'); 50 | } 51 | const statusCode = ctx.status || 0; 52 | LogModel.createLog( 53 | { 54 | message, 55 | user_id: ctx.currentUser.id, 56 | username: ctx.currentUser.username, 57 | status_code: statusCode, 58 | method: ctx.request.method, 59 | path: ctx.request.path, 60 | permission 61 | }, 62 | true 63 | ); 64 | } 65 | } 66 | 67 | /** 68 | * 通过当前的路由名找到对应的权限录入信息 69 | * @param ctx koa 的 context 70 | */ 71 | function findAuthAndModule (ctx) { 72 | const routeName = ctx._matchedRouteName || ctx.routerName; 73 | const endpoint = `${ctx.method} ${routeName}`; 74 | return routeMetaInfo.get(endpoint); 75 | } 76 | 77 | /** 78 | * 解析模板 79 | * @param template 消息模板 80 | * @param user 用户 81 | * @param response 82 | * @param request 83 | */ 84 | function parseTemplate (template, user, response, request) { 85 | let res = []; 86 | let son; 87 | while ((son = REG_XP.exec(template)) !== null) { 88 | res.push(son[0]); 89 | } 90 | if (res) { 91 | res.forEach(item => { 92 | const index = item.indexOf('.'); 93 | assert(index !== -1, item + '中必须包含 . ,且为一个'); 94 | const obj = item.substring(0, index); 95 | const prop = item.substring(index + 1, item.length); 96 | let it; 97 | switch (obj) { 98 | case 'user': 99 | it = get(user, prop, ''); 100 | break; 101 | case 'response': 102 | it = get(response, prop, ''); 103 | break; 104 | case 'request': 105 | it = get(request, prop, ''); 106 | break; 107 | default: 108 | it = ''; 109 | break; 110 | } 111 | template = template.replace(`{${item}}`, it); 112 | }); 113 | } 114 | return template; 115 | } 116 | -------------------------------------------------------------------------------- /app/model/book.js: -------------------------------------------------------------------------------- 1 | import { InfoCrudMixin } from 'lin-mizar'; 2 | import { merge } from 'lodash'; 3 | import { Sequelize, Model } from 'sequelize'; 4 | import sequelize from '../lib/db'; 5 | 6 | class Book extends Model { 7 | toJSON () { 8 | const origin = { 9 | id: this.id, 10 | title: this.title, 11 | author: this.author, 12 | summary: this.summary, 13 | image: this.image 14 | }; 15 | return origin; 16 | } 17 | } 18 | 19 | Book.init( 20 | { 21 | id: { 22 | type: Sequelize.INTEGER, 23 | primaryKey: true, 24 | autoIncrement: true 25 | }, 26 | title: { 27 | type: Sequelize.STRING(50), 28 | allowNull: false 29 | }, 30 | author: { 31 | type: Sequelize.STRING(30), 32 | allowNull: true, 33 | defaultValue: '未名' 34 | }, 35 | summary: { 36 | type: Sequelize.STRING(1000), 37 | allowNull: true 38 | }, 39 | image: { 40 | type: Sequelize.STRING(100), 41 | allowNull: true 42 | } 43 | }, 44 | merge( 45 | { 46 | sequelize, 47 | tableName: 'book', 48 | modelName: 'book' 49 | }, 50 | InfoCrudMixin.options 51 | ) 52 | ); 53 | 54 | export { Book }; 55 | -------------------------------------------------------------------------------- /app/model/file.js: -------------------------------------------------------------------------------- 1 | import { Model, Sequelize } from 'sequelize'; 2 | import { InfoCrudMixin } from 'lin-mizar'; 3 | import { merge } from 'lodash'; 4 | import sequelize from '../lib/db'; 5 | 6 | class File extends Model { 7 | static async createRecord (args, commit) { 8 | const record = File.build(args); 9 | commit && (await record.save()); 10 | return record; 11 | } 12 | } 13 | 14 | File.init( 15 | { 16 | id: { 17 | type: Sequelize.INTEGER, 18 | primaryKey: true, 19 | autoIncrement: true 20 | }, 21 | path: { 22 | type: Sequelize.STRING({ length: 500 }), 23 | allowNull: false 24 | }, 25 | type: { 26 | type: Sequelize.STRING({ length: 10 }), 27 | allowNull: false, 28 | defaultValue: 'LOCAL', 29 | comment: 'LOCAL 本地,REMOTE 远程' 30 | }, 31 | name: { 32 | type: Sequelize.STRING(100), 33 | allowNull: false 34 | }, 35 | extension: { 36 | type: Sequelize.STRING(50) 37 | }, 38 | size: { 39 | type: Sequelize.INTEGER, 40 | allowNull: true 41 | }, 42 | // 建立索引,方便搜索 43 | // 域名配置 44 | md5: { 45 | type: Sequelize.STRING(40), 46 | allowNull: true, 47 | comment: '图片md5值,防止上传重复图片' 48 | } 49 | }, 50 | merge( 51 | { 52 | sequelize, 53 | tableName: 'lin_file', 54 | modelName: 'file', 55 | indexes: [ 56 | { 57 | name: 'md5_del', 58 | unique: true, 59 | fields: ['md5', 'delete_time'] 60 | } 61 | ] 62 | }, 63 | InfoCrudMixin.options 64 | ) 65 | ); 66 | 67 | export { File as FileModel }; 68 | -------------------------------------------------------------------------------- /app/model/group-permission.js: -------------------------------------------------------------------------------- 1 | import { Model, Sequelize } from 'sequelize'; 2 | import sequelize from '../lib/db'; 3 | 4 | class GroupPermission extends Model {} 5 | 6 | GroupPermission.init( 7 | { 8 | id: { 9 | type: Sequelize.INTEGER, 10 | primaryKey: true, 11 | autoIncrement: true 12 | }, 13 | group_id: { 14 | type: Sequelize.INTEGER, 15 | allowNull: false, 16 | comment: '分组id' 17 | }, 18 | permission_id: { 19 | type: Sequelize.INTEGER, 20 | allowNull: false, 21 | comment: '权限id' 22 | } 23 | }, 24 | { 25 | sequelize, 26 | timestamps: false, 27 | tableName: 'lin_group_permission', 28 | modelName: 'group_permission', 29 | indexes: [ 30 | { 31 | name: 'group_id_permission_id', 32 | using: 'BTREE', 33 | fields: ['group_id', 'permission_id'] 34 | } 35 | ] 36 | } 37 | ); 38 | 39 | export { GroupPermission as GroupPermissionModel }; 40 | -------------------------------------------------------------------------------- /app/model/group.js: -------------------------------------------------------------------------------- 1 | import { Model, Sequelize } from 'sequelize'; 2 | import { InfoCrudMixin } from 'lin-mizar'; 3 | import { has, get, merge } from 'lodash'; 4 | import sequelize from '../lib/db'; 5 | 6 | class Group extends Model { 7 | toJSON () { 8 | const origin = { 9 | id: this.id, 10 | name: this.name, 11 | info: this.info 12 | }; 13 | if (has(this, 'permissions')) { 14 | return { ...origin, permissions: get(this, 'permissions', []) }; 15 | } 16 | return origin; 17 | } 18 | } 19 | 20 | Group.init( 21 | { 22 | id: { 23 | type: Sequelize.INTEGER, 24 | primaryKey: true, 25 | autoIncrement: true 26 | }, 27 | name: { 28 | type: Sequelize.STRING({ length: 60 }), 29 | allowNull: false, 30 | comment: '分组名称,例如:搬砖者' 31 | }, 32 | info: { 33 | type: Sequelize.STRING({ length: 255 }), 34 | allowNull: true, 35 | comment: '分组信息:例如:搬砖的人' 36 | }, 37 | level: { 38 | type: Sequelize.INTEGER(2), 39 | defaultValue: 3, 40 | comment: '分组级别 1:root 2:guest 3:user(root、guest分组只能存在一个)' 41 | } 42 | }, 43 | merge( 44 | { 45 | sequelize, 46 | tableName: 'lin_group', 47 | modelName: 'group', 48 | indexes: [ 49 | { 50 | name: 'name_del', 51 | unique: true, 52 | fields: ['name', 'delete_time'] 53 | } 54 | ] 55 | }, 56 | InfoCrudMixin.options 57 | ) 58 | ); 59 | 60 | export { Group as GroupModel }; 61 | -------------------------------------------------------------------------------- /app/model/log.js: -------------------------------------------------------------------------------- 1 | import { Model, Sequelize } from 'sequelize'; 2 | import { InfoCrudMixin } from 'lin-mizar'; 3 | import sequelize from '../lib/db'; 4 | import { merge } from 'lodash'; 5 | 6 | class Log extends Model { 7 | toJSON () { 8 | const origin = { 9 | id: this.id, 10 | message: this.message, 11 | time: this.create_time, 12 | user_id: this.user_id, 13 | username: this.username, 14 | status_code: this.status_code, 15 | method: this.method, 16 | path: this.path, 17 | permission: this.permission 18 | }; 19 | return origin; 20 | } 21 | 22 | static createLog (args, commit) { 23 | const log = Log.build(args); 24 | commit && log.save(); 25 | return log; 26 | } 27 | } 28 | 29 | Log.init( 30 | { 31 | id: { 32 | type: Sequelize.INTEGER, 33 | primaryKey: true, 34 | autoIncrement: true 35 | }, 36 | message: { 37 | type: Sequelize.STRING({ length: 450 }) 38 | }, 39 | user_id: { 40 | type: Sequelize.INTEGER, 41 | allowNull: false 42 | }, 43 | username: { 44 | type: Sequelize.STRING(20) 45 | }, 46 | status_code: { 47 | type: Sequelize.INTEGER 48 | }, 49 | method: { 50 | type: Sequelize.STRING(20) 51 | }, 52 | path: { 53 | type: Sequelize.STRING(50) 54 | }, 55 | permission: { 56 | type: Sequelize.STRING(100) 57 | } 58 | }, 59 | merge( 60 | { 61 | sequelize, 62 | tableName: 'lin_log', 63 | modelName: 'log' 64 | }, 65 | InfoCrudMixin.options 66 | ) 67 | ); 68 | 69 | export { Log as LogModel }; 70 | -------------------------------------------------------------------------------- /app/model/permission.js: -------------------------------------------------------------------------------- 1 | import { Model, Sequelize, Op } from 'sequelize'; 2 | import { routeMetaInfo, InfoCrudMixin } from 'lin-mizar'; 3 | import sequelize from '../lib/db'; 4 | import { GroupPermissionModel } from './group-permission'; 5 | import { MountType } from '../lib/type'; 6 | import { merge } from 'lodash'; 7 | 8 | class Permission extends Model { 9 | toJSON () { 10 | const origin = { 11 | id: this.id, 12 | name: this.name, 13 | module: this.module 14 | }; 15 | return origin; 16 | } 17 | 18 | static async initPermission () { 19 | let transaction; 20 | try { 21 | transaction = await sequelize.transaction(); 22 | const info = Array.from(routeMetaInfo.values()); 23 | const permissions = await this.findAll(); 24 | 25 | for (const { permission: permissionName, module: moduleName } of info) { 26 | const exist = permissions.find( 27 | p => p.name === permissionName && p.module === moduleName 28 | ); 29 | // 如果不存在这个 permission 则创建之 30 | if (!exist) { 31 | await this.create( 32 | { 33 | name: permissionName, 34 | module: moduleName 35 | }, 36 | { transaction } 37 | ); 38 | } 39 | } 40 | 41 | const permissionIds = []; 42 | for (const permission of permissions) { 43 | const exist = info.find( 44 | meta => 45 | meta.permission === permission.name && 46 | meta.module === permission.module 47 | ); 48 | // 如果能找到这个 meta 则挂载之,否则卸载之 49 | if (exist) { 50 | permission.mount = MountType.Mount; 51 | } else { 52 | permission.mount = MountType.Unmount; 53 | permissionIds.push(permission.id); 54 | } 55 | await permission.save({ 56 | transaction 57 | }); 58 | } 59 | 60 | // 相应地要解除关联关系 61 | if (permissionIds.length) { 62 | await GroupPermissionModel.destroy({ 63 | where: { 64 | permission_id: { 65 | [Op.in]: permissionIds 66 | } 67 | }, 68 | transaction 69 | }); 70 | } 71 | await transaction.commit(); 72 | } catch (error) { 73 | if (transaction) await transaction.rollback(); 74 | } 75 | } 76 | } 77 | 78 | Permission.init( 79 | { 80 | id: { 81 | type: Sequelize.INTEGER, 82 | primaryKey: true, 83 | autoIncrement: true 84 | }, 85 | name: { 86 | type: Sequelize.STRING({ length: 60 }), 87 | comment: '权限名称,例如:访问首页', 88 | allowNull: false 89 | }, 90 | module: { 91 | type: Sequelize.STRING({ length: 50 }), 92 | comment: '权限所属模块,例如:人员管理', 93 | allowNull: false 94 | }, 95 | mount: { 96 | type: Sequelize.BOOLEAN, 97 | comment: '0:关闭 1:开启', 98 | defaultValue: 1 99 | } 100 | }, 101 | merge( 102 | { 103 | sequelize, 104 | tableName: 'lin_permission', 105 | modelName: 'permission' 106 | }, 107 | InfoCrudMixin.options 108 | ) 109 | ); 110 | 111 | export { Permission as PermissionModel }; 112 | -------------------------------------------------------------------------------- /app/model/user-group.js: -------------------------------------------------------------------------------- 1 | import { Model, Sequelize } from 'sequelize'; 2 | import sequelize from '../lib/db'; 3 | 4 | class UserGroup extends Model {} 5 | 6 | UserGroup.init( 7 | { 8 | id: { 9 | type: Sequelize.INTEGER, 10 | primaryKey: true, 11 | autoIncrement: true 12 | }, 13 | group_id: { 14 | type: Sequelize.INTEGER, 15 | allowNull: false, 16 | comment: '分组id' 17 | }, 18 | user_id: { 19 | type: Sequelize.INTEGER, 20 | allowNull: false, 21 | comment: '用户id' 22 | } 23 | }, 24 | { 25 | sequelize, 26 | timestamps: false, 27 | tableName: 'lin_user_group', 28 | modelName: 'user_group', 29 | indexes: [ 30 | { 31 | name: 'user_id_group_id', 32 | using: 'BTREE', 33 | fields: ['user_id', 'group_id'] 34 | } 35 | ] 36 | } 37 | ); 38 | 39 | export { UserGroup as UserGroupModel }; 40 | -------------------------------------------------------------------------------- /app/model/user.js: -------------------------------------------------------------------------------- 1 | import { 2 | NotFound, 3 | verify, 4 | AuthFailed, 5 | generate, 6 | Failed, 7 | config, 8 | InfoCrudMixin 9 | } from 'lin-mizar'; 10 | import sequelize from '../lib/db'; 11 | import { IdentityType } from '../lib/type'; 12 | import { Model, Sequelize } from 'sequelize'; 13 | import { get, has, unset, merge } from 'lodash'; 14 | 15 | class UserIdentity extends Model { 16 | checkPassword (raw) { 17 | if (!this.credential || this.credential === '') { 18 | return false; 19 | } 20 | return verify(raw, this.credential); 21 | } 22 | 23 | static async verify (username, password) { 24 | const user = await this.findOne({ 25 | where: { 26 | identity_type: IdentityType.Password, 27 | identifier: username 28 | } 29 | }); 30 | if (!user) { 31 | throw new AuthFailed({ code: 10031 }); 32 | } 33 | if (!user.checkPassword(password)) { 34 | throw new AuthFailed({ code: 10031 }); 35 | } 36 | return user; 37 | } 38 | 39 | static async changePassword (currentUser, oldPassword, newPassword) { 40 | const user = await this.findOne({ 41 | where: { 42 | identity_type: IdentityType.Password, 43 | identifier: currentUser.username 44 | } 45 | }); 46 | if (!user) { 47 | throw new NotFound({ code: 10021 }); 48 | } 49 | if (!user.checkPassword(oldPassword)) { 50 | throw new Failed({ 51 | code: 10011 52 | }); 53 | } 54 | user.credential = generate(newPassword); 55 | await user.save(); 56 | } 57 | 58 | static async resetPassword (currentUser, newPassword) { 59 | const user = await this.findOne({ 60 | where: { 61 | identity_type: IdentityType.Password, 62 | identifier: currentUser.username 63 | } 64 | }); 65 | if (!user) { 66 | throw new NotFound({ code: 10021 }); 67 | } 68 | user.credential = generate(newPassword); 69 | await user.save(); 70 | } 71 | } 72 | 73 | UserIdentity.init( 74 | { 75 | id: { 76 | type: Sequelize.INTEGER, 77 | primaryKey: true, 78 | autoIncrement: true 79 | }, 80 | user_id: { 81 | type: Sequelize.INTEGER, 82 | allowNull: false, 83 | comment: '用户id' 84 | }, 85 | identity_type: { 86 | type: Sequelize.STRING({ length: 100 }), 87 | allowNull: false, 88 | comment: '登录类型(手机号 邮箱 用户名)或第三方应用名称(微信 微博等)' 89 | }, 90 | identifier: { 91 | type: Sequelize.STRING({ length: 100 }), 92 | comment: '标识(手机号 邮箱 用户名或第三方应用的唯一标识)' 93 | }, 94 | credential: { 95 | type: Sequelize.STRING({ length: 100 }), 96 | comment: '密码凭证(站内的保存密码,站外的不保存或保存token)' 97 | } 98 | }, 99 | merge( 100 | { 101 | sequelize, 102 | tableName: 'lin_user_identity', 103 | modelName: 'user_identity' 104 | }, 105 | InfoCrudMixin.options 106 | ) 107 | ); 108 | 109 | class User extends Model { 110 | toJSON () { 111 | const origin = { 112 | id: this.id, 113 | username: this.username, 114 | nickname: this.nickname, 115 | email: this.email, 116 | avatar: this.avatar 117 | ? `${config.getItem('siteDomain', 'http://localhost')}/assets/${ 118 | this.avatar 119 | }` 120 | : '' 121 | }; 122 | if (has(this, 'groups')) { 123 | return { ...origin, groups: get(this, 'groups', []) }; 124 | } else if (has(this, 'permissions')) { 125 | unset(origin, 'username'); 126 | return { 127 | ...origin, 128 | admin: get(this, 'admin', false), 129 | permissions: get(this, 'permissions', []) 130 | }; 131 | } 132 | return origin; 133 | } 134 | } 135 | 136 | User.init( 137 | { 138 | id: { 139 | type: Sequelize.INTEGER, 140 | primaryKey: true, 141 | autoIncrement: true 142 | }, 143 | username: { 144 | type: Sequelize.STRING({ length: 24 }), 145 | allowNull: false, 146 | comment: '用户名,唯一' 147 | }, 148 | nickname: { 149 | type: Sequelize.STRING({ length: 24 }), 150 | comment: '用户昵称' 151 | }, 152 | avatar: { 153 | // 用户默认生成图像,为null 154 | type: Sequelize.STRING({ length: 500 }), 155 | comment: '头像url' 156 | // get() { 157 | // return config.getItem('siteDomain').replace(/\/+$/, '') + '/assets/' + this.getDataValue('avatar') 158 | // } 159 | }, 160 | email: { 161 | type: Sequelize.STRING({ length: 100 }), 162 | allowNull: true 163 | } 164 | }, 165 | merge( 166 | { 167 | sequelize, 168 | tableName: 'lin_user', 169 | modelName: 'user', 170 | indexes: [ 171 | { 172 | name: 'username_del', 173 | unique: true, 174 | fields: ['username', 'delete_time'] 175 | }, 176 | { 177 | name: 'email_del', 178 | unique: true, 179 | fields: ['email', 'delete_time'] 180 | } 181 | ] 182 | }, 183 | InfoCrudMixin.options 184 | ) 185 | ); 186 | 187 | export { User as UserModel, UserIdentity as UserIdentityModel }; 188 | -------------------------------------------------------------------------------- /app/plugin/poem/app/controller.js: -------------------------------------------------------------------------------- 1 | import Router from 'koa-router'; 2 | import { PoemListValidator, PoemSearchValidator } from './validator'; 3 | import { Poem } from './model'; 4 | 5 | const api = new Router({ prefix: '/poem' }); 6 | 7 | api.get('/all', async ctx => { 8 | const validator = await new PoemListValidator().validate(ctx); 9 | const poems = await Poem.getAll(validator); 10 | ctx.json(poems); 11 | }); 12 | 13 | api.get('/search', async ctx => { 14 | const validator = await new PoemSearchValidator().validate(ctx); 15 | const poems = await Poem.search(validator.get('query.q')); 16 | ctx.json(poems); 17 | }); 18 | 19 | api.get('/authors', async ctx => { 20 | const authors = await Poem.getAuthors(); 21 | ctx.json(authors); 22 | }); 23 | 24 | export { api }; 25 | -------------------------------------------------------------------------------- /app/plugin/poem/app/index.js: -------------------------------------------------------------------------------- 1 | import { api } from './controller'; 2 | import { Poem } from './model'; 3 | 4 | export { Poem, api }; 5 | -------------------------------------------------------------------------------- /app/plugin/poem/app/model.js: -------------------------------------------------------------------------------- 1 | import { InfoCrudMixin } from 'lin-mizar'; 2 | import { merge } from 'lodash'; 3 | import { Sequelize, Model } from 'sequelize'; 4 | import sequelize from '../../../lib/db'; 5 | 6 | const { config } = require('lin-mizar/lin/config'); 7 | 8 | class Poem extends Model { 9 | static async search (q) { 10 | const poems = await Poem.findAll({ 11 | where: { 12 | title: { 13 | [Sequelize.Op.like]: '%' + q + '%' 14 | } 15 | } 16 | }); 17 | return poems; 18 | } 19 | 20 | static async getAll (validator) { 21 | const condition = { 22 | delete_time: null 23 | }; 24 | validator.get('query.author') && 25 | (condition.author = validator.get('query.author')); 26 | const poems = await Poem.findAll({ 27 | where: { 28 | delete_time: null 29 | }, 30 | limit: validator.get('query.count') 31 | ? validator.get('query.count') 32 | : config.getItem('poem.limit') 33 | }); 34 | return poems; 35 | } 36 | 37 | static async getAuthors () { 38 | const authors = await sequelize.query( 39 | 'select author from poem group by author having count(author)>0' 40 | ); 41 | const res = authors[0].map(it => it.author); 42 | return res; 43 | } 44 | 45 | toJSON () { 46 | const origin = { 47 | id: this.id, 48 | title: this.title, 49 | author: this.author, 50 | dynasty: this.dynasty, 51 | content: this.content, 52 | image: this.image, 53 | create_time: this.createTime 54 | }; 55 | return origin; 56 | } 57 | } 58 | 59 | Poem.init( 60 | { 61 | id: { 62 | type: Sequelize.INTEGER, 63 | autoIncrement: true, 64 | primaryKey: true 65 | }, 66 | title: { 67 | type: Sequelize.STRING(50), 68 | allowNull: false, 69 | comment: '标题' 70 | }, 71 | author: { 72 | type: Sequelize.STRING(50), 73 | defaultValue: '未名', 74 | comment: '作者' 75 | }, 76 | dynasty: { 77 | type: Sequelize.STRING(50), 78 | defaultValue: '未知', 79 | comment: '朝代' 80 | }, 81 | content: { 82 | type: Sequelize.TEXT, 83 | allowNull: false, 84 | comment: '内容,以/来分割每一句,以|来分割宋词的上下片', 85 | get () { 86 | const raw = this.getDataValue('content'); 87 | /** 88 | * @type Array 89 | */ 90 | const lis = raw.split('|'); 91 | const res = lis.map(x => x.split('/')); 92 | return res; 93 | } 94 | }, 95 | image: { 96 | type: Sequelize.STRING(255), 97 | defaultValue: '', 98 | comment: '配图' 99 | } 100 | }, 101 | merge( 102 | { 103 | tableName: 'poem', 104 | modelName: 'poem', 105 | sequelize: sequelize 106 | }, 107 | InfoCrudMixin.options 108 | ) 109 | ); 110 | 111 | export { Poem }; 112 | -------------------------------------------------------------------------------- /app/plugin/poem/app/validator.js: -------------------------------------------------------------------------------- 1 | import { LinValidator, Rule } from 'lin-mizar'; 2 | 3 | class PoemListValidator extends LinValidator { 4 | constructor () { 5 | super(); 6 | this.count = [ 7 | new Rule('isOptional'), 8 | new Rule('isInt', '必须在1~100之间取值', { min: 1, max: 100 }) 9 | ]; 10 | this.author = new Rule('isOptional'); 11 | } 12 | } 13 | 14 | class PoemSearchValidator extends LinValidator { 15 | constructor () { 16 | super(); 17 | this.q = new Rule('isNotEmpty', '必须传入搜索关键字'); 18 | } 19 | } 20 | 21 | export { PoemListValidator, PoemSearchValidator }; 22 | -------------------------------------------------------------------------------- /app/plugin/poem/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { limit: 20 }; 4 | -------------------------------------------------------------------------------- /app/starter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | const { config } = require('lin-mizar/lin/config'); 6 | 7 | // router.post('/cms/file', async ctx => { 8 | // ctx.body = 'Hello World'; 9 | // const files = await ctx.multipart(); 10 | // console.log(files) 11 | // if (files.length < 1) { 12 | // throw new Error('未找到符合条件的文件资源'); 13 | // } 14 | // const uploader = new LocalUploader('assets'); 15 | // const arr = await uploader.upload(files); 16 | // }); 17 | 18 | /** 19 | * 初始化并获取配置 20 | */ 21 | function applyConfig () { 22 | // 获取工作目录 23 | const baseDir = path.resolve(__dirname, '../'); 24 | config.init(baseDir); 25 | const files = fs.readdirSync(path.resolve(`${baseDir}/app/config`)); 26 | 27 | // 加载 config 目录下的配置文件 28 | for (const file of files) { 29 | config.getConfigFromFile(`app/config/${file}`); 30 | } 31 | 32 | // 加载其它配置文件 33 | config.getConfigFromFile('app/extension/file/config.js'); 34 | config.getConfigFromFile('app/extension/socket/config.js'); 35 | } 36 | 37 | const run = async () => { 38 | applyConfig(); 39 | const { createApp } = require('./app'); 40 | const app = await createApp(); 41 | const port = config.getItem('port'); 42 | app.listen(port, () => { 43 | console.log(`listening at http://localhost:${port}`); 44 | }); 45 | }; 46 | 47 | // 启动应用 48 | run(); 49 | -------------------------------------------------------------------------------- /app/validator/admin.js: -------------------------------------------------------------------------------- 1 | import { Rule, LinValidator } from 'lin-mizar'; 2 | import { isOptional } from '../lib/util'; 3 | import { PaginateValidator, PositiveIdValidator } from './common'; 4 | import validator from 'validator'; 5 | 6 | class AdminUsersValidator extends PaginateValidator { 7 | constructor () { 8 | super(); 9 | this.group_id = [ 10 | new Rule('isOptional'), 11 | new Rule('isInt', '分组id必须为正整数', { min: 1 }) 12 | ]; 13 | } 14 | } 15 | 16 | class ResetPasswordValidator extends PositiveIdValidator { 17 | constructor () { 18 | super(); 19 | this.new_password = new Rule( 20 | 'matches', 21 | '密码长度必须在6~22位之间,包含字符、数字和 _ ', 22 | /^[A-Za-z0-9_*&$#@]{6,22}$/ 23 | ); 24 | this.confirm_password = new Rule('isNotEmpty', '确认密码不可为空'); 25 | } 26 | 27 | validateConfirmPassword (data) { 28 | if (!data.body.new_password || !data.body.confirm_password) { 29 | return [false, '两次输入的密码不一致,请重新输入']; 30 | } 31 | const ok = data.body.new_password === data.body.confirm_password; 32 | if (ok) { 33 | return ok; 34 | } else { 35 | return [false, '两次输入的密码不一致,请重新输入']; 36 | } 37 | } 38 | } 39 | 40 | class UpdateUserInfoValidator extends PositiveIdValidator { 41 | validateGroupIds (data) { 42 | const ids = data.body.group_ids; 43 | if (!Array.isArray(ids) || ids.length < 1) { 44 | return [false, '至少选择一个分组']; 45 | } 46 | for (let id of ids) { 47 | if (typeof id === 'number') { 48 | id = String(id); 49 | } 50 | if (!validator.isInt(id, { min: 1 })) { 51 | return [false, '每个id值必须为正整数']; 52 | } 53 | } 54 | return true; 55 | } 56 | } 57 | 58 | class UpdateGroupValidator extends PositiveIdValidator { 59 | constructor () { 60 | super(); 61 | this.name = new Rule('isNotEmpty', '请输入分组名称'); 62 | this.info = new Rule('isOptional'); 63 | } 64 | } 65 | 66 | class RemovePermissionsValidator extends LinValidator { 67 | constructor () { 68 | super(); 69 | this.group_id = new Rule('isInt', '分组id必须正整数'); 70 | } 71 | 72 | validatePermissionIds (data) { 73 | const ids = data.body.permission_ids; 74 | if (!ids) { 75 | return [false, '请输入permission_ids字段']; 76 | } 77 | if (!Array.isArray(ids)) { 78 | return [false, '每个id值必须为正整数']; 79 | } 80 | for (let id of ids) { 81 | if (typeof id === 'number') { 82 | id = String(id); 83 | } 84 | if (!validator.isInt(id, { min: 1 })) { 85 | return [false, '每个id值必须为正整数']; 86 | } 87 | } 88 | return true; 89 | } 90 | } 91 | 92 | class DispatchPermissionsValidator extends LinValidator { 93 | constructor () { 94 | super(); 95 | this.group_id = new Rule('isInt', '分组id必须正整数'); 96 | } 97 | 98 | validatePermissionIds (data) { 99 | const ids = data.body.permission_ids; 100 | if (!ids) { 101 | return [false, '请输入permission_ids字段']; 102 | } 103 | if (!Array.isArray(ids)) { 104 | return [false, '每个id值必须为正整数']; 105 | } 106 | for (let id of ids) { 107 | if (typeof id === 'number') { 108 | id = String(id); 109 | } 110 | if (!validator.isInt(id, { min: 1 })) { 111 | return [false, '每个id值必须为正整数']; 112 | } 113 | } 114 | return true; 115 | } 116 | } 117 | 118 | class NewGroupValidator extends LinValidator { 119 | constructor () { 120 | super(); 121 | this.name = new Rule('isNotEmpty', '请输入分组名称'); 122 | this.info = new Rule('isOptional'); 123 | } 124 | 125 | validatePermissionIds (data) { 126 | const ids = data.body.permission_ids; 127 | if (isOptional(ids)) { 128 | return true; 129 | } 130 | if (!Array.isArray(ids)) { 131 | return [false, '每个id值必须为正整数']; 132 | } 133 | for (let id of ids) { 134 | if (typeof id === 'number') { 135 | id = String(id); 136 | } 137 | if (!validator.isInt(id, { min: 1 })) { 138 | return [false, '每个id值必须为正整数']; 139 | } 140 | } 141 | return true; 142 | } 143 | } 144 | 145 | class DispatchPermissionValidator extends LinValidator { 146 | constructor () { 147 | super(); 148 | this.group_id = new Rule('isInt', '分组id必须正整数'); 149 | this.permission_id = new Rule('isNotEmpty', '请输入permission_id字段'); 150 | } 151 | } 152 | 153 | export { 154 | AdminUsersValidator, 155 | ResetPasswordValidator, 156 | UpdateGroupValidator, 157 | UpdateUserInfoValidator, 158 | DispatchPermissionValidator, 159 | DispatchPermissionsValidator, 160 | NewGroupValidator, 161 | RemovePermissionsValidator 162 | }; 163 | -------------------------------------------------------------------------------- /app/validator/book.js: -------------------------------------------------------------------------------- 1 | import { LinValidator, Rule } from 'lin-mizar'; 2 | 3 | class BookSearchValidator extends LinValidator { 4 | constructor () { 5 | super(); 6 | this.q = new Rule('isNotEmpty', '必须传入搜索关键字'); 7 | } 8 | } 9 | 10 | class CreateOrUpdateBookValidator extends LinValidator { 11 | constructor () { 12 | super(); 13 | this.title = new Rule('isNotEmpty', '必须传入图书名'); 14 | this.author = new Rule('isNotEmpty', '必须传入图书作者'); 15 | this.summary = new Rule('isNotEmpty', '必须传入图书综述'); 16 | this.image = new Rule('isLength', '图书插图的url长度必须在0~100之间', { 17 | min: 0, 18 | max: 100 19 | }); 20 | } 21 | } 22 | 23 | export { CreateOrUpdateBookValidator, BookSearchValidator }; 24 | -------------------------------------------------------------------------------- /app/validator/common.js: -------------------------------------------------------------------------------- 1 | import { LinValidator, Rule, config } from 'lin-mizar'; 2 | 3 | class PositiveIdValidator extends LinValidator { 4 | constructor () { 5 | super(); 6 | this.id = new Rule('isInt', 'id必须为正整数', { min: 1 }); 7 | } 8 | } 9 | 10 | class PaginateValidator extends LinValidator { 11 | constructor () { 12 | super(); 13 | this.count = [ 14 | new Rule('isOptional', '', config.getItem('countDefault')), 15 | new Rule('isInt', 'count必须为正整数', { min: 1 }) 16 | ]; 17 | this.page = [ 18 | new Rule('isOptional', '', config.getItem('pageDefault')), 19 | new Rule('isInt', 'page必须为整数,且大于等于0', { min: 0 }) 20 | ]; 21 | } 22 | } 23 | 24 | export { PaginateValidator, PositiveIdValidator }; 25 | -------------------------------------------------------------------------------- /app/validator/log.js: -------------------------------------------------------------------------------- 1 | import { Rule, checkDateFormat } from 'lin-mizar'; 2 | import { PaginateValidator } from './common'; 3 | import { isOptional } from '../lib/util'; 4 | 5 | class LogFindValidator extends PaginateValidator { 6 | constructor () { 7 | super(); 8 | this.name = new Rule('isOptional'); 9 | } 10 | 11 | validateStart (data) { 12 | const start = data.query.start; 13 | // 如果 start 为可选 14 | if (isOptional(start)) { 15 | return true; 16 | } 17 | const ok = checkDateFormat(start); 18 | if (ok) { 19 | return ok; 20 | } else { 21 | return [false, '请输入正确格式开始时间', 'start']; 22 | } 23 | } 24 | 25 | validateEnd (data) { 26 | if (!data.query) { 27 | return true; 28 | } 29 | const end = data.query.end; 30 | if (isOptional(end)) { 31 | return true; 32 | } 33 | const ok = checkDateFormat(end); 34 | if (ok) { 35 | return ok; 36 | } else { 37 | return [false, '请输入正确格式结束时间', 'end']; 38 | } 39 | } 40 | } 41 | 42 | export { LogFindValidator }; 43 | -------------------------------------------------------------------------------- /app/validator/user.js: -------------------------------------------------------------------------------- 1 | import { config, LinValidator, Rule } from 'lin-mizar'; 2 | import { isOptional } from '../lib/util'; 3 | import validator from 'validator'; 4 | 5 | class RegisterValidator extends LinValidator { 6 | constructor () { 7 | super(); 8 | this.username = [ 9 | new Rule('isNotEmpty', '用户名不可为空'), 10 | new Rule('isLength', '用户名长度必须在2~20之间', 2, 20) 11 | ]; 12 | this.email = [ 13 | new Rule('isOptional'), 14 | new Rule('isEmail', '电子邮箱不符合规范,请输入正确的邮箱') 15 | ]; 16 | this.password = [ 17 | new Rule( 18 | 'matches', 19 | '密码长度必须在6~22位之间,包含字符、数字和 _ ', 20 | /^[A-Za-z0-9_*&$#@]{6,22}$/ 21 | ) 22 | ]; 23 | this.confirm_password = new Rule('isNotEmpty', '确认密码不可为空'); 24 | } 25 | 26 | validateConfirmPassword (data) { 27 | if (!data.body.password || !data.body.confirm_password) { 28 | return [false, '两次输入的密码不一致,请重新输入']; 29 | } 30 | const ok = data.body.password === data.body.confirm_password; 31 | if (ok) { 32 | return ok; 33 | } else { 34 | return [false, '两次输入的密码不一致,请重新输入']; 35 | } 36 | } 37 | 38 | validateGroupIds (data) { 39 | const ids = data.body.group_ids; 40 | if (isOptional(ids)) { 41 | return true; 42 | } 43 | if (!Array.isArray(ids)) { 44 | return [false, '每个id值必须为正整数']; 45 | } 46 | for (let id of ids) { 47 | if (typeof id === 'number') { 48 | id = String(id); 49 | } 50 | if (!validator.isInt(id, { min: 1 })) { 51 | return [false, '每个id值必须为正整数']; 52 | } 53 | } 54 | return true; 55 | } 56 | } 57 | 58 | class LoginValidator extends LinValidator { 59 | constructor () { 60 | super(); 61 | this.username = new Rule('isNotEmpty', '用户名不可为空'); 62 | this.password = new Rule('isNotEmpty', '密码不可为空'); 63 | 64 | if (config.getItem('loginCaptchaEnabled', false)) { 65 | this.captcha = new Rule('isNotEmpty', '验证码不能为空'); 66 | } 67 | } 68 | } 69 | 70 | /** 71 | * 更新用户信息 72 | */ 73 | class UpdateInfoValidator extends LinValidator { 74 | constructor () { 75 | super(); 76 | this.email = [ 77 | new Rule('isOptional'), 78 | new Rule('isEmail', '电子邮箱不符合规范,请输入正确的邮箱') 79 | ]; 80 | this.nickname = [ 81 | new Rule('isOptional'), 82 | new Rule('isLength', '昵称长度必须在2~10之间', 2, 24) 83 | ]; 84 | this.username = [ 85 | new Rule('isOptional'), 86 | new Rule('isLength', '用户名长度必须在2~10之间', 2, 24) 87 | ]; 88 | this.avatar = [ 89 | new Rule('isOptional'), 90 | new Rule('isLength', '头像的url长度必须在0~500之间', { 91 | min: 0, 92 | max: 500 93 | }) 94 | ]; 95 | } 96 | } 97 | 98 | class ChangePasswordValidator extends LinValidator { 99 | constructor () { 100 | super(); 101 | this.new_password = new Rule( 102 | 'matches', 103 | '密码长度必须在6~22位之间,包含字符、数字和 _ ', 104 | /^[A-Za-z0-9_*&$#@]{6,22}$/ 105 | ); 106 | this.confirm_password = new Rule('isNotEmpty', '确认密码不可为空'); 107 | this.old_password = new Rule('isNotEmpty', '请输入旧密码'); 108 | } 109 | 110 | validateConfirmPassword (data) { 111 | if (!data.body.new_password || !data.body.confirm_password) { 112 | return [false, '两次输入的密码不一致,请重新输入']; 113 | } 114 | const ok = data.body.new_password === data.body.confirm_password; 115 | if (ok) { 116 | return ok; 117 | } else { 118 | return [false, '两次输入的密码不一致,请重新输入']; 119 | } 120 | } 121 | } 122 | 123 | class AvatarUpdateValidator extends LinValidator { 124 | constructor () { 125 | super(); 126 | this.avatar = new Rule('isNotEmpty', '必须传入头像的url链接'); 127 | } 128 | } 129 | 130 | export { 131 | ChangePasswordValidator, 132 | UpdateInfoValidator, 133 | LoginValidator, 134 | RegisterValidator, 135 | AvatarUpdateValidator 136 | }; 137 | -------------------------------------------------------------------------------- /code.md: -------------------------------------------------------------------------------- 1 | # code 2 | 3 | ## 核心库内置已使用状态码 4 | 5 | 0 成功 6 | 7 | 9999 服务器未知错误 8 | 9 | 10000 认证失败 10 | 11 | 10020 资源不存在 12 | 13 | 10030 参数错误 14 | 15 | 10040 令牌失效 16 | 17 | 10050 令牌过期 18 | 19 | 10060 字段重复 20 | 21 | 10070 禁止操作 22 | 23 | 10080 请求方法不允许 24 | 25 | 10100 refresh token 获取失败 26 | 27 | 10110 文件体积过大 28 | 29 | 10120 文件数量过多 30 | 31 | 10130 文件扩展名不符合规范 32 | 33 | 10140 请求过于频繁,请稍后重试 34 | 35 | ## 项目使用的状态码 36 | 37 | 具体看 `/app/config/code-message.js` 文件 38 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('@babel/register'); 2 | 3 | require('./app/starter'); 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | testEnvironment: 'node', 6 | coverageDirectory: 'coverage', 7 | testPathIgnorePatterns: ['/node_modules/'], 8 | testMatch: [ 9 | '**/?(*.)(spec).js?(x)' 10 | ], 11 | transform: { 12 | '^.+\\.[t|j]sx?$': 'babel-jest' 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lin-cms-koa", 3 | "version": "0.3.11", 4 | "description": "simple and practical CMS implememted by koa", 5 | "main": "app/starter.js", 6 | "scripts": { 7 | "test": "jest test", 8 | "start:dev": "cross-env NODE_ENV=development nodemon", 9 | "start:prod": "cross-env NODE_ENV=production node index", 10 | "prettier": "prettier --write app/**/*.js app/*.js app/**/**/*.js app/**/**/**/*.js test/**/*.js && eslint app test --fix" 11 | }, 12 | "keywords": [ 13 | "lin", 14 | "cms", 15 | "koa" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/TaleLin/lin-cms-koa.git" 20 | }, 21 | "author": "Pedro/Shirmy/Evan", 22 | "license": "MIT", 23 | "devDependencies": { 24 | "@babel/core": "^7.4.5", 25 | "@babel/plugin-proposal-decorators": "^7.4.4", 26 | "@babel/register": "^7.8.6", 27 | "@types/eslint": "^4.16.6", 28 | "@types/jest": "^24.0.11", 29 | "@types/koa": "^2.0.48", 30 | "@types/koa-bodyparser": "^4.2.2", 31 | "@types/koa__cors": "^2.2.3", 32 | "@types/prettier": "1.16.1", 33 | "@types/supertest": "^2.0.7", 34 | "babel-jest": "^26.0.1", 35 | "babel-preset-env": "^1.7.0", 36 | "cross-env": "^5.2.0", 37 | "eslint": "^5.15.1", 38 | "eslint-config-standard": "^12.0.0", 39 | "eslint-plugin-import": "^2.16.0", 40 | "eslint-plugin-jest": "^22.3.0", 41 | "eslint-plugin-node": "^8.0.1", 42 | "eslint-plugin-promise": "^4.0.1", 43 | "eslint-plugin-standard": "^4.0.0", 44 | "jest": "^24.9.0", 45 | "nodemon": "^1.18.10", 46 | "prettier": "1.16.4", 47 | "supertest": "^3.4.2" 48 | }, 49 | "dependencies": { 50 | "@koa/cors": "^2.2.3", 51 | "crypto": "^1.0.1", 52 | "koa": "^2.7.0", 53 | "koa-bodyparser": "^4.2.1", 54 | "koa-mount": "^4.0.0", 55 | "koa-static": "^5.0.0", 56 | "lin-mizar": "^0.3.9", 57 | "mysql2": "^2.1.0", 58 | "sequelize": "^5.21.13", 59 | "sharp": "^0.29.0", 60 | "svg-captcha": "^1.4.0", 61 | "validator": "^13.7.0", 62 | "ws": "^7.4.4" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /schema.sql: -------------------------------------------------------------------------------- 1 | SET NAMES utf8mb4; 2 | SET FOREIGN_KEY_CHECKS = 0; 3 | 4 | -- ---------------------------- 5 | -- 文件表 6 | -- ---------------------------- 7 | DROP TABLE IF EXISTS lin_file; 8 | CREATE TABLE lin_file 9 | ( 10 | id int(10) unsigned NOT NULL AUTO_INCREMENT, 11 | path varchar(500) NOT NULL, 12 | type varchar(10) NOT NULL DEFAULT 'LOCAL' COMMENT 'LOCAL 本地,REMOTE 远程', 13 | name varchar(100) NOT NULL, 14 | extension varchar(50) DEFAULT NULL, 15 | size int(11) DEFAULT NULL, 16 | md5 varchar(40) DEFAULT NULL COMMENT 'md5值,防止上传重复文件', 17 | create_time datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 18 | update_time datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), 19 | delete_time datetime(3) DEFAULT NULL, 20 | PRIMARY KEY (id), 21 | UNIQUE KEY md5_del (md5, delete_time) 22 | ) ENGINE = InnoDB 23 | DEFAULT CHARSET = utf8mb4 24 | COLLATE = utf8mb4_general_ci; 25 | 26 | -- ---------------------------- 27 | -- 日志表 28 | -- ---------------------------- 29 | DROP TABLE IF EXISTS lin_log; 30 | CREATE TABLE lin_log 31 | ( 32 | id int(10) unsigned NOT NULL AUTO_INCREMENT, 33 | message varchar(450) DEFAULT NULL, 34 | user_id int(10) unsigned NOT NULL, 35 | username varchar(24) DEFAULT NULL, 36 | status_code int(11) DEFAULT NULL, 37 | method varchar(20) DEFAULT NULL, 38 | path varchar(50) DEFAULT NULL, 39 | permission varchar(100) DEFAULT NULL, 40 | create_time datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 41 | update_time datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), 42 | delete_time datetime(3) DEFAULT NULL, 43 | PRIMARY KEY (id) 44 | ) ENGINE = InnoDB 45 | DEFAULT CHARSET = utf8mb4 46 | COLLATE = utf8mb4_general_ci; 47 | 48 | -- ---------------------------- 49 | -- 权限表 50 | -- ---------------------------- 51 | DROP TABLE IF EXISTS lin_permission; 52 | CREATE TABLE lin_permission 53 | ( 54 | id int(10) unsigned NOT NULL AUTO_INCREMENT, 55 | name varchar(60) NOT NULL COMMENT '权限名称,例如:访问首页', 56 | module varchar(50) NOT NULL COMMENT '权限所属模块,例如:人员管理', 57 | mount tinyint(1) NOT NULL DEFAULT 1 COMMENT '0:关闭 1:开启', 58 | create_time datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 59 | update_time datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), 60 | delete_time datetime(3) DEFAULT NULL, 61 | PRIMARY KEY (id) 62 | ) ENGINE = InnoDB 63 | DEFAULT CHARSET = utf8mb4 64 | COLLATE = utf8mb4_general_ci; 65 | 66 | -- ---------------------------- 67 | -- 分组表 68 | -- ---------------------------- 69 | DROP TABLE IF EXISTS lin_group; 70 | CREATE TABLE lin_group 71 | ( 72 | id int(10) unsigned NOT NULL AUTO_INCREMENT, 73 | name varchar(60) NOT NULL COMMENT '分组名称,例如:搬砖者', 74 | info varchar(255) DEFAULT NULL COMMENT '分组信息:例如:搬砖的人', 75 | level tinyint(2) NOT NULL DEFAULT 3 COMMENT '分组级别 1:root 2:guest 3:user(root、guest分组只能存在一个)', 76 | create_time datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 77 | update_time datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), 78 | delete_time datetime(3) DEFAULT NULL, 79 | PRIMARY KEY (id), 80 | UNIQUE KEY name_del (name, delete_time) 81 | ) ENGINE = InnoDB 82 | DEFAULT CHARSET = utf8mb4 83 | COLLATE = utf8mb4_general_ci; 84 | 85 | -- ---------------------------- 86 | -- 分组-权限表 87 | -- ---------------------------- 88 | DROP TABLE IF EXISTS lin_group_permission; 89 | CREATE TABLE lin_group_permission 90 | ( 91 | id int(10) unsigned NOT NULL AUTO_INCREMENT, 92 | group_id int(10) unsigned NOT NULL COMMENT '分组id', 93 | permission_id int(10) unsigned NOT NULL COMMENT '权限id', 94 | PRIMARY KEY (id), 95 | KEY group_id_permission_id (group_id, permission_id) USING BTREE COMMENT '联合索引' 96 | ) ENGINE = InnoDB 97 | DEFAULT CHARSET = utf8mb4 98 | COLLATE = utf8mb4_general_ci; 99 | 100 | -- ---------------------------- 101 | -- 用户基本信息表 102 | -- ---------------------------- 103 | DROP TABLE IF EXISTS lin_user; 104 | CREATE TABLE lin_user 105 | ( 106 | id int(10) unsigned NOT NULL AUTO_INCREMENT, 107 | username varchar(24) NOT NULL COMMENT '用户名,唯一', 108 | nickname varchar(24) DEFAULT NULL COMMENT '用户昵称', 109 | avatar varchar(500) DEFAULT NULL COMMENT '头像url', 110 | email varchar(100) DEFAULT NULL COMMENT '邮箱', 111 | create_time datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 112 | update_time datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), 113 | delete_time datetime(3) DEFAULT NULL, 114 | PRIMARY KEY (id), 115 | UNIQUE KEY username_del (username, delete_time), 116 | UNIQUE KEY email_del (email, delete_time) 117 | ) ENGINE = InnoDB 118 | DEFAULT CHARSET = utf8mb4 119 | COLLATE = utf8mb4_general_ci; 120 | 121 | -- ---------------------------- 122 | -- 用户授权信息表 123 | # id 124 | # user_id 125 | # identity_type 登录类型(手机号 邮箱 用户名)或第三方应用名称(微信 微博等) 126 | # identifier 标识(手机号 邮箱 用户名或第三方应用的唯一标识) 127 | # credential 密码凭证(站内的保存密码,站外的不保存或保存token) 128 | -- ---------------------------- 129 | DROP TABLE IF EXISTS lin_user_identity; 130 | CREATE TABLE lin_user_identity 131 | ( 132 | id int(10) unsigned NOT NULL AUTO_INCREMENT, 133 | user_id int(10) unsigned NOT NULL COMMENT '用户id', 134 | identity_type varchar(100) NOT NULL, 135 | identifier varchar(100), 136 | credential varchar(100), 137 | create_time datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 138 | update_time datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), 139 | delete_time datetime(3) DEFAULT NULL, 140 | PRIMARY KEY (id) 141 | ) ENGINE = InnoDB 142 | DEFAULT CHARSET = utf8mb4 143 | COLLATE = utf8mb4_general_ci; 144 | 145 | DROP TABLE IF EXISTS book; 146 | CREATE TABLE book 147 | ( 148 | id int(11) NOT NULL AUTO_INCREMENT, 149 | title varchar(50) NOT NULL, 150 | author varchar(30) DEFAULT NULL, 151 | summary varchar(1000) DEFAULT NULL, 152 | image varchar(100) DEFAULT NULL, 153 | create_time datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 154 | update_time datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), 155 | delete_time datetime(3) DEFAULT NULL, 156 | PRIMARY KEY (id) 157 | ) ENGINE = InnoDB 158 | DEFAULT CHARSET = utf8mb4 159 | COLLATE = utf8mb4_general_ci; 160 | 161 | 162 | -- ---------------------------- 163 | -- 用户-分组表 164 | -- ---------------------------- 165 | DROP TABLE IF EXISTS lin_user_group; 166 | CREATE TABLE lin_user_group 167 | ( 168 | id int(10) unsigned NOT NULL AUTO_INCREMENT, 169 | user_id int(10) unsigned NOT NULL COMMENT '用户id', 170 | group_id int(10) unsigned NOT NULL COMMENT '分组id', 171 | PRIMARY KEY (id), 172 | KEY user_id_group_id (user_id, group_id) USING BTREE COMMENT '联合索引' 173 | ) ENGINE = InnoDB 174 | DEFAULT CHARSET = utf8mb4 175 | COLLATE = utf8mb4_general_ci; 176 | 177 | SET FOREIGN_KEY_CHECKS = 1; 178 | 179 | -- ---------------------------- 180 | -- 插入超级管理员 181 | -- 插入root分组 182 | -- ---------------------------- 183 | BEGIN; 184 | INSERT INTO lin_user(id, username, nickname) 185 | VALUES (1, 'root', 'root'); 186 | 187 | INSERT INTO lin_user_identity (id, user_id, identity_type, identifier, credential) 188 | 189 | VALUES (1, 1, 'USERNAME_PASSWORD', 'root', 190 | 'sha1$c419e500$1$84869e5560ebf3de26b6690386484929456d6c07'); 191 | 192 | INSERT INTO lin_group(id, name, info, level) 193 | VALUES (1, 'root', '超级用户组', 1); 194 | 195 | INSERT INTO lin_group(id, name, info, level) 196 | VALUES (2, 'guest', '游客组', 2); 197 | 198 | INSERT INTO lin_user_group(id, user_id, group_id) 199 | VALUES (1, 1, 1); 200 | 201 | COMMIT; 202 | -------------------------------------------------------------------------------- /test/api/cms/admin.test.js: -------------------------------------------------------------------------------- 1 | import '../../helper/initial'; 2 | import request from 'supertest'; 3 | import { generate } from 'lin-mizar'; 4 | import { createApp } from '../../../app/app'; 5 | import { IdentityType } from '../../../app/lib/type'; 6 | import sequelize from '../../../app/lib/db'; 7 | import { saveTokens, getToken } from '../../helper/token'; 8 | import { get, isNumber, isArray } from 'lodash'; 9 | 10 | describe('/cms/admin', () => { 11 | const { UserModel, UserIdentityModel } = require('../../../app/model/user'); 12 | const { GroupModel } = require('../../../app/model/group'); 13 | const { 14 | GroupPermissionModel 15 | } = require('../../../app/model/group-permission'); 16 | const { UserGroupModel } = require('../../../app/model/user-group'); 17 | const { PermissionModel } = require('../../../app/model/permission'); 18 | 19 | let app; 20 | 21 | let token; 22 | 23 | beforeAll(async done => { 24 | console.log('start admin'); 25 | // 初始化 app 26 | app = await createApp(); 27 | done(); 28 | }); 29 | 30 | afterAll(async done => { 31 | setTimeout(async () => { 32 | await sequelize.close(); 33 | done(); 34 | }, 500); 35 | }); 36 | 37 | beforeEach(async done => { 38 | await sequelize.sync({ force: true }); 39 | await UserModel.create({ username: 'root', nickname: 'root' }); 40 | await UserIdentityModel.create({ 41 | user_id: 1, 42 | identity_type: IdentityType.Password, 43 | identifier: 'root', 44 | credential: 'sha1$c419e500$1$84869e5560ebf3de26b6690386484929456d6c07' 45 | }); 46 | await GroupModel.create({ name: 'root', info: '超级用户组', level: 1 }); 47 | await GroupModel.create({ name: 'guest', info: '游客组', level: 2 }); 48 | await UserGroupModel.create({ user_id: 1, group_id: 1 }); 49 | done(); 50 | }); 51 | 52 | it('超级管理员登录', async () => { 53 | const response = await request(app.callback()) 54 | .post('/cms/user/login') 55 | .send({ 56 | username: 'root', 57 | password: '123456' 58 | }); 59 | saveTokens(response.body); 60 | token = getToken(); 61 | expect(response.status).toBe(200); 62 | expect(response.type).toMatch(/json/); 63 | }); 64 | 65 | it('查询所有可分配的权限', async () => { 66 | await PermissionModel.create({ name: '查看信息', module: '信息' }); 67 | 68 | const response = await request(app.callback()) 69 | .get('/cms/admin/permission') 70 | .auth(token, { 71 | type: 'bearer' 72 | }); 73 | expect(response.status).toBe(200); 74 | expect(response.type).toMatch(/json/); 75 | const is = isArray(get(response, 'body.信息')); 76 | expect(is).toBeTruthy(); 77 | }); 78 | 79 | it('查询所有用户', async () => { 80 | const response = await request(app.callback()) 81 | .get('/cms/admin/users') 82 | .auth(token, { 83 | type: 'bearer' 84 | }); 85 | expect(response.status).toBe(200); 86 | expect(response.type).toMatch(/json/); 87 | expect(get(response, 'body.count')).toBe(10); 88 | const is = isNumber(get(response, 'body.total')); 89 | expect(is).toBeTruthy(); 90 | }); 91 | 92 | it('插入用户信息、分组、权限,查询所有用户', async () => { 93 | const user = await UserModel.create({ 94 | username: 'shirmy', 95 | email: 'shirmy@gmail.com' 96 | }); 97 | const group = await GroupModel.create({ name: '研发组', info: '研发大佬' }); 98 | await UserGroupModel.create({ group_id: group.id, user_id: user.id }); 99 | 100 | const permission = await PermissionModel.create({ 101 | name: '查看信息', 102 | module: '信息' 103 | }); 104 | await GroupPermissionModel.create({ 105 | group_id: group.id, 106 | permission_id: permission.id 107 | }); 108 | 109 | const response = await request(app.callback()) 110 | .get('/cms/admin/users') 111 | .auth(token, { 112 | type: 'bearer' 113 | }) 114 | .send({ 115 | group_id: group.id 116 | }); 117 | expect(response.status).toBe(200); 118 | expect(response.type).toMatch(/json/); 119 | expect(get(response, 'body.count')).toBe(10); 120 | const is = isNumber(get(response, 'body.total')); 121 | expect(is).toBeTruthy(); 122 | }); 123 | 124 | it('修改用户密码', async () => { 125 | const user = await UserModel.create({ 126 | username: 'shirmy', 127 | email: 'shirmy@gmail.com' 128 | }); 129 | const group = await GroupModel.create({ name: '研发组', info: '研发大佬' }); 130 | await UserGroupModel.create({ group_id: group.id, user_id: user.id }); 131 | await UserIdentityModel.create({ 132 | user_id: user.id, 133 | identity_type: IdentityType.Password, 134 | identifier: user.username, 135 | credential: generate('123456') 136 | }); 137 | 138 | const newPassword = '654321'; 139 | 140 | const response = await request(app.callback()) 141 | .put(`/cms/admin/user/${user.id}/password`) 142 | .auth(token, { 143 | type: 'bearer' 144 | }) 145 | .send({ 146 | new_password: newPassword, 147 | confirm_password: newPassword 148 | }); 149 | expect(response.status).toBe(201); 150 | expect(response.type).toMatch(/json/); 151 | expect(get(response, 'body.code')).toBe(4); 152 | expect(get(response, 'body.message')).toBe('密码修改成功'); 153 | }); 154 | 155 | it('删除用户', async () => { 156 | const user = await UserModel.create({ 157 | username: 'shirmy', 158 | email: 'shirmy@gmail.com' 159 | }); 160 | const group = await GroupModel.create({ name: '研发组', info: '研发大佬' }); 161 | await UserGroupModel.create({ group_id: group.id, user_id: user.id }); 162 | 163 | const response = await request(app.callback()) 164 | .delete(`/cms/admin/user/${user.id}`) 165 | .auth(token, { 166 | type: 'bearer' 167 | }); 168 | expect(response.status).toBe(201); 169 | expect(response.type).toMatch(/json/); 170 | expect(get(response, 'body.code')).toBe(5); 171 | expect(get(response, 'body.message')).toBe('删除用户成功'); 172 | }); 173 | 174 | it('更新用户', async () => { 175 | const user = await UserModel.create({ 176 | username: 'shirmy', 177 | email: 'shirmy@gmail.com' 178 | }); 179 | const group = await GroupModel.create({ name: '研发组', info: '研发大佬' }); 180 | await UserGroupModel.create({ group_id: group.id, user_id: user.id }); 181 | 182 | const response = await request(app.callback()) 183 | .put(`/cms/admin/user/${user.id}`) 184 | .auth(token, { type: 'bearer' }) 185 | .send({ 186 | group_ids: [group.id] 187 | }); 188 | expect(response.status).toBe(201); 189 | expect(response.type).toMatch(/json/); 190 | expect(get(response, 'body.code')).toBe(6); 191 | expect(get(response, 'body.message')).toBe('更新用户成功'); 192 | }); 193 | 194 | it('查询所有权限组及其权限', async () => { 195 | const group = await GroupModel.create({ name: '研发组', info: '研发大佬' }); 196 | const permission = await PermissionModel.create({ 197 | name: '查看信息', 198 | module: '信息' 199 | }); 200 | await GroupPermissionModel.create({ 201 | group_id: group.id, 202 | permission_id: permission.id 203 | }); 204 | 205 | const response = await request(app.callback()) 206 | .get('/cms/admin/group') 207 | .auth(token, { type: 'bearer' }); 208 | 209 | expect(response.status).toBe(200); 210 | expect(response.type).toMatch(/json/); 211 | expect(get(response, 'body.count')).toBe(10); 212 | const is = isNumber(get(response, 'body.total')); 213 | expect(is).toBeTruthy(); 214 | }); 215 | 216 | it('查询所有权限组', async () => { 217 | const group = await GroupModel.create({ name: '研发组', info: '研发大佬' }); 218 | const permission = await PermissionModel.create({ 219 | name: '查看信息', 220 | module: '信息' 221 | }); 222 | await GroupPermissionModel.create({ 223 | group_id: group.id, 224 | permission_id: permission.id 225 | }); 226 | 227 | const response = await request(app.callback()) 228 | .get('/cms/admin/group/all') 229 | .auth(token, { type: 'bearer' }); 230 | 231 | expect(response.status).toBe(200); 232 | expect(response.type).toMatch(/json/); 233 | const is = isArray(get(response, 'body')); 234 | expect(is).toBeTruthy(); 235 | }); 236 | 237 | it('查询一个权限组及其权限', async () => { 238 | const group = await GroupModel.create({ name: '研发组', info: '研发大佬' }); 239 | const permission = await PermissionModel.create({ 240 | name: '查看信息', 241 | module: '信息' 242 | }); 243 | await GroupPermissionModel.create({ 244 | group_id: group.id, 245 | permission_id: permission.id 246 | }); 247 | 248 | const response = await request(app.callback()) 249 | .get(`/cms/admin/group/${group.id}`) 250 | .auth(token, { type: 'bearer' }); 251 | 252 | expect(response.status).toBe(200); 253 | expect(response.type).toMatch(/json/); 254 | expect(get(response, 'body.name')).toBe(group.name); 255 | const hasPermission = !!get(response, 'body.permissions').find( 256 | v => v.id === permission.id 257 | ); 258 | expect(hasPermission).toBeTruthy(); 259 | }); 260 | 261 | it('新建权限组', async () => { 262 | const permission = await PermissionModel.create({ 263 | name: '查看信息', 264 | module: '信息' 265 | }); 266 | 267 | const response = await request(app.callback()) 268 | .post('/cms/admin/group') 269 | .auth(token, { type: 'bearer' }) 270 | .send({ 271 | name: 'new group name', 272 | info: 'new group info', 273 | permission_ids: [permission.id] 274 | }); 275 | expect(response.status).toBe(201); 276 | expect(response.type).toMatch(/json/); 277 | expect(get(response, 'body.code')).toBe(15); 278 | expect(get(response, 'body.message')).toBe('新建分组成功'); 279 | }); 280 | 281 | it('更新一个权限组', async () => { 282 | const group = await GroupModel.create({ name: '研发组', info: '研发大佬' }); 283 | const permission = await PermissionModel.create({ 284 | name: '查看信息', 285 | module: '信息' 286 | }); 287 | await GroupPermissionModel.create({ 288 | group_id: group.id, 289 | permission_id: permission.id 290 | }); 291 | 292 | const response = await request(app.callback()) 293 | .put(`/cms/admin/group/${group.id}`) 294 | .auth(token, { type: 'bearer' }) 295 | .send({ 296 | name: 'new group name', 297 | info: 'new group info' 298 | }); 299 | expect(response.status).toBe(201); 300 | expect(response.type).toMatch(/json/); 301 | expect(get(response, 'body.code')).toBe(7); 302 | expect(get(response, 'body.message')).toBe('更新分组成功'); 303 | 304 | const newGroup = await GroupModel.findByPk(group.id); 305 | expect(newGroup.name).toBe('new group name'); 306 | }); 307 | 308 | it('删除一个权限组', async () => { 309 | const group = await GroupModel.create({ name: '研发组', info: '研发大佬' }); 310 | 311 | const response = await request(app.callback()) 312 | .delete(`/cms/admin/group/${group.id}`) 313 | .auth(token, { type: 'bearer' }); 314 | expect(response.status).toBe(201); 315 | expect(response.type).toMatch(/json/); 316 | expect(get(response, 'body.code')).toBe(8); 317 | expect(get(response, 'body.message')).toBe('删除分组成功'); 318 | }); 319 | 320 | it('分配单个权限', async () => { 321 | const group = await GroupModel.create({ name: '研发组', info: '研发大佬' }); 322 | const permission = await PermissionModel.create({ 323 | name: '查看信息', 324 | module: '信息' 325 | }); 326 | 327 | const response = await request(app.callback()) 328 | .post('/cms/admin/permission/dispatch') 329 | .auth(token, { type: 'bearer' }) 330 | .send({ 331 | group_id: group.id, 332 | permission_id: permission.id 333 | }); 334 | 335 | expect(response.status).toBe(201); 336 | expect(response.type).toMatch(/json/); 337 | expect(get(response, 'body.code')).toBe(9); 338 | expect(get(response, 'body.message')).toBe('添加权限成功'); 339 | }); 340 | 341 | it('分配多个权限', async () => { 342 | const group = await GroupModel.create({ name: '研发组', info: '研发大佬' }); 343 | const permission = await PermissionModel.create({ 344 | name: '查看信息', 345 | module: '信息' 346 | }); 347 | const permission1 = await PermissionModel.create({ 348 | name: '查看研发组的信息', 349 | module: '信息' 350 | }); 351 | 352 | const response = await request(app.callback()) 353 | .post('/cms/admin/permission/dispatch/batch') 354 | .auth(token, { type: 'bearer' }) 355 | .send({ 356 | group_id: group.id, 357 | permission_ids: [permission.id, permission1.id] 358 | }); 359 | 360 | expect(response.status).toBe(201); 361 | expect(response.type).toMatch(/json/); 362 | expect(get(response, 'body.code')).toBe(9); 363 | expect(get(response, 'body.message')).toBe('添加权限成功'); 364 | }); 365 | 366 | it('删除多个权限', async () => { 367 | const group = await GroupModel.create({ name: '研发组', info: '研发大佬' }); 368 | const permission = await PermissionModel.create({ 369 | name: '查看信息', 370 | module: '信息' 371 | }); 372 | const permission1 = await PermissionModel.create({ 373 | name: '查看研发组的信息', 374 | module: '信息' 375 | }); 376 | await GroupPermissionModel.create({ 377 | group_id: group.id, 378 | permission_id: permission.id 379 | }); 380 | await GroupPermissionModel.create({ 381 | group_id: group.id, 382 | permission_id: permission1.id 383 | }); 384 | 385 | const response = await request(app.callback()) 386 | .post('/cms/admin/permission/remove') 387 | .auth(token, { type: 'bearer' }) 388 | .send({ 389 | group_id: group.id, 390 | permission_ids: [permission.id, permission1.id] 391 | }); 392 | 393 | expect(response.status).toBe(201); 394 | expect(response.type).toMatch(/json/); 395 | expect(get(response, 'body.code')).toBe(10); 396 | expect(get(response, 'body.message')).toBe('删除权限成功'); 397 | }); 398 | }); 399 | -------------------------------------------------------------------------------- /test/api/cms/test1.test.js: -------------------------------------------------------------------------------- 1 | import '../../helper/initial'; 2 | import request from 'supertest'; 3 | import { createApp } from '../../../app/app'; 4 | import sequelize from '../../../app/lib/db'; 5 | 6 | describe('test1.test.js', () => { 7 | // 必须,app示例 8 | let app; 9 | 10 | beforeAll(async () => { 11 | // 初始化app示例 12 | app = await createApp(); 13 | }); 14 | 15 | afterAll(() => { 16 | // 最后关闭数据库 17 | setTimeout(() => { 18 | sequelize.close(); 19 | }, 500); 20 | }); 21 | 22 | // 测试 api 的函数 23 | // 测试 api的 URL 为 /cms/test/ 24 | test('测试/cms/test/', async () => { 25 | const response = await request(app.callback()).get('/cms/test/'); 26 | expect(response.status).toBe(200); 27 | expect(response.type).toMatch(/html/); 28 | }); 29 | 30 | // 这个测试不会通过,缺少认证,可以参考 user2.test.js 添加 bearer 31 | test('测试/cms/user/register 输入不规范用户名', async () => { 32 | const response = await request(app.callback()) 33 | .post('/cms/user/register') 34 | .send({ 35 | username: 'p', 36 | password: '123456', 37 | confirm_password: '123456' 38 | }); 39 | expect(response.status).toBe(400); 40 | expect(response.body).toHaveProperty('code', 10030); 41 | expect(response.type).toMatch(/json/); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/api/cms/user1.test.js: -------------------------------------------------------------------------------- 1 | import '../../helper/initial'; 2 | import request from 'supertest'; 3 | import { createApp } from '../../../app/app'; 4 | import sequelize from '../../../app/lib/db'; 5 | import { saveTokens } from '../../helper/token'; 6 | 7 | describe('user1.test.js', () => { 8 | let app; 9 | 10 | beforeAll(async () => { 11 | app = await createApp(); 12 | }); 13 | 14 | afterAll(() => { 15 | setTimeout(() => { 16 | sequelize.close(); 17 | }, 500); 18 | }); 19 | 20 | test('测试/cms/user/login 登陆,用户名不存在', async () => { 21 | const response = await request(app.callback()) 22 | .post('/cms/user/login') 23 | .send({ 24 | username: 'llllll', 25 | password: '123456' 26 | }); 27 | expect(response.status).toBe(404); 28 | expect(response.type).toMatch(/json/); 29 | }); 30 | 31 | test('测试/cms/user/login 登陆,密码错误', async () => { 32 | const response = await request(app.callback()) 33 | .post('/cms/user/login') 34 | .send({ 35 | username: 'root', 36 | password: '147258' 37 | }); 38 | expect(response.status).toBe(401); 39 | expect(response.body).toHaveProperty('code', 10031); 40 | expect(response.type).toMatch(/json/); 41 | }); 42 | 43 | test('测试/cms/user/login 登陆成功', async () => { 44 | const response = await request(app.callback()) 45 | .post('/cms/user/login') 46 | .send({ 47 | username: 'root', 48 | password: '123456' 49 | }); 50 | saveTokens(response.body); 51 | expect(response.status).toBe(200); 52 | expect(response.type).toMatch(/json/); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/api/cms/user2.test.js: -------------------------------------------------------------------------------- 1 | import '../../helper/initial'; 2 | import request from 'supertest'; 3 | import { createApp } from '../../../app/app'; 4 | import sequelize from '../../../app/lib/db'; 5 | import { getToken } from '../../helper/token'; 6 | 7 | describe('user2.test.js', () => { 8 | let app; 9 | 10 | beforeAll(async () => { 11 | app = await createApp(); 12 | }); 13 | 14 | afterAll(() => { 15 | setTimeout(() => { 16 | sequelize.close(); 17 | }, 500); 18 | }); 19 | 20 | test('测试/cms/user/register 不输入邮箱,重复密码错误', async () => { 21 | const token = getToken(); 22 | const response = await request(app.callback()) 23 | .post('/cms/user/register') 24 | .auth(token, { 25 | type: 'bearer' 26 | }) 27 | .send({ 28 | username: 'pedro', 29 | password: '123456', 30 | confirm_password: '123455' 31 | }); 32 | expect(response.status).toBe(400); 33 | expect(response.body).toHaveProperty('code', 10030); 34 | expect(response.type).toMatch(/json/); 35 | }); 36 | 37 | test('测试/cms/user/register 输入不正确邮箱', async () => { 38 | const token = getToken(); 39 | const response = await request(app.callback()) 40 | .post('/cms/user/register') 41 | .auth(token, { 42 | type: 'bearer' 43 | }) 44 | .send({ 45 | username: 'pedro', 46 | email: '8680909709j', 47 | password: '123456', 48 | confirm_password: '123456' 49 | }); 50 | expect(response.status).toBe(400); 51 | expect(response.body).toHaveProperty('code', 10030); 52 | expect(response.type).toMatch(/json/); 53 | }); 54 | 55 | test('测试/cms/user/register 输入不规范用户名', async () => { 56 | const token = getToken(); 57 | const response = await request(app.callback()) 58 | .post('/cms/user/register') 59 | .auth(token, { 60 | type: 'bearer' 61 | }) 62 | .send({ 63 | username: 'p', 64 | password: '123456', 65 | confirm_password: '123456' 66 | }); 67 | expect(response.status).toBe(400); 68 | expect(response.body).toHaveProperty('code', 10030); 69 | expect(response.type).toMatch(/json/); 70 | }); 71 | 72 | test('测试/cms/user/register 输入不规范分组id', async () => { 73 | const token = getToken(); 74 | const response = await request(app.callback()) 75 | .post('/cms/user/register') 76 | .auth(token, { 77 | type: 'bearer' 78 | }) 79 | .send({ 80 | username: 'pedro', 81 | group_ids: 0, 82 | password: '123456', 83 | confirm_password: '123456' 84 | }); 85 | expect(response.status).toBe(400); 86 | expect(response.body).toHaveProperty('code', 10030); 87 | expect(response.type).toMatch(/json/); 88 | }); 89 | 90 | // test('测试/cms/user/register 正常注册', async () => { 91 | // const token = getToken() 92 | // const response = await request(app.callback()) 93 | // .post('/cms/user/register') 94 | // .auth(token, { 95 | // type: 'bearer' 96 | // }) 97 | // .send({ 98 | // username: 'peter', 99 | // email: '123456@gmail.com', 100 | // group_ids: [], 101 | // password: '123456', 102 | // confirm_password: '123456', 103 | // }); 104 | // expect(response.status).toBe(201); 105 | // expect(response.type).toMatch(/json/); 106 | // }); 107 | 108 | test('测试/cms/user/register 用户名重复错误', async () => { 109 | const token = getToken(); 110 | const response = await request(app.callback()) 111 | .post('/cms/user/register') 112 | .auth(token, { 113 | type: 'bearer' 114 | }) 115 | .send({ 116 | username: 'peter', 117 | email: '654321@gmail.com', 118 | password: '123456', 119 | confirm_password: '123456' 120 | }); 121 | expect(response.status).toBe(400); 122 | expect(response.body).toHaveProperty('code', 10071); 123 | expect(response.type).toMatch(/json/); 124 | }); 125 | 126 | test('测试/cms/user/register 邮箱重复错误', async () => { 127 | const token = getToken(); 128 | const response = await request(app.callback()) 129 | .post('/cms/user/register') 130 | .auth(token, { 131 | type: 'bearer' 132 | }) 133 | .send({ 134 | username: 'ooooo', 135 | email: '123456@gmail.com', 136 | password: '123456', 137 | confirm_password: '123456' 138 | }); 139 | expect(response.status).toBe(400); 140 | expect(response.body).toHaveProperty('code', 10076); 141 | expect(response.type).toMatch(/json/); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /test/helper/fake-book/fake-book.js: -------------------------------------------------------------------------------- 1 | import '../initial'; 2 | import sequelize from '../../../app/lib/db'; 3 | import { Book } from '../../../app/model/book'; 4 | 5 | const run = async () => { 6 | await Book.bulkCreate([ 7 | { 8 | title: '深入理解计算机系统', 9 | author: 'Randal E.Bryant', 10 | summary: 11 | '从程序员的视角,看计算机系统!\n本书适用于那些想要写出更快、更可靠程序的程序员。通过掌握程序是如何映射到系统上,以及程序是如何执行的,读者能够更好的理解程序的行为为什么是这样的,以及效率低下是如何造成的。', 12 | image: 'https://img3.doubanio.com/lpic/s1470003.jpg' 13 | }, 14 | { 15 | title: 'C程序设计语言', 16 | author: '(美)Brian W. Kernighan', 17 | summary: 18 | '在计算机发展的历史上,没有哪一种程序设计语言像C语言这样应用广泛。本书原著即为C语言的设计者之一Dennis M.Ritchie和著名计算机科学家Brian W.Kernighan合著的一本介绍C语言的权威经典著作。', 19 | image: 'https://img3.doubanio.com/lpic/s1106934.jpg' 20 | } 21 | ]); 22 | setTimeout(() => { 23 | sequelize.close(); 24 | }, 500); 25 | }; 26 | 27 | run(); 28 | -------------------------------------------------------------------------------- /test/helper/fake-book/index.js: -------------------------------------------------------------------------------- 1 | require('@babel/register'); 2 | 3 | require('./fake-book'); 4 | -------------------------------------------------------------------------------- /test/helper/fake/fake.js: -------------------------------------------------------------------------------- 1 | import '../initial'; 2 | import sequelize from '../../../app/lib/db'; 3 | import { MountType, IdentityType } from '../../../app/lib/type'; 4 | import { generate } from 'lin-mizar'; 5 | import { UserModel, UserIdentityModel } from '../../../app/model/user'; 6 | import { GroupModel } from '../../../app/model/group'; 7 | import { PermissionModel } from '../../../app/model/permission'; 8 | import { GroupPermissionModel } from '../../../app/model/group-permission'; 9 | 10 | /** 11 | * 如果创建失败,请确保你的数据库中没有同名的分组和同名的用户 12 | */ 13 | const run = async () => { 14 | // 创建权限组 15 | const group = new GroupModel(); 16 | group.name = '普通分组'; 17 | group.info = '就是一个分组而已'; 18 | await group.save(); 19 | 20 | // 创建用户 21 | const user = new UserModel(); 22 | user.username = 'pedro'; 23 | await user.save(); 24 | 25 | // 创建用户密码 26 | await UserIdentityModel.create({ 27 | user_id: user.id, 28 | identity_type: IdentityType.Password, 29 | identifier: user.username, 30 | credential: generate('123456') 31 | }); 32 | 33 | // 在运行 app 的时候会获取路由中定义好的权限并插入,这里需要找到 id 来关联权限组 34 | const permission = await PermissionModel.findOne({ 35 | where: { 36 | name: '删除图书', 37 | module: '图书', 38 | mount: MountType.Mount 39 | } 40 | }); 41 | 42 | // 关联 permission 权限和 group 权限组 43 | await GroupPermissionModel.create({ 44 | group_id: group.id, 45 | permission_id: permission.id 46 | }); 47 | 48 | setTimeout(() => { 49 | sequelize.close(); 50 | }, 500); 51 | }; 52 | 53 | run(); 54 | -------------------------------------------------------------------------------- /test/helper/fake/index.js: -------------------------------------------------------------------------------- 1 | require('@babel/register'); 2 | 3 | require('./fake'); 4 | -------------------------------------------------------------------------------- /test/helper/initial.js: -------------------------------------------------------------------------------- 1 | const { config } = require('lin-mizar/lin/config'); 2 | 3 | // 初始化数据库配置 4 | (() => { 5 | const settings = require('../../app/config/setting'); 6 | const secure = require('./secure'); 7 | const codeMessage = require('../../app/config/code-message'); 8 | config.getConfigFromObj({ 9 | ...settings, 10 | ...secure, 11 | ...codeMessage 12 | }); 13 | })(); 14 | -------------------------------------------------------------------------------- /test/helper/poem/index.js: -------------------------------------------------------------------------------- 1 | require('@babel/register'); 2 | 3 | require('./poem'); 4 | -------------------------------------------------------------------------------- /test/helper/poem/poem.js: -------------------------------------------------------------------------------- 1 | import '../initial'; 2 | import sequelize from '../../../app/lib/db'; 3 | import { Poem } from '../../../app/plugin/poem/app/model'; 4 | 5 | const run = async () => { 6 | await Poem.sync(); 7 | await Poem.bulkCreate([ 8 | { 9 | title: '生查子·元夕', 10 | author: '欧阳修', 11 | content: 12 | '去年元夜时/花市灯如昼/月上柳梢头/人约黄昏后|今年元夜时/月与灯依旧/不见去年人/泪湿春衫袖', 13 | dynasty: '宋代', 14 | image: 'http://yanlan.oss-cn-shenzhen.aliyuncs.com/gqmgbmu06yO2zHD.png' 15 | }, 16 | { 17 | title: '临江仙·送钱穆父', 18 | author: '苏轼', 19 | content: 20 | '一别都门三改火/天涯踏尽红尘/依然一笑作春温/无波真古井/有节是秋筠|惆怅孤帆连夜发/送行淡月微云/尊前不用翠眉颦/人生如逆旅/我亦是行人', 21 | dynasty: '宋代', 22 | image: 'http://yanlan.oss-cn-shenzhen.aliyuncs.com/gqmgbmu06yO2zHD.png' 23 | }, 24 | { 25 | title: '春望词四首', 26 | author: '薛涛', 27 | content: 28 | '花开不同赏/花落不同悲/欲问相思处/花开花落时/揽草结同心/将以遗知音/春愁正断绝/春鸟复哀吟/风花日将老/佳期犹渺渺/不结同心人/空结同心草/那堪花满枝/翻作两相思/玉箸垂朝镜/春风知不知', 29 | dynasty: '唐代', 30 | image: 'http://yanlan.oss-cn-shenzhen.aliyuncs.com/gqmgbmu06yO2zHD.png' 31 | }, 32 | { 33 | title: '长相思', 34 | author: '纳兰性德', 35 | content: 36 | '山一程/水一程/身向榆关那畔行/夜深千帐灯|风一更/雪一更/聒碎乡心梦不成/故园无此声', 37 | dynasty: '清代', 38 | image: 'http://yanlan.oss-cn-shenzhen.aliyuncs.com/gqmgbmu06yO2zHD.png' 39 | }, 40 | { 41 | title: '离思五首·其四', 42 | author: '元稹', 43 | content: '曾经沧海难为水/除却巫山不是云/取次花丛懒回顾/半缘修道半缘君', 44 | dynasty: '唐代', 45 | image: 'http://yanlan.oss-cn-shenzhen.aliyuncs.com/gqmgbmu06yO2zHD.png' 46 | }, 47 | { 48 | title: '浣溪沙', 49 | author: '晏殊', 50 | content: 51 | '一曲新词酒一杯/去年天气旧亭台/夕阳西下几时回|无可奈何花落去/似曾相识燕归来/小园香径独徘徊', 52 | dynasty: '宋代', 53 | image: 'http://yanlan.oss-cn-shenzhen.aliyuncs.com/gqmgbmu06yO2zHD.png' 54 | }, 55 | { 56 | title: '浣溪沙', 57 | author: '纳兰性德', 58 | content: 59 | '残雪凝辉冷画屏/落梅横笛已三更/更无人处月胧明|我是人间惆怅客/知君何事泪纵横/断肠声里忆平生', 60 | dynasty: '清代', 61 | image: 'http://yanlan.oss-cn-shenzhen.aliyuncs.com/gqmgbmu06yO2zHD.png' 62 | } 63 | ]); 64 | setTimeout(() => { 65 | sequelize.close(); 66 | }, 500); 67 | }; 68 | 69 | run(); 70 | -------------------------------------------------------------------------------- /test/helper/secure.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | db: { 5 | database: 'lin-cms-test', 6 | host: 'localhost', 7 | dialect: 'mysql', 8 | port: 3306, 9 | username: 'root', 10 | password: '123456', 11 | logging: false, 12 | timezone: '+08:00' 13 | }, 14 | secret: 15 | '\x88W\xf09\x91\x07\x98\x89\x87\x96\xa0A\xc68\xf9\xecJJU\x17\xc5V\xbe\x8b\xef\xd7\xd8\xd3\xe6\x95*4' 16 | }; 17 | -------------------------------------------------------------------------------- /test/helper/token.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | exports.saveTokens = function saveTokens (data) { 4 | fs.writeFileSync('../../tokens.json', JSON.stringify(data)); 5 | }; 6 | 7 | exports.getToken = function getToken (type = 'access_token') { 8 | const buf = fs.readFileSync('../../tokens.json'); 9 | return JSON.parse(buf.toString())[type]; 10 | }; 11 | --------------------------------------------------------------------------------