├── .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 |
13 |
14 |
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 = ` `;
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 = ` `;
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 |
--------------------------------------------------------------------------------