├── .gitignore ├── .travis.yml ├── README.md ├── app.js ├── app ├── api │ ├── blog │ │ ├── article.js │ │ ├── author.js │ │ ├── blog.js │ │ ├── category.js │ │ ├── message.js │ │ └── tag.js │ └── v1 │ │ ├── article.js │ │ ├── author.js │ │ ├── blog.js │ │ ├── category.js │ │ ├── file.js │ │ ├── message.js │ │ └── tag.js ├── dao │ ├── article.js │ ├── articleAuthor.js │ ├── articleTag.js │ ├── author.js │ ├── blog.js │ ├── category.js │ ├── comment.js │ ├── message.js │ └── tag.js ├── lib │ ├── enums.js │ ├── helper.js │ ├── upload.js │ └── util.js ├── models │ ├── article.js │ ├── articleAuthor.js │ ├── articleTag.js │ ├── author.js │ ├── category.js │ ├── comment.js │ ├── friend.js │ ├── index.js │ ├── message.js │ └── tag.js └── validators │ ├── article.js │ ├── author.js │ ├── blog.js │ ├── category.js │ ├── common.js │ ├── message.js │ └── tag.js ├── config └── config.js.sample ├── core ├── db.js ├── http-exception.js ├── init.js ├── lin-validator.js ├── multipart.js └── util.js ├── id_rsa.enc ├── middleware ├── auth.js └── exception.js ├── package-lock.json ├── package.json └── script └── deploy.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | 4 | # local env files 5 | .env.local 6 | .env.*.local 7 | 8 | # Log files 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Editor directories and files 14 | .idea 15 | .vscode 16 | *.suo 17 | *.ntvs* 18 | *.njsproj 19 | *.sln 20 | *.sw? 21 | 22 | # others 23 | config/config.js 24 | package-lock.json 25 | yarn.lock 26 | config.js 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - node_modules 5 | node_js: stable 6 | branches: 7 | only: 8 | - master 9 | install: 10 | - npm install 11 | before_install: 12 | - openssl aes-256-cbc -K $encrypted_72aaa3a73d74_key -iv $encrypted_72aaa3a73d74_iv -in id_rsa.enc -out ~/.ssh/id_rsa -d 13 | - chmod 600 ~/.ssh/id_rsa 14 | after_success: 15 | - ssh "$DEPLOY_USER"@"$DEPLOY_HOST" -o StrictHostKeyChecking=no 'cd /data/smile-blog-koa && git pull && bash ./script/deploy.sh' 16 | addons: 17 | ssh_known_hosts: 18 | - "$DEPLOY_HOST" 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## smile-blog-koa 2 | 3 | [![Build Status](https://www.travis-ci.org/smileShirmy/smile-blog-koa.svg?branch=master)](https://www.travis-ci.org/smileShirmy/smile-blog-koa) 4 | 5 | - 权限控制 6 | - 无感知Token刷新 7 | - 支持七牛云文件上传 8 | - HTTPS反向代理 9 | - Koa2 + Sequelize 10 | - MySQL 11 | 12 | 该项目为服务端部分,其它部分可点击下面的链接 13 | 14 | - 展示前端 [smile-blog-nuxt](https://github.com/smileShirmy/smile-blog-nuxt) 15 | - 管理后台 [smile-blog-admin](https://github.com/smileShirmy/smile-blog-admin) 16 | - 服务端 [smile-blog-koa](https://github.com/smileShirmy/smile-blog-koa) 17 | 18 | 19 | ## Setup 20 | 21 | - 需要把`config`目录下的`config.js.sample`重命名为`config.js`,然后进行相关参数的配置 22 | - 开始需要关闭权限校验中间件,通过`Postman`创建一个超级管理员(看最下面) 23 | - 启动该项目前需要全局安装`nodemon`和`pm2` 24 | 25 | ```bash 26 | npm install -g nodemon 27 | npm install -g pm2 28 | ``` 29 | 30 | ```bash 31 | # install 32 | npm install 33 | 34 | # development 35 | nodemon 36 | 37 | # production 38 | pm2 start app 39 | ``` 40 | 41 | ### 创建超级管理员 42 | 43 | 1. 打开`app/api/v1/article.js`,找到`authorApi.post('/')`接口,去掉`new Auth().m`中间件 44 | 2. 打开`Postman`发送`POST`请求,`Content-Type`设置为`application/json`,`body`输入以下内容: 45 | 46 | ```javascript 47 | { 48 | name: '用户名', 49 | avatar: '填图片地址', 50 | email: '填email', 51 | description: '用户描述信息', 52 | auth: '32', // 32代表超级管理员权限 53 | password: '', // 密码 英文+数字组合,至少六位 54 | } 55 | ``` 56 | 57 | 3. 再把刚刚去掉的中间加回去 58 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | require('module-alias/register') 2 | 3 | const Koa = require('koa') 4 | const parser = require('koa-bodyparser') 5 | const InitManager = require('./core/init') 6 | const catchError = require('./middleware/exception') 7 | const cors = require('koa2-cors'); 8 | const multipart = require('./core/multipart') 9 | 10 | const app = new Koa() 11 | 12 | app.use(cors()) 13 | app.use(catchError) 14 | app.use(parser()) 15 | multipart(app) 16 | 17 | InitManager.initCore(app) 18 | 19 | app.listen(3000, () => { 20 | console.log('listening port 3000') 21 | }) -------------------------------------------------------------------------------- /app/api/blog/article.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router') 2 | 3 | const { PositiveIntegerValidator } = require('@validator/common') 4 | const { 5 | CreateCommentValidator, 6 | ReplyCommentValidator, 7 | GetArticlesValidator, 8 | SearchArticlesValidator 9 | } = require('@validator/article') 10 | const { success } = require('../../lib/helper') 11 | 12 | const { ArticleDao } = require('@dao/article') 13 | const { CommentDao } = require('@dao/comment') 14 | 15 | const articleApi = new Router({ 16 | prefix: '/v1/blog/article' 17 | }) 18 | 19 | const ArticleDto = new ArticleDao() 20 | const CommentDto = new CommentDao() 21 | 22 | // 获取文章详情 23 | articleApi.get('/', async (ctx) => { 24 | const v = await new PositiveIntegerValidator().validate(ctx) 25 | const article = await ArticleDto.getArticle(v.get('query.id')) 26 | 27 | ctx.body = article 28 | }) 29 | 30 | // 点赞某篇文章 31 | articleApi.put('/like', async (ctx) => { 32 | const v = await new PositiveIntegerValidator().validate(ctx) 33 | const id = v.get('body.id') 34 | await ArticleDto.likeArticle(id) 35 | success('点赞文章成功') 36 | }) 37 | 38 | // 获取全部文章 39 | articleApi.get('/blog/articles', async (ctx) => { 40 | // 文章必须是公开的 1 公开 2 私密 41 | ctx.query.publicId = 1 42 | // 文章必须是已发布的 1 已发布 2 草稿 43 | ctx.query.statusId = 1 44 | // 文章包括非精选和精选 45 | ctx.query.starId = '0' 46 | const v = await new GetArticlesValidator().validate(ctx) 47 | 48 | const result = await ArticleDto.getArticles(v, true) 49 | ctx.body = result 50 | }) 51 | 52 | // 搜索文章 53 | articleApi.get('/search/articles', async (ctx) => { 54 | const v = await new SearchArticlesValidator().validate(ctx) 55 | 56 | const result = await ArticleDto.searchArticles(v) 57 | ctx.body = result 58 | }) 59 | 60 | // 获取归档 61 | articleApi.get('/archive', async (ctx) => { 62 | const archive = await ArticleDto.getArchive() 63 | ctx.body = archive 64 | }) 65 | 66 | // 获取所有精选文章 67 | articleApi.get('/star/articles', async (ctx) => { 68 | const articles = await ArticleDto.getStarArticles() 69 | ctx.body = articles 70 | }) 71 | 72 | // 添加评论 73 | articleApi.post('/add/comment', async (ctx) => { 74 | const v = await new CreateCommentValidator().validate(ctx, { 75 | id: 'articleId' 76 | }) 77 | const articleId = v.get('body.articleId') 78 | await CommentDto.createComment(v, articleId) 79 | success('添加评论成功') 80 | }) 81 | 82 | // 获取文章下的全部评论 83 | articleApi.get('/get/comment', async (ctx) => { 84 | const v = await new PositiveIntegerValidator().validate(ctx, { 85 | id: 'articleId' 86 | }) 87 | const articleId = v.get('query.articleId') 88 | const comments = await CommentDto.getComments(articleId) 89 | ctx.body = comments 90 | }) 91 | 92 | // 点赞某条评论 93 | articleApi.put('/like/comment', async (ctx) => { 94 | const v = await new PositiveIntegerValidator().validate(ctx) 95 | const id = v.get('body.id') 96 | await CommentDto.likeComment(id) 97 | success('点赞评论成功') 98 | }) 99 | 100 | // 回复某条评论 101 | articleApi.post('/reply/comment', async (ctx) => { 102 | const v = await new ReplyCommentValidator().validate(ctx, { 103 | id: 'articleId' 104 | }) 105 | const articleId = v.get('body.articleId') 106 | const parentId = v.get('body.parentId') 107 | await CommentDto.replyComment(v, articleId, parentId) 108 | success('回复成功') 109 | }) 110 | 111 | module.exports = articleApi -------------------------------------------------------------------------------- /app/api/blog/author.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router') 2 | 3 | const { PositiveIntegerValidator } = require('@validator/common') 4 | const { AuthorDao } = require('@dao/author') 5 | const AuthorDto = new AuthorDao() 6 | 7 | const authorApi = new Router({ 8 | prefix: '/v1/blog/author' 9 | }) 10 | 11 | // 获取作者详情 12 | authorApi.get('/detail', async (ctx) => { 13 | const v = await new PositiveIntegerValidator().validate(ctx) 14 | const id = v.get('query.id') 15 | 16 | const author = await AuthorDto.getAuthorDetail(id) 17 | ctx.body = author 18 | }) 19 | 20 | // 获取全部作者 21 | authorApi.get('/authors', async (ctx) => { 22 | const authors = await AuthorDto.getAuthors() 23 | ctx.body = authors 24 | }) 25 | 26 | module.exports = authorApi -------------------------------------------------------------------------------- /app/api/blog/blog.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router') 2 | 3 | const { BlogDao } = require('@dao/blog') 4 | 5 | const blogApi = new Router({ 6 | prefix: '/v1/blog/blog' 7 | }) 8 | 9 | const blogDto = new BlogDao() 10 | 11 | // 获取友链 12 | blogApi.get('/friend/friends', async (ctx) => { 13 | const friends = await blogDto.getFriends() 14 | ctx.body = friends 15 | }) 16 | 17 | module.exports = blogApi -------------------------------------------------------------------------------- /app/api/blog/category.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router') 2 | 3 | const { PositiveIntegerValidator } = require('@validator/common') 4 | const { CategoryDao } = require('@dao/category') 5 | 6 | const categoryApi = new Router({ 7 | prefix: '/v1/blog/category' 8 | }) 9 | 10 | const CategoryDto = new CategoryDao() 11 | 12 | // 获取所有分类 13 | categoryApi.get('/categories', async (ctx) => { 14 | const categories = await CategoryDto.getCategories() 15 | ctx.body = categories 16 | }) 17 | 18 | // 获取分类详情 19 | categoryApi.get('/', async (ctx) => { 20 | const v = await new PositiveIntegerValidator().validate(ctx) 21 | const id = v.get('query.id') 22 | const category = await CategoryDto.getCategory(id) 23 | ctx.body = category 24 | }) 25 | 26 | module.exports = categoryApi -------------------------------------------------------------------------------- /app/api/blog/message.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router') 2 | 3 | const { PaginateValidator } = require('@validator/common') 4 | const { CreateMessageValidator } = require('@validator/message') 5 | const { success } = require('../../lib/helper') 6 | const { MessageDao } = require('@dao/message') 7 | 8 | const MessageDto = new MessageDao() 9 | 10 | const messageApi = new Router({ 11 | prefix: '/v1/blog/message' 12 | }) 13 | 14 | // 创建留言 15 | messageApi.post('/', async (ctx) => { 16 | const v = await new CreateMessageValidator().validate(ctx) 17 | await MessageDto.createMessage(v) 18 | success('新建留言成功') 19 | }) 20 | 21 | // 获取所有留言 22 | messageApi.get('/messages', async (ctx) => { 23 | const v = await new PaginateValidator().validate(ctx) 24 | const { rows, total } = await MessageDto.getMessages(v) 25 | ctx.body = { 26 | collection: rows, 27 | total, 28 | } 29 | }) 30 | 31 | module.exports = messageApi 32 | -------------------------------------------------------------------------------- /app/api/blog/tag.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router') 2 | 3 | const { TagDao } = require('@dao/tag') 4 | 5 | const tagApi = new Router({ 6 | prefix: '/v1/blog/tag' 7 | }) 8 | 9 | const TagDto = new TagDao() 10 | 11 | // 获取所有标签 12 | tagApi.get('/tags', async (ctx) => { 13 | const tags = await TagDto.getTags() 14 | ctx.body = tags 15 | }) 16 | 17 | module.exports = tagApi -------------------------------------------------------------------------------- /app/api/v1/article.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router') 2 | 3 | const { PositiveIntegerValidator } = require('@validator/common') 4 | const { 5 | CreateOrUpdateArticleValidator, 6 | GetArticlesValidator, 7 | SetPublicValidator, 8 | SetStarValidator, 9 | } = require('@validator/article') 10 | const { success } = require('../../lib/helper') 11 | const { Auth } = require('../../../middleware/auth') 12 | 13 | const { ArticleDao } = require('@dao/article') 14 | const { CommentDao } = require('@dao/comment') 15 | 16 | const articleApi = new Router({ 17 | prefix: '/v1/article' 18 | }) 19 | 20 | // 实例图片:https://resource.shirmy.me/lighthouse.jpeg 21 | 22 | const ArticleDto = new ArticleDao() 23 | const CommentDto = new CommentDao() 24 | 25 | // 创建文章 26 | articleApi.post('/', new Auth().m, async (ctx) => { 27 | const v = await new CreateOrUpdateArticleValidator().validate(ctx) 28 | await ArticleDto.createArticle(v) 29 | success('新建文章成功') 30 | }) 31 | 32 | // 更新文章 33 | articleApi.put('/', new Auth().m, async (ctx) => { 34 | const v = await new CreateOrUpdateArticleValidator().validate(ctx) 35 | await ArticleDto.updateArticle(v) 36 | success('更新文章成功') 37 | }) 38 | 39 | // 获取文章详情 40 | articleApi.get('/', new Auth().m, async (ctx) => { 41 | const v = await new PositiveIntegerValidator().validate(ctx) 42 | const article = await ArticleDto.getArticle(v.get('query.id')) 43 | 44 | ctx.body = article 45 | }) 46 | 47 | // 管理后台 获取全部文章 48 | articleApi.get('/articles', new Auth().m, async (ctx) => { 49 | const v = await new GetArticlesValidator().validate(ctx) 50 | 51 | const result = await ArticleDto.getArticles(v) 52 | ctx.body = result 53 | }) 54 | 55 | // 删除某篇文章,需要最高权限 56 | articleApi.delete('/', new Auth(32).m, async (ctx) => { 57 | const v = await new PositiveIntegerValidator().validate(ctx) 58 | const id = v.get('query.id') 59 | await ArticleDto.deleteArticle(id) 60 | success('删除文章成功') 61 | }) 62 | 63 | // 设置某篇文章为 公开 或 私密 64 | articleApi.put('/public', new Auth().m, async (ctx) => { 65 | const v = await new SetPublicValidator().validate(ctx) 66 | const id = v.get('query.id') 67 | const publicId = v.get('body.publicId') 68 | 69 | await ArticleDto.updateArticlePublic(id, publicId) 70 | success(`设为${publicId === 1 ? '公开' : '私密'}成功`) 71 | }) 72 | 73 | // 设置某篇文章为 精选 或 非精选 74 | articleApi.put('/star', new Auth().m, async (ctx) => { 75 | const v = await new SetStarValidator().validate(ctx) 76 | const id = v.get('query.id') 77 | const starId = v.get('body.starId') 78 | 79 | await ArticleDto.updateArticleStar(id, starId) 80 | success(`设为${starId === 2 ? '精选' : '非精选'}成功`) 81 | }) 82 | 83 | // 获取文章下的全部评论 84 | articleApi.get('/get/comment', new Auth().m, async (ctx) => { 85 | const v = await new PositiveIntegerValidator().validate(ctx, { 86 | id: 'articleId' 87 | }) 88 | const articleId = v.get('query.articleId') 89 | const comments = await CommentDto.getComments(articleId) 90 | ctx.body = comments 91 | }) 92 | 93 | // 删除某条评论 需要最高权限 94 | articleApi.delete('/del/comment', new Auth(32).m, async (ctx) => { 95 | const v = await new PositiveIntegerValidator().validate(ctx) 96 | const id = v.get('query.id') 97 | await CommentDto.deleteComment(id) 98 | success('删除评论成功') 99 | }) 100 | 101 | module.exports = articleApi -------------------------------------------------------------------------------- /app/api/v1/author.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router') 2 | 3 | const { success } = require('../../lib/helper') 4 | const { CreateAuthorValidator, UpdateAuthorValidator, LoginValidator, PasswordValidator, SelfPasswordValidator, AvatarUpdateValidator } = require('@validator/author') 5 | const { PositiveIntegerValidator } = require('@validator/common') 6 | const { Auth, RefreshAuth, generateToken } = require('../../../middleware/auth') 7 | const { getSafeParamId } = require('../../lib/util') 8 | const { Forbidden } = require('@exception') 9 | const { TokenType } = require('../../lib/enums') 10 | 11 | const { AuthorDao } = require('@dao/author') 12 | const { ArticleAuthorDao } = require('@dao/articleAuthor') 13 | 14 | const AuthorDto = new AuthorDao() 15 | const ArticleAuthorDto = new ArticleAuthorDao() 16 | 17 | const authorApi = new Router({ 18 | prefix: '/v1/author' 19 | }) 20 | 21 | // 创建作者 22 | // 如果需要创建超级管理员 请先去掉 new Auth().m 23 | authorApi.post('/', new Auth().m, async (ctx) => { 24 | const v = await new CreateAuthorValidator().validate(ctx) 25 | 26 | await AuthorDto.createAuthor(v) 27 | success('创建用户成功') 28 | }) 29 | 30 | // 更新作者信息 31 | authorApi.put('/info', new Auth().m, async (ctx) => { 32 | const v = await new UpdateAuthorValidator().validate(ctx) 33 | const id = getSafeParamId(v) 34 | 35 | await AuthorDto.updateAuthor(v, id) 36 | success('更新用户成功') 37 | }) 38 | 39 | // 修改用户头像 40 | authorApi.put('/avatar', new Auth().m, async (ctx) => { 41 | const v = await new AvatarUpdateValidator().validate(ctx) 42 | const avatar = v.get('body.avatar') 43 | const id = ctx.currentAuthor.id 44 | 45 | await AuthorDto.updateAvatar(avatar, id) 46 | success('更新头像成功') 47 | }) 48 | 49 | // 超级管理员修改作者的密码 50 | authorApi.put('/password', new Auth(32).m, async (ctx) => { 51 | const v = await new PasswordValidator().validate(ctx) 52 | const id = getSafeParamId(v) 53 | 54 | await AuthorDto.changePassword(v, id) 55 | success('修改作者密码成功') 56 | }) 57 | 58 | // 修改自己的密码 59 | authorApi.put('/password/self', new Auth().m, new Auth().m, async (ctx) => { 60 | const v = await new SelfPasswordValidator().validate(ctx) 61 | const id = ctx.currentAuthor.id 62 | 63 | await AuthorDto.changeSelfPassword(v, id) 64 | success('修改密码成功') 65 | }) 66 | 67 | // 删除作者,需要最高权限 32 才能删除 68 | authorApi.delete('/', new Auth(32).m, async (ctx) => { 69 | const v = await new PositiveIntegerValidator().validate(ctx) 70 | const id = getSafeParamId(v) 71 | 72 | const authorId = ctx.currentAuthor.id 73 | if (id === authorId) { 74 | throw new Forbidden({ 75 | msg: '不能删除自己' 76 | }) 77 | } 78 | await AuthorDto.deleteAuthor(id) 79 | success('删除作者成功') 80 | }) 81 | 82 | // 登录 83 | authorApi.post('/login', async (ctx) => { 84 | const v = await new LoginValidator().validate(ctx) 85 | const name = v.get('body.name') 86 | const password = v.get('body.password') 87 | 88 | const author = await AuthorDto.verifyPassword(ctx, name, password) 89 | 90 | const accessToken = generateToken(author.id, author.auth, TokenType.ACCESS, { expiresIn: global.config.security.accessExp }) 91 | const refreshToken = generateToken(author.id, author.auth, TokenType.REFRESH, { expiresIn: global.config.security.refreshExp }) 92 | ctx.body = { 93 | accessToken, 94 | refreshToken 95 | } 96 | }) 97 | 98 | /** 99 | * 守卫函数,用户刷新令牌,统一异常 100 | */ 101 | authorApi.get('/refresh', new RefreshAuth().m, async (ctx) => { 102 | const author = ctx.currentAuthor 103 | 104 | const accessToken = generateToken(author.id, author.auth, TokenType.ACCESS, { expiresIn: global.config.security.accessExp }) 105 | const refreshToken = generateToken(author.id, author.auth, TokenType.REFRESH, { expiresIn: global.config.security.refreshExp }) 106 | 107 | ctx.body = { 108 | accessToken, 109 | refreshToken 110 | } 111 | }) 112 | 113 | // 获取除了管理员之外的全部作者 114 | authorApi.get('/authors/admin', new Auth().m, async (ctx) => { 115 | const authors = await AuthorDto.getAdminAuthors() 116 | ctx.body = authors 117 | }) 118 | 119 | // 获取全部作者 120 | authorApi.get('/authors', new Auth().m, async (ctx) => { 121 | const authors = await AuthorDto.getAuthors() 122 | ctx.body = authors 123 | }) 124 | 125 | authorApi.get('/info', new Auth().m, async (ctx) => { 126 | ctx.body = ctx.currentAuthor 127 | }) 128 | 129 | module.exports = authorApi -------------------------------------------------------------------------------- /app/api/v1/blog.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router') 2 | 3 | const { Auth } = require('../../../middleware/auth') 4 | const { CreateOrUpdateFriendValidator } = require('@validator/blog') 5 | const { PositiveIntegerValidator } = require('@validator/common') 6 | 7 | const { getSafeParamId } = require('../../lib/util') 8 | const { success } = require('../../lib/helper') 9 | 10 | const { BlogDao } = require('@dao/blog') 11 | 12 | const blogApi = new Router({ 13 | prefix: '/v1/blog' 14 | }) 15 | 16 | const blogDto = new BlogDao() 17 | 18 | // 获取友链 19 | blogApi.get('/friend/friends', new Auth().m, async (ctx) => { 20 | const friends = await blogDto.getFriends() 21 | ctx.body = friends 22 | }) 23 | 24 | // 添加友链 25 | blogApi.post('/friend', new Auth().m, async (ctx) => { 26 | const v = await new CreateOrUpdateFriendValidator().validate(ctx) 27 | await blogDto.createFriend(v) 28 | success('新建友链成功') 29 | }) 30 | 31 | // 修改友链 32 | blogApi.put('/friend', new Auth().m, async (ctx) => { 33 | const v = await new CreateOrUpdateFriendValidator().validate(ctx) 34 | const id = getSafeParamId(v) 35 | await blogDto.updateFriend(v, id) 36 | success('修改友链成功') 37 | }) 38 | 39 | // 删除友链 40 | blogApi.delete('/friend', new Auth().m, async (ctx) => { 41 | const v = await new PositiveIntegerValidator().validate(ctx) 42 | const id = v.get('query.id') 43 | await blogDto.deleteFriend(id) 44 | success('删除标签成功') 45 | }) 46 | 47 | module.exports = blogApi -------------------------------------------------------------------------------- /app/api/v1/category.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router') 2 | 3 | const { CreateOrUpdateCategoryValidator } = require('@validator/category') 4 | const { PositiveIntegerValidator } = require('@validator/common') 5 | const { success } = require('../../lib/helper') 6 | const { getSafeParamId } = require('../../lib/util') 7 | const { Auth } = require('../../../middleware/auth') 8 | 9 | const { CategoryDao } = require('@dao/category') 10 | 11 | const categoryApi = new Router({ 12 | prefix: '/v1/category' 13 | }) 14 | 15 | const CategoryDto = new CategoryDao() 16 | 17 | // 获取所有分类 18 | categoryApi.get('/categories', new Auth().m, async (ctx) => { 19 | const categories = await CategoryDto.getCategories() 20 | ctx.body = categories 21 | }) 22 | 23 | categoryApi.post('/', new Auth().m, async (ctx) => { 24 | const v = await new CreateOrUpdateCategoryValidator().validate(ctx) 25 | await CategoryDto.createCategory(v) 26 | success('新建分类成功') 27 | }) 28 | 29 | categoryApi.put('/', new Auth().m, async (ctx) => { 30 | const v = await new CreateOrUpdateCategoryValidator().validate(ctx) 31 | const id = getSafeParamId(v) 32 | await CategoryDto.updateCategory(v, id) 33 | success('更新分类成功') 34 | }) 35 | 36 | // 删除分类 需要最高权限才能删除分诶 37 | categoryApi.delete('/', new Auth(32).m, async (ctx) => { 38 | const v = await new PositiveIntegerValidator().validate(ctx) 39 | const id = v.get('query.id') 40 | await CategoryDto.deleteCategory(id) 41 | success('删除分类成功') 42 | }) 43 | 44 | module.exports = categoryApi -------------------------------------------------------------------------------- /app/api/v1/file.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router') 2 | 3 | const { UpLoader } = require('../../lib/upload') 4 | const { Auth } = require('../../../middleware/auth') 5 | 6 | const fileApi = new Router({ 7 | prefix: '/v1/file' 8 | }) 9 | 10 | fileApi.post('/', new Auth().m, async (ctx) => { 11 | const files = await ctx.multipart() 12 | 13 | const upLoader = new UpLoader(`blog/`) 14 | const arr = await upLoader.upload(files) 15 | ctx.body = arr 16 | }) 17 | 18 | module.exports = fileApi -------------------------------------------------------------------------------- /app/api/v1/message.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router') 2 | 3 | const { PositiveIntegerValidator, PaginateValidator } = require('@validator/common') 4 | const { success } = require('../../lib/helper') 5 | const { Auth } = require('../../../middleware/auth') 6 | const { MessageDao } = require('@dao/message') 7 | 8 | const MessageDto = new MessageDao() 9 | 10 | const messageApi = new Router({ 11 | prefix: '/v1/message' 12 | }) 13 | 14 | // 获取所有留言 15 | messageApi.get('/messages', new Auth().m, async (ctx) => { 16 | const v = await new PaginateValidator().validate(ctx) 17 | const { rows, total } = await MessageDto.getMessages(v) 18 | ctx.body = { 19 | collection: rows, 20 | total, 21 | } 22 | }) 23 | 24 | // 删除留言,需要最高权限才能删除留言 25 | messageApi.delete('/', new Auth(32).m, async (ctx) => { 26 | const v = await new PositiveIntegerValidator().validate(ctx) 27 | const id = v.get('query.id') 28 | await MessageDto.deleteMessage(id) 29 | success('删除留言成功') 30 | }) 31 | 32 | module.exports = messageApi 33 | -------------------------------------------------------------------------------- /app/api/v1/tag.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router') 2 | 3 | const { success } = require('../../lib/helper') 4 | const { CreateOrUpdateTagValidator } = require('@validator/tag') 5 | const { getSafeParamId } = require('../../lib/util') 6 | const { PositiveIntegerValidator } = require('@validator/common') 7 | const { Auth } = require('../../../middleware/auth') 8 | 9 | const { TagDao } = require('@dao/tag') 10 | 11 | const tagApi = new Router({ 12 | prefix: '/v1/tag' 13 | }) 14 | 15 | const TagDto = new TagDao() 16 | 17 | tagApi.get('/tags', new Auth().m, async (ctx) => { 18 | const tags = await TagDto.getTags() 19 | ctx.body = tags 20 | }) 21 | 22 | tagApi.post('/', new Auth().m, async (ctx) => { 23 | const v = await new CreateOrUpdateTagValidator().validate(ctx) 24 | await TagDto.createTag(v) 25 | success('新建标签成功') 26 | }) 27 | 28 | tagApi.put('/', new Auth().m, async (ctx) => { 29 | const v = await new CreateOrUpdateTagValidator().validate(ctx) 30 | const id = getSafeParamId(v) 31 | await TagDto.updateTag(v, id) 32 | success('更新标签成功') 33 | }) 34 | 35 | // 删除标签,需要最高权限才能删除留言 36 | tagApi.delete('/', new Auth(32).m, async (ctx) => { 37 | const v = await new PositiveIntegerValidator().validate(ctx) 38 | const id = v.get('query.id') 39 | await TagDto.deleteTag(id) 40 | success('删除标签成功') 41 | }) 42 | 43 | module.exports = tagApi -------------------------------------------------------------------------------- /app/dao/article.js: -------------------------------------------------------------------------------- 1 | const { sequelize } = require('../../core/db') 2 | const { omitBy, isUndefined, intersection, unset } = require('lodash') 3 | const { Op } = require('sequelize') 4 | 5 | const { NotFound, Forbidden } = require('@exception') 6 | 7 | const { Article, Tag, Author, Comment, Category } = require('@models') 8 | const { ArticleTagDao } = require('@dao/articleTag') 9 | const { ArticleAuthorDao } = require('@dao/articleAuthor') 10 | const { CategoryDao } = require('@dao/category') 11 | 12 | const ArticleTagDto = new ArticleTagDao() 13 | const ArticleAuthorDto = new ArticleAuthorDao() 14 | const CategoryDto = new CategoryDao() 15 | 16 | class ArticleDao { 17 | async createArticle(v) { 18 | const article = await Article.findOne({ 19 | where: { 20 | title: v.get('body.title') 21 | } 22 | }) 23 | if (article) { 24 | throw new Forbidden({ 25 | msg: '存在同名文章' 26 | }) 27 | } 28 | const categoryId = v.get('body.categoryId') 29 | const category = await CategoryDto.getCategory(categoryId) 30 | if (!category) { 31 | throw new Forbidden({ 32 | msg: '未能找到相关分类' 33 | }) 34 | } 35 | return sequelize.transaction(async t => { 36 | const result = await Article.create({ 37 | title: v.get('body.title'), 38 | content: v.get('body.content'), 39 | description: v.get('body.description'), 40 | cover: v.get('body.cover'), 41 | created_date: v.get('body.createdDate'), 42 | category_id: categoryId, 43 | public: v.get('body.public'), 44 | status: v.get('body.status'), 45 | star: v.get('body.star'), 46 | like: 0 47 | }, { transaction: t }) 48 | 49 | const articleId = result.getDataValue('id') 50 | await ArticleTagDto.createArticleTag(articleId, v.get('body.tags'), { transaction: t }) 51 | await ArticleAuthorDto.createArticleAuthor(articleId, v.get('body.authors'), { transaction: t }) 52 | }) 53 | } 54 | 55 | // 编辑某篇文章 56 | async updateArticle(v) { 57 | const id = v.get('body.id') 58 | const article = await Article.findByPk(id) 59 | if (!article) { 60 | throw new NotFound({ 61 | msg: '没有找到相关文章' 62 | }) 63 | } 64 | const tags = v.get('body.tags') 65 | const authors = v.get('body.authors') 66 | 67 | // step1: 先删除相关关联 68 | const isDeleteAuthor = await ArticleAuthorDto.deleteArticleAuthor(id, authors) 69 | const isDeleteTag = await ArticleTagDto.deleteArticleTag(id, tags) 70 | 71 | // step2: 再创建关联 72 | if (isDeleteAuthor) { 73 | await ArticleAuthorDto.createArticleAuthor(id, authors) 74 | } 75 | if (isDeleteTag) { 76 | await ArticleTagDto.createArticleTag(id, tags) 77 | } 78 | 79 | // step3: 更新文章 80 | article.title = v.get('body.title') 81 | article.content = v.get('body.content'), 82 | article.description = v.get('body.description'), 83 | article.cover = v.get('body.cover') 84 | article.created_date = v.get('body.createdDate') 85 | article.category_id = v.get('body.categoryId') 86 | article.public = v.get('body.public') 87 | article.status = v.get('body.status') 88 | article.star = v.get('body.star') 89 | article.save() 90 | } 91 | 92 | // 获取文章详情 93 | async getArticle(id) { 94 | const article = await Article.scope('frontShow').findOne({ 95 | where: { 96 | id 97 | }, 98 | include: [ 99 | { 100 | model: Tag, 101 | as: 'tags', 102 | attributes: ['id', 'name'] 103 | }, 104 | { 105 | model: Author, 106 | as: 'authors', 107 | attributes: ['id', 'name'] 108 | }, 109 | { 110 | model: Category, 111 | as: 'category', 112 | attributes: ['id', 'name'] 113 | } 114 | ], 115 | attributes: { 116 | exclude: ['public', 'status', 'description'] 117 | } 118 | }) 119 | if (!article) { 120 | throw new NotFound({ 121 | msg: '没有找到相关文章' 122 | }) 123 | } 124 | // 获取这篇文章相关分类下的文章列表(除了自己) 125 | const categoryArticles = await Article.scope('frontShow').findAll({ 126 | limit: 10, 127 | order: [ 128 | ['created_date', 'DESC'] 129 | ], 130 | where: { 131 | category_id: article.category_id, 132 | id: { 133 | [Op.not]: id 134 | } 135 | }, 136 | attributes: ['id', 'created_date', 'title'] 137 | }) 138 | article.exclude = ['category_id'] 139 | 140 | await article.increment('views', { by: 1 }) 141 | 142 | article.setDataValue('categoryArticles', categoryArticles) 143 | 144 | return article 145 | } 146 | 147 | async likeArticle(id) { 148 | const article = await Article.findByPk(id) 149 | if (!article) { 150 | throw new NotFound({ 151 | msg: '没有找到相关文章' 152 | }) 153 | } 154 | await article.increment('like', { by: 1 }) 155 | } 156 | 157 | // 把文章设为私密或公开 158 | async updateArticlePublic(id, publicId) { 159 | const article = await Article.findByPk(id) 160 | if (!article) { 161 | throw new NotFound({ 162 | msg: '没有找到相关文章' 163 | }) 164 | } 165 | article.public = publicId 166 | article.save() 167 | } 168 | 169 | // 把文章设为精选(2)或非精选(1) 170 | async updateArticleStar(id, starId) { 171 | if (starId === 2) { 172 | const articles = await Article.findAll({ 173 | attributes: ['id'] 174 | }) 175 | if (articles.length === 10) { 176 | throw new Forbidden({ 177 | msg: '最多只能设置10篇精选文章' 178 | }) 179 | } 180 | } 181 | const article = await Article.findByPk(id) 182 | if (!article) { 183 | throw new NotFound({ 184 | msg: '没有找到相关文章' 185 | }) 186 | } 187 | article.star = starId 188 | article.save() 189 | } 190 | 191 | // 获取所有精选文章 192 | async getStarArticles() { 193 | const articles = await Article.scope('frontShow').findAll({ 194 | where: { 195 | star: 2, // 精选 196 | }, 197 | include: [ 198 | { 199 | model: Author, 200 | as: 'authors', 201 | attributes: ['id', 'name'] 202 | }, 203 | { 204 | model: Category, 205 | as: 'category', 206 | attributes: ['id', 'name', 'cover'] 207 | } 208 | ], 209 | attributes: ['id', 'title', 'cover', 'created_date'], 210 | }) 211 | return articles 212 | } 213 | 214 | // 获取历史归档 215 | async getArchive() { 216 | const articles = await Article.scope('frontShow').findAll({ 217 | order: [ 218 | ['created_date', 'DESC'] 219 | ], 220 | include: [ 221 | { 222 | model: Author, 223 | as: 'authors', 224 | attributes: ['id', 'name', 'avatar'] 225 | } 226 | ], 227 | attributes: ['id', 'title', 'created_date'] 228 | }) 229 | return articles 230 | } 231 | 232 | /** 233 | * 获取所有文章 234 | * @param {Object} v 操作对象 235 | * @param {Boolean} isFont 是否展示端 236 | */ 237 | async getArticles(v, isFont = false) { 238 | const categoryId = v.get('query.categoryId') 239 | const authorId = v.get('query.authorId') 240 | const tagId = v.get('query.tagId') 241 | const publicId = v.get('query.publicId') 242 | const statusId = v.get('query.statusId') 243 | const starId = v.get('query.starId') 244 | const search = v.get('query.search') 245 | const start = v.get('query.page'); 246 | const pageCount = v.get('query.count'); 247 | 248 | // step1: 获取关联表的文章 id 交集 249 | let ids = [] 250 | if (authorId !== 0 || tagId !== 0) { 251 | // 求交集 252 | if (authorId !== 0 && tagId !== 0) { 253 | const arr1 = await ArticleAuthorDto.getArticleIds(authorId) 254 | const arr2 = await ArticleTagDto.getArticleIds(tagId) 255 | ids = intersection(arr1, arr2) 256 | } 257 | 258 | // 查询该标签下是否有文章 259 | if (tagId !== 0 && authorId === 0) { 260 | ids = await ArticleTagDto.getArticleIds(tagId) 261 | } 262 | 263 | // 查询该作者下是否有文章 264 | if (authorId !== 0 && tagId === 0) { 265 | ids = await ArticleAuthorDto.getArticleIds(authorId) 266 | } 267 | 268 | // 如果作者和标签都没有查询到文章 269 | if (!ids.length) { 270 | return [] 271 | } 272 | } 273 | 274 | // step2: 获取筛选条件 275 | let query = { 276 | category_id: categoryId === 0 ? undefined : categoryId, 277 | status: statusId === 0 ? undefined : statusId, 278 | public: publicId === 0 ? undefined : publicId, 279 | star: starId === 0 ? undefined : starId 280 | } 281 | 282 | // 忽略值为空的key 283 | let target = omitBy(query, isUndefined) 284 | let opIn = ids.length ? { 285 | id: { 286 | [Op.in]: ids 287 | } 288 | } : {} 289 | let like = search ? { 290 | [Op.or]: [ 291 | { 292 | title: { 293 | [Op.like]: `${search}%`, 294 | } 295 | }, 296 | { 297 | content: { 298 | [Op.like]: `${search}%`, 299 | } 300 | } 301 | ] 302 | } : {} 303 | 304 | // step3: 构建查询条件 305 | const where = { 306 | ...target, 307 | ...opIn, 308 | ...like 309 | } 310 | 311 | const { rows, count } = await Article.findAndCountAll({ 312 | where, 313 | distinct: true, 314 | offset: start * pageCount, 315 | limit: pageCount, 316 | order: [ 317 | ['created_date', 'DESC'] 318 | ], 319 | include: [ 320 | { 321 | model: Author, 322 | attributes: ['id', 'name', 'avatar'], 323 | as: 'authors' 324 | }, 325 | { 326 | model: Tag, 327 | as: 'tags' 328 | }, 329 | { 330 | model: Category, 331 | attributes: ['id', 'name'], 332 | as: 'category', 333 | }, 334 | { 335 | model: Comment, 336 | as: 'comments', 337 | attributes: ['id'] 338 | }, 339 | ], 340 | attributes: { 341 | exclude: isFont 342 | ? ['content', 'public', 'status'] 343 | : ['content'] 344 | }, 345 | }) 346 | 347 | const articles = JSON.parse(JSON.stringify(rows)) 348 | articles.forEach(v => { 349 | v.comment_count = v.comments.length 350 | unset(v, 'category_id') 351 | unset(v, 'comments') 352 | }) 353 | 354 | return { 355 | articles, 356 | total: count 357 | } 358 | } 359 | 360 | // 前端展示搜索文章 361 | async searchArticles(v) { 362 | const search = v.get('query.search') 363 | const start = v.get('query.page'); 364 | const pageCount = v.get('query.count'); 365 | 366 | const { rows, count } = await Article.scope('frontShow').findAndCountAll({ 367 | where: { 368 | [Op.or]: [ 369 | { 370 | title: { 371 | [Op.like]: `${search}%`, 372 | } 373 | }, 374 | { 375 | content: { 376 | [Op.like]: `${search}%`, 377 | } 378 | } 379 | ] 380 | }, 381 | order: [ 382 | ['created_date', 'DESC'] 383 | ], 384 | attributes: ['id', 'title', 'created_date', 'star'], 385 | offset: start * pageCount, 386 | limit: pageCount, 387 | }) 388 | 389 | return { 390 | articles: rows, 391 | total: count 392 | } 393 | } 394 | 395 | async deleteArticle(id) { 396 | const article = await Article.findOne({ 397 | where: { 398 | id 399 | } 400 | }) 401 | if (!article) { 402 | throw new NotFound({ 403 | msg: '没有找到相关文章' 404 | }) 405 | } 406 | 407 | // 删除相关关联 408 | await ArticleAuthorDto.deleteArticleAuthor(id) 409 | await ArticleTagDto.deleteArticleTag(id) 410 | article.destroy() 411 | } 412 | 413 | // 获取谋篇文章内容 414 | async getContent(id) { 415 | const content = await Article.findOne({ 416 | where: { 417 | id 418 | }, 419 | attributes: ['content'] 420 | }) 421 | return content 422 | } 423 | } 424 | 425 | module.exports = { 426 | ArticleDao 427 | } 428 | -------------------------------------------------------------------------------- /app/dao/articleAuthor.js: -------------------------------------------------------------------------------- 1 | const { Op } = require('sequelize') 2 | 3 | const { Author, ArticleAuthor } = require('@models') 4 | 5 | class ArticleAuthorDao { 6 | async createArticleAuthor(articleId, authors, options = {}) { 7 | const arr = typeof authors === 'string' ? JSON.parse(authors) : authors 8 | for (let i = 0; i < arr.length; i++) { 9 | await ArticleAuthor.create({ 10 | article_id: articleId, 11 | author_id: arr[i] 12 | }, {...options}) 13 | } 14 | } 15 | 16 | async getArticleAuthor(articleId, options = {}) { 17 | const result = await ArticleAuthor.findAll({ 18 | where: { 19 | article_id: articleId 20 | } 21 | }) 22 | let ids = result.map(v => v.author_id) 23 | return await Author.findAll({ 24 | where: { 25 | id: { 26 | [Op.in]: ids 27 | } 28 | }, 29 | ...options 30 | }) 31 | } 32 | 33 | async getArticleIds(authorId) { 34 | const result = await ArticleAuthor.findAll({ 35 | where: { 36 | author_id: authorId 37 | } 38 | }) 39 | return result.map(v => v.article_id) 40 | } 41 | 42 | async deleteArticleAuthor(articleId, authors = []) { 43 | const result = await ArticleAuthor.findAll({ 44 | where: { 45 | article_id: articleId 46 | } 47 | }) 48 | // 如果 id 相同则不再需要删除 49 | if (authors.length && result.map(v => v.author_id).join('') === authors.join('')) { 50 | return false 51 | } else { 52 | for (let i = 0; i < result.length; i++) { 53 | await result[i].destroy() 54 | } 55 | return true 56 | } 57 | } 58 | } 59 | 60 | module.exports = { 61 | ArticleAuthorDao 62 | } -------------------------------------------------------------------------------- /app/dao/articleTag.js: -------------------------------------------------------------------------------- 1 | const { Op } = require('sequelize') 2 | 3 | const { Tag, ArticleTag } = require('@models') 4 | 5 | class ArticleTagDao { 6 | async createArticleTag(articleId, tags, options = {}) { 7 | const arr = typeof tags === 'string' ? JSON.parse(tags) : tags 8 | for (let i = 0; i < arr.length; i++) { 9 | await ArticleTag.create({ 10 | article_id: articleId, 11 | tag_id: arr[i] 12 | }, {...options}) 13 | } 14 | } 15 | 16 | async getArticleTag(articleId) { 17 | const result = await ArticleTag.findAll({ 18 | where: { 19 | article_id: articleId 20 | } 21 | }) 22 | let ids = result.map(v => v.tag_id) 23 | return await Tag.findAll({ 24 | where: { 25 | id: { 26 | [Op.in]: ids 27 | } 28 | } 29 | }) 30 | } 31 | 32 | async getArticleIds(tagId) { 33 | const result = await ArticleTag.findAll({ 34 | where: { 35 | tag_id: tagId 36 | } 37 | }) 38 | return result.map(v => v.article_id) 39 | } 40 | 41 | async deleteArticleTag(articleId, tags = []) { 42 | const result = await ArticleTag.findAll({ 43 | where: { 44 | article_id: articleId 45 | } 46 | }) 47 | // 如果 id 相同则不再需要删除 48 | if (tags.length && result.map(v => v.tag_id).join('') === tags.join('')) { 49 | return false 50 | } else { 51 | for (let i = 0; i < result.length; i++) { 52 | await result[i].destroy() 53 | } 54 | return true 55 | } 56 | } 57 | } 58 | 59 | module.exports = { 60 | ArticleTagDao 61 | } -------------------------------------------------------------------------------- /app/dao/author.js: -------------------------------------------------------------------------------- 1 | const { Op } = require('sequelize') 2 | 3 | const { Author, ArticleAuthor } = require('@models') 4 | const { Forbidden, NotFound, ParameterException } = require('@exception') 5 | const { AuthType } = require('../lib/enums') 6 | const bcrypt = require('bcryptjs') 7 | 8 | class AuthorDao { 9 | async createAuthor(v) { 10 | const name = v.get('body.name') 11 | const author = await Author.findOne({ 12 | where: { 13 | name 14 | } 15 | }) 16 | if (author) { 17 | throw new Forbidden({ 18 | msg: '已存在该作者名' 19 | }) 20 | } 21 | await Author.create({ 22 | name: v.get('body.name'), 23 | avatar: v.get('body.avatar'), 24 | email: v.get('body.email'), 25 | description: v.get('body.description'), 26 | password: v.get('body.password'), 27 | auth: v.get('body.auth') 28 | }) 29 | } 30 | 31 | async getAuthorDetail(id) { 32 | const author = await Author.findOne({ 33 | where: { 34 | id, 35 | }, 36 | attributes: { exclude: ['auth'] } 37 | }) 38 | return author 39 | } 40 | 41 | async updateAuthor(v, id) { 42 | const author = await Author.findByPk(id) 43 | if (!author) { 44 | throw new NotFound({ 45 | msg: '没有找到相关作者' 46 | }) 47 | } 48 | author.avatar = v.get('body.avatar') 49 | author.email = v.get('body.email') 50 | author.description = v.get('body.description') 51 | author.auth = v.get('body.auth') 52 | author.save() 53 | } 54 | 55 | async updateAvatar(avatar, id) { 56 | const author = await Author.findByPk(id) 57 | if (!author) { 58 | throw new NotFound({ 59 | msg: '没有找到相关作者' 60 | }) 61 | } 62 | author.avatar = avatar 63 | author.save() 64 | return author 65 | } 66 | 67 | async deleteAuthor(id) { 68 | const author = await Author.findOne({ 69 | where: { 70 | id 71 | } 72 | }) 73 | if (!author) { 74 | throw new NotFound({ 75 | msg: '没有找到相关作者' 76 | }) 77 | } 78 | const result = await ArticleAuthor.findOne({ 79 | where: { 80 | author_id: id 81 | } 82 | }) 83 | if (result) { 84 | throw new Forbidden({ 85 | msg: '该作者下有文章,禁止删除' 86 | }) 87 | } 88 | author.destroy() 89 | } 90 | 91 | async changePassword(v, id) { 92 | const author = await Author.findByPk(id) 93 | if (!author) { 94 | throw new NotFound({ 95 | msg: '没有找到相关作者' 96 | }) 97 | } 98 | author.password = v.get('body.password') 99 | author.save() 100 | } 101 | 102 | async changeSelfPassword(v, id) { 103 | const author = await Author.findByPk(id) 104 | if (!author) { 105 | throw new NotFound({ 106 | msg: '没有找到相关作者' 107 | }) 108 | } 109 | const correct = bcrypt.compareSync(v.get('body.oldPassword'), author.password) 110 | if (!correct) { 111 | throw new ParameterException('原始密码不正确') 112 | } 113 | author.password = v.get('body.password') 114 | author.save() 115 | } 116 | 117 | async verifyPassword(ctx, name, password) { 118 | const author = await Author.findOne({ 119 | where: { 120 | name 121 | } 122 | }) 123 | if (!author) { 124 | throw new NotFound('作者不存在') 125 | } 126 | const correct = bcrypt.compareSync(password, author.password) 127 | if (!correct) { 128 | throw new ParameterException('密码不正确') 129 | } 130 | 131 | return author 132 | } 133 | 134 | async getAuthors() { 135 | const authors = await Author.findAll({ 136 | attributes: { exclude: ['auth'] } 137 | }) 138 | return authors 139 | } 140 | 141 | // 获取除了管理员之外的全部作者 142 | async getAdminAuthors() { 143 | const authors = await Author.findAll({ 144 | where: { 145 | auth: { 146 | [Op.ne]: AuthType.SUPER_ADMIN 147 | } 148 | }, 149 | attributes: { exclude: ['auth'] } 150 | }) 151 | return authors 152 | } 153 | } 154 | 155 | module.exports = { 156 | AuthorDao 157 | } -------------------------------------------------------------------------------- /app/dao/blog.js: -------------------------------------------------------------------------------- 1 | const { NotFound, Forbidden } = require('@exception') 2 | const { Friend } = require('@models') 3 | 4 | class BlogDao { 5 | // 创建友链 6 | async createFriend(v) { 7 | const friend = await Friend.findOne({ 8 | where: { 9 | name: v.get('body.name') 10 | } 11 | }) 12 | if (friend) { 13 | throw new Forbidden({ 14 | msg: '已经存在该友链名' 15 | }) 16 | } 17 | return await Friend.create({ 18 | name: v.get('body.name'), 19 | link: v.get('body.link'), 20 | avatar: v.get('body.avatar') 21 | }) 22 | } 23 | 24 | // 修改友链 25 | async updateFriend(v, id) { 26 | const friend = await Friend.findByPk(id) 27 | if (!friend) { 28 | throw new NotFound({ 29 | msg: '没有找到相关友链' 30 | }) 31 | } 32 | friend.name = v.get('body.name'), 33 | friend.link = v.get('body.link'), 34 | friend.avatar = v.get('body.avatar') 35 | friend.save() 36 | } 37 | 38 | // 获取友链 39 | async getFriends() { 40 | const friends = Friend.findAll() 41 | return friends 42 | } 43 | 44 | // 删除友链 45 | async deleteFriend(id) { 46 | const friend = await Friend.findOne({ 47 | where: { 48 | id 49 | } 50 | }) 51 | if (!friend) { 52 | throw new NotFound({ 53 | msg: '没有找到相关友链' 54 | }) 55 | } 56 | friend.destroy() 57 | } 58 | } 59 | 60 | module.exports = { 61 | BlogDao 62 | } -------------------------------------------------------------------------------- /app/dao/category.js: -------------------------------------------------------------------------------- 1 | const { NotFound, Forbidden } = require('@exception') 2 | const { Category, Article } = require('@models') 3 | 4 | class CategoryDao { 5 | async createCategory(v) { 6 | const category = await Category.findOne({ 7 | where: { 8 | name: v.get('body.name') 9 | } 10 | }) 11 | if (category) { 12 | throw new Forbidden({ 13 | msg: '分类已存在' 14 | }) 15 | } 16 | return await Category.create({ 17 | name: v.get('body.name'), 18 | cover: v.get('body.cover'), 19 | description: v.get('body.description') 20 | }) 21 | } 22 | 23 | async getCategory(id, options = {}) { 24 | const category = await Category.findOne({ 25 | where: { 26 | id 27 | }, 28 | ...options 29 | }) 30 | return category 31 | } 32 | 33 | async getCategories() { 34 | const categories = await Category.findAll() 35 | return categories 36 | } 37 | 38 | async updateCategory(v, id) { 39 | const category = await Category.findByPk(id) 40 | if (!category) { 41 | throw new NotFound({ 42 | msg: '没有找到相关分类' 43 | }) 44 | } 45 | category.name = v.get('body.name') 46 | category.description = v.get('body.description') 47 | category.cover = v.get('body.cover') 48 | category.save() 49 | } 50 | 51 | async deleteCategory(id) { 52 | const category = await Category.findOne({ 53 | where: { 54 | id 55 | } 56 | }) 57 | if (!category) { 58 | throw new NotFound({ 59 | msg: '没有找到相关分类' 60 | }) 61 | } 62 | const article = await Article.findOne({ 63 | where: { 64 | category_id: id 65 | } 66 | }) 67 | if (article) { 68 | throw new Forbidden({ 69 | msg: '该分类下有文章,禁止删除' 70 | }) 71 | } 72 | category.destroy() 73 | } 74 | } 75 | 76 | module.exports = { 77 | CategoryDao 78 | } -------------------------------------------------------------------------------- /app/dao/comment.js: -------------------------------------------------------------------------------- 1 | const { NotFound } = require('@exception') 2 | const { Comment, Article } = require('@models') 3 | 4 | class CommentDao { 5 | async createComment(v, articleId) { 6 | const article = await Article.findByPk(articleId) 7 | if (!article) { 8 | throw new NotFound({ 9 | msg: '没有找到相关文章' 10 | }) 11 | } 12 | return await Comment.create({ 13 | nickname: v.get('body.nickname'), 14 | content: v.get('body.content'), 15 | email: v.get('body.email'), 16 | website: v.get('body.website'), 17 | article_id: articleId 18 | }) 19 | } 20 | 21 | async getComments(articleId) { 22 | let comments = await Comment.findAll({ 23 | where: { 24 | article_id: articleId 25 | }, 26 | order: [ 27 | ['created_at', 'DESC'] 28 | ], 29 | attributes: { exclude: ['article_id', 'ArticleId'] } 30 | }) 31 | comments.forEach(v => { 32 | v.setDataValue('created_date', v.created_at) 33 | }) 34 | return comments 35 | } 36 | 37 | async deleteComment(id) { 38 | const comment = await Comment.findOne({ 39 | where: { 40 | id 41 | } 42 | }) 43 | if (!comment) { 44 | throw new NotFound({ 45 | msg: '没有找到相关评论' 46 | }) 47 | } 48 | comment.destroy() 49 | } 50 | 51 | async likeComment(id) { 52 | const comment = await Comment.findByPk(id) 53 | if (!comment) { 54 | throw new NotFound({ 55 | msg: '没有找到相关评论' 56 | }) 57 | } 58 | await comment.increment('like', { by: 1 }) 59 | } 60 | 61 | async replyComment(v, articleId, parentId) { 62 | const article = await Article.findByPk(articleId) 63 | if (!article) { 64 | throw new NotFound({ 65 | msg: '没有找到相关文章' 66 | }) 67 | } 68 | const comment = await Comment.findByPk(parentId) 69 | if (!comment) { 70 | throw new NotFound({ 71 | msg: '没有找到相关评论' 72 | }) 73 | } 74 | return await Comment.create({ 75 | parent_id: parentId, 76 | article_id: articleId, 77 | nickname: v.get('body.nickname'), 78 | content: v.get('body.content'), 79 | email: v.get('body.email'), 80 | website: v.get('body.website'), 81 | }) 82 | } 83 | } 84 | 85 | module.exports = { 86 | CommentDao 87 | } -------------------------------------------------------------------------------- /app/dao/message.js: -------------------------------------------------------------------------------- 1 | const { Message } = require('@models') 2 | 3 | class MessageDao { 4 | async createMessage(v) { 5 | return await Message.create({ 6 | nickname: v.get('body.nickname'), 7 | content: v.get('body.content') 8 | }) 9 | } 10 | 11 | async getMessages(v) { 12 | const start = v.get('query.page'); 13 | const pageCount = v.get('query.count'); 14 | 15 | const { rows, count } = await Message.findAndCountAll({ 16 | order: [ 17 | ['id', 'DESC'] 18 | ], 19 | offset: start * pageCount, 20 | limit: pageCount, 21 | }) 22 | return { 23 | rows, 24 | total: count 25 | } 26 | } 27 | 28 | async deleteMessage(id) { 29 | const message = await Message.findOne({ 30 | where: { 31 | id 32 | } 33 | }) 34 | if (!message) { 35 | throw new NotFound({ 36 | msg: '没有找到相关留言' 37 | }) 38 | } 39 | message.destroy() 40 | } 41 | } 42 | 43 | module.exports = { 44 | MessageDao 45 | } 46 | -------------------------------------------------------------------------------- /app/dao/tag.js: -------------------------------------------------------------------------------- 1 | const { NotFound, Forbidden } = require('@exception') 2 | const { Tag, ArticleTag } = require('@models') 3 | 4 | class TagDao { 5 | async createTag(v) { 6 | const tag = await Tag.findOne({ 7 | where: { 8 | name: v.get('body.name') 9 | } 10 | }) 11 | if (tag) { 12 | throw new Forbidden({ 13 | msg: '标签已存在' 14 | }) 15 | } 16 | return await Tag.create({ 17 | name: v.get('body.name') 18 | }) 19 | } 20 | 21 | async getTag(id) { 22 | const tag = Tag.findOne({ 23 | where: { 24 | id 25 | } 26 | }) 27 | return tag 28 | } 29 | 30 | async getTags() { 31 | const tags = Tag.findAll() 32 | return tags 33 | } 34 | 35 | async updateTag(v, id) { 36 | const tag = await Tag.findByPk(id) 37 | if (!tag) { 38 | throw new NotFound({ 39 | msg: '没有找到相关标签' 40 | }) 41 | } 42 | tag.name = v.get('body.name') 43 | tag.save() 44 | } 45 | 46 | async deleteTag(id) { 47 | const tag = await Tag.findOne({ 48 | where: { 49 | id 50 | } 51 | }) 52 | if (!tag) { 53 | throw new NotFound({ 54 | msg: '没有找到相关标签' 55 | }) 56 | } 57 | const result = await ArticleTag.findOne({ 58 | where: { 59 | tag_id: id 60 | } 61 | }) 62 | if (result) { 63 | throw new Forbidden({ 64 | msg: '该标签下有文章,禁止删除' 65 | }) 66 | } 67 | tag.destroy() 68 | } 69 | } 70 | 71 | module.exports = { 72 | TagDao 73 | } -------------------------------------------------------------------------------- /app/lib/enums.js: -------------------------------------------------------------------------------- 1 | function isThisType(val){ 2 | for(let key in this){ 3 | if(this[key] === val){ 4 | return true 5 | } 6 | } 7 | return false 8 | } 9 | 10 | const AuthType = { 11 | USER: 8, 12 | ADMIN: 16, 13 | SUPER_ADMIN: 32, 14 | isThisType 15 | } 16 | 17 | const TokenType = { 18 | ACCESS: 'access', 19 | REFRESH: 'refresh' 20 | } 21 | 22 | 23 | module.exports = { 24 | AuthType, 25 | TokenType 26 | } 27 | -------------------------------------------------------------------------------- /app/lib/helper.js: -------------------------------------------------------------------------------- 1 | const { Success } = require('@exception') 2 | 3 | function success(msg, errorCode) { 4 | throw new Success(msg, errorCode) 5 | } 6 | 7 | module.exports = { 8 | success 9 | } -------------------------------------------------------------------------------- /app/lib/upload.js: -------------------------------------------------------------------------------- 1 | const qiniu = require('qiniu') 2 | 3 | class UpLoader { 4 | constructor(prefix) { 5 | this.prefix = prefix || '' 6 | } 7 | 8 | async upload(files) { 9 | // 上传凭证 10 | const accessKey = global.config.qiniu.accessKey 11 | const secretKey = global.config.qiniu.secretKey 12 | const bucket = global.config.qiniu.bucket 13 | const siteDomain = global.config.qiniu.siteDomain 14 | 15 | let promises = [] 16 | 17 | for (const file of files) { 18 | const key = this.prefix + file.filename 19 | // 文件覆盖 20 | const putPolicy = new qiniu.rs.PutPolicy({ 21 | scope: `${bucket}:${key}` 22 | }) 23 | const mac = new qiniu.auth.digest.Mac(accessKey, secretKey) 24 | const uploadToken = putPolicy.uploadToken(mac) 25 | 26 | // ReadableStream 对象的上传 27 | const config = new qiniu.conf.Config() 28 | config.zone = qiniu.zone.Zone_z2 29 | config.useHttpsDomain = true 30 | 31 | const formUploader = new qiniu.form_up.FormUploader(config) 32 | const putExtra = new qiniu.form_up.PutExtra() 33 | const readableStream = file 34 | 35 | const promise = new Promise((resolve, reject) => { 36 | formUploader.putStream(uploadToken, key, readableStream, putExtra, (respErr, respBody, respInfo) => { 37 | if (respErr) { 38 | reject(respErr) 39 | } 40 | 41 | if (respInfo.statusCode === 200) { 42 | const url = siteDomain + respBody.key 43 | resolve(url) 44 | } else { 45 | // 614 文件已存在 46 | reject(respInfo) 47 | } 48 | }) 49 | }) 50 | promises.push(promise) 51 | } 52 | 53 | try { 54 | return Promise.all(promises) 55 | } catch (error) { 56 | throw new Error('文件上传失败') 57 | } 58 | } 59 | } 60 | 61 | module.exports = { 62 | UpLoader 63 | } -------------------------------------------------------------------------------- /app/lib/util.js: -------------------------------------------------------------------------------- 1 | const { toSafeInteger, isInteger } = require('lodash') 2 | const { ParametersException } = require('@exception') 3 | 4 | function getSafeParamId(ctx) { 5 | const id = toSafeInteger(ctx.get('query.id')) 6 | if (!isInteger(id)) { 7 | throw new ParametersException({ 8 | msg: '路由参数错误' 9 | }) 10 | } 11 | return id 12 | } 13 | 14 | module.exports = { 15 | getSafeParamId 16 | } 17 | -------------------------------------------------------------------------------- /app/models/article.js: -------------------------------------------------------------------------------- 1 | const { sequelize } = require('../../core/db') 2 | const { Sequelize, Model } = require('sequelize') 3 | 4 | class Article extends Model { 5 | 6 | } 7 | 8 | Article.init({ 9 | title: { 10 | type: Sequelize.STRING, 11 | allowNull: false 12 | }, 13 | content: { 14 | type: Sequelize.TEXT, 15 | allowNull: false 16 | }, 17 | cover: { 18 | type: Sequelize.STRING(255), 19 | defaultValue: '' 20 | }, 21 | description: { 22 | type: Sequelize.STRING(255), 23 | allowNull: false 24 | }, 25 | category_id: { 26 | type: Sequelize.INTEGER, 27 | allowNull: false 28 | }, 29 | created_date: { 30 | type: Sequelize.DATE, 31 | allowNull: false 32 | }, 33 | public: { 34 | type: Sequelize.INTEGER, 35 | allowNull: false 36 | }, 37 | status: { 38 | type: Sequelize.INTEGER, 39 | allowNull: false 40 | }, 41 | like: { 42 | type: Sequelize.INTEGER, 43 | allowNull: false, 44 | defaultValue: 0 45 | }, 46 | star: { 47 | type: Sequelize.INTEGER, 48 | allowNull: false, 49 | defaultValue: 0 50 | }, 51 | views: { 52 | type: Sequelize.INTEGER, 53 | allowNull: false, 54 | defaultValue: 0 55 | } 56 | }, { 57 | sequelize, 58 | tableName: 'article' 59 | }) 60 | 61 | module.exports = { 62 | Article 63 | } 64 | -------------------------------------------------------------------------------- /app/models/articleAuthor.js: -------------------------------------------------------------------------------- 1 | const { sequelize } = require('../../core/db') 2 | const { Sequelize, Model } = require('sequelize') 3 | 4 | class ArticleAuthor extends Model { 5 | 6 | } 7 | 8 | ArticleAuthor.init({ 9 | article_id: { 10 | type: Sequelize.INTEGER, 11 | allowNull: false 12 | }, 13 | author_id: { 14 | type: Sequelize.INTEGER, 15 | allowNull: false 16 | } 17 | }, { 18 | sequelize, 19 | tableName: 'articleAuthor' 20 | }) 21 | 22 | module.exports = { 23 | ArticleAuthor 24 | } -------------------------------------------------------------------------------- /app/models/articleTag.js: -------------------------------------------------------------------------------- 1 | const { sequelize } = require('../../core/db') 2 | const { Sequelize, Model } = require('sequelize') 3 | 4 | class ArticleTag extends Model { 5 | 6 | } 7 | 8 | ArticleTag.init({ 9 | article_id: { 10 | type: Sequelize.INTEGER, 11 | allowNull: false 12 | }, 13 | tag_id: { 14 | type: Sequelize.INTEGER, 15 | allowNull: false 16 | } 17 | }, { 18 | sequelize, 19 | tableName: 'articleTag' 20 | }) 21 | 22 | module.exports = { 23 | ArticleTag 24 | } -------------------------------------------------------------------------------- /app/models/author.js: -------------------------------------------------------------------------------- 1 | const { Sequelize, Model } = require('sequelize') 2 | const { sequelize } = require('../../core/db') 3 | const bcrypt = require('bcryptjs') 4 | const { AuthType } = require('../lib/enums') 5 | 6 | class Author extends Model { 7 | toJSON() { 8 | let origin = { 9 | id: this.id, 10 | name: this.name, 11 | avatar: this.avatar, 12 | email: this.email, 13 | description: this.description, 14 | auth: this.auth 15 | } 16 | return origin 17 | } 18 | } 19 | 20 | Author.init({ 21 | name: { 22 | type: Sequelize.STRING(32), 23 | allowNull: false 24 | }, 25 | avatar: { 26 | type: Sequelize.STRING(255), 27 | allowNull: false, 28 | defaultValue: '', 29 | }, 30 | email: { 31 | type: Sequelize.STRING(320), 32 | allowNull: false 33 | }, 34 | description: { 35 | type: Sequelize.STRING(255), 36 | allowNull: false, 37 | }, 38 | auth: { 39 | type: Sequelize.TINYINT, 40 | allowNull: false, 41 | defaultValue: AuthType.USER 42 | }, 43 | password: { 44 | type: Sequelize.STRING(127), 45 | allowNull: false, 46 | set(origin) { 47 | const salt = bcrypt.genSaltSync(10) 48 | const val = bcrypt.hashSync(origin, salt) 49 | this.setDataValue('password', val) 50 | }, 51 | get() { 52 | return this.getDataValue('password'); 53 | } 54 | } 55 | }, { 56 | sequelize, 57 | tableName: 'author' 58 | }) 59 | 60 | module.exports = { 61 | Author 62 | } -------------------------------------------------------------------------------- /app/models/category.js: -------------------------------------------------------------------------------- 1 | const { sequelize } = require('../../core/db') 2 | const { Sequelize, Model } = require('sequelize') 3 | 4 | class Category extends Model { 5 | toJSON() { 6 | let origin = { 7 | id: this.id, 8 | name: this.name, 9 | description: this.description, 10 | cover: this.cover 11 | } 12 | return origin 13 | } 14 | } 15 | 16 | Category.init({ 17 | name: { 18 | type: Sequelize.STRING(64), 19 | allowNull: false 20 | }, 21 | description: { 22 | type: Sequelize.STRING(255), 23 | allowNull: false, 24 | }, 25 | cover: { 26 | type: Sequelize.STRING(255), 27 | allowNull: false 28 | }, 29 | }, { 30 | sequelize, 31 | tableName: 'category' 32 | }) 33 | 34 | module.exports = { 35 | Category 36 | } 37 | -------------------------------------------------------------------------------- /app/models/comment.js: -------------------------------------------------------------------------------- 1 | const { sequelize } = require('../../core/db') 2 | const { Sequelize, Model } = require('sequelize') 3 | 4 | class Comment extends Model { 5 | 6 | } 7 | 8 | Comment.init({ 9 | parent_id: { 10 | type: Sequelize.INTEGER, 11 | defaultValue: 0, 12 | allowNull: false, 13 | }, 14 | nickname: { 15 | type: Sequelize.STRING(32), 16 | allowNull: false 17 | }, 18 | content: { 19 | type: Sequelize.STRING(1023), 20 | allowNull: false 21 | }, 22 | like: { 23 | type: Sequelize.INTEGER, 24 | defaultValue: 0, 25 | allowNull: false 26 | }, 27 | email: { 28 | type: Sequelize.STRING(320), 29 | allowNull: true 30 | }, 31 | website: { 32 | type: Sequelize.STRING(255), 33 | allowNull: true, 34 | }, 35 | article_id: { 36 | type: Sequelize.INTEGER, 37 | allowNull: false 38 | } 39 | }, { 40 | sequelize, 41 | tableName: 'comment' 42 | }) 43 | 44 | module.exports = { 45 | Comment 46 | } 47 | -------------------------------------------------------------------------------- /app/models/friend.js: -------------------------------------------------------------------------------- 1 | const { sequelize } = require('../../core/db') 2 | const { Sequelize, Model } = require('sequelize') 3 | 4 | class Friend extends Model { 5 | toJSON() { 6 | let origin = { 7 | id: this.id, 8 | name: this.name, 9 | link: this.link, 10 | avatar: this.avatar 11 | } 12 | return origin 13 | } 14 | } 15 | 16 | Friend.init({ 17 | name: { 18 | type: Sequelize.STRING(64), 19 | allowNull: false 20 | }, 21 | link: { 22 | type: Sequelize.STRING(255), 23 | allowNull: false 24 | }, 25 | avatar: { 26 | type: Sequelize.STRING(255), 27 | defaultValue: '' 28 | }, 29 | }, { 30 | sequelize, 31 | tableName: 'friend' 32 | }) 33 | 34 | module.exports = { 35 | Friend 36 | } 37 | -------------------------------------------------------------------------------- /app/models/index.js: -------------------------------------------------------------------------------- 1 | const { Article } = require('./article') 2 | const { ArticleAuthor } = require('./articleAuthor') 3 | const { ArticleTag } = require('./articleTag') 4 | const { Author } = require('./author') 5 | const { Category } = require('./category') 6 | const { Comment } = require('./comment') 7 | const { Message } = require('./message') 8 | const { Tag } = require('./tag') 9 | const { Friend } = require('./friend') 10 | 11 | // 关联作者和评论 12 | Article.hasMany(Comment, { 13 | as: 'comments', 14 | constraints: false, 15 | }) 16 | 17 | // 关联作者和分类 18 | Article.belongsTo(Category, { 19 | foreignKey: 'category_id', 20 | as: 'category', 21 | constraints: false, 22 | }) 23 | 24 | // 关联文章和作者 25 | Article.belongsToMany(Author, { 26 | through: { 27 | model: ArticleAuthor, 28 | unique: false 29 | }, 30 | foreignKey: 'article_id', 31 | constraints: false, 32 | as: 'authors' 33 | }) 34 | 35 | Author.belongsToMany(Article, { 36 | through: { 37 | model: ArticleAuthor, 38 | unique: false 39 | }, 40 | foreignKey: 'author_id', 41 | constraints: false 42 | }) 43 | 44 | // 关联文章和标签 45 | Article.belongsToMany(Tag, { 46 | through: { 47 | model: ArticleTag, 48 | unique: false 49 | }, 50 | foreignKey: 'article_id', 51 | constraints: false, 52 | as: 'tags' 53 | }) 54 | 55 | Tag.belongsToMany(Article, { 56 | through: { 57 | model: ArticleTag, 58 | unique: false 59 | }, 60 | foreignKey: 'tag_id', 61 | constraints: false 62 | }) 63 | 64 | module.exports = { 65 | Article, 66 | ArticleAuthor, 67 | ArticleTag, 68 | Author, 69 | Category, 70 | Comment, 71 | Message, 72 | Tag, 73 | Friend 74 | } 75 | -------------------------------------------------------------------------------- /app/models/message.js: -------------------------------------------------------------------------------- 1 | const { sequelize } = require('../../core/db') 2 | const { Sequelize, Model } = require('sequelize') 3 | 4 | class Message extends Model { 5 | toJSON() { 6 | let origin = { 7 | id: this.id, 8 | nickname: this.nickname, 9 | content: this.content, 10 | createTime: this.createTime 11 | } 12 | return origin 13 | } 14 | } 15 | 16 | Message.init({ 17 | nickname: { 18 | type: Sequelize.STRING(32) 19 | }, 20 | content: { 21 | type: Sequelize.STRING(1023), 22 | allowNull: false 23 | } 24 | }, { 25 | sequelize, 26 | tableName: 'message', 27 | getterMethods: { 28 | createTime() { 29 | return this.getDataValue('created_at') 30 | } 31 | } 32 | }) 33 | 34 | module.exports = { 35 | Message 36 | } -------------------------------------------------------------------------------- /app/models/tag.js: -------------------------------------------------------------------------------- 1 | const { sequelize } = require('../../core/db') 2 | const { Sequelize, Model } = require('sequelize') 3 | 4 | class Tag extends Model { 5 | toJSON() { 6 | let origin = { 7 | id: this.id, 8 | name: this.name 9 | } 10 | return origin 11 | } 12 | } 13 | 14 | Tag.init({ 15 | name: { 16 | type: Sequelize.STRING(64), 17 | allowNull: false 18 | } 19 | }, { 20 | sequelize, 21 | tableName: 'tag' 22 | }) 23 | 24 | module.exports = { 25 | Tag 26 | } -------------------------------------------------------------------------------- /app/validators/article.js: -------------------------------------------------------------------------------- 1 | const { LinValidator, Rule } = require('../../core/lin-validator') 2 | const { PositiveIntegerValidator, PaginateValidator } = require('./common') 3 | 4 | class CreateOrUpdateArticleValidator extends LinValidator { 5 | constructor() { 6 | super() 7 | this.validateTags = checkTags 8 | this.validateAuthors = checkAuthors 9 | this.title = [ 10 | new Rule('isLength', '标题必须在1~64个字符之间', { 11 | min: 1, 12 | max: 64 13 | }) 14 | ] 15 | this.content = [ 16 | new Rule('isLength', '文章内容不能为空', { 17 | min: 1 18 | }) 19 | ] 20 | this.cover = [ 21 | new Rule('isOptional'), 22 | new Rule('isURL', '不符合URL规范') 23 | ] 24 | this.createdDate = new Rule('isNotEmpty', '创建时间不能为空'); 25 | this.categoryId = [ 26 | new Rule('isInt', '分类ID需要是正整数', { 27 | min: 1 28 | }) 29 | ], 30 | this.status = [ 31 | new Rule('isInt', '文章状态需要为正整数', { 32 | min: 1 33 | }) 34 | ], 35 | this.public = [ 36 | new Rule('isInt', '文章公开需要为正整数', { 37 | min: 1 38 | }) 39 | ] 40 | this.star = [ 41 | new Rule('isInt', '文章精选需要为正整数', { 42 | min: 1 43 | }) 44 | ] 45 | } 46 | } 47 | 48 | function checkTags(val) { 49 | let tags = val.body.tags 50 | if (!tags) { 51 | throw new Error('tags是必须参数') 52 | } 53 | try { 54 | if (typeof tags === 'string') { 55 | tags = JSON.parse(tags) 56 | } 57 | } catch (error) { 58 | throw new Error('tags参数不合法') 59 | } 60 | if (!Array.isArray(tags)) { 61 | throw new Error('tags必须是元素都为正整数的数组') 62 | } 63 | } 64 | 65 | function checkAuthors(val) { 66 | let authors = val.body.authors 67 | if (!authors) { 68 | throw new Error('authors是必须参数') 69 | } 70 | try { 71 | if (typeof tags === 'string') { 72 | authors = JSON.parse(authors) 73 | } 74 | } catch (error) { 75 | throw new Error('authors参数不合法') 76 | } 77 | if (!Array.isArray(authors)) { 78 | throw new Error('authors必须是元素都为正整数的数组') 79 | } 80 | } 81 | 82 | class GetArticlesValidator extends PaginateValidator { 83 | constructor() { 84 | super() 85 | this.categoryId = [ 86 | new Rule('isInt', '需要是整数', { 87 | min: 0 88 | }) 89 | ] 90 | this.authorId = [ 91 | new Rule('isInt', '需要是整数', { 92 | min: 0 93 | }) 94 | ] 95 | this.tagId = [ 96 | new Rule('isInt', '需要是整数', { 97 | min: 0 98 | }) 99 | ] 100 | this.publicId = [ 101 | new Rule('isInt', '需要是整数', { 102 | min: 0 103 | }) 104 | ] 105 | this.statusId = [ 106 | new Rule('isInt', '需要是整数', { 107 | min: 0 108 | }) 109 | ] 110 | this.starId = [ 111 | new Rule('isInt', '需要是整数', { 112 | min: 0 113 | }) 114 | ] 115 | this.search = [ 116 | new Rule('isOptional'), 117 | new Rule('isLength', '关键词必须在1~10个字符之间', { 118 | min: 1, 119 | max: 10 120 | }) 121 | ] 122 | } 123 | } 124 | 125 | class SearchArticlesValidator extends PaginateValidator { 126 | constructor() { 127 | super() 128 | this.search = [ 129 | new Rule('isLength', '关键词必须在1~10个字符之间', { 130 | min: 1, 131 | max: 10 132 | }) 133 | ] 134 | } 135 | } 136 | 137 | class SetPublicValidator extends PositiveIntegerValidator { 138 | constructor() { 139 | super() 140 | this.publicId = [ 141 | new Rule('isInt', '需要是整数', { 142 | min: 0 143 | }) 144 | ] 145 | } 146 | } 147 | 148 | class SetStarValidator extends PositiveIntegerValidator { 149 | constructor() { 150 | super() 151 | this.starId = [ 152 | new Rule('isInt', '需要是整数', { 153 | min: 0 154 | }) 155 | ] 156 | } 157 | } 158 | 159 | class CreateCommentValidator extends PositiveIntegerValidator { 160 | constructor() { 161 | super() 162 | this.nickname = [ 163 | new Rule('isLength', '昵称必须在1~32个字符之间', { 164 | min: 1, 165 | max: 32 166 | }) 167 | ], 168 | this.content = [ 169 | new Rule('isLength', '内容必须在1~1023个字符之间', { 170 | min: 1, 171 | max: 1023 172 | }) 173 | ] 174 | this.email = [ 175 | new Rule('isOptional'), 176 | new Rule('isEmail', '不符合Email规范') 177 | ] 178 | this.website = [ 179 | new Rule('isOptional'), 180 | new Rule('isURL', '不符合URL规范') 181 | ] 182 | } 183 | } 184 | 185 | class ReplyCommentValidator extends CreateCommentValidator { 186 | constructor() { 187 | super() 188 | this.parentId = [ 189 | new Rule('isInt', '需要是正整数', { 190 | min: 1 191 | }) 192 | ] 193 | } 194 | } 195 | 196 | 197 | module.exports = { 198 | CreateOrUpdateArticleValidator, 199 | CreateCommentValidator, 200 | ReplyCommentValidator, 201 | GetArticlesValidator, 202 | SetPublicValidator, 203 | SetStarValidator, 204 | SearchArticlesValidator 205 | } -------------------------------------------------------------------------------- /app/validators/author.js: -------------------------------------------------------------------------------- 1 | const { LinValidator, Rule } = require('../../core/lin-validator') 2 | const { AuthType } = require('../lib/enums') 3 | 4 | class UpdateAuthorValidator extends LinValidator { 5 | constructor() { 6 | super() 7 | this.email = [ 8 | new Rule('isEmail', '不符合Email规范') 9 | ] 10 | this.avatar = [ 11 | new Rule('isURL', '不符合URL规范') 12 | ], 13 | this.description = [ 14 | new Rule('isLength', '描述长度为1~255个字符', { 15 | min: 1, 16 | max: 255 17 | }) 18 | ] 19 | this.validateAuth = checkAuth 20 | } 21 | } 22 | 23 | function checkAuth(val) { 24 | let auth = val.body.auth 25 | if (!auth) { 26 | throw new Error('auth是必须参数') 27 | } 28 | auth = parseInt(auth) 29 | if (!AuthType.isThisType(auth)) { 30 | throw new Error('auth参数不合法') 31 | } 32 | } 33 | 34 | class CreateAuthorValidator extends UpdateAuthorValidator { 35 | constructor() { 36 | super() 37 | this.name = [ 38 | new Rule('isLength', '昵称长度为4~32个字符', { 39 | min: 4, 40 | max: 32 41 | }) 42 | ] 43 | this.password = [ 44 | new Rule('isLength', '密码长度为6~32个字符', { 45 | min: 6, 46 | max: 32 47 | }), 48 | new Rule('matches', '密码不符合规范', '^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]') 49 | ] 50 | } 51 | } 52 | 53 | class PasswordValidator extends LinValidator { 54 | constructor() { 55 | super() 56 | this.password = [ 57 | new Rule('isLength', '密码长度为6~32个字符', { 58 | min: 6, 59 | max: 32 60 | }), 61 | new Rule('matches', '密码不符合规范', '^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]') 62 | ] 63 | } 64 | } 65 | 66 | class SelfPasswordValidator extends PasswordValidator { 67 | constructor() { 68 | super() 69 | this.oldPassword = [ 70 | new Rule('isLength', '密码长度为6~32个字符', { 71 | min: 6, 72 | max: 32 73 | }), 74 | new Rule('matches', '密码不符合规范', '^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]') 75 | ] 76 | } 77 | } 78 | 79 | class LoginValidator extends LinValidator { 80 | constructor() { 81 | super() 82 | this.nickname = new Rule('isNotEmpty', '昵称不可为空'); 83 | this.password = new Rule('isNotEmpty', '密码不可为空'); 84 | } 85 | } 86 | 87 | class AvatarUpdateValidator extends LinValidator { 88 | constructor() { 89 | super() 90 | this.avatar = new Rule('isNotEmpty', '必须传入头像的url链接'); 91 | } 92 | } 93 | 94 | module.exports = { 95 | SelfPasswordValidator, 96 | CreateAuthorValidator, 97 | LoginValidator, 98 | UpdateAuthorValidator, 99 | PasswordValidator, 100 | AvatarUpdateValidator 101 | } -------------------------------------------------------------------------------- /app/validators/blog.js: -------------------------------------------------------------------------------- 1 | const { LinValidator, Rule } = require('../../core/lin-validator') 2 | 3 | class CreateOrUpdateFriendValidator extends LinValidator { 4 | constructor() { 5 | super() 6 | this.name = [ 7 | new Rule('isLength', '友链必须在1~64个字符之间', { 8 | min: 1, 9 | max: 64 10 | }) 11 | ], 12 | this.link = [ 13 | new Rule('isURL', '不符合URL规范') 14 | ], 15 | this.avatar = [ 16 | new Rule('isOptional'), 17 | new Rule('isURL', '不符合URL规范') 18 | ] 19 | } 20 | } 21 | 22 | module.exports = { 23 | CreateOrUpdateFriendValidator 24 | } 25 | -------------------------------------------------------------------------------- /app/validators/category.js: -------------------------------------------------------------------------------- 1 | const { LinValidator, Rule } = require('../../core/lin-validator') 2 | 3 | class CreateOrUpdateCategoryValidator extends LinValidator { 4 | constructor() { 5 | super() 6 | this.name = [ 7 | new Rule('isLength', '分类名必须在1~64个字符之间', { 8 | min: 1, 9 | max: 64 10 | }) 11 | ] 12 | this.cover = [ 13 | new Rule('isURL', '不符合URL规范') 14 | ] 15 | this.description = [ 16 | new Rule('isLength', '分类描述必须在1~255个字符之间', { 17 | min: 1, 18 | max: 255 19 | }) 20 | ] 21 | } 22 | } 23 | 24 | module.exports = { 25 | CreateOrUpdateCategoryValidator 26 | } 27 | -------------------------------------------------------------------------------- /app/validators/common.js: -------------------------------------------------------------------------------- 1 | const { LinValidator, Rule } = require('../../core/lin-validator') 2 | 3 | class PositiveIntegerValidator extends LinValidator { 4 | constructor() { 5 | super() 6 | this.id = [ 7 | new Rule('isInt', '需要是正整数', { 8 | min: 1 9 | }) 10 | ] 11 | } 12 | } 13 | 14 | class PaginateValidator extends LinValidator { 15 | constructor() { 16 | super() 17 | this.page = [ 18 | new Rule('isOptional', '', global.config.paginate.pageDefault), 19 | new Rule('isInt', 'page必须为整数,且大于等于0', { 20 | min: 0 21 | }) 22 | ] 23 | this.count = [ 24 | new Rule('isOptional', '', global.config.paginate.countDefault), 25 | new Rule('isInt', 'count必须为正整数', { 26 | min: 1 27 | }) 28 | ] 29 | } 30 | } 31 | 32 | class NotEmptyValidator extends LinValidator { 33 | constructor() { 34 | super() 35 | this.token = [ 36 | new Rule('isLength', '不允许为空', { 37 | min: 1 38 | }) 39 | ] 40 | } 41 | } 42 | 43 | 44 | module.exports = { 45 | PositiveIntegerValidator, 46 | PaginateValidator, 47 | NotEmptyValidator 48 | } -------------------------------------------------------------------------------- /app/validators/message.js: -------------------------------------------------------------------------------- 1 | const { LinValidator, Rule } = require('../../core/lin-validator') 2 | 3 | class CreateMessageValidator extends LinValidator { 4 | constructor() { 5 | super() 6 | this.nickname = [ 7 | new Rule('isOptional'), 8 | new Rule('isLength', '昵称必须在1~32个字符之间', { 9 | min: 1, 10 | max: 32 11 | }) 12 | ], 13 | this.content = [ 14 | new Rule('isLength', '内容必须在1~1023个字符之间', { 15 | min: 1, 16 | max: 1023 17 | }) 18 | ] 19 | } 20 | } 21 | 22 | module.exports = { 23 | CreateMessageValidator 24 | } 25 | -------------------------------------------------------------------------------- /app/validators/tag.js: -------------------------------------------------------------------------------- 1 | const { LinValidator, Rule } = require('../../core/lin-validator') 2 | 3 | class CreateOrUpdateTagValidator extends LinValidator { 4 | constructor() { 5 | super() 6 | this.name = [ 7 | new Rule('isLength', '标签名必须在1~64个字符之间', { 8 | min: 1, 9 | max: 64 10 | }) 11 | ] 12 | } 13 | } 14 | 15 | module.exports = { 16 | CreateOrUpdateTagValidator 17 | } 18 | -------------------------------------------------------------------------------- /config/config.js.sample: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | environment: 'dev', 3 | database: { 4 | dbName: 'blog', 5 | host: 'localhost', 6 | port: 3306, 7 | user: '', 8 | password: '', 9 | logging: false, 10 | timezone: '+08:00' 11 | }, 12 | paginate: { 13 | pageDefault: 0, // 默认页码 14 | countDefault: 10 // 默认一页的数量 15 | }, 16 | // JWT 17 | security: { 18 | // secretKey 需要比较复杂的字符串 19 | secretKey: 'secretKey', 20 | accessExp: 60 * 60, // 1h 21 | // accessExp: 20, // 20s 测试令牌过期 22 | // refreshExp 设置refresh_token的过期时间,默认一个月 23 | refreshExp: 60 * 60 * 24 * 30, 24 | }, 25 | // 文件上传 26 | file: { 27 | singleLimit: 1024 * 1024 * 2, // 单个文件大小限制 28 | totalLimit: 1024 * 1024 * 20, // 多个文件大小限制 29 | count: 10, // 单次最大上传数量 30 | exclude: [] // 禁止上传格式 31 | // include:[] 32 | }, 33 | // 七牛相关配置 34 | qiniu: { 35 | accessKey: '', 36 | secretKey: '', 37 | bucket: '', 38 | siteDomain: '' 39 | }, 40 | host: 'http://localhost:3000' 41 | } 42 | -------------------------------------------------------------------------------- /core/db.js: -------------------------------------------------------------------------------- 1 | const { Sequelize, Model } = require('sequelize') 2 | const { unset, clone, isArray } = require('lodash') 3 | 4 | const { 5 | dbName, 6 | host, 7 | port, 8 | user, 9 | password, 10 | logging, 11 | timezone 12 | } = require('../config/config').database 13 | 14 | const sequelize = new Sequelize(dbName, user, password, { 15 | dialect: 'mysql', 16 | host, 17 | port, 18 | logging, 19 | timezone, 20 | define: { 21 | timestamps: true, 22 | paranoid: true, 23 | createdAt: 'created_at', 24 | updatedAt: 'updated_at', 25 | deletedAt: 'deleted_at', 26 | underscored: true, 27 | scopes: { 28 | bh: { 29 | attributes: { 30 | exclude: ['updated_at', 'deleted_at', 'created_at'] 31 | }, 32 | }, 33 | frontShow: { 34 | where: { 35 | public: 1, 36 | status: 1 37 | } 38 | } 39 | } 40 | } 41 | }) 42 | 43 | // 设为 true 会重新创建数据表 44 | sequelize.sync({ 45 | force: false 46 | }) 47 | 48 | // 全局序列化 49 | Model.prototype.toJSON = function () { 50 | let data = clone(this.dataValues) 51 | unset(data, 'updated_at') 52 | unset(data, 'created_at') 53 | unset(data, 'deleted_at') 54 | 55 | if (isArray(this.exclude)) { 56 | this.exclude.forEach(value => { 57 | unset(data, value) 58 | }) 59 | } 60 | return data 61 | } 62 | 63 | module.exports = { 64 | sequelize 65 | } 66 | -------------------------------------------------------------------------------- /core/http-exception.js: -------------------------------------------------------------------------------- 1 | class HttpException extends Error { 2 | constructor(msg = '服务器异常', errorCode = 10000, code = 400) { 3 | super() 4 | this.msg = msg 5 | this.errorCode = errorCode 6 | this.code = code 7 | } 8 | } 9 | 10 | class ParameterException extends HttpException { 11 | constructor(msg, errorCode) { 12 | super() 13 | this.msg = msg || '参数错误' 14 | this.errorCode = errorCode || 10000 15 | this.code = 400 16 | } 17 | } 18 | 19 | class Success extends HttpException { 20 | constructor(msg, errorCode) { 21 | super() 22 | this.msg = msg || 'ok' 23 | this.errorCode = errorCode || 0 24 | this.code = 201 25 | } 26 | } 27 | 28 | class NotFound extends HttpException { 29 | constructor(msg, errorCode) { 30 | super() 31 | this.msg = msg || '资源不存在' 32 | this.errorCode = errorCode || 10004 33 | this.code = 404 34 | } 35 | } 36 | 37 | class Forbidden extends HttpException { 38 | constructor(msg, errorCode) { 39 | super() 40 | this.msg = msg || '禁止访问' 41 | this.errorCode = errorCode || 10003 42 | this.code = 403 43 | } 44 | } 45 | 46 | class AuthFailed extends HttpException { 47 | constructor(msg, errorCode) { 48 | super() 49 | this.msg = msg || '认证失败' 50 | this.errorCode = errorCode || 10010 51 | this.code = 401 52 | } 53 | } 54 | 55 | class InvalidToken extends HttpException { 56 | constructor(msg, errorCode) { 57 | super() 58 | this.msg = msg || '令牌失效' 59 | this.errorCode = errorCode || 10020 60 | this.code = 401 61 | } 62 | } 63 | 64 | class ExpiredToken extends HttpException { 65 | constructor(msg, errorCode) { 66 | super() 67 | this.msg = msg || '令牌过期' 68 | this.errorCode = errorCode || 10030 69 | this.code = 422 70 | } 71 | } 72 | 73 | class RefreshException extends HttpException { 74 | constructor(msg, errorCode) { 75 | super() 76 | this.msg = msg || 'refresh token 获取失败' 77 | this.errorCode = errorCode || 10100 78 | this.code = 401 79 | } 80 | } 81 | 82 | class FileTooLargeException extends HttpException { 83 | constructor(msg, errorCode) { 84 | super() 85 | this.msg = msg || '文件体积过大' 86 | this.errorCode = errorCode || 10110 87 | this.code = 413 88 | } 89 | } 90 | 91 | class FileTooManyException extends HttpException { 92 | constructor(msg, errorCode) { 93 | super() 94 | this.msg = msg || '文件数量过多' 95 | this.errorCode = errorCode || 10120 96 | this.code = 413 97 | } 98 | } 99 | 100 | class FileExtensionException extends HttpException { 101 | constructor(msg, errorCode) { 102 | super() 103 | this.msg = msg || '文件扩展名不符合规范' 104 | this.errorCode = errorCode || 10130 105 | this.code = 401 106 | } 107 | } 108 | 109 | module.exports = { 110 | HttpException, 111 | ParameterException, 112 | Success, 113 | NotFound, 114 | Forbidden, 115 | AuthFailed, 116 | InvalidToken, 117 | ExpiredToken, 118 | RefreshException, 119 | FileTooLargeException, 120 | FileTooManyException, 121 | FileExtensionException, 122 | } -------------------------------------------------------------------------------- /core/init.js: -------------------------------------------------------------------------------- 1 | const requireDirectory = require('require-directory') 2 | const Router = require('koa-router') 3 | 4 | class InitManager { 5 | static initCore(app) { 6 | // 入口 7 | InitManager.app = app 8 | InitManager.initLoadRoutes() 9 | InitManager.loadConfig() 10 | } 11 | 12 | static loadConfig(path = '') { 13 | const configPath = path || process.cwd() + '/config/config.js' 14 | const config = require(configPath) 15 | global.config = config 16 | } 17 | 18 | static initLoadRoutes() { 19 | const appDirectory = `${process.cwd()}/app/api` 20 | requireDirectory(module, appDirectory, { 21 | visit: whenLoadingModule 22 | }) 23 | 24 | function whenLoadingModule(obj) { 25 | if (obj instanceof Router) { 26 | InitManager.app.use(obj.routes()) 27 | } 28 | } 29 | } 30 | } 31 | 32 | module.exports = InitManager -------------------------------------------------------------------------------- /core/lin-validator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Lin-Validator v2 3 | */ 4 | 5 | const validator = require('validator') 6 | const { 7 | ParameterException 8 | } = require('./http-exception') 9 | const { 10 | get, 11 | last, 12 | set, 13 | cloneDeep 14 | } = require("lodash") 15 | const { 16 | findMembers 17 | } = require('./util') 18 | 19 | 20 | class LinValidator { 21 | constructor() { 22 | this.data = {} 23 | this.parsed = {} 24 | } 25 | 26 | 27 | _assembleAllParams(ctx) { 28 | return { 29 | body: ctx.request.body, 30 | query: ctx.request.query, 31 | path: ctx.params, 32 | header: ctx.request.header 33 | } 34 | } 35 | 36 | get(path, parsed = true) { 37 | if (parsed) { 38 | const value = get(this.parsed, path, null) 39 | if (value == null) { 40 | const keys = path.split('.') 41 | const key = last(keys) 42 | return get(this.parsed.default, key) 43 | } 44 | return value 45 | } else { 46 | return get(this.data, path) 47 | } 48 | } 49 | 50 | _findMembersFilter(key) { 51 | if (/validate([A-Z])\w+/g.test(key)) { 52 | return true 53 | } 54 | if (this[key] instanceof Array) { 55 | this[key].forEach(value => { 56 | const isRuleType = value instanceof Rule 57 | if (!isRuleType) { 58 | throw new Error('验证数组必须全部为Rule类型') 59 | } 60 | }) 61 | return true 62 | } 63 | return false 64 | } 65 | 66 | async validate(ctx, alias = {}) { 67 | this.alias = alias 68 | let params = this._assembleAllParams(ctx) 69 | this.data = cloneDeep(params) 70 | this.parsed = cloneDeep(params) 71 | 72 | const memberKeys = findMembers(this, { 73 | filter: this._findMembersFilter.bind(this) 74 | }) 75 | 76 | const errorMsgs = [] 77 | // const map = new Map(memberKeys) 78 | for (let key of memberKeys) { 79 | const result = await this._check(key, alias) 80 | if (!result.success) { 81 | errorMsgs.push(result.msg) 82 | } 83 | } 84 | if (errorMsgs.length != 0) { 85 | throw new ParameterException(errorMsgs) 86 | } 87 | ctx.v = this 88 | return this 89 | } 90 | 91 | async _check(key, alias = {}) { 92 | const isCustomFunc = typeof (this[key]) == 'function' ? true : false 93 | let result; 94 | if (isCustomFunc) { 95 | try { 96 | await this[key](this.data) 97 | result = new RuleResult(true) 98 | } catch (error) { 99 | result = new RuleResult(false, error.msg || error.message || '参数错误') 100 | } 101 | // 函数验证 102 | } else { 103 | // 属性验证, 数组,内有一组Rule 104 | const rules = this[key] 105 | const ruleField = new RuleField(rules) 106 | // 别名替换 107 | key = alias[key] ? alias[key] : key 108 | const param = this._findParam(key) 109 | 110 | result = ruleField.validate(param.value) 111 | 112 | if (result.pass) { 113 | // 如果参数路径不存在,往往是因为用户传了空值,而又设置了默认值 114 | if (param.path.length == 0) { 115 | set(this.parsed, ['default', key], result.legalValue) 116 | } else { 117 | set(this.parsed, param.path, result.legalValue) 118 | } 119 | } 120 | } 121 | if (!result.pass) { 122 | const msg = `${isCustomFunc ? '' : key}${result.msg}` 123 | return { 124 | msg: msg, 125 | success: false 126 | } 127 | } 128 | return { 129 | msg: 'ok', 130 | success: true 131 | } 132 | } 133 | 134 | _findParam(key) { 135 | let value 136 | value = get(this.data, ['query', key]) 137 | if (value) { 138 | return { 139 | value, 140 | path: ['query', key] 141 | } 142 | } 143 | value = get(this.data, ['body', key]) 144 | if (value) { 145 | return { 146 | value, 147 | path: ['body', key] 148 | } 149 | } 150 | value = get(this.data, ['path', key]) 151 | if (value) { 152 | return { 153 | value, 154 | path: ['path', key] 155 | } 156 | } 157 | value = get(this.data, ['header', key]) 158 | if (value) { 159 | return { 160 | value, 161 | path: ['header', key] 162 | } 163 | } 164 | return { 165 | value: null, 166 | path: [] 167 | } 168 | } 169 | } 170 | 171 | class RuleResult { 172 | constructor(pass, msg = '') { 173 | Object.assign(this, { 174 | pass, 175 | msg 176 | }) 177 | } 178 | } 179 | 180 | class RuleFieldResult extends RuleResult { 181 | constructor(pass, msg = '', legalValue = null) { 182 | super(pass, msg) 183 | this.legalValue = legalValue 184 | } 185 | } 186 | 187 | class Rule { 188 | constructor(name, msg, ...params) { 189 | Object.assign(this, { 190 | name, 191 | msg, 192 | params 193 | }) 194 | } 195 | 196 | validate(field) { 197 | if (this.name == 'isOptional') 198 | return new RuleResult(true) 199 | if (!validator[this.name](field + '', ...this.params)) { 200 | return new RuleResult(false, this.msg || this.message || '参数错误') 201 | } 202 | return new RuleResult(true, '') 203 | } 204 | } 205 | 206 | class RuleField { 207 | constructor(rules) { 208 | this.rules = rules 209 | } 210 | 211 | validate(field) { 212 | if (field == null) { 213 | // 如果字段为空 214 | const allowEmpty = this._allowEmpty() 215 | const defaultValue = this._hasDefault() 216 | if (allowEmpty) { 217 | return new RuleFieldResult(true, '', defaultValue) 218 | } else { 219 | return new RuleFieldResult(false, '字段是必填参数') 220 | } 221 | } 222 | 223 | const filedResult = new RuleFieldResult(false) 224 | for (let rule of this.rules) { 225 | let result = rule.validate(field) 226 | if (!result.pass) { 227 | filedResult.msg = result.msg 228 | filedResult.legalValue = null 229 | // 一旦一条校验规则不通过,则立即终止这个字段的验证 230 | return filedResult 231 | } 232 | } 233 | return new RuleFieldResult(true, '', this._convert(field)) 234 | } 235 | 236 | _convert(value) { 237 | for (let rule of this.rules) { 238 | if (rule.name == 'isInt') { 239 | return parseInt(value) 240 | } 241 | if (rule.name == 'isFloat') { 242 | return parseFloat(value) 243 | } 244 | if (rule.name == 'isBoolean') { 245 | return value ? true : false 246 | } 247 | } 248 | return value 249 | } 250 | 251 | _allowEmpty() { 252 | for (let rule of this.rules) { 253 | if (rule.name == 'isOptional') { 254 | return true 255 | } 256 | } 257 | return false 258 | } 259 | 260 | _hasDefault() { 261 | for (let rule of this.rules) { 262 | const defaultValue = rule.params[0] 263 | if (rule.name == 'isOptional') { 264 | return defaultValue 265 | } 266 | } 267 | } 268 | } 269 | 270 | 271 | 272 | module.exports = { 273 | Rule, 274 | LinValidator 275 | } -------------------------------------------------------------------------------- /core/multipart.js: -------------------------------------------------------------------------------- 1 | const busboy = require('co-busboy') 2 | const streamWormhole = require('stream-wormhole') 3 | const path = require('path') 4 | const { FileExtensionException, FileTooLargeException, FileTooManyException } = require('@exception') 5 | const { cloneDeep } = require('lodash') 6 | 7 | const multipart = (app) => { 8 | app.context.multipart = async function (opts) { 9 | // multipart/form-data 10 | if (!this.is('multipart')) { 11 | throw new Error('Content-Type must be multipart/*') 12 | } 13 | // field指表单中的非文件 14 | const parts = busboy(this, { autoFields: opts && opts.autoFields }) 15 | let part 16 | let totalSize = 0 17 | const files = [] 18 | while ((part = await parts()) != null ) { 19 | if (part.length) { 20 | 21 | } else { 22 | if (!part.filename) { 23 | await streamWormhole(part) 24 | } 25 | // 检查 extension 26 | const ext = path.extname(part.filename) 27 | if (!checkFileExtension(ext, opts && opts.include, opts && opts.exclude)) { 28 | throw new FileExtensionException({ 29 | msg: `不支持类型为${ext}的文件` 30 | }) 31 | } 32 | const { valid, conf } = checkSingleFileSize(part._readableState.length, opts && opts.singleLimit) 33 | if (!valid) { 34 | throw new FileTooLargeException({ 35 | msg: `文件单个大小不能超过${conf}b` 36 | }) 37 | } 38 | // 计算那总大小 39 | totalSize += part._readableState.length 40 | const tmp = cloneDeep(part) 41 | files.push(tmp) 42 | // 恢复再次接受 data 43 | part.resume() 44 | } 45 | } 46 | const { valid, conf } = checkFileCount(files.length, opts && opts.fileCount) 47 | if (!valid) { 48 | throw new FileTooManyException({ 49 | msg: `上传文件数量不能超过${conf}` 50 | }) 51 | } 52 | const { valid: valid1, conf: conf1 } = checkTotalFileSize(totalSize, opts && opts.totalLimit) 53 | if (!valid1) { 54 | throw new FileTooLargeException({ 55 | msg: `总文件体积不能超过${conf1}` 56 | }) 57 | } 58 | return files 59 | } 60 | } 61 | 62 | function checkSingleFileSize(size, singleLimit) { 63 | // 默认 2M 64 | const confSize = singleLimit ? singleLimit : global.config.file.singleLimit || 1024 * 1024 * 2 65 | return { 66 | valid: confSize > size, 67 | conf: confSize 68 | } 69 | } 70 | 71 | function checkTotalFileSize(size, totalLimit) { 72 | // 默认 20M 73 | const confSize = totalLimit ? totalLimit : global.config.file.totalLimit || 1024 * 1024 * 20 74 | return { 75 | valid: confSize > size, 76 | conf: confSize 77 | }; 78 | } 79 | 80 | function checkFileExtension(ext, include, exclude) { 81 | const fileInclude = include ? include : global.config.file.include 82 | const fileExclude = exclude ? exclude : global.config.file.exclude 83 | 84 | // 如果两者都有取fileInclude,有一者则用一者 85 | if (fileInclude && fileExclude) { 86 | if (!Array.isArray(fileInclude)) { 87 | throw new Error('file_include must an array!') 88 | } 89 | return fileInclude.includes(ext) 90 | } 91 | else if (fileInclude && !fileExclude) { 92 | // 有include,无exclude 93 | if (!Array.isArray(fileInclude)) { 94 | throw new Error('file_include must an array!') 95 | } 96 | return fileInclude.includes(ext) 97 | } 98 | else if (fileExclude && !fileInclude) { 99 | // 有exclude,无include 100 | if (!Array.isArray(fileExclude)) { 101 | throw new Error('file_exclude must an array!') 102 | } 103 | return !fileExclude.includes(ext) 104 | } 105 | else { 106 | // 二者都没有 107 | return true 108 | } 109 | } 110 | 111 | function checkFileCount(count, fileCount) { 112 | // 默认 10 113 | const confCount = fileCount ? fileCount : global.config.file.fileCount || 10 114 | return { 115 | valid: confCount > count, 116 | conf: confCount 117 | }; 118 | } 119 | 120 | module.exports = multipart -------------------------------------------------------------------------------- /core/util.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken') 2 | /*** 3 | * 4 | */ 5 | const findMembers = function (instance, { 6 | prefix, 7 | specifiedType, 8 | filter 9 | }) { 10 | // 递归函数 11 | function _find(instance) { 12 | //基线条件(跳出递归) 13 | if (instance.__proto__ === null) 14 | return [] 15 | 16 | let names = Reflect.ownKeys(instance) 17 | names = names.filter((name) => { 18 | // 过滤掉不满足条件的属性或方法名 19 | return _shouldKeep(name) 20 | }) 21 | 22 | return [...names, ..._find(instance.__proto__)] 23 | } 24 | 25 | function _shouldKeep(value) { 26 | if (filter) { 27 | if (filter(value)) { 28 | return true 29 | } 30 | } 31 | if (prefix) 32 | if (value.startsWith(prefix)) 33 | return true 34 | if (specifiedType) 35 | if (instance[value] instanceof specifiedType) 36 | return true 37 | } 38 | 39 | return _find(instance) 40 | } 41 | 42 | module.exports = { 43 | findMembers 44 | } 45 | -------------------------------------------------------------------------------- /id_rsa.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smileShirmy/smile-blog-koa/d506a6f1e41ea988c15ac18efb78b6ba5c6371bc/id_rsa.enc -------------------------------------------------------------------------------- /middleware/auth.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken') 2 | const { Author } = require('@models') 3 | const { TokenType } = require('../app/lib/enums') 4 | 5 | const { Forbidden, AuthFailed, NotFound, InvalidToken, ExpiredToken, RefreshException } = require('@exception') 6 | 7 | /** 8 | * 解析请求头 9 | * @param ctx koa 的context 10 | * @param type 令牌的类型 11 | */ 12 | async function parseHeader(ctx, type = TokenType.ACCESS) { 13 | if (!ctx.header || !ctx.header.authorization) { 14 | throw new AuthFailed({ msg: '认证失败,请检查请求令牌是否正确' }) 15 | } 16 | const parts = ctx.header.authorization.split(' ') 17 | if (parts.length === 2) { 18 | // Bearer 字段 19 | const schema = parts[0] 20 | // token 字段 21 | const token = parts[1] 22 | if (/^Bearer$/i.test(schema)) { 23 | let decode 24 | try { 25 | decode = jwt.verify(token, global.config.security.secretKey) 26 | } catch (error) { 27 | // 需要重新刷新令牌 28 | if (error.name === 'TokenExpiredError') { 29 | throw new ExpiredToken({ msg: '认证失败,token已过期' }) 30 | } else { 31 | throw new InvalidToken({ msg: '认证失败,令牌失效'}) 32 | } 33 | } 34 | 35 | if (!decode.type || decode.type !== type) { 36 | throw new AuthFailed({ msg: '请使用正确类型的令牌' }) 37 | } 38 | if (!decode.scope || decode.scope < ctx.level) { 39 | throw new Forbidden({ msg: '权限不足' }) 40 | } 41 | 42 | const author = await Author.findByPk(decode.uid) 43 | if (!author) { 44 | throw new NotFound({ msg: '没有找到相关作者' }) 45 | } 46 | 47 | // 把 author 挂在 ctx 上 48 | ctx.currentAuthor = author 49 | 50 | // 往令牌中保存数据 51 | ctx.auth = { 52 | uid: decode.uid, 53 | scope: decode.scope 54 | } 55 | } 56 | } else { 57 | throw new AuthFailed() 58 | } 59 | } 60 | 61 | /** 62 | * 生成令牌 63 | * @param {number} uid 64 | * @param {number} scope 65 | * @param {TokenType} type 66 | * @param {Object} options 67 | */ 68 | const generateToken = function (uid, scope, type = TokenType.ACCESS, options) { 69 | const secretKey = global.config.security.secretKey 70 | const token = jwt.sign({ 71 | uid, 72 | scope, 73 | type 74 | }, secretKey, { 75 | expiresIn: options.expiresIn 76 | }) 77 | return token 78 | } 79 | 80 | /** 81 | * 守卫函数,用户登陆即可访问 82 | */ 83 | const loginRequired = async function (ctx, next) { 84 | if (ctx.request.method !== 'OPTIONS') { 85 | await parseHeader(ctx, TokenType.ACCESS) 86 | await next() 87 | } else { 88 | await next() 89 | } 90 | } 91 | 92 | /** 93 | * 守卫函数,用户刷新令牌,统一异常 94 | */ 95 | const refreshTokenRequiredWithUnifyException = async function (ctx, next) { 96 | if (ctx.request.method !== 'OPTIONS') { 97 | try { 98 | await parseHeader(ctx, TokenType.REFRESH) 99 | } catch (e) { 100 | throw new RefreshException() 101 | } 102 | await next() 103 | } else { 104 | await next() 105 | } 106 | } 107 | 108 | class Auth { 109 | constructor(level) { 110 | this.level = level || 1 111 | } 112 | 113 | get m() { 114 | return async (ctx, next) => { 115 | ctx.level = this.level 116 | return await loginRequired(ctx, next) 117 | } 118 | } 119 | } 120 | 121 | class RefreshAuth { 122 | constructor(level) { 123 | this.level = level || 1 124 | } 125 | 126 | get m() { 127 | return async (ctx, next) => { 128 | ctx.level = this.level 129 | return await refreshTokenRequiredWithUnifyException(ctx, next) 130 | } 131 | } 132 | } 133 | 134 | 135 | module.exports = { 136 | Auth, 137 | RefreshAuth, 138 | generateToken 139 | } 140 | -------------------------------------------------------------------------------- /middleware/exception.js: -------------------------------------------------------------------------------- 1 | const { HttpException } = require('@exception') 2 | 3 | const catchError = async (ctx, next) => { 4 | try { 5 | await next() 6 | } catch (error) { 7 | const isHttpException = error instanceof HttpException 8 | const isDev = global.config.environment = 'dev' 9 | if (isDev && !isHttpException) { 10 | throw error 11 | } 12 | if (isHttpException) { 13 | ctx.body = { 14 | msg: error.msg, 15 | errorCode: error.errorCode, 16 | request: `${ctx.method}: ${ctx.path}` 17 | } 18 | ctx.status = error.code 19 | } 20 | else { 21 | ctx.body = { 22 | msg: 'we made a mistake O(∩_∩)O~~', 23 | errorCode: 999, 24 | request: `${ctx.method}: ${ctx.path}` 25 | } 26 | ctx.status = 500 27 | } 28 | } 29 | } 30 | 31 | module.exports = catchError -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smile-blog-koa", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/node": { 8 | "version": "12.6.2", 9 | "resolved": "https://registry.npm.taobao.org/@types/node/download/@types/node-12.6.2.tgz?cache=0&sync_timestamp=1562743044136&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fnode%2Fdownload%2F%40types%2Fnode-12.6.2.tgz", 10 | "integrity": "sha1-pczsartgYNXyDSVvsD7XQ+l3SZk=" 11 | }, 12 | "accepts": { 13 | "version": "1.3.7", 14 | "resolved": "https://registry.npm.taobao.org/accepts/download/accepts-1.3.7.tgz", 15 | "integrity": "sha1-UxvHJlF6OytB+FACHGzBXqq1B80=", 16 | "requires": { 17 | "mime-types": "~2.1.24", 18 | "negotiator": "0.6.2" 19 | } 20 | }, 21 | "address": { 22 | "version": "1.1.0", 23 | "resolved": "https://registry.npm.taobao.org/address/download/address-1.1.0.tgz", 24 | "integrity": "sha1-744EeEf80sW29QwWll+ST9mf5wk=" 25 | }, 26 | "agentkeepalive": { 27 | "version": "3.3.0", 28 | "resolved": "https://registry.npm.taobao.org/agentkeepalive/download/agentkeepalive-3.3.0.tgz", 29 | "integrity": "sha1-bV3lgpr9O+JxIgGjknX9EcZRhXw=", 30 | "requires": { 31 | "humanize-ms": "^1.2.1" 32 | } 33 | }, 34 | "any-promise": { 35 | "version": "1.3.0", 36 | "resolved": "https://registry.npm.taobao.org/any-promise/download/any-promise-1.3.0.tgz", 37 | "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" 38 | }, 39 | "axios": { 40 | "version": "0.19.0", 41 | "resolved": "https://registry.npm.taobao.org/axios/download/axios-0.19.0.tgz", 42 | "integrity": "sha1-jgm/89kSLhM/e4EByPvdAO09Krg=", 43 | "requires": { 44 | "follow-redirects": "1.5.10", 45 | "is-buffer": "^2.0.2" 46 | } 47 | }, 48 | "bcryptjs": { 49 | "version": "2.4.3", 50 | "resolved": "https://registry.npm.taobao.org/bcryptjs/download/bcryptjs-2.4.3.tgz", 51 | "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" 52 | }, 53 | "black-hole-stream": { 54 | "version": "0.0.1", 55 | "resolved": "https://registry.npm.taobao.org/black-hole-stream/download/black-hole-stream-0.0.1.tgz", 56 | "integrity": "sha1-M7ega58edFPWBBuCl0SB0hUq6kI=" 57 | }, 58 | "bluebird": { 59 | "version": "3.5.5", 60 | "resolved": "https://registry.npm.taobao.org/bluebird/download/bluebird-3.5.5.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbluebird%2Fdownload%2Fbluebird-3.5.5.tgz", 61 | "integrity": "sha1-qNCv1zJR7/u9X+OEp31zADwXpx8=" 62 | }, 63 | "buffer-equal-constant-time": { 64 | "version": "1.0.1", 65 | "resolved": "https://registry.npm.taobao.org/buffer-equal-constant-time/download/buffer-equal-constant-time-1.0.1.tgz", 66 | "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" 67 | }, 68 | "busboy": { 69 | "version": "0.2.14", 70 | "resolved": "https://registry.npm.taobao.org/busboy/download/busboy-0.2.14.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbusboy%2Fdownload%2Fbusboy-0.2.14.tgz", 71 | "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", 72 | "requires": { 73 | "dicer": "0.2.5", 74 | "readable-stream": "1.1.x" 75 | } 76 | }, 77 | "bytes": { 78 | "version": "3.1.0", 79 | "resolved": "http://registry.npm.taobao.org/bytes/download/bytes-3.1.0.tgz", 80 | "integrity": "sha1-9s95M6Ng4FiPqf3oVlHNx/gF0fY=" 81 | }, 82 | "cache-content-type": { 83 | "version": "1.0.1", 84 | "resolved": "https://registry.npm.taobao.org/cache-content-type/download/cache-content-type-1.0.1.tgz", 85 | "integrity": "sha1-A1zeKwjuISn0qDFeqPAKANuhRTw=", 86 | "requires": { 87 | "mime-types": "^2.1.18", 88 | "ylru": "^1.2.0" 89 | } 90 | }, 91 | "chan": { 92 | "version": "0.6.1", 93 | "resolved": "https://registry.npm.taobao.org/chan/download/chan-0.6.1.tgz", 94 | "integrity": "sha1-7ArRMuW8YsJ+8QzL/E2NzYygBkA=" 95 | }, 96 | "cls-bluebird": { 97 | "version": "2.1.0", 98 | "resolved": "https://registry.npm.taobao.org/cls-bluebird/download/cls-bluebird-2.1.0.tgz", 99 | "integrity": "sha1-N+8eCAqP+1XC9BZPU28ZGeeWiu4=", 100 | "requires": { 101 | "is-bluebird": "^1.0.2", 102 | "shimmer": "^1.1.0" 103 | } 104 | }, 105 | "co": { 106 | "version": "4.6.0", 107 | "resolved": "http://registry.npm.taobao.org/co/download/co-4.6.0.tgz", 108 | "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" 109 | }, 110 | "co-body": { 111 | "version": "6.0.0", 112 | "resolved": "https://registry.npm.taobao.org/co-body/download/co-body-6.0.0.tgz", 113 | "integrity": "sha1-lluTN9f1ZVSAeHRx9CN2ZIIIJ+M=", 114 | "requires": { 115 | "inflation": "^2.0.0", 116 | "qs": "^6.5.2", 117 | "raw-body": "^2.3.3", 118 | "type-is": "^1.6.16" 119 | } 120 | }, 121 | "co-busboy": { 122 | "version": "1.4.0", 123 | "resolved": "https://registry.npm.taobao.org/co-busboy/download/co-busboy-1.4.0.tgz", 124 | "integrity": "sha1-rJuFxKlm8Dt99V1TdGoNyck/p0E=", 125 | "requires": { 126 | "black-hole-stream": "~0.0.1", 127 | "busboy": "^0.2.8", 128 | "chan": "^0.6.1" 129 | } 130 | }, 131 | "content-disposition": { 132 | "version": "0.5.3", 133 | "resolved": "http://registry.npm.taobao.org/content-disposition/download/content-disposition-0.5.3.tgz", 134 | "integrity": "sha1-4TDK9+cnkIfFYWwgB9BIVpiYT70=", 135 | "requires": { 136 | "safe-buffer": "5.1.2" 137 | } 138 | }, 139 | "content-type": { 140 | "version": "1.0.4", 141 | "resolved": "http://registry.npm.taobao.org/content-type/download/content-type-1.0.4.tgz", 142 | "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=" 143 | }, 144 | "cookies": { 145 | "version": "0.7.3", 146 | "resolved": "https://registry.npm.taobao.org/cookies/download/cookies-0.7.3.tgz", 147 | "integrity": "sha1-eRLOIfvy6MLacM8cPzUa7PWdrfo=", 148 | "requires": { 149 | "depd": "~1.1.2", 150 | "keygrip": "~1.0.3" 151 | } 152 | }, 153 | "copy-to": { 154 | "version": "2.0.1", 155 | "resolved": "https://registry.npm.taobao.org/copy-to/download/copy-to-2.0.1.tgz", 156 | "integrity": "sha1-JoD7uAaKSNCGVrYJgJK9r8kG9KU=" 157 | }, 158 | "core-util-is": { 159 | "version": "1.0.2", 160 | "resolved": "http://registry.npm.taobao.org/core-util-is/download/core-util-is-1.0.2.tgz", 161 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 162 | }, 163 | "crc32": { 164 | "version": "0.2.2", 165 | "resolved": "https://registry.npm.taobao.org/crc32/download/crc32-0.2.2.tgz", 166 | "integrity": "sha1-etIg1v/c0Rn5/BJ6d3LKzqOQpLo=" 167 | }, 168 | "debug": { 169 | "version": "3.1.0", 170 | "resolved": "http://registry.npm.taobao.org/debug/download/debug-3.1.0.tgz", 171 | "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", 172 | "requires": { 173 | "ms": "2.0.0" 174 | } 175 | }, 176 | "deep-equal": { 177 | "version": "1.0.1", 178 | "resolved": "http://registry.npm.taobao.org/deep-equal/download/deep-equal-1.0.1.tgz", 179 | "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" 180 | }, 181 | "default-user-agent": { 182 | "version": "1.0.0", 183 | "resolved": "https://registry.npm.taobao.org/default-user-agent/download/default-user-agent-1.0.0.tgz", 184 | "integrity": "sha1-FsRu/cq6PtxF8k8r1IaLAbfCrcY=", 185 | "requires": { 186 | "os-name": "~1.0.3" 187 | } 188 | }, 189 | "delegates": { 190 | "version": "1.0.0", 191 | "resolved": "http://registry.npm.taobao.org/delegates/download/delegates-1.0.0.tgz", 192 | "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" 193 | }, 194 | "denque": { 195 | "version": "1.4.1", 196 | "resolved": "https://registry.npm.taobao.org/denque/download/denque-1.4.1.tgz", 197 | "integrity": "sha1-Z0T/dkHBSMP4ppwwflEjXB9KN88=" 198 | }, 199 | "depd": { 200 | "version": "1.1.2", 201 | "resolved": "http://registry.npm.taobao.org/depd/download/depd-1.1.2.tgz", 202 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 203 | }, 204 | "destroy": { 205 | "version": "1.0.4", 206 | "resolved": "http://registry.npm.taobao.org/destroy/download/destroy-1.0.4.tgz", 207 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 208 | }, 209 | "dicer": { 210 | "version": "0.2.5", 211 | "resolved": "https://registry.npm.taobao.org/dicer/download/dicer-0.2.5.tgz", 212 | "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", 213 | "requires": { 214 | "readable-stream": "1.1.x", 215 | "streamsearch": "0.1.2" 216 | } 217 | }, 218 | "digest-header": { 219 | "version": "0.0.1", 220 | "resolved": "https://registry.npm.taobao.org/digest-header/download/digest-header-0.0.1.tgz", 221 | "integrity": "sha1-Ecz23uxXZqw3l0TZAcEsuklRS+Y=", 222 | "requires": { 223 | "utility": "0.1.11" 224 | } 225 | }, 226 | "dottie": { 227 | "version": "2.0.1", 228 | "resolved": "https://registry.npm.taobao.org/dottie/download/dottie-2.0.1.tgz", 229 | "integrity": "sha1-aXrZ1yAE23V00h+JJGajwoWJNlk=" 230 | }, 231 | "ecdsa-sig-formatter": { 232 | "version": "1.0.11", 233 | "resolved": "https://registry.npm.taobao.org/ecdsa-sig-formatter/download/ecdsa-sig-formatter-1.0.11.tgz", 234 | "integrity": "sha1-rg8PothQRe8UqBfao86azQSJ5b8=", 235 | "requires": { 236 | "safe-buffer": "^5.0.1" 237 | } 238 | }, 239 | "ee-first": { 240 | "version": "1.1.1", 241 | "resolved": "http://registry.npm.taobao.org/ee-first/download/ee-first-1.1.1.tgz", 242 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 243 | }, 244 | "encodeurl": { 245 | "version": "1.0.2", 246 | "resolved": "http://registry.npm.taobao.org/encodeurl/download/encodeurl-1.0.2.tgz", 247 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 248 | }, 249 | "error-inject": { 250 | "version": "1.0.0", 251 | "resolved": "https://registry.npm.taobao.org/error-inject/download/error-inject-1.0.0.tgz", 252 | "integrity": "sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc=" 253 | }, 254 | "escape-html": { 255 | "version": "1.0.3", 256 | "resolved": "http://registry.npm.taobao.org/escape-html/download/escape-html-1.0.3.tgz", 257 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 258 | }, 259 | "follow-redirects": { 260 | "version": "1.5.10", 261 | "resolved": "http://registry.npm.taobao.org/follow-redirects/download/follow-redirects-1.5.10.tgz", 262 | "integrity": "sha1-e3qfmuov3/NnhqlP9kPtB/T/Xio=", 263 | "requires": { 264 | "debug": "=3.1.0" 265 | } 266 | }, 267 | "formstream": { 268 | "version": "1.1.0", 269 | "resolved": "https://registry.npm.taobao.org/formstream/download/formstream-1.1.0.tgz", 270 | "integrity": "sha1-UfOXDyYTbrCtRDBN5M67UCB7RHk=", 271 | "requires": { 272 | "destroy": "^1.0.4", 273 | "mime": "^1.3.4", 274 | "pause-stream": "~0.0.11" 275 | }, 276 | "dependencies": { 277 | "mime": { 278 | "version": "1.6.0", 279 | "resolved": "https://registry.npm.taobao.org/mime/download/mime-1.6.0.tgz?cache=0&sync_timestamp=1560034758817&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmime%2Fdownload%2Fmime-1.6.0.tgz", 280 | "integrity": "sha1-Ms2eXGRVO9WNGaVor0Uqz/BJgbE=" 281 | } 282 | } 283 | }, 284 | "fresh": { 285 | "version": "0.5.2", 286 | "resolved": "http://registry.npm.taobao.org/fresh/download/fresh-0.5.2.tgz", 287 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 288 | }, 289 | "generate-function": { 290 | "version": "2.3.1", 291 | "resolved": "https://registry.npm.taobao.org/generate-function/download/generate-function-2.3.1.tgz", 292 | "integrity": "sha1-8GlhdpDBDIaOc7hGV0Z2T5fDR58=", 293 | "requires": { 294 | "is-property": "^1.0.2" 295 | } 296 | }, 297 | "http-assert": { 298 | "version": "1.4.1", 299 | "resolved": "https://registry.npm.taobao.org/http-assert/download/http-assert-1.4.1.tgz", 300 | "integrity": "sha1-xfcl1neqfoc+9zYZm4lobM6zeHg=", 301 | "requires": { 302 | "deep-equal": "~1.0.1", 303 | "http-errors": "~1.7.2" 304 | } 305 | }, 306 | "http-errors": { 307 | "version": "1.7.2", 308 | "resolved": "http://registry.npm.taobao.org/http-errors/download/http-errors-1.7.2.tgz", 309 | "integrity": "sha1-T1ApzxMjnzEDblsuVSkrz7zIXI8=", 310 | "requires": { 311 | "depd": "~1.1.2", 312 | "inherits": "2.0.3", 313 | "setprototypeof": "1.1.1", 314 | "statuses": ">= 1.5.0 < 2", 315 | "toidentifier": "1.0.0" 316 | } 317 | }, 318 | "humanize-ms": { 319 | "version": "1.2.1", 320 | "resolved": "https://registry.npm.taobao.org/humanize-ms/download/humanize-ms-1.2.1.tgz", 321 | "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", 322 | "requires": { 323 | "ms": "^2.0.0" 324 | } 325 | }, 326 | "iconv-lite": { 327 | "version": "0.4.24", 328 | "resolved": "http://registry.npm.taobao.org/iconv-lite/download/iconv-lite-0.4.24.tgz", 329 | "integrity": "sha1-ICK0sl+93CHS9SSXSkdKr+czkIs=", 330 | "requires": { 331 | "safer-buffer": ">= 2.1.2 < 3" 332 | } 333 | }, 334 | "inflation": { 335 | "version": "2.0.0", 336 | "resolved": "https://registry.npm.taobao.org/inflation/download/inflation-2.0.0.tgz", 337 | "integrity": "sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8=" 338 | }, 339 | "inflection": { 340 | "version": "1.12.0", 341 | "resolved": "https://registry.npm.taobao.org/inflection/download/inflection-1.12.0.tgz", 342 | "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=" 343 | }, 344 | "inherits": { 345 | "version": "2.0.3", 346 | "resolved": "http://registry.npm.taobao.org/inherits/download/inherits-2.0.3.tgz", 347 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 348 | }, 349 | "is-bluebird": { 350 | "version": "1.0.2", 351 | "resolved": "https://registry.npm.taobao.org/is-bluebird/download/is-bluebird-1.0.2.tgz", 352 | "integrity": "sha1-CWQ5Bg9KpBGr7hkUOoTWpVNG1uI=" 353 | }, 354 | "is-buffer": { 355 | "version": "2.0.3", 356 | "resolved": "http://registry.npm.taobao.org/is-buffer/download/is-buffer-2.0.3.tgz", 357 | "integrity": "sha1-Ts8/z3ScvR5HJonhCaxmJhol5yU=" 358 | }, 359 | "is-generator-function": { 360 | "version": "1.0.7", 361 | "resolved": "https://registry.npm.taobao.org/is-generator-function/download/is-generator-function-1.0.7.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fis-generator-function%2Fdownload%2Fis-generator-function-1.0.7.tgz", 362 | "integrity": "sha1-0hMuUpuwAAp/gHlNS99c1eWBNSI=" 363 | }, 364 | "is-property": { 365 | "version": "1.0.2", 366 | "resolved": "https://registry.npm.taobao.org/is-property/download/is-property-1.0.2.tgz", 367 | "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" 368 | }, 369 | "isarray": { 370 | "version": "0.0.1", 371 | "resolved": "http://registry.npm.taobao.org/isarray/download/isarray-0.0.1.tgz", 372 | "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" 373 | }, 374 | "jsonwebtoken": { 375 | "version": "8.5.1", 376 | "resolved": "https://registry.npm.taobao.org/jsonwebtoken/download/jsonwebtoken-8.5.1.tgz", 377 | "integrity": "sha1-AOceC431TCEhofJhN98igGc7zA0=", 378 | "requires": { 379 | "jws": "^3.2.2", 380 | "lodash.includes": "^4.3.0", 381 | "lodash.isboolean": "^3.0.3", 382 | "lodash.isinteger": "^4.0.4", 383 | "lodash.isnumber": "^3.0.3", 384 | "lodash.isplainobject": "^4.0.6", 385 | "lodash.isstring": "^4.0.1", 386 | "lodash.once": "^4.0.0", 387 | "ms": "^2.1.1", 388 | "semver": "^5.6.0" 389 | }, 390 | "dependencies": { 391 | "ms": { 392 | "version": "2.1.2", 393 | "resolved": "https://registry.npm.taobao.org/ms/download/ms-2.1.2.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fms%2Fdownload%2Fms-2.1.2.tgz", 394 | "integrity": "sha1-0J0fNXtEP0kzgqjrPM0YOHKuYAk=" 395 | } 396 | } 397 | }, 398 | "jwa": { 399 | "version": "1.4.1", 400 | "resolved": "https://registry.npm.taobao.org/jwa/download/jwa-1.4.1.tgz", 401 | "integrity": "sha1-dDwymFy56YZVUw1TZBtmyGRbA5o=", 402 | "requires": { 403 | "buffer-equal-constant-time": "1.0.1", 404 | "ecdsa-sig-formatter": "1.0.11", 405 | "safe-buffer": "^5.0.1" 406 | } 407 | }, 408 | "jws": { 409 | "version": "3.2.2", 410 | "resolved": "https://registry.npm.taobao.org/jws/download/jws-3.2.2.tgz", 411 | "integrity": "sha1-ABCZ82OUaMlBQADpmZX6UvtHgwQ=", 412 | "requires": { 413 | "jwa": "^1.4.1", 414 | "safe-buffer": "^5.0.1" 415 | } 416 | }, 417 | "keygrip": { 418 | "version": "1.0.3", 419 | "resolved": "https://registry.npm.taobao.org/keygrip/download/keygrip-1.0.3.tgz", 420 | "integrity": "sha1-OZ1wnwrtK6sKBZ4M3TpQI6BT4dw=" 421 | }, 422 | "koa": { 423 | "version": "2.7.0", 424 | "resolved": "https://registry.npm.taobao.org/koa/download/koa-2.7.0.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fkoa%2Fdownload%2Fkoa-2.7.0.tgz", 425 | "integrity": "sha1-fgCENQaUK52CxswzdJ9lfG5eet8=", 426 | "requires": { 427 | "accepts": "^1.3.5", 428 | "cache-content-type": "^1.0.0", 429 | "content-disposition": "~0.5.2", 430 | "content-type": "^1.0.4", 431 | "cookies": "~0.7.1", 432 | "debug": "~3.1.0", 433 | "delegates": "^1.0.0", 434 | "depd": "^1.1.2", 435 | "destroy": "^1.0.4", 436 | "error-inject": "^1.0.0", 437 | "escape-html": "^1.0.3", 438 | "fresh": "~0.5.2", 439 | "http-assert": "^1.3.0", 440 | "http-errors": "^1.6.3", 441 | "is-generator-function": "^1.0.7", 442 | "koa-compose": "^4.1.0", 443 | "koa-convert": "^1.2.0", 444 | "koa-is-json": "^1.0.0", 445 | "on-finished": "^2.3.0", 446 | "only": "~0.0.2", 447 | "parseurl": "^1.3.2", 448 | "statuses": "^1.5.0", 449 | "type-is": "^1.6.16", 450 | "vary": "^1.1.2" 451 | } 452 | }, 453 | "koa-bodyparser": { 454 | "version": "4.2.1", 455 | "resolved": "https://registry.npm.taobao.org/koa-bodyparser/download/koa-bodyparser-4.2.1.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fkoa-bodyparser%2Fdownload%2Fkoa-bodyparser-4.2.1.tgz", 456 | "integrity": "sha1-TX2stebbEQZkm1ldnlzLFYtvOyk=", 457 | "requires": { 458 | "co-body": "^6.0.0", 459 | "copy-to": "^2.0.1" 460 | } 461 | }, 462 | "koa-compose": { 463 | "version": "4.1.0", 464 | "resolved": "https://registry.npm.taobao.org/koa-compose/download/koa-compose-4.1.0.tgz", 465 | "integrity": "sha1-UHMGuTcZAdtBEhyBLpI9DWfT6Hc=" 466 | }, 467 | "koa-convert": { 468 | "version": "1.2.0", 469 | "resolved": "https://registry.npm.taobao.org/koa-convert/download/koa-convert-1.2.0.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fkoa-convert%2Fdownload%2Fkoa-convert-1.2.0.tgz", 470 | "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", 471 | "requires": { 472 | "co": "^4.6.0", 473 | "koa-compose": "^3.0.0" 474 | }, 475 | "dependencies": { 476 | "koa-compose": { 477 | "version": "3.2.1", 478 | "resolved": "https://registry.npm.taobao.org/koa-compose/download/koa-compose-3.2.1.tgz", 479 | "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", 480 | "requires": { 481 | "any-promise": "^1.1.0" 482 | } 483 | } 484 | } 485 | }, 486 | "koa-is-json": { 487 | "version": "1.0.0", 488 | "resolved": "https://registry.npm.taobao.org/koa-is-json/download/koa-is-json-1.0.0.tgz", 489 | "integrity": "sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=" 490 | }, 491 | "koa-router": { 492 | "version": "7.4.0", 493 | "resolved": "https://registry.npm.taobao.org/koa-router/download/koa-router-7.4.0.tgz", 494 | "integrity": "sha1-ruH3rcAtXLMdfWdGXJ6syCXoxeA=", 495 | "requires": { 496 | "debug": "^3.1.0", 497 | "http-errors": "^1.3.1", 498 | "koa-compose": "^3.0.0", 499 | "methods": "^1.0.1", 500 | "path-to-regexp": "^1.1.1", 501 | "urijs": "^1.19.0" 502 | }, 503 | "dependencies": { 504 | "koa-compose": { 505 | "version": "3.2.1", 506 | "resolved": "https://registry.npm.taobao.org/koa-compose/download/koa-compose-3.2.1.tgz", 507 | "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", 508 | "requires": { 509 | "any-promise": "^1.1.0" 510 | } 511 | } 512 | } 513 | }, 514 | "koa-send": { 515 | "version": "5.0.0", 516 | "resolved": "https://registry.npm.taobao.org/koa-send/download/koa-send-5.0.0.tgz", 517 | "integrity": "sha1-XoRB4H71VzdzTXztJbhC5QZG5+s=", 518 | "requires": { 519 | "debug": "^3.1.0", 520 | "http-errors": "^1.6.3", 521 | "mz": "^2.7.0", 522 | "resolve-path": "^1.4.0" 523 | } 524 | }, 525 | "koa-static": { 526 | "version": "5.0.0", 527 | "resolved": "https://registry.npm.taobao.org/koa-static/download/koa-static-5.0.0.tgz", 528 | "integrity": "sha1-XpL8lrU3rVIZ9CUxnJW2R3J3aUM=", 529 | "requires": { 530 | "debug": "^3.1.0", 531 | "koa-send": "^5.0.0" 532 | } 533 | }, 534 | "koa2-cors": { 535 | "version": "2.0.6", 536 | "resolved": "https://registry.npm.taobao.org/koa2-cors/download/koa2-cors-2.0.6.tgz", 537 | "integrity": "sha1-mtI986C5u4RTC0b1lE8/tXYIZVQ=" 538 | }, 539 | "lodash": { 540 | "version": "4.17.14", 541 | "resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.14.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.14.tgz", 542 | "integrity": "sha1-nOSHrmbJYlT+ILWZ8htoFgKAeLo=" 543 | }, 544 | "lodash.includes": { 545 | "version": "4.3.0", 546 | "resolved": "https://registry.npm.taobao.org/lodash.includes/download/lodash.includes-4.3.0.tgz", 547 | "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" 548 | }, 549 | "lodash.isboolean": { 550 | "version": "3.0.3", 551 | "resolved": "https://registry.npm.taobao.org/lodash.isboolean/download/lodash.isboolean-3.0.3.tgz", 552 | "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" 553 | }, 554 | "lodash.isinteger": { 555 | "version": "4.0.4", 556 | "resolved": "https://registry.npm.taobao.org/lodash.isinteger/download/lodash.isinteger-4.0.4.tgz", 557 | "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" 558 | }, 559 | "lodash.isnumber": { 560 | "version": "3.0.3", 561 | "resolved": "https://registry.npm.taobao.org/lodash.isnumber/download/lodash.isnumber-3.0.3.tgz", 562 | "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" 563 | }, 564 | "lodash.isplainobject": { 565 | "version": "4.0.6", 566 | "resolved": "https://registry.npm.taobao.org/lodash.isplainobject/download/lodash.isplainobject-4.0.6.tgz", 567 | "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" 568 | }, 569 | "lodash.isstring": { 570 | "version": "4.0.1", 571 | "resolved": "https://registry.npm.taobao.org/lodash.isstring/download/lodash.isstring-4.0.1.tgz", 572 | "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" 573 | }, 574 | "lodash.once": { 575 | "version": "4.1.1", 576 | "resolved": "https://registry.npm.taobao.org/lodash.once/download/lodash.once-4.1.1.tgz", 577 | "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" 578 | }, 579 | "long": { 580 | "version": "4.0.0", 581 | "resolved": "https://registry.npm.taobao.org/long/download/long-4.0.0.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flong%2Fdownload%2Flong-4.0.0.tgz", 582 | "integrity": "sha1-mntxz7fTYaGU6lVSQckvdGjVvyg=" 583 | }, 584 | "lru-cache": { 585 | "version": "4.1.5", 586 | "resolved": "http://registry.npm.taobao.org/lru-cache/download/lru-cache-4.1.5.tgz", 587 | "integrity": "sha1-i75Q6oW+1ZvJ4z3KuCNe6bz0Q80=", 588 | "requires": { 589 | "pseudomap": "^1.0.2", 590 | "yallist": "^2.1.2" 591 | } 592 | }, 593 | "media-typer": { 594 | "version": "0.3.0", 595 | "resolved": "https://registry.npm.taobao.org/media-typer/download/media-typer-0.3.0.tgz", 596 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 597 | }, 598 | "methods": { 599 | "version": "1.1.2", 600 | "resolved": "http://registry.npm.taobao.org/methods/download/methods-1.1.2.tgz", 601 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 602 | }, 603 | "mime": { 604 | "version": "2.3.1", 605 | "resolved": "https://registry.npm.taobao.org/mime/download/mime-2.3.1.tgz?cache=0&sync_timestamp=1560034758817&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmime%2Fdownload%2Fmime-2.3.1.tgz", 606 | "integrity": "sha1-sWIcVNY7l8R9PP5/chX31kUXw2k=" 607 | }, 608 | "mime-db": { 609 | "version": "1.40.0", 610 | "resolved": "https://registry.npm.taobao.org/mime-db/download/mime-db-1.40.0.tgz", 611 | "integrity": "sha1-plBX6ZjbCQ9zKmj2wnbTh9QSbDI=" 612 | }, 613 | "mime-types": { 614 | "version": "2.1.24", 615 | "resolved": "https://registry.npm.taobao.org/mime-types/download/mime-types-2.1.24.tgz", 616 | "integrity": "sha1-tvjQs+lR77d97eyhlM/20W9nb4E=", 617 | "requires": { 618 | "mime-db": "1.40.0" 619 | } 620 | }, 621 | "minimist": { 622 | "version": "1.2.0", 623 | "resolved": "http://registry.npm.taobao.org/minimist/download/minimist-1.2.0.tgz", 624 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" 625 | }, 626 | "module-alias": { 627 | "version": "2.2.0", 628 | "resolved": "https://registry.npm.taobao.org/module-alias/download/module-alias-2.2.0.tgz", 629 | "integrity": "sha1-ouMidTgWQiUr8MUUBfegmjZ0ebU=" 630 | }, 631 | "moment": { 632 | "version": "2.24.0", 633 | "resolved": "https://registry.npm.taobao.org/moment/download/moment-2.24.0.tgz", 634 | "integrity": "sha1-DQVdU/UFKqZTyfbraLtdEr9cK1s=" 635 | }, 636 | "moment-timezone": { 637 | "version": "0.5.26", 638 | "resolved": "https://registry.npm.taobao.org/moment-timezone/download/moment-timezone-0.5.26.tgz", 639 | "integrity": "sha1-wCZ8oJroRjGqPcM/Zb7b5ujg13I=", 640 | "requires": { 641 | "moment": ">= 2.9.0" 642 | } 643 | }, 644 | "ms": { 645 | "version": "2.0.0", 646 | "resolved": "https://registry.npm.taobao.org/ms/download/ms-2.0.0.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fms%2Fdownload%2Fms-2.0.0.tgz", 647 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 648 | }, 649 | "mysql2": { 650 | "version": "1.6.5", 651 | "resolved": "https://registry.npm.taobao.org/mysql2/download/mysql2-1.6.5.tgz", 652 | "integrity": "sha1-ZpUwT6LOeT3aXJjou+xly9Lmy50=", 653 | "requires": { 654 | "denque": "^1.4.0", 655 | "generate-function": "^2.3.1", 656 | "iconv-lite": "^0.4.24", 657 | "long": "^4.0.0", 658 | "lru-cache": "^4.1.3", 659 | "named-placeholders": "^1.1.2", 660 | "seq-queue": "^0.0.5", 661 | "sqlstring": "^2.3.1" 662 | } 663 | }, 664 | "mz": { 665 | "version": "2.7.0", 666 | "resolved": "https://registry.npm.taobao.org/mz/download/mz-2.7.0.tgz", 667 | "integrity": "sha1-lQCAV6Vsr63CvGPd5/n/aVWUjjI=", 668 | "requires": { 669 | "any-promise": "^1.0.0", 670 | "object-assign": "^4.0.1", 671 | "thenify-all": "^1.0.0" 672 | } 673 | }, 674 | "named-placeholders": { 675 | "version": "1.1.2", 676 | "resolved": "https://registry.npm.taobao.org/named-placeholders/download/named-placeholders-1.1.2.tgz", 677 | "integrity": "sha1-zrH7/1C2szSStc8hTM9eOc7z0Og=", 678 | "requires": { 679 | "lru-cache": "^4.1.3" 680 | } 681 | }, 682 | "negotiator": { 683 | "version": "0.6.2", 684 | "resolved": "https://registry.npm.taobao.org/negotiator/download/negotiator-0.6.2.tgz", 685 | "integrity": "sha1-/qz3zPUlp3rpY0Q2pkiD/+yjRvs=" 686 | }, 687 | "object-assign": { 688 | "version": "4.1.1", 689 | "resolved": "http://registry.npm.taobao.org/object-assign/download/object-assign-4.1.1.tgz", 690 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 691 | }, 692 | "on-finished": { 693 | "version": "2.3.0", 694 | "resolved": "http://registry.npm.taobao.org/on-finished/download/on-finished-2.3.0.tgz", 695 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 696 | "requires": { 697 | "ee-first": "1.1.1" 698 | } 699 | }, 700 | "only": { 701 | "version": "0.0.2", 702 | "resolved": "https://registry.npm.taobao.org/only/download/only-0.0.2.tgz", 703 | "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=" 704 | }, 705 | "os-name": { 706 | "version": "1.0.3", 707 | "resolved": "https://registry.npm.taobao.org/os-name/download/os-name-1.0.3.tgz", 708 | "integrity": "sha1-GzefZINa98Wn9JizV8uVIVwVnt8=", 709 | "requires": { 710 | "osx-release": "^1.0.0", 711 | "win-release": "^1.0.0" 712 | } 713 | }, 714 | "osx-release": { 715 | "version": "1.1.0", 716 | "resolved": "https://registry.npm.taobao.org/osx-release/download/osx-release-1.1.0.tgz", 717 | "integrity": "sha1-8heRGigTaUmvG/kwiyQeJzfTzWw=", 718 | "requires": { 719 | "minimist": "^1.1.0" 720 | } 721 | }, 722 | "parseurl": { 723 | "version": "1.3.3", 724 | "resolved": "https://registry.npm.taobao.org/parseurl/download/parseurl-1.3.3.tgz", 725 | "integrity": "sha1-naGee+6NEt/wUT7Vt2lXeTvC6NQ=" 726 | }, 727 | "path-is-absolute": { 728 | "version": "1.0.1", 729 | "resolved": "http://registry.npm.taobao.org/path-is-absolute/download/path-is-absolute-1.0.1.tgz", 730 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 731 | }, 732 | "path-to-regexp": { 733 | "version": "1.7.0", 734 | "resolved": "http://registry.npm.taobao.org/path-to-regexp/download/path-to-regexp-1.7.0.tgz", 735 | "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", 736 | "requires": { 737 | "isarray": "0.0.1" 738 | } 739 | }, 740 | "pause-stream": { 741 | "version": "0.0.11", 742 | "resolved": "https://registry.npm.taobao.org/pause-stream/download/pause-stream-0.0.11.tgz", 743 | "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", 744 | "requires": { 745 | "through": "~2.3" 746 | } 747 | }, 748 | "pseudomap": { 749 | "version": "1.0.2", 750 | "resolved": "http://registry.npm.taobao.org/pseudomap/download/pseudomap-1.0.2.tgz", 751 | "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" 752 | }, 753 | "qiniu": { 754 | "version": "7.2.2", 755 | "resolved": "https://registry.npm.taobao.org/qiniu/download/qiniu-7.2.2.tgz", 756 | "integrity": "sha1-lEJRk/ax29CT/XhYMHCmTd5sxec=", 757 | "requires": { 758 | "agentkeepalive": "3.3.0", 759 | "crc32": "0.2.2", 760 | "encodeurl": "^1.0.1", 761 | "formstream": "1.1.0", 762 | "mime": "2.3.1", 763 | "tunnel-agent": "0.6.0", 764 | "urllib": "2.22.0" 765 | } 766 | }, 767 | "qs": { 768 | "version": "6.7.0", 769 | "resolved": "http://registry.npm.taobao.org/qs/download/qs-6.7.0.tgz?cache=0&other_urls=http%3A%2F%2Fregistry.npm.taobao.org%2Fqs%2Fdownload%2Fqs-6.7.0.tgz", 770 | "integrity": "sha1-QdwaAV49WB8WIXdr4xr7KHapsbw=" 771 | }, 772 | "raw-body": { 773 | "version": "2.4.0", 774 | "resolved": "https://registry.npm.taobao.org/raw-body/download/raw-body-2.4.0.tgz", 775 | "integrity": "sha1-oc5vucm8NWylLoklarWQWeE9AzI=", 776 | "requires": { 777 | "bytes": "3.1.0", 778 | "http-errors": "1.7.2", 779 | "iconv-lite": "0.4.24", 780 | "unpipe": "1.0.0" 781 | } 782 | }, 783 | "readable-stream": { 784 | "version": "1.1.14", 785 | "resolved": "https://registry.npm.taobao.org/readable-stream/download/readable-stream-1.1.14.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Freadable-stream%2Fdownload%2Freadable-stream-1.1.14.tgz", 786 | "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", 787 | "requires": { 788 | "core-util-is": "~1.0.0", 789 | "inherits": "~2.0.1", 790 | "isarray": "0.0.1", 791 | "string_decoder": "~0.10.x" 792 | } 793 | }, 794 | "require-directory": { 795 | "version": "2.1.1", 796 | "resolved": "http://registry.npm.taobao.org/require-directory/download/require-directory-2.1.1.tgz", 797 | "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" 798 | }, 799 | "resolve-path": { 800 | "version": "1.4.0", 801 | "resolved": "https://registry.npm.taobao.org/resolve-path/download/resolve-path-1.4.0.tgz", 802 | "integrity": "sha1-xL2p9e+y/OZSR4c6s2u02DT+Fvc=", 803 | "requires": { 804 | "http-errors": "~1.6.2", 805 | "path-is-absolute": "1.0.1" 806 | }, 807 | "dependencies": { 808 | "http-errors": { 809 | "version": "1.6.3", 810 | "resolved": "http://registry.npm.taobao.org/http-errors/download/http-errors-1.6.3.tgz", 811 | "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", 812 | "requires": { 813 | "depd": "~1.1.2", 814 | "inherits": "2.0.3", 815 | "setprototypeof": "1.1.0", 816 | "statuses": ">= 1.4.0 < 2" 817 | } 818 | }, 819 | "setprototypeof": { 820 | "version": "1.1.0", 821 | "resolved": "http://registry.npm.taobao.org/setprototypeof/download/setprototypeof-1.1.0.tgz", 822 | "integrity": "sha1-0L2FU2iHtv58DYGMuWLZ2RxU5lY=" 823 | } 824 | } 825 | }, 826 | "retry-as-promised": { 827 | "version": "3.2.0", 828 | "resolved": "https://registry.npm.taobao.org/retry-as-promised/download/retry-as-promised-3.2.0.tgz", 829 | "integrity": "sha1-dp9j1Ta+xHg1SdsHd8tW2t2dhUM=", 830 | "requires": { 831 | "any-promise": "^1.3.0" 832 | } 833 | }, 834 | "safe-buffer": { 835 | "version": "5.1.2", 836 | "resolved": "http://registry.npm.taobao.org/safe-buffer/download/safe-buffer-5.1.2.tgz", 837 | "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=" 838 | }, 839 | "safer-buffer": { 840 | "version": "2.1.2", 841 | "resolved": "http://registry.npm.taobao.org/safer-buffer/download/safer-buffer-2.1.2.tgz", 842 | "integrity": "sha1-RPoWGwGHuVSd2Eu5GAL5vYOFzWo=" 843 | }, 844 | "semver": { 845 | "version": "5.7.0", 846 | "resolved": "https://registry.npm.taobao.org/semver/download/semver-5.7.0.tgz?cache=0&sync_timestamp=1559063729249&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsemver%2Fdownload%2Fsemver-5.7.0.tgz", 847 | "integrity": "sha1-eQp89v6lRZuslhELKbYEEtyP+Ws=" 848 | }, 849 | "seq-queue": { 850 | "version": "0.0.5", 851 | "resolved": "https://registry.npm.taobao.org/seq-queue/download/seq-queue-0.0.5.tgz", 852 | "integrity": "sha1-1WgS4cAXpuTnw+Ojeh2m143TyT4=" 853 | }, 854 | "sequelize": { 855 | "version": "5.10.1", 856 | "resolved": "https://registry.npm.taobao.org/sequelize/download/sequelize-5.10.1.tgz", 857 | "integrity": "sha1-rBJhT+4v1AQXxN9YXcwouizPLFc=", 858 | "requires": { 859 | "bluebird": "^3.5.0", 860 | "cls-bluebird": "^2.1.0", 861 | "debug": "^4.1.1", 862 | "dottie": "^2.0.0", 863 | "inflection": "1.12.0", 864 | "lodash": "^4.17.11", 865 | "moment": "^2.24.0", 866 | "moment-timezone": "^0.5.21", 867 | "retry-as-promised": "^3.1.0", 868 | "semver": "^6.1.1", 869 | "sequelize-pool": "^2.3.0", 870 | "toposort-class": "^1.0.1", 871 | "uuid": "^3.2.1", 872 | "validator": "^10.11.0", 873 | "wkx": "^0.4.6" 874 | }, 875 | "dependencies": { 876 | "debug": { 877 | "version": "4.1.1", 878 | "resolved": "http://registry.npm.taobao.org/debug/download/debug-4.1.1.tgz", 879 | "integrity": "sha1-O3ImAlUQnGtYnO4FDx1RYTlmR5E=", 880 | "requires": { 881 | "ms": "^2.1.1" 882 | } 883 | }, 884 | "ms": { 885 | "version": "2.1.2", 886 | "resolved": "https://registry.npm.taobao.org/ms/download/ms-2.1.2.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fms%2Fdownload%2Fms-2.1.2.tgz", 887 | "integrity": "sha1-0J0fNXtEP0kzgqjrPM0YOHKuYAk=" 888 | }, 889 | "semver": { 890 | "version": "6.2.0", 891 | "resolved": "https://registry.npm.taobao.org/semver/download/semver-6.2.0.tgz", 892 | "integrity": "sha1-TYE9lZCq+KkZJpPWyFuTRN5ZAds=" 893 | }, 894 | "validator": { 895 | "version": "10.11.0", 896 | "resolved": "https://registry.npm.taobao.org/validator/download/validator-10.11.0.tgz", 897 | "integrity": "sha1-ADEI6m6amHTTHMyeUAaFbM12sig=" 898 | } 899 | } 900 | }, 901 | "sequelize-pool": { 902 | "version": "2.3.0", 903 | "resolved": "https://registry.npm.taobao.org/sequelize-pool/download/sequelize-pool-2.3.0.tgz", 904 | "integrity": "sha1-ZPH+h0QigXLEdPUwYEthM75kmT0=" 905 | }, 906 | "setprototypeof": { 907 | "version": "1.1.1", 908 | "resolved": "http://registry.npm.taobao.org/setprototypeof/download/setprototypeof-1.1.1.tgz", 909 | "integrity": "sha1-fpWsskqpL1iF4KvvW6ExMw1K5oM=" 910 | }, 911 | "shimmer": { 912 | "version": "1.2.1", 913 | "resolved": "https://registry.npm.taobao.org/shimmer/download/shimmer-1.2.1.tgz", 914 | "integrity": "sha1-YQhZ994ye1h+/r9QH7QxF/mv8zc=" 915 | }, 916 | "sqlstring": { 917 | "version": "2.3.1", 918 | "resolved": "https://registry.npm.taobao.org/sqlstring/download/sqlstring-2.3.1.tgz", 919 | "integrity": "sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A=" 920 | }, 921 | "statuses": { 922 | "version": "1.5.0", 923 | "resolved": "http://registry.npm.taobao.org/statuses/download/statuses-1.5.0.tgz", 924 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 925 | }, 926 | "stream-wormhole": { 927 | "version": "1.1.0", 928 | "resolved": "https://registry.npm.taobao.org/stream-wormhole/download/stream-wormhole-1.1.0.tgz", 929 | "integrity": "sha1-MAr/Rs7VU8/sZCoFJRiFQXaTwz0=" 930 | }, 931 | "streamsearch": { 932 | "version": "0.1.2", 933 | "resolved": "https://registry.npm.taobao.org/streamsearch/download/streamsearch-0.1.2.tgz", 934 | "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" 935 | }, 936 | "string_decoder": { 937 | "version": "0.10.31", 938 | "resolved": "http://registry.npm.taobao.org/string_decoder/download/string_decoder-0.10.31.tgz", 939 | "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" 940 | }, 941 | "thenify": { 942 | "version": "3.3.0", 943 | "resolved": "https://registry.npm.taobao.org/thenify/download/thenify-3.3.0.tgz", 944 | "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=", 945 | "requires": { 946 | "any-promise": "^1.0.0" 947 | } 948 | }, 949 | "thenify-all": { 950 | "version": "1.6.0", 951 | "resolved": "https://registry.npm.taobao.org/thenify-all/download/thenify-all-1.6.0.tgz", 952 | "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", 953 | "requires": { 954 | "thenify": ">= 3.1.0 < 4" 955 | } 956 | }, 957 | "through": { 958 | "version": "2.3.8", 959 | "resolved": "http://registry.npm.taobao.org/through/download/through-2.3.8.tgz", 960 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" 961 | }, 962 | "toidentifier": { 963 | "version": "1.0.0", 964 | "resolved": "https://registry.npm.taobao.org/toidentifier/download/toidentifier-1.0.0.tgz", 965 | "integrity": "sha1-fhvjRw8ed5SLxD2Uo8j013UrpVM=" 966 | }, 967 | "toposort-class": { 968 | "version": "1.0.1", 969 | "resolved": "https://registry.npm.taobao.org/toposort-class/download/toposort-class-1.0.1.tgz", 970 | "integrity": "sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg=" 971 | }, 972 | "tunnel-agent": { 973 | "version": "0.6.0", 974 | "resolved": "http://registry.npm.taobao.org/tunnel-agent/download/tunnel-agent-0.6.0.tgz", 975 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 976 | "requires": { 977 | "safe-buffer": "^5.0.1" 978 | } 979 | }, 980 | "type-is": { 981 | "version": "1.6.18", 982 | "resolved": "https://registry.npm.taobao.org/type-is/download/type-is-1.6.18.tgz", 983 | "integrity": "sha1-TlUs0F3wlGfcvE73Od6J8s83wTE=", 984 | "requires": { 985 | "media-typer": "0.3.0", 986 | "mime-types": "~2.1.24" 987 | } 988 | }, 989 | "unpipe": { 990 | "version": "1.0.0", 991 | "resolved": "http://registry.npm.taobao.org/unpipe/download/unpipe-1.0.0.tgz", 992 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 993 | }, 994 | "urijs": { 995 | "version": "1.19.1", 996 | "resolved": "https://registry.npm.taobao.org/urijs/download/urijs-1.19.1.tgz", 997 | "integrity": "sha1-Ww/1MMDL3oOG9jQiNbpcpumV0lo=" 998 | }, 999 | "urllib": { 1000 | "version": "2.22.0", 1001 | "resolved": "https://registry.npm.taobao.org/urllib/download/urllib-2.22.0.tgz", 1002 | "integrity": "sha1-KWXcSuEnpvtpW32yfTGE8X2Cy0I=", 1003 | "requires": { 1004 | "any-promise": "^1.3.0", 1005 | "content-type": "^1.0.2", 1006 | "debug": "^2.6.0", 1007 | "default-user-agent": "^1.0.0", 1008 | "digest-header": "^0.0.1", 1009 | "ee-first": "~1.1.1", 1010 | "humanize-ms": "^1.2.0", 1011 | "iconv-lite": "^0.4.15", 1012 | "qs": "^6.4.0", 1013 | "statuses": "^1.3.1" 1014 | }, 1015 | "dependencies": { 1016 | "debug": { 1017 | "version": "2.6.9", 1018 | "resolved": "http://registry.npm.taobao.org/debug/download/debug-2.6.9.tgz", 1019 | "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", 1020 | "requires": { 1021 | "ms": "2.0.0" 1022 | } 1023 | } 1024 | } 1025 | }, 1026 | "utility": { 1027 | "version": "0.1.11", 1028 | "resolved": "https://registry.npm.taobao.org/utility/download/utility-0.1.11.tgz", 1029 | "integrity": "sha1-/eYM+bTkdRlHoM9dEEzik2ciZxU=", 1030 | "requires": { 1031 | "address": ">=0.0.1" 1032 | } 1033 | }, 1034 | "uuid": { 1035 | "version": "3.3.2", 1036 | "resolved": "http://registry.npm.taobao.org/uuid/download/uuid-3.3.2.tgz", 1037 | "integrity": "sha1-G0r0lV6zB3xQHCOHL8ZROBFYcTE=" 1038 | }, 1039 | "validator": { 1040 | "version": "11.1.0", 1041 | "resolved": "https://registry.npm.taobao.org/validator/download/validator-11.1.0.tgz", 1042 | "integrity": "sha1-rBjKxC4KpZArYD16XZt4J+I0asQ=" 1043 | }, 1044 | "vary": { 1045 | "version": "1.1.2", 1046 | "resolved": "http://registry.npm.taobao.org/vary/download/vary-1.1.2.tgz", 1047 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 1048 | }, 1049 | "win-release": { 1050 | "version": "1.1.1", 1051 | "resolved": "https://registry.npm.taobao.org/win-release/download/win-release-1.1.1.tgz", 1052 | "integrity": "sha1-X6VeAr58qTTt/BJmVjLoSbcuUgk=", 1053 | "requires": { 1054 | "semver": "^5.0.1" 1055 | } 1056 | }, 1057 | "wkx": { 1058 | "version": "0.4.7", 1059 | "resolved": "https://registry.npm.taobao.org/wkx/download/wkx-0.4.7.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fwkx%2Fdownload%2Fwkx-0.4.7.tgz", 1060 | "integrity": "sha1-ug5PnnhelcmXWFbBg08ZqVxlz7U=", 1061 | "requires": { 1062 | "@types/node": "*" 1063 | } 1064 | }, 1065 | "yallist": { 1066 | "version": "2.1.2", 1067 | "resolved": "http://registry.npm.taobao.org/yallist/download/yallist-2.1.2.tgz", 1068 | "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" 1069 | }, 1070 | "ylru": { 1071 | "version": "1.2.1", 1072 | "resolved": "https://registry.npm.taobao.org/ylru/download/ylru-1.2.1.tgz", 1073 | "integrity": "sha1-9Xa2M0FUeYnB3nuiiHYJI7J/6E8=" 1074 | } 1075 | } 1076 | } 1077 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smile-blog-koa", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "start:dev": "nodemon --inspect-brk", 8 | "start:prod": "node app.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/smileShirmy/smile-blog-koa.git" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/smileShirmy/smile-blog-koa/issues" 19 | }, 20 | "homepage": "https://github.com/smileShirmy/smile-blog-koa#readme", 21 | "dependencies": { 22 | "axios": "^0.19.0", 23 | "bcryptjs": "^2.4.3", 24 | "co-busboy": "^1.4.0", 25 | "jsonwebtoken": "^8.5.1", 26 | "koa": "^2.7.0", 27 | "koa-bodyparser": "^4.2.1", 28 | "koa-router": "^7.4.0", 29 | "koa-static": "^5.0.0", 30 | "koa2-cors": "^2.0.6", 31 | "lodash": "^4.17.14", 32 | "module-alias": "^2.2.0", 33 | "mysql2": "^1.6.5", 34 | "qiniu": "^7.2.2", 35 | "require-directory": "^2.1.1", 36 | "sequelize": "^5.10.1", 37 | "stream-wormhole": "^1.1.0", 38 | "validator": "^11.1.0" 39 | }, 40 | "_moduleAliases": { 41 | "@root": ".", 42 | "@models": "app/models", 43 | "@dao": "app/dao", 44 | "@validator": "app/validators", 45 | "@exception": "core/http-exception" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /script/deploy.sh: -------------------------------------------------------------------------------- 1 | pm2 stop all 2 | 3 | npm install 4 | 5 | pm2 restart all 6 | 7 | echo 'success' 8 | --------------------------------------------------------------------------------