diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f65920..56d8d46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,17 +24,18 @@ 初始化以下 API: + - 获取图形验证码 + - 上传文件 + - 获取文件 - 注册 - - 账户激活 + - 登录 - 忘记密码 - 重置密码 - - 头像上传 + - 获取当前登录用户信息 - 更新个人信息 - 修改密码 - - GitHub 登录 - - 获取图形验证码 - - 登录 - - 获取当前用户信息 + - 获取用户消息 + - 获取系统消息 - 获取积分榜用户列表 - 根据ID获取用户信息 - 获取用户动态 @@ -44,33 +45,28 @@ - 获取用户粉丝列表 - 获取用户关注列表 - 关注或者取消关注用户 - - 创建话题 - - 删除话题 - - 编辑话题 - 获取话题列表 - 搜索话题列表 - 获取无人回复的话题 + - 创建话题 + - 删除话题 + - 编辑话题 - 根据ID获取话题详情 - 喜欢或者取消喜欢话题 - 收藏或者取消收藏话题 - 创建回复 - - 删除回复 - - 编辑回复 - - 回复点赞或者取消点赞 - - 获取用户消息 - - 获取系统消息 - - 获取本周新增用户数 - - 获取上周新增用户数 - - 获取用户总数 + - 点赞回复 + - 获取系统概览 - 获取用户列表 - 新增用户 - 删除用户(超管物理删除) + - 更新用户 - 设为星标用户 - 锁定用户(封号) - - 获取本周新增话题数 - - 获取上周新增话题数 - - 获取话题总数 + - 获取话题列表 + - 创建话题 - 删除话题(超管物理删除) + - 更新话题 - 话题置顶 - 话题加精 - 话题锁定(封贴) diff --git a/packages/server/app.js b/packages/server/app.js index 859e4ab..9cf207f 100644 --- a/packages/server/app.js +++ b/packages/server/app.js @@ -1,42 +1,42 @@ const Koa = require('koa'); const koaBody = require('koa-body'); const koaJwt = require('koa-jwt'); -const path = require('path'); -const { jwt: { SECRET }, SERVER_PORT, FILE_LIMIT } = require('../../config'); +const { + jwt: { SECRET }, + SERVER_PORT, + FILE_LIMIT, +} = require('../../config'); const router = require('./router'); const logger = require('./utils/logger'); -const ErrorHandler = require('./middlewares/error-handler'); +const errorHandler = require('./middleware/error-handler'); require('./db/mongodb'); -const app = module.exports = new Koa(); +const app = (module.exports = new Koa()); // middleware app - .use(koaBody({ - multipart: true, - formidable: { - uploadDir: `${__dirname}/uploads`, - keepExtensions: true, - multiples: false, - maxFieldsSize: FILE_LIMIT, // 限制上传文件大小为 512kb - onFileBegin(name, file) { - const dir = path.dirname(file.path); - file.path = path.join(dir, file.name); - } - } - })) - .use(koaJwt({ - secret: SECRET, - passthrough: true - })) - .use(ErrorHandler.handleError); + .use( + koaBody({ + multipart: true, + formidable: { + uploadDir: `${__dirname}/upload`, + keepExtensions: true, + multiples: false, + maxFieldsSize: FILE_LIMIT, // 限制上传文件大小为 512kb + }, + }), + ) + .use( + koaJwt({ + secret: SECRET, + passthrough: true, + }), + ) + .use(errorHandler); // router -app - .use(router.rt) - .use(router.v1) - .use(router.v2); +app.use(router.rt).use(router.be); // 404 app.use(ctx => { diff --git a/packages/server/controller/aider.js b/packages/server/controller/aider.js new file mode 100644 index 0000000..14a859f --- /dev/null +++ b/packages/server/controller/aider.js @@ -0,0 +1,141 @@ +const fs = require('fs'); +const path = require('path'); +const { BMP24 } = require('gd-bmp'); +const moment = require('moment'); +const UserModel = require('../model/user'); +const TopicModel = require('../model/topic'); + +class Aider { + constructor() { + this.getCaptcha = this.getCaptcha.bind(this); + } + + // 生成随机数 + _rand(min, max) { + return (Math.random() * (max - min + 1) + min) | 0; + } + + getCaptcha(ctx) { + const { + width = 100, + height = 40, + textColor = 'a1a1a1', + bgColor = 'ffffff', + } = ctx.query; + + // 设置画布 + const img = new BMP24(width, height); + // 设置背景 + img.fillRect(0, 0, width, height, `0x${bgColor}`); + + let token = ''; + + // 随机字符列表 + const p = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'; + // 组成token + for (let i = 0; i < 5; i++) { + token += p.charAt((Math.random() * p.length) | 0); + } + + // 字符定位于背景 x,y 轴位置 + let x = 10, + y = 2; + + for (let i = 0; i < token.length; i++) { + y = 2 + this._rand(-4, 4); + // 画字符 + img.drawChar(token[i], x, y, BMP24.font12x24, `0x${textColor}`); + x += 12 + this._rand(4, 8); + } + + const url = `data:image/bmp;base64,${img.getFileData().toString('base64')}`; + + ctx.body = { token, url }; + } + + // 上传文件 + async upload(ctx) { + const { id } = ctx.state.user; + const { file } = ctx.request.files; + + if (!file) { + ctx.throw(400, '请上传文件'); + } + + const filename = `avatar_${id}${path.extname(file.path)}`; + + await new Promise((resolve, reject) => { + fs.rename(file.path, `${path.dirname(file.path)}/${filename}`, err => { + if (err) reject(err); + resolve(); + }); + }); + + ctx.body = filename; + } + + // 获取文件 + async getFile(ctx) { + const { filename } = ctx.params; + const file = path.join(__dirname, '../upload', filename); + ctx.set('Content-Type', 'image/png'); + ctx.body = fs.readFileSync(file); + } + + // 获取系统概览 + async dashboard(ctx) { + const curWeekStart = moment().startOf('week'); + const curWeekEnd = moment().endOf('week'); + const preWeekStart = moment() + .startOf('week') + .subtract(1, 'w'); + const preWeekEnd = moment() + .endOf('week') + .subtract(1, 'w'); + + // 本周新增用户数, 上周新增用户数, 用户总数 + const [curWeekAddUser, preWeekAddUser, userTotal] = await Promise.all([ + UserModel.countDocuments({ + created_at: { + $gte: curWeekStart, + $lt: curWeekEnd, + }, + }), + UserModel.countDocuments({ + created_at: { + $gte: preWeekStart, + $lt: preWeekEnd, + }, + }), + UserModel.countDocuments(), + ]); + + // 本周新增话题数, 上周新增话题数, 话题总数 + const [curWeekAddTopic, preWeekAddTopic, topicTotal] = await Promise.all([ + TopicModel.countDocuments({ + created_at: { + $gte: curWeekStart, + $lt: curWeekEnd, + }, + }), + TopicModel.countDocuments({ + created_at: { + $gte: preWeekStart, + $lt: preWeekEnd, + }, + }), + TopicModel.countDocuments(), + ]); + + ctx.body = { + curWeekAddUser, + preWeekAddUser, + userTotal, + curWeekAddTopic, + preWeekAddTopic, + topicTotal, + }; + } +} + +module.exports = new Aider(); diff --git a/packages/server/controller/topic.js b/packages/server/controller/topic.js new file mode 100644 index 0000000..7b9a517 --- /dev/null +++ b/packages/server/controller/topic.js @@ -0,0 +1,868 @@ +// const moment = require('moment'); +const { Types } = require('mongoose'); +const ActionModel = require('../model/action'); +const TopicModel = require('../model/topic'); +const UserModel = require('../model/user'); +const ReplyModel = require('../model/reply'); +const NoticeModel = require('../model/notice'); + +class Topic { + // 获取列表 + async getTopicList(ctx) { + const { tab = 'all', page = 1, size = 10 } = ctx.query; + + const query = { + is_lock: false, + is_delete: false, + }; + + if (tab === 'good') { + query.is_good = true; + } else if (tab !== 'all') { + query.tab = tab; + } + + const [list, total] = await Promise.all([ + TopicModel.aggregate([ + { $match: query }, + { + $sort: { + top: -1, + good: -1, + last_reply_at: -1, + created_at: -1, + }, + }, + { + $skip: (page - 1) * size, + }, + { + $limit: +size, + }, + { + $lookup: { + from: 'user', + localField: 'aid', + foreignField: '_id', + as: 'author', + }, + }, + { + $unwind: { + path: '$author', + preserveNullAndEmptyArrays: true, + }, + }, + { + $project: { + is_top: 1, + is_good: 1, + like_count: 1, + collect_count: 1, + reply_count: 1, + visit_count: 1, + tab: 1, + title: 1, + author_id: '$author._id', + author_name: '$author.nickname', + author_avatar: '$author.avatar', + }, + }, + ]), + TopicModel.countDocuments(query), + ]); + + ctx.body = { + list, + total, + }; + } + + // 搜索话题 + async searchTopic(ctx) { + const { title = '', page = 1, size = 10 } = ctx.query; + + const query = { + title: { $regex: title }, + is_lock: false, + is_delete: false, + }; + + const [list, total] = await Promise.all([ + TopicModel.aggregate([ + { + $match: query, + }, + { + $sort: { + top: -1, + good: -1, + last_reply_at: -1, + created_at: -1, + }, + }, + { + $skip: (page - 1) * size, + }, + { + $limit: +size, + }, + { + $lookup: { + from: 'user', + localField: 'aid', + foreignField: '_id', + as: 'author', + }, + }, + { + $unwind: { + path: '$author', + preserveNullAndEmptyArrays: true, + }, + }, + { + $project: { + is_top: 1, + is_good: 1, + like_count: 1, + collect_count: 1, + reply_count: 1, + visit_count: 1, + tab: 1, + title: 1, + author_id: '$author._id', + author_name: '$author.nickname', + author_avatar: '$author.avatar', + }, + }, + ]), + TopicModel.countDocuments(query), + ]); + + ctx.body = { + list, + total, + }; + } + + // 获取无人回复话题 + async getNoReplyTopic(ctx) { + const { count = 10 } = ctx.query; + + const data = await TopicModel.find({ + is_lock: false, + is_delete: false, + reply_count: 0, + }) + .sort({ top: -1, good: -1, last_reply_at: -1, created_at: -1 }) + .select({ id: 1, title: 1 }) + .limit(+count); + + ctx.body = data; + } + + // 创建话题 + async createTopic(ctx) { + const { id } = ctx.state.user; + const { tab, title, content } = ctx.request.body; + + try { + if (!tab) { + throw new Error('话题标签不能为空'); + } else if (!title) { + throw new Error('话题标题不能为空'); + } else if (!content) { + throw new Error('话题内容不能为空'); + } + } catch (err) { + ctx.throw(400, err.message); + } + + // 创建话题 + const topic = await TopicModel.create({ + tab, + title, + content, + aid: id, + }); + + // 积分、话题数量累积 + await UserModel.updateOne( + { _id: id }, + { + $inc: { + score: 1, + topic_count: 1, + }, + }, + ); + + // 创建行为 + await ActionModel.create({ + type: 'create', + aid: id, + tid: topic.id, + }); + + ctx.body = ''; + } + + // 删除话题 + async deleteTopic(ctx) { + const { id } = ctx.state.user; + const { tid } = ctx.params; + + const topic = await TopicModel.findOne({ + _id: tid, + is_delete: false, + }); + + if (!topic) { + ctx.throw(404, '话题不存在'); + } + + if (!topic.aid.equals(id)) { + ctx.throw(403, '不能删除别人的话题'); + } + + // 话题伪删除、行为反向 + await Promise.all([ + TopicModel.updateOne( + { + tid, + }, + { + is_delete: true, + }, + ), + ActionModel.updateOne( + { type: 'create', aid: id, tid }, + { + is_un: false, + }, + ), + ]); + + ctx.body = ''; + } + + // 编辑话题 + async updateTopic(ctx) { + const { id } = ctx.state.user; + const { tid } = ctx.params; + const { tab, title, content } = ctx.request.body; + + const topic = await TopicModel.findOne({ + _id: tid, + is_delete: false, + }); + + if (!topic) { + ctx.throw(404, '话题不存在'); + } + + if (!topic.aid.equals(id)) { + ctx.throw(403, '不能编辑别人的话题'); + } + + await TopicModel.updateOne( + { + tid, + }, + { + tab: tab || topic.tab, + title: title || topic.title, + content: content || topic.content, + }, + ); + + ctx.body = ''; + } + + // 获取话题详情 + async getTopicById(ctx) { + const { user } = ctx.state; + const { tid } = ctx.params; + + // 访问计数 + const result = await TopicModel.updateOne( + { + _id: tid, + is_delete: false, + }, + { + $inc: { + visit_count: 1, + }, + }, + { + new: true, + }, + ); + + if (!result || !result.n) { + ctx.throw(404, '话题不存在'); + } + + let aid = Types.ObjectId(); + if (user) aid = user.id; + + // 获取详情 + const [data] = await TopicModel.aggregate([ + { $match: { _id: Types.ObjectId(tid) } }, + { + $lookup: { + from: 'user', + localField: 'aid', + foreignField: '_id', + as: 'author', + }, + }, + { + $unwind: { + path: '$author', + preserveNullAndEmptyArrays: true, + }, + }, + { + $lookup: { + from: 'reply', + let: { tid: '$_id' }, + pipeline: [ + { + $match: { + $expr: { + $eq: ['$tid', '$$tid'], + }, + }, + }, + { + $lookup: { + from: 'user', + localField: 'aid', + foreignField: '_id', + as: 'author', + }, + }, + { + $unwind: { + path: '$author', + preserveNullAndEmptyArrays: true, + }, + }, + { + $lookup: { + from: 'action', + let: { tid: '$_id' }, + pipeline: [ + { + $match: { + type: 'up', + aid: Types.ObjectId(aid), + $expr: { + $eq: ['$tid', '$$tid'], + }, + }, + }, + ], + as: 'up', + }, + }, + { + $unwind: { + path: '$up', + preserveNullAndEmptyArrays: true, + }, + }, + { + $project: { + content: 1, + created_at: 1, + is_up: { $ifNull: ['$up.is_un', false] }, + author_id: '$author._id', + author_nickname: '$author.nickname', + author_avatar: '$author.avatar', + }, + }, + ], + as: 'replys', + }, + }, + { + $lookup: { + from: 'action', + let: { tid: '$_id' }, + pipeline: [ + { + $match: { + type: 'like', + aid: Types.ObjectId(aid), + $expr: { + $eq: ['$tid', '$$tid'], + }, + }, + }, + ], + as: 'like', + }, + }, + { + $unwind: { + path: '$like', + preserveNullAndEmptyArrays: true, + }, + }, + { + $lookup: { + from: 'action', + let: { tid: '$_id' }, + pipeline: [ + { + $match: { + type: 'collect', + aid: Types.ObjectId(aid), + $expr: { + $eq: ['$tid', '$$tid'], + }, + }, + }, + ], + as: 'collect', + }, + }, + { + $unwind: { + path: '$collect', + preserveNullAndEmptyArrays: true, + }, + }, + { + $project: { + is_top: 1, + is_good: 1, + like_count: 1, + collect_count: 1, + reply_count: 1, + visit_count: 1, + tab: 1, + title: 1, + content: 1, + created_at: 1, + author_id: '$author._id', + author_nickname: '$author.nickname', + author_avatar: '$author.avatar', + author_location: '$author.location', + author_signature: '$author.signature', + author_score: '$author.score', + replys: 1, + is_like: { $ifNull: ['$like.is_un', false] }, + is_collect: { $ifNull: ['$collect.is_un', false] }, + }, + }, + ]); + + ctx.body = data; + } + + // 喜欢或者取消喜欢话题 + async liekTopic(ctx) { + const { id } = ctx.state.user; + const { tid } = ctx.params; + + const topic = await TopicModel.findOne({ + _id: tid, + is_delete: false, + }); + + if (!topic) { + ctx.throw(404, '话题不存在'); + } + + if (topic.aid.equals(id)) { + ctx.throw(403, '不能喜欢自己的话题'); + } + + // 行为反向 + const actionParam = { + type: 'like', + aid: id, + tid: topic._id, + }; + + let action = await ActionModel.findOne(actionParam); + + if (action) { + action = await ActionModel.updateOne( + actionParam, + { is_un: !action.is_un }, + { new: true }, + ); + } else { + action = await ActionModel.create(actionParam); + } + + // 点赞数加减、积分加减 + await Promise.all([ + TopicModel.updateOne( + { tid }, + { + $inc: { + like_count: action.is_un ? 1 : -1, + }, + }, + ), + UserModel.updateOne( + { _id: topic.aid }, + { + $inc: { + score: action.is_un ? 5 : -5, + }, + }, + ), + ]); + + if (action.is_un) { + await NoticeModel.updateOne( + { + type: 'like', + aid: id, + uid: topic.aid, + tid, + }, + {}, + { upsert: true }, + ); + } + + ctx.body = action.is_un ? 'like' : 'un_like'; + } + + // 收藏或者取消收藏话题 + async collectTopic(ctx) { + const { id } = ctx.state.user; + const { tid } = ctx.params; + + const topic = await TopicModel.findOne({ + _id: tid, + is_delete: false, + }); + + if (!topic) { + ctx.throw(404, '话题不存在'); + } + + if (topic.aid.equals(id)) { + ctx.throw(403, '不能喜欢自己的话题'); + } + + // 行为反向 + const actionParam = { + type: 'collect', + aid: id, + tid: topic._id, + }; + + let action = await ActionModel.findOne(actionParam); + + if (action) { + action = await ActionModel.updateOne( + actionParam, + { is_un: !action.is_un }, + { new: true }, + ); + } else { + action = await ActionModel.create(actionParam); + } + + // 点赞数加减、积分加减 + await Promise.all([ + TopicModel.updateOne( + { tid }, + { + $inc: { + collect_count: action.is_un ? 1 : -1, + }, + }, + ), + UserModel.updateOne( + { + _id: topic.aid, + }, + { + $inc: { + score: action.is_un ? 3 : -3, + }, + }, + ), + ]); + + if (action.is_un) { + await NoticeModel.updateOne( + { + type: 'collect', + aid: id, + uid: topic.aid, + tid, + }, + {}, + { upsert: true }, + ); + } + + ctx.body = action.is_un ? 'collect' : 'un_collect'; + } + + // 创建回复 + async createReply(ctx) { + const { id } = ctx.state.user; + const { tid } = ctx.params; + const { content } = ctx.request.body; + + const topic = await TopicModel.findOne({ + _id: tid, + is_delete: false, + }); + + if (!topic) { + ctx.throw(404, '话题不存在'); + } + + if (!content) { + ctx.throw(400, '回复内容不能为空'); + } + + // 创建回复 + const reply = await ReplyModel.create({ + content, + aid: id, + tid, + }); + + // 更新话题相关信息 + await TopicModel.updateOne( + { tid }, + { + $inc: { + reply_count: 1, + }, + last_reply_id: reply._id, + last_reply_at: new Date(), + }, + ); + + // 发送提醒 + await NoticeModel.create({ + type: 'reply', + uid: topic.aid, + aid: id, + tid, + }); + + ctx.body = ''; + } + + // 点赞回复 + async upReply(ctx) { + const { id } = ctx.state.user; + const { rid } = ctx.params; + + const reply = await ReplyModel.findById(rid); + + if (!reply) { + ctx.throw(404, '回复不存在'); + } + + if (reply.aid.equals(id)) { + ctx.throw(403, '不能给自己点赞哟'); + } + + // 行为反向 + const action = await ActionModel.findOne({ + type: 'up', + aid: id, + tid: reply._id, + }); + + if (!action) { + await Promise.all([ + ActionModel.create({ + type: 'up', + aid: id, + tid: reply._id, + }), + NoticeModel.create({ + type: 'up', + aid: id, + uid: reply.aid, + }), + ]); + } + + ctx.body = ''; + } + + // 获取话题列表 + async roleGetTopicList(ctx) { + const { page, size } = ctx.query; + + const [list, total] = await Promise.all([ + TopicModel.find({}) + .select({ password: 0 }) + .sort({ created_at: -1 }) + .limit(+size) + .skip((+page - 1) * size), + TopicModel.countDocuments(), + ]); + + ctx.body = { list, total }; + } + + // 创建话题 + async roleCreateTopic(ctx) { + const { id } = ctx.state.user; + const { tab, title, content } = ctx.request.body; + + try { + if (!tab) { + throw new Error('话题标签不能为空'); + } else if (!title) { + throw new Error('话题标题不能为空'); + } else if (!content) { + throw new Error('话题内容不能为空'); + } + } catch (err) { + ctx.throw(400, err.message); + } + + // 创建话题 + const topic = await TopicModel.create({ + tab, + title, + content, + aid: id, + }); + + // 积分、话题数量累积 + await UserModel.updateOne( + { _id: id }, + { + $inc: { + score: 1, + topic_count: 1, + }, + }, + ); + + // 创建行为 + await ActionModel.create({ + type: 'create', + aid: id, + tid: topic.id, + }); + + ctx.body = ''; + } + + // 删除话题(超管物理删除) + async roleDeleteTopic(ctx) { + const { tid } = ctx.params; + + const topic = await TopicModel.findByIdAndDelete(tid); + + if (!topic) { + ctx.throw(404, '话题不存在'); + } + + await ActionModel.deleteOne({ + type: 'create', + aid: topic.aid, + tid, + }); + + ctx.body = ''; + } + + // 更新话题 + async roleUpdateTopic(ctx) { + const { tid } = ctx.params; + const { tab, title, content } = ctx.request.body; + const updated = {}; + + if (tab) updated.tab = tab; + if (title) updated.title = title; + if (content) updated.content = content; + + const topic = await TopicModel.findByIdAndUpdate(tid, updated); + + if (!topic) { + ctx.throw(404, '话题不存在'); + } + + ctx.body = ''; + } + + // 话题置顶 + async roleTopTopic(ctx) { + const { tid } = ctx.params; + const topic = await TopicModel.findById(tid); + + if (!topic) { + ctx.throw(404, '话题不存在'); + } + + await TopicModel.findByIdAndUpdate(tid, { + is_top: !topic.is_top, + }); + + // 用户积分变化 + await UserModel.findByIdAndUpdate(topic.aid, { + $inc: { + score: topic.is_top ? -20 : 20, + }, + }); + + ctx.body = topic.is_top ? 'un_top' : 'top'; + } + + // 话题加精 + async roleGoodTopic(ctx) { + const { tid } = ctx.params; + const topic = await TopicModel.findById(tid); + + if (!topic) { + ctx.throw(404, '话题不存在'); + } + + await TopicModel.findByIdAndUpdate(tid, { + is_good: !topic.is_good, + }); + + // 用户积分变化 + await UserModel.findByIdAndUpdate(topic.aid, { + $inc: { + score: topic.is_good ? -10 : 10, + }, + }); + + ctx.body = topic.is_good ? 'un_good' : 'good'; + } + + // 话题锁定(封贴) + async roleLockTopic(ctx) { + const { tid } = ctx.params; + const topic = await TopicModel.findById(tid); + + if (!topic) { + ctx.throw(404, '话题不存在'); + } + + await TopicModel.findByIdAndUpdate(tid, { + is_lock: !topic.is_lock, + }); + + ctx.body = topic.is_lock ? 'un_lock' : 'lock'; + } +} + +module.exports = new Topic(); diff --git a/packages/server/controller/user.js b/packages/server/controller/user.js new file mode 100644 index 0000000..3530515 --- /dev/null +++ b/packages/server/controller/user.js @@ -0,0 +1,897 @@ +const { Types } = require('mongoose'); +const jwt = require('jsonwebtoken'); +const uuid = require('uuid/v4'); +const bcrypt = require('bcryptjs'); +const crypto = require('crypto'); +const { + jwt: { SECRET, EXPIRSE, REFRESH }, + SALT_WORK_FACTOR, +} = require('../../../config'); +const redis = require('../db/redis'); +const UserModel = require('../model/user'); +const ReplyModel = require('../model/reply'); +const TopicModel = require('../model/topic'); +const ActionModel = require('../model/action'); +const NoticeModel = require('../model/notice'); + +const EMAIL_REGEXP = /^([A-Za-z0-9_\-.])+@([A-Za-z0-9_\-.])+\.([A-Za-z]{2,4})$/; +const PASSWORD_REGEXP = /(?!^(\d+|[a-zA-Z]+|[~!@#$%^&*?]+)$)^[\w~!@#$%^&*?].{6,18}/; + +const forgetPassKey = key => `FORGET_PASS_${key}`; + +class User { + constructor() { + this.signup = this.signup.bind(this); + this.signin = this.signin.bind(this); + this.forgetPass = this.forgetPass.bind(this); + this.resetPass = this.resetPass.bind(this); + this.updatePass = this.updatePass.bind(this); + this.getUserNotice = this.getUserNotice.bind(this); + this.getSystemNotice = this.getSystemNotice.bind(this); + this.roleCreateUser = this.roleCreateUser.bind(this); + } + + // 密码加密 + async _encryption(password) { + const salt = await bcrypt.genSalt(SALT_WORK_FACTOR); + const hash = await bcrypt.hash(password, salt); + return hash; + } + + // 密码比较 + async _comparePass(pass, passTrue) { + const isMatch = await bcrypt.compare(pass, passTrue); + return isMatch; + } + + // md5加密 + async _md5(value) { + const md5 = crypto.createHash('md5'); + return md5.update(value).digest('hex'); + } + + // 注册 + async signup(ctx) { + const { email, password, nickname } = ctx.request.body; + + try { + if (!email || !EMAIL_REGEXP.test(email)) { + throw new Error('邮箱格式不正确'); + } else if (!password || !PASSWORD_REGEXP.test(password)) { + throw new Error( + '密码必须为数字、字母和特殊字符其中两种组成并且在6至18位之间', + ); + } else if (!nickname || nickname.length > 10 || nickname.length < 2) { + throw new Error('昵称必须在2至10位之间'); + } + } catch (err) { + ctx.throw(400, err.message); + } + + let existUser; + + existUser = await UserModel.findOne({ email }); + + if (existUser) { + ctx.throw(409, '邮箱已经注册过了'); + } + + existUser = await UserModel.findOne({ nickname }); + + if (existUser) { + ctx.throw(409, '昵称已经注册过了'); + } + + // 密码加密 + const bcryptPassword = await this._encryption(password); + + await UserModel.create({ + email, + password: bcryptPassword, + nickname, + }); + + ctx.body = ''; + } + + // 登录 + async signin(ctx) { + const { email, password } = ctx.request.body; + + // 校验邮箱 + if (!email || !EMAIL_REGEXP.test(email)) { + ctx.throw(400, '邮箱格式错误'); + } + + const user = await UserModel.findOne({ email }); + + // 判断用户是否存在, 提示防遍历 + if (!user) { + ctx.throw(400, '用户名或密码错误'); + } + + const isMatch = await this._comparePass(password, user.password); + + if (!isMatch) { + ctx.throw(400, '用户名或密码错误'); + } + + // 返回JWT + const token = jwt.sign( + { + id: user.id, + role: user.role, + exp: Date.now() + EXPIRSE, + ref: Date.now() + REFRESH, + }, + SECRET, + ); + + ctx.body = `Bearer ${token}`; + } + + // 忘记密码 + async forgetPass(ctx) { + const { email } = ctx.request.body; + + // 校验邮箱 + if (!email || !EMAIL_REGEXP.test(email)) { + ctx.throw(400, '邮箱格式错误'); + } + + const user = await UserModel.findOne({ email }); + + // 判断用户是否存在 + if (!user) { + ctx.throw(404, '尚未注册'); + } + + // 加密 + const token = await this._md5(`${user.email}&${uuid()}`); + await redis.set(forgetPassKey(email), token, 'EX', 60 * 30); + + ctx.body = token; + } + + // 重置密码 + async resetPass(ctx) { + const { email, token, new_pass } = ctx.request.body; + + const secret = await redis.get(forgetPassKey(email)); + + if (secret !== token) { + ctx.throw(400, '链接不正确'); + } + + if (!new_pass || !PASSWORD_REGEXP.test(new_pass)) { + ctx.throw( + 400, + '新密码必须为数字、字母和特殊字符其中两种组成并且在6-18位之间', + ); + } + + const password = await this._encryption(new_pass); + await UserModel.updateOne({ email }, { password }); + + ctx.body = ''; + } + + // 获取当前用户信息 + async getUser(ctx) { + const { id } = ctx.state.user; + const user = await UserModel.findById(id, { + _id: 0, + email: 1, + nickname: 1, + avatar: 1, + location: 1, + signature: 1, + }); + ctx.body = user; + } + + // 更新个人信息 + async updateSetting(ctx) { + const { id } = ctx.state.user; + const { nickname, location, signature } = ctx.request.body; + + const user = await UserModel.findOne({ nickname }); + + if (user && user._id.toString() !== id) { + ctx.throw(409, '昵称已经注册过了'); + } + + await UserModel.updateOne( + { _id: id }, + { + nickname, + location, + signature, + }, + ); + + ctx.body = ''; + } + + // 修改密码 + async updatePass(ctx) { + const { id } = ctx.state.user; + const { old_pass, new_pass } = ctx.request.body; + + try { + if (!old_pass) { + throw new Error('旧密码不能为空'); + } else if (!new_pass || !PASSWORD_REGEXP.test(new_pass)) { + throw new Error( + '新密码必须为数字、字母和特殊字符其中两种组成并且在6-18位之间', + ); + } + } catch (err) { + ctx.throw(400, err.message); + } + + const user = await UserModel.findById(id); + const isMatch = await this._comparePass(old_pass, user.password); + + if (!isMatch) { + ctx.throw(400, '旧密码错误'); + } + + const password = await this._encryption(new_pass); + + await UserModel.updateOne( + { + _id: id, + }, + { + password, + }, + ); + + ctx.body = ''; + } + + // 转化消息格式 + async _normalNotice(item) { + const data = {}; + + switch (item.type) { + case 'like': + data.author = await UserModel.findById( + item.author_id, + 'id nickname avatar', + ); + data.topic = await TopicModel.findById(item.topic_id, 'id title'); + data.typeName = '喜欢了'; + break; + case 'collect': + data.author = await UserModel.findById( + item.author_id, + 'id nickname avatar', + ); + data.topic = await TopicModel.findById(item.topic_id, 'id title'); + data.typeName = '收藏了'; + break; + case 'follow': + data.author = await UserModel.findById( + item.author_id, + 'id nickname avatar', + ); + data.typeName = '新的关注者'; + break; + case 'reply': + data.author = await UserModel.findById( + item.author_id, + 'id nickname avatar', + ); + data.topic = await TopicModel.findById(item.topic_id, 'id title'); + data.typeName = '回复了'; + break; + case 'up': + data.author = await UserModel.findById( + item.author_id, + 'id nickname avatar', + ); + data.topic = await TopicModel.findById(item.topic_id, 'id title'); + data.reply = await ReplyModel.findById(item.reply_id, 'id content'); + data.typeName = '点赞了'; + break; + default: + data.typeName = '系统消息'; + break; + } + + return { ...data, ...item }; + } + + // 获取用户消息 + async getUserNotice(ctx) { + const { id } = ctx.state.user; + + const notices = await NoticeModel.find({ + uid: id, + type: { $ne: 'system' }, + }); + + const data = await Promise.all( + notices.map(item => { + return new Promise(resolve => { + resolve(this._normalNotice(item.toObject())); + }); + }), + ); + + ctx.body = data; + } + + // 获取系统消息 + async getSystemNotice(ctx) { + const { id } = ctx.state.user; + + const notices = await NoticeModel.find({ + uid: id, + type: 'system', + }); + + const data = await Promise.all( + notices.map(item => { + return new Promise(resolve => { + resolve(this._normalNotice(item.toObject())); + }); + }), + ); + + ctx.body = data; + } + + // 获取积分榜用户列表 + async getUserTop(ctx) { + const { count = 10 } = ctx.query; + const data = await UserModel.find({}) + .select({ + nickname: 1, + avatar: 1, + score: 1, + topic_count: 1, + like_count: 1, + collect_count: 1, + follow_count: 1, + }) + .sort({ + score: -1, + }) + .limit(+count); + + ctx.body = data; + } + + // 根据ID获取用户信息 + async getUserById(ctx) { + const { uid } = ctx.params; + const { user: current } = ctx.state; + + const user = await UserModel.findById(uid); + + if (!user) { + ctx.throw(404, '用户不存在'); + } + + let aid = Types.ObjectId(); + if (current) aid = current.id; + + const [data] = await UserModel.aggregate([ + { + $match: { + _id: Types.ObjectId(uid), + }, + }, + { + $lookup: { + from: 'action', + let: { tid: '$_id' }, + pipeline: [ + { + $match: { + type: 'follow', + aid: Types.ObjectId(aid), + $expr: { + $eq: ['$tid', '$$tid'], + }, + }, + }, + ], + as: 'follow', + }, + }, + { + $unwind: { + path: '$follow', + preserveNullAndEmptyArrays: true, + }, + }, + { + $project: { + nickname: 1, + avatar: 1, + location: 1, + signature: 1, + score: 1, + like_count: 1, + collect_count: 1, + follower_count: 1, + following_count: 1, + is_follow: { $ifNull: ['$follow.is_un', false] }, + }, + }, + ]); + + ctx.body = data; + } + + // 获取用户动态 + async getUserAction(ctx) { + const { uid } = ctx.params; + + const user = await UserModel.findById(uid); + + if (!user) { + ctx.throw(404, '用户不存在'); + } + + const actions = await ActionModel.find({ aid: uid, is_un: true }); + + const result = await Promise.all( + actions.map(item => { + return new Promise(resolve => { + if (item.type === 'follow') { + resolve( + UserModel.findById(item.tid, 'id nickname signature avatar'), + ); + } else { + resolve(TopicModel.findById(item.tid, 'id title')); + } + }); + }), + ); + + const data = actions.map((item, i) => { + return { + ...result[i].toObject(), + type: item.type, + }; + }); + + ctx.body = data; + } + + // 获取用户专栏的列表 + async getUserCreate(ctx) { + const { uid } = ctx.params; + + const user = await UserModel.findById(uid); + + if (!user) { + ctx.throw(404, '用户不存在'); + } + + const data = await ActionModel.aggregate([ + { + $match: { + type: 'create', + aid: Types.ObjectId(uid), + is_un: true, + }, + }, + { + $lookup: { + from: 'topic', + localField: 'tid', + foreignField: '_id', + as: 'topic', + }, + }, + { + $unwind: { + path: '$topic', + preserveNullAndEmptyArrays: true, + }, + }, + { + $project: { + topic_id: '$topic._id', + topic_title: '$topic.title', + topic_like_count: '$topic.like_count', + topic_collect_count: '$topic.collect_count', + topic_visit_count: '$topic.visit_count', + }, + }, + ]); + + ctx.body = data; + } + + // 获取用户喜欢列表 + async getUserLike(ctx) { + const { uid } = ctx.params; + + const user = await UserModel.findById(uid); + + if (!user) { + ctx.throw(404, '用户不存在'); + } + + const data = await ActionModel.aggregate([ + { + $match: { + type: 'like', + aid: Types.ObjectId(uid), + is_un: true, + }, + }, + { + $lookup: { + from: 'topic', + localField: 'tid', + foreignField: '_id', + as: 'topic', + }, + }, + { + $unwind: { + path: '$topic', + preserveNullAndEmptyArrays: true, + }, + }, + { + $project: { + topic_id: '$topic._id', + topic_title: '$topic.title', + }, + }, + ]); + + ctx.body = data; + } + + // 获取用户收藏列表 + async getUserCollect(ctx) { + const { uid } = ctx.params; + + const user = await UserModel.findById(uid); + + if (!user) { + ctx.throw(404, '用户不存在'); + } + + const data = await ActionModel.aggregate([ + { + $match: { + type: 'collect', + aid: Types.ObjectId(uid), + is_un: true, + }, + }, + { + $lookup: { + from: 'topic', + localField: 'tid', + foreignField: '_id', + as: 'topic', + }, + }, + { + $unwind: { + path: '$topic', + preserveNullAndEmptyArrays: true, + }, + }, + { + $project: { + topic_id: '$topic._id', + topic_title: '$topic.title', + }, + }, + ]); + + ctx.body = data; + } + + // 获取用户粉丝列表 + async getUserFollower(ctx) { + const { uid } = ctx.params; + + const user = await UserModel.findById(uid); + + if (!user) { + ctx.throw(404, '用户不存在'); + } + + const data = await ActionModel.aggregate([ + { + $match: { + type: 'follow', + tid: Types.ObjectId(uid), + is_un: true, + }, + }, + { + $lookup: { + from: 'user', + localField: 'aid', + foreignField: '_id', + as: 'user', + }, + }, + { + $unwind: { + path: '$user', + preserveNullAndEmptyArrays: true, + }, + }, + { + $project: { + user_id: '$user._id', + user_nickname: '$user.nickname', + user_avatar: '$user.avatar', + }, + }, + ]); + + ctx.body = data; + } + + // 获取用户关注的人列表 + async getUserFollowing(ctx) { + const { uid } = ctx.params; + + const user = await UserModel.findById(uid); + + if (!user) { + ctx.throw(404, '用户不存在'); + } + + const data = await ActionModel.aggregate([ + { + $match: { + type: 'follow', + aid: Types.ObjectId(uid), + is_un: true, + }, + }, + { + $lookup: { + from: 'user', + localField: 'tid', + foreignField: '_id', + as: 'user', + }, + }, + { + $unwind: { + path: '$user', + preserveNullAndEmptyArrays: true, + }, + }, + { + $project: { + user_id: '$user._id', + user_nickname: '$user.nickname', + user_avatar: '$user.avatar', + }, + }, + ]); + + ctx.body = data; + } + + // 关注或者取消关注用户 + async followUser(ctx) { + const { uid } = ctx.params; + const { id } = ctx.state.user; + + const user = await UserModel.findById(uid); + + if (!user) { + ctx.throw(404, '用户不存在'); + } + + if (uid === id) { + ctx.throw(403, '不能关注你自己'); + } + + // 行为反向 + const actionParam = { + type: 'follow', + aid: id, + tid: uid, + }; + + let action = await ActionModel.findOne(actionParam); + + if (action) { + action = await ActionModel.updateOne( + actionParam, + { is_un: !action.is_un }, + { new: true }, + ); + } else { + action = await ActionModel.create(actionParam); + } + + await Promise.all([ + // 当前用户关注数 +/- 1; + UserModel.findByIdAndUpdate(id, { + $inc: { + following_count: action.is_un ? 1 : -1, + }, + }), + // 被关注用户粉丝数 +/- 1; + UserModel.findByIdAndUpdate(uid, { + $inc: { + follower_count: action.is_un ? 1 : -1, + }, + }), + ]); + + // 推送通知 + if (action.is_un) { + await NoticeModel.updateOne( + { + type: 'follow', + aid: id, + uid, + }, + {}, + { upsert: true }, + ); + } + + ctx.body = ''; + } + + // 用户列表 + async roleGetUserList(ctx) { + const { page, size } = ctx.query; + + const [list, total] = await Promise.all([ + UserModel.find({}) + .select({ password: 0 }) + .sort({ created_at: -1 }) + .limit(+size) + .skip((+page - 1) * size), + UserModel.countDocuments(), + ]); + + ctx.body = { list, total }; + } + + // 创建用户 + async roleCreateUser(ctx) { + const { user } = ctx.state; + const { email, password, nickname, role } = ctx.request.body; + + try { + if (!email || !EMAIL_REGEXP.test(email)) { + throw new Error('邮箱格式不正确'); + } else if (!password || !PASSWORD_REGEXP.test(password)) { + throw new Error( + '密码必须为数字、字母和特殊字符其中两种组成并且在6至18位之间', + ); + } else if (!nickname || nickname.length > 10 || nickname.length < 2) { + throw new Error('昵称必须在2至10位之间'); + } else if (role < 0 || role > 100) { + throw new Error('权限值必须在0至100之间'); + } else if (user.role < role) { + throw new Error('权限值不能大于当前用户的权限值'); + } + } catch (err) { + ctx.throw(400, err.message); + } + + let existUser; + + existUser = await UserModel.findOne({ email }); + if (existUser) { + ctx.throw(409, '手机号已经存在'); + } + + existUser = await UserModel.findOne({ nickname }); + if (existUser) { + ctx.throw(409, '昵称已经存在'); + } + + const bcryptPassword = await this._encryption(password); + + await UserModel.create({ + email, + password: bcryptPassword, + nickname, + role, + }); + + ctx.body = ''; + } + + // 删除用户(超管物理删除) + async roleDeleteUser(ctx) { + const { uid } = ctx.params; + + await UserModel.findByIdAndDelete(uid); + + ctx.body = ''; + } + + // 更新用户 + async roleUpdateUser(ctx) { + const { uid } = ctx.params; + const { nickname, location, signature } = ctx.request.body; + + const user = await UserModel.findOne({ nickname }); + + if (user && user._id.toString() !== uid) { + ctx.throw(409, '昵称已经注册过了'); + } + + await UserModel.updateOne( + { _id: uid }, + { + nickname, + location, + signature, + }, + ); + + ctx.body = ''; + } + + // 设为星标用户 + async roleStarUser(ctx) { + const { uid } = ctx.params; + + const user = await UserModel.findById(uid); + + if (!user) { + ctx.throw(404, '未查询到用户'); + } + + await UserModel.updateOne( + { + _id: uid, + }, + { + is_star: !user.is_star, + }, + ); + + ctx.body = user.is_star ? 'un_star' : 'star'; + } + + // 锁定用户(封号) + async roleLockUser(ctx) { + const { uid } = ctx.params; + const { user: currentUser } = ctx.state; + + const user = await UserModel.findById(uid); + + if (!user) { + ctx.throw(404, '未查询到用户'); + } + + if (currentUser.role < user.role) { + ctx.throw(403, '不能操作权限值高于自身的用户'); + } + + await UserModel.updateOne( + { + _id: uid, + }, + { + is_lock: !user.is_lock, + }, + ); + + ctx.body = user.is_lock ? 'un_lock' : 'lock'; + } +} + +module.exports = new User(); diff --git a/packages/server/controllers/aider.js b/packages/server/controllers/aider.js deleted file mode 100644 index 1fc16b6..0000000 --- a/packages/server/controllers/aider.js +++ /dev/null @@ -1,48 +0,0 @@ -const { BMP24 } = require('gd-bmp'); -const Base = require('./base'); - -class Aider extends Base { - constructor() { - super(); - this.getCaptcha = this.getCaptcha.bind(this); - } - - getCaptcha(ctx) { - const { - width = 100, - height = 40, - textColor = 'a1a1a1', - bgColor = 'ffffff' - } = ctx.query; - - // 设置画布 - const img = new BMP24(width, height); - // 设置背景 - img.fillRect(0, 0, width, height, `0x${bgColor}`); - - let token = ''; - - // 随机字符列表 - const p = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'; - // 组成token - for (let i = 0; i < 5; i++) { - token += p.charAt(Math.random() * p.length | 0); - } - - // 字符定位于背景 x,y 轴位置 - let x = 10, y = 2; - - for (let i = 0; i < token.length; i++) { - y = 2 + this._rand(-4, 4); - // 画字符 - img.drawChar(token[i], x, y, BMP24.font12x24, `0x${textColor}`); - x += 12 + this._rand(4, 8); - } - - const url = `data:image/bmp;base64,${img.getFileData().toString('base64')}`; - - ctx.body = { token, url }; - } -} - -module.exports = new Aider(); diff --git a/packages/server/controllers/base.js b/packages/server/controllers/base.js deleted file mode 100644 index 07b11e7..0000000 --- a/packages/server/controllers/base.js +++ /dev/null @@ -1,90 +0,0 @@ -const fs = require('fs'); -const bcrypt = require('bcryptjs'); -const qiniu = require('qiniu'); -const nodemailer = require('nodemailer'); -const crypto = require('crypto'); -const { - qn: { ACCESS_KEY, SECRET_KEY, BUCKET_NAME, ZONE }, - mail: { HOST, PORT, SECURE, AUTH }, - SALT_WORK_FACTOR, -} = require('../../../config'); - -module.exports = class Base { - // 生成随机数 - _rand (min, max) { - return Math.random() * (max - min + 1) + min | 0; - } - - // 密码加密 - async _encryption(password) { - const salt = await bcrypt.genSalt(SALT_WORK_FACTOR); - const hash = await bcrypt.hash(password, salt); - return hash; - } - - // 密码比较 - async _comparePass(pass, passTrue) { - const isMatch = await bcrypt.compare(pass, passTrue); - return isMatch; - } - - // md5加密 - async _md5(value) { - const md5 = crypto.createHash('md5'); - return md5.update(value).digest('hex'); - } - - // 七牛图片上传 - _uploadImgByQn(name, local) { - if (process.env.NODE_ENV === 'test') return ''; - - const mac = new qiniu.auth.digest.Mac(ACCESS_KEY, SECRET_KEY); - const putPolicy = new qiniu.rs.PutPolicy({ - scope: `${BUCKET_NAME}:${name}` - }); - - const uploadToken = putPolicy.uploadToken(mac); - - const config = new qiniu.conf.Config(); - config.zone = qiniu.zone[ZONE]; - const formUploader = new qiniu.form_up.FormUploader(config); - const putExtra = new qiniu.form_up.PutExtra(); - - return new Promise((resolve, reject) => { - formUploader.putFile(uploadToken, name, local, putExtra, (err, body, info) => { - fs.unlinkSync(local); - if (err) reject(err); - if (info.statusCode === 200) { - resolve(body.key); - } else { - reject(body.error); - } - }); - }); - } - - // 邮件发送 - _sendMail(email, content) { - if (process.env.NODE_ENV === 'test') return ''; - - const transporter = nodemailer.createTransport({ - host: HOST, - port: PORT, - secure: SECURE, - auth: AUTH, - }); - - const opts = { - from: 'Mints(薄荷糖社区) ', - to: email, - html: content, - }; - - return new Promise((resolve, reject) => { - transporter.sendMail(opts, (err, info) => { - if (err) reject(err); - resolve(info); - }); - }); - } -}; diff --git a/packages/server/controllers/user.js b/packages/server/controllers/user.js deleted file mode 100644 index 80f6eeb..0000000 --- a/packages/server/controllers/user.js +++ /dev/null @@ -1,321 +0,0 @@ -const jwt = require('jsonwebtoken'); -const uuid = require('uuid/v4'); -const rq = require('request-promise'); -const Base = require('./base'); -const { jwt: { SECRET, EXPIRSE, REFRESH }, qn: { DONAME } } = require('../../../config'); -const redis = require('../db/redis'); -const UserProxy = require('../proxy/user'); - -const ENV = process.env.NODE_ENV; - -class User extends Base { - constructor() { - super(); - this.signup = this.signup.bind(this); - this.signin = this.signin.bind(this); - this.forgetPass = this.forgetPass.bind(this); - this.resetPass = this.resetPass.bind(this); - this.updatePass = this.updatePass.bind(this); - this.uploadAvatar = this.uploadAvatar.bind(this); - } - - // GitHub 登录 - async github(ctx) { - const { accessToken } = ctx.request.body; - let profile; - - try { - const res = await rq({ - url: 'https://api.github.com/user', - method: 'GET', - headers: { - Authorization: `token ${accessToken}`, - 'user-agent': 'node.js', - } - }); - - profile = JSON.parse(res); - } catch(err) { - ctx.throw(403, 'GitHub 授权失败'); - } - - if (!profile.email) { - ctx.throw(401, 'GitHub 授权失败'); - } - - let existUser = await UserProxy.getOne({ email: profile.email }); - - if (existUser) { - if (!existUser.github_id) { - existUser.avatar = profile.avatar_url; - existUser.location = profile.location; - existUser.signature = profile.bio; - existUser.github_id = profile.id; - existUser.github_username = profile.name; - // existUser.github_access_token = accessToken; - - await existUser.save(); - } - } else { - existUser = await UserProxy.create({ - email: profile.email, - nickname: profile.name, - avatar: profile.avatar_url, - location: profile.location, - signature: profile.bio, - github_id: profile.id, - github_username: profile.name, - // github_access_token: accessToken, - }); - } - - const token = jwt.sign( - { - id: existUser.id, - role: existUser.role, - exp: Date.now() + EXPIRSE, - ref: Date.now() + REFRESH, - }, - SECRET - ); - - ctx.body = `Bearer ${token}`; - } - - // 注册 - async signup(ctx) { - const { email, password, nickname } = ctx.request.body; - - try { - if (!email || !/^([A-Za-z0-9_\-.])+@([A-Za-z0-9_\-.])+\.([A-Za-z]{2,4})$/.test(email)) { - throw new Error('邮箱格式不正确'); - } else if (!password || !/(?!^(\d+|[a-zA-Z]+|[~!@#$%^&*?]+)$)^[\w~!@#$%^&*?].{6,18}/.test(password)) { - throw new Error('密码必须为数字、字母和特殊字符其中两种组成并且在6至18位之间'); - } else if (!nickname || nickname.length > 6 || nickname.length < 2) { - throw new Error('昵称必须在2至6位之间'); - } - } catch(err) { - ctx.throw(400, err.message); - } - - let existUser; - - existUser = await UserProxy.getOne({ email }); - if (existUser) { - ctx.throw(409, '邮箱已经注册过了'); - } - - existUser = await UserProxy.getOne({ nickname }); - if (existUser) { - ctx.throw(409, '昵称已经注册过了'); - } - - const bcryptPassword = await this._encryption(password); - await UserProxy.create({ - email, - password: bcryptPassword, - nickname - }); - - // 加密 - const token = await this._md5(`${email}&${uuid()}`); - await redis.set(email, token, 'EX', 60 * 30); - - const url = `/set_active?token=${token}&email=${email}`; - - if (ENV !== 'production') { - ctx.body = url; - } else { - await this._sendMail(email, url); - ctx.body = ''; - } - } - - // 账户激活 - async setActive(ctx) { - const { email, token } = ctx.query; - const secret = await redis.get(email); - - if (secret !== token) { - ctx.throw(400, '链接未通过校验'); - } - - await UserProxy.update({ email }, { active: true }); - - ctx.body = ''; - } - - // 登录 - async signin(ctx) { - const { email, password } = ctx.request.body; - - // 校验邮箱 - if (!email || !/^([A-Za-z0-9_\-.])+@([A-Za-z0-9_\-.])+\.([A-Za-z]{2,4})$/.test(email)) { - ctx.throw(400, '邮箱格式错误'); - } - - const user = await UserProxy.getOne({ email }); - - // 判断用户是否存在 - if (!user) { - ctx.throw(404, '尚未注册'); - } - - if (!user.active) { - ctx.throw(401, '邮箱账户尚未激活'); - } - - const isMatch = await this._comparePass(password, user.password); - - if (!isMatch) { - ctx.throw(400, '密码错误'); - } - - // 返回JWT - const token = jwt.sign( - { - id: user.id, - role: user.role, - exp: Date.now() + EXPIRSE, - ref: Date.now() + REFRESH, - }, - SECRET - ); - - ctx.body = `Bearer ${token}`; - } - - // 忘记密码 - async forgetPass (ctx) { - const { email } = ctx.request.body; - - // 校验邮箱 - if (!email || !/^([A-Za-z0-9_\-.])+@([A-Za-z0-9_\-.])+\.([A-Za-z]{2,4})$/.test(email)) { - ctx.throw(400, '邮箱格式错误'); - } - - const user = await UserProxy.getOne({ email }); - - // 判断用户是否存在 - if (!user) { - ctx.throw(404, '尚未注册'); - } - - if (!user.active) { - ctx.throw(401, '邮箱账户尚未激活'); - } - - // 随机的uuid - const secret = uuid(); - const key = `${user.email}&${secret}`; - - // 加密 - const token = await this._md5(key); - await redis.set(email, token, 'EX', 60 * 30); - - const url = `/reset_pass?token=${token}&email=${email}`; - - if (ENV === 'production') { - await this._sendMail(email, url); - ctx.body = ''; - } else { - ctx.body = url; - } - } - - // 重置密码 - async resetPass (ctx) { - const { email, token } = ctx.query; - const { newPass } = ctx.request.body; - - const secret = await redis.get(email); - - if (secret !== token) { - ctx.throw(400, '链接未通过校验'); - } - - if (!newPass || !/(?!^(\d+|[a-zA-Z]+|[~!@#$%^&*?]+)$)^[\w~!@#$%^&*?].{6,18}/.test(newPass)) { - ctx.throw(400, '新密码必须为数字、字母和特殊字符其中两种组成并且在6-18位之间'); - } - - const password = await this._encryption(newPass); - await UserProxy.update({ email }, { password }); - - ctx.body = ''; - } - - // 获取当前用户信息 - async getCurrentUser(ctx) { - const { id } = ctx.state.user; - const user = await UserProxy.getById(id, 'email nickname avatar location signature score'); - ctx.body = user; - } - - // 更新个人信息 - async updateSetting(ctx) { - const { id } = ctx.state.user; - const { nickname } = ctx.request.body; - - const user = await UserProxy.getOne({ nickname }); - - if (user && user.id !== id) { - ctx.throw(409, '昵称已经注册过了'); - } - - await UserProxy.update({ _id: id }, ctx.request.body); - ctx.body = ''; - } - - // 修改密码 - async updatePass(ctx) { - const { id } = ctx.state.user; - const { oldPass, newPass } = ctx.request.body; - - try { - if (!oldPass) { - throw new Error('旧密码不能为空'); - } else if (!newPass || !/(?!^(\d+|[a-zA-Z]+|[~!@#$%^&*?]+)$)^[\w~!@#$%^&*?].{6,18}/.test(newPass)) { - throw new Error('新密码必须为数字、字母和特殊字符其中两种组成并且在6-18位之间'); - } - } catch(err) { - ctx.throw(400, err.message); - } - - const user = await UserProxy.getById(id); - const isMatch = await this._comparePass(oldPass, user.password); - - if (isMatch) { - const bcryptPassword = await this._encryption(newPass); - // 更新用户密码 - user.password = bcryptPassword; - await user.save(); - ctx.body = ''; - } else { - ctx.throw(400, '旧密码错误'); - } - } - - // 头像上传 - async uploadAvatar(ctx) { - const { id } = ctx.state.user; - const { avatar } = ctx.request.files; - - if (ENV === 'production') { - try { - const avatarName = await this._uploadImgByQn(`avatar_${id}.${avatar.path.split('.')[1]}`, avatar.path); - ctx.body = `${DONAME}/${avatarName}`; - } catch(err) { - throw new Error(err); - } - } else { - ctx.body = ''; - } - } - - // 发送验证邮件 - sendMail(ctx) { - ctx.body = ''; - } -} - -module.exports = new User(); diff --git a/packages/server/controllers/v1/notice.js b/packages/server/controllers/v1/notice.js deleted file mode 100644 index 22818ad..0000000 --- a/packages/server/controllers/v1/notice.js +++ /dev/null @@ -1,98 +0,0 @@ -const NoticeProxy = require('../../proxy/notice'); -const UserProxy = require('../../proxy/user'); -const TopicProxy = require('../../proxy/topic'); -const ReplyProxy = require('../../proxy/reply'); - -class Notice { - constructor() { - this.getUserNotice = this.getUserNotice.bind(this); - this.getSystemNotice = this.getSystemNotice.bind(this); - } - - // 转化消息格式 - async normalNotice(item) { - const data = {}; - - switch (item.type) { - case 'like': - data.author = await UserProxy.getById(item.author_id, 'id nickname avatar'); - data.topic = await TopicProxy.getById(item.topic_id, 'id title'); - data.typeName = '喜欢了'; - break; - case 'collect': - data.author = await UserProxy.getById(item.author_id, 'id nickname avatar'); - data.topic = await TopicProxy.getById(item.topic_id, 'id title'); - data.typeName = '收藏了'; - break; - case 'follow': - data.author = await UserProxy.getById(item.author_id, 'id nickname avatar'); - data.typeName = '新的关注者'; - break; - case 'reply': - data.author = await UserProxy.getById(item.author_id, 'id nickname avatar'); - data.topic = await TopicProxy.getById(item.topic_id, 'id title'); - data.typeName = '回复了'; - break; - case 'at': - data.author = await UserProxy.getById(item.author_id, 'id nickname avatar'); - data.topic = await TopicProxy.getById(item.topic_id, 'id title'); - data.reply = await ReplyProxy.getById(item.reply_id, 'id content'); - data.typeName = '@了'; - break; - case 'up': - data.author = await UserProxy.getById(item.author_id, 'id nickname avatar'); - data.topic = await TopicProxy.getById(item.topic_id, 'id title'); - data.reply = await ReplyProxy.getById(item.reply_id, 'id content'); - data.typeName = '点赞了'; - break; - default: - data.typeName = '系统消息'; - break; - } - - return { ...data, ...item }; - } - - // 获取用户消息 - async getUserNotice(ctx) { - const { id } = ctx.state.user; - - const query = { - target_id: id - }; - - const option = { - nor: [{ type: 'system' }] - }; - - const notices = await NoticeProxy.get(query, '', option); - - const data = await Promise.all(notices.map(item => { - return new Promise(resolve => { - resolve(this.normalNotice(item.toObject())); - }); - })); - - ctx.body = data; - } - - // 获取系统消息 - async getSystemNotice(ctx) { - const { id } = ctx.state.user; - - const query = { - target_id: id, - type: 'system' - }; - - const notices = await NoticeProxy.get(query); - - const data = notices.map(item => { - return this.normalNotice(item.toObject()); - }); - - ctx.body = data; - } -} - -module.exports = new Notice(); diff --git a/packages/server/controllers/v1/reply.js b/packages/server/controllers/v1/reply.js deleted file mode 100644 index f7dce51..0000000 --- a/packages/server/controllers/v1/reply.js +++ /dev/null @@ -1,156 +0,0 @@ -const ReplyProxy = require('../../proxy/reply'); -const TopicProxy = require('../../proxy/topic'); -const NoticeProxy = require('../../proxy/notice'); - -class Reply { - // 创建回复 - async createReply(ctx) { - const { id } = ctx.state.user; - const { tid } = ctx.params; - - const topic = await TopicProxy.getById(tid); - - if (!topic) { - ctx.throw(404, '话题不存在'); - } - - const { content, reply_id } = ctx.request.body; - - if (!content) { - ctx.throw(400, '回复内容不能为空'); - } - - const _reply = { - content, - author_id: id, - topic_id: tid, - }; - - if (reply_id) { - _reply.reply_id = reply_id; - } - - // 创建回复 - const reply = await ReplyProxy.create(_reply); - - // 修改最后一次回复 - topic.reply_count += 1; - topic.last_reply = id; - await topic.save(); - - // 发送提醒 - if (reply_id) { - await NoticeProxy.create({ - type: 'at', - author_id: id, - target_id: topic.author_id, - topic_id: topic.id, - reply_id: reply.id - }); - } else { - await NoticeProxy.create({ - type: 'reply', - author_id: id, - target_id: topic.author_id, - topic_id: topic.id - }); - } - - ctx.body = ''; - } - - // 删除回复 - async deleteReply(ctx) { - const { id } = ctx.state.user; - const { rid } = ctx.params; - - const reply = await ReplyProxy.getById(rid); - - if (!reply) { - ctx.throw(404, '回复不存在'); - } - - if (!reply.author_id.equals(id)) { - ctx.throw(403, '不能删除别人的回复'); - } - - // 修改话题回复数 - const topic = await TopicProxy.getById(reply.topic_id); - - topic.reply_count -= 1; - await topic.save(); - - // 删除回复 - await ReplyProxy.deleteById(rid); - - ctx.body = ''; - } - - // 编辑回复 - async updateReply(ctx) { - const { id } = ctx.state.user; - const { rid } = ctx.params; - - const reply = await ReplyProxy.getById(rid); - - if (!reply) { - ctx.throw(404, '回复不存在'); - } - - if (!reply.author_id.equals(id)) { - ctx.throw(403, '不能编辑别人的评论'); - } - - const { content } = ctx.request.body; - - if (!content) { - ctx.throw(400, '回复内容不能为空'); - } - - reply.content = content; - await reply.save(); - - ctx.body = ''; - } - - // 回复点赞或者取消点赞 - async upOrDownReply(ctx) { - const { id } = ctx.state.user; - const { rid } = ctx.params; - - const reply = await ReplyProxy.getById(rid); - - if (!reply) { - ctx.throw(404, '回复不存在'); - } - - if (reply.author_id.equals(id)) { - ctx.throw(403, '不能给自己点赞哟'); - } - - let action; - - const upIndex = reply.ups.indexOf(id); - - if (upIndex === -1) { - reply.ups.push(id); - action = 'up'; - // 发送提醒 - await NoticeProxy.create({ - type: 'up', - author_id: id, - target_id: reply.author_id, - reply_id: reply.id - }); - } else { - reply.ups.splice(upIndex, 1); - action = 'down'; - } - - await reply.save(); - - ctx.body = action; - } -} - -module.exports = new Reply(); diff --git a/packages/server/controllers/v1/topic.js b/packages/server/controllers/v1/topic.js deleted file mode 100644 index 8854b4a..0000000 --- a/packages/server/controllers/v1/topic.js +++ /dev/null @@ -1,430 +0,0 @@ -const TopicProxy = require('../../proxy/topic'); -const UserProxy = require('../../proxy/user'); -const ActionProxy = require('../../proxy/action'); -const ReplyProxy = require('../../proxy/reply'); -const NoticeProxy = require('../../proxy/notice'); -const config = require('../../../../config'); - -class Topic { - // 创建话题 - async createTopic(ctx) { - const { id } = ctx.state.user; - const { tab, title, content } = ctx.request.body; - - try { - if (!tab) { - throw new Error('话题所属标签不能为空'); - } else if (!title) { - throw new Error('话题标题不能为空'); - } else if (!content) { - throw new Error('话题内容不能为空'); - } - } catch(err) { - ctx.throw(400, err.message); - } - - // 创建话题 - const topic = await TopicProxy.create({ - tab, - title, - content, - author_id: id - }); - - // 查询作者 - const author = await UserProxy.getById(id); - - // 积分累计 - author.score += 1; - // 话题数量累计 - author.topic_count += 1; - // 更新用户信息 - await author.save(); - - // 创建行为 - await ActionProxy.create({ - type: 'create', - author_id: author.id, - target_id: topic.id - }); - - ctx.body = ''; - } - - // 删除话题 - async deleteTopic(ctx) { - const { id } = ctx.state.user; - const { tid } = ctx.params; - - const topic = await TopicProxy.getById(tid); - - if (!topic) { - ctx.throw(404, '话题不存在'); - } - - if (!topic.author_id.equals(id)) { - ctx.throw(403, '不能删除别人的话题'); - } - - // 改变为删除状态 - topic.delete = true; - await topic.save(); - - // 查询作者 - const author = await UserProxy.getById(topic.author_id); - - // 积分减去 - author.score -= 1; - // 话题数量减少 - author.topic_count -= 1; - // 更新用户信息 - await author.save(); - - // 更新行为 - const conditions = { - type: 'create', - author_id: author.id, - target_id: topic.id - }; - - const action = await ActionProxy.getOne(conditions); - - await ActionProxy.update(conditions, { - ...action.toObject(), - is_un: true - }); - - ctx.body = ''; - } - - // 编辑话题 - async updateTopic(ctx) { - const { id } = ctx.state.user; - const { tid } = ctx.params; - - const topic = await TopicProxy.getById(tid); - - if (!topic) { - ctx.throw(404, '话题不存在'); - } - - if (!topic.author_id.equals(id)) { - ctx.throw(403, '不能编辑别人的话题'); - } - - // 更新内容 - const { - tab = topic.tab, - title = topic.title, - content = topic.content - } = ctx.request.body; - - await TopicProxy.update({ _id: tid }, { - ...topic.toObject(), - tab, - title, - content - }); - - ctx.body = ''; - } - - // 获取列表 - async getTopicList(ctx) { - const tab = ctx.query.tab || 'all'; - const page = parseInt(ctx.query.page) || 1; - const size = parseInt(ctx.query.size) || 10; - - let query = { - lock: false, - delete: false - }; - - if (!tab || tab === 'all') { - query = { - lock: false, - delete: false - }; - } else if (tab === 'good') { - query.good = true; - } else { - query.tab = tab; - } - - const options = { - skip: (page - 1) * size, - limit: size, - sort: '-top -last_reply_at' - }; - - const count = await TopicProxy.count(query); - const topics = await TopicProxy.get(query, '-lock -delete', options); - - const promiseAuthor = await Promise.all(topics.map(item => { - return new Promise(resolve => { - resolve(UserProxy.getById(item.author_id, 'id nickname avatar')); - }); - })); - - const list = topics.map((item, i) => { - return { - ...item.toObject({ - virtuals: true - }), - author: promiseAuthor[i], - }; - }); - - ctx.body = { - topics: list, - currentPage: page, - total: count, - totalPage: Math.ceil(count / size), - currentTab: tab, - tabs: config.tabs, - size - }; - } - - // 搜索话题 - async searchTopic(ctx) { - const title = ctx.query.title || ''; - const page = parseInt(ctx.query.page) || 1; - const size = parseInt(ctx.query.size) || 10; - - const query = { - title: { $regex: title }, - lock: false, - delete: false - }; - - const option = { - skip: (page - 1) * size, - limit: size, - sort: '-top -last_reply_at' - }; - - const count = await TopicProxy.count(query); - const topics = await TopicProxy.get(query, '-lock -delete', option); - - const promiseAuthor = await Promise.all(topics.map(item => { - return new Promise(resolve => { - resolve(UserProxy.getById(item.author_id, 'id nickname avatar')); - }); - })); - - - const list = topics.map((item, i) => { - return { - ...item.toObject({ - virtuals: true - }), - author: promiseAuthor[i], - }; - }); - - ctx.body = { - topics: list, - currentPage: page, - total: count, - totalPage: Math.ceil(count / size), - size - }; - } - - // 获取无人回复话题 - async getNoReplyTopic(ctx) { - const count = parseInt(ctx.query.count) || 10; - - const query = { - lock: false, - delete: false, - reply_count: 0 - }; - - const options = { - limit: count, - sort: '-top -good' - }; - - const topics = await TopicProxy.get(query, 'id title', options); - - ctx.body = topics; - } - - // 获取话题详情 - async getTopicById(ctx) { - const { tid } = ctx.params; - - const topic = await TopicProxy.getById(tid); - - if (!topic) { - ctx.throw(404, '话题不存在'); - } - - // 访问计数 - topic.visit_count += 1; - await topic.save(); - - // 作者 - const author = await UserProxy.getById(topic.author_id, 'id nickname avatar location signature score'); - // 回复 - let replies = await ReplyProxy.get({ topic_id: topic.id }); - const reuslt = await Promise.all(replies.map(item => { - return new Promise(resolve => { - resolve(UserProxy.getById(item.author_id, 'id nickname avatar')); - }); - })); - - replies = replies.map((item, i) => ({ - ...item.toObject(), - author: reuslt[i], - create_at_ago: item.create_at_ago() - })); - - // 状态 - let like; - let collect; - - const { user } = ctx.state; - - if (user) { - like = await ActionProxy.getOne({ - type: 'like', - author_id: user.id, - target_id: topic.id - }); - collect = await ActionProxy.getOne({ - type: 'collect', - author_id: user.id, - target_id: topic.id - }); - } - - like = (like && !like.is_un) || false; - collect = (collect && !collect.is_un) || false; - - ctx.body = { - topic: topic.toObject({ virtuals: true }), - author, - replies, - like, - collect - }; - } - - // 喜欢或者取消喜欢话题 - async likeOrUnLike(ctx) { - const { id } = ctx.state.user; - const { tid } = ctx.params; - - const topic = await TopicProxy.getById(tid); - - if (!topic) { - ctx.throw(404, '话题不存在'); - } - - if (topic.author_id.equals(id)) { - ctx.throw(403, '不能喜欢自己的话题哟'); - } - - const author = await UserProxy.getById(topic.author_id); - - const actionParam = { - type: 'like', - author_id: id, - target_id: topic.id - }; - - let action; - - action = await ActionProxy.getOne(actionParam); - - if (action) { - action.is_un = !action.is_un; - await action.save(); - } else { - action = await ActionProxy.create(actionParam); - } - - if (action.is_un) { - topic.like_count -= 1; - await topic.save(); - author.like_count -= 1; - author.score -= 10; - await author.save(); - } else { - topic.like_count += 1; - await topic.save(); - author.like_count += 1; - author.score += 10; - await author.save(); - await NoticeProxy.create({ - type: 'like', - author_id: id, - target_id: topic.author_id, - topic_id: topic.id - }); - } - - ctx.body = action.toObject({ virtuals: true }).actualType; - } - - // 收藏或者取消收藏话题 - async collectOrUnCollect(ctx) { - const { id } = ctx.state.user; - const { tid } = ctx.params; - - const topic = await TopicProxy.getById(tid); - - if (!topic) { - ctx.throw(404, '话题不存在'); - } - - if (topic.author_id.equals(id)) { - ctx.throw(403, '不能收藏自己的话题哟'); - } - - const author = await UserProxy.getById(topic.author_id); - - const actionParam = { - type: 'collect', - author_id: id, - target_id: topic.id - }; - - let action; - action = await ActionProxy.getOne(actionParam); - - if (action) { - action.is_un = !action.is_un; - await action.save(); - } else { - action = await ActionProxy.create(actionParam); - } - - if (action.is_un) { - topic.collect_count -= 1; - topic.save(); - author.collect_count -= 1; - author.score -= 3; - author.save(); - } else { - topic.collect_count += 1; - topic.save(); - author.collect_count += 1; - author.score += 3; - await author.save(); - await NoticeProxy.create({ - type: 'collect', - author_id: id, - target_id: topic.author_id, - topic_id: topic.id - }); - } - - ctx.body = action.toObject({ virtuals: true }).actualType; - } -} - -module.exports = new Topic(); diff --git a/packages/server/controllers/v1/user.js b/packages/server/controllers/v1/user.js deleted file mode 100644 index 61e84ad..0000000 --- a/packages/server/controllers/v1/user.js +++ /dev/null @@ -1,189 +0,0 @@ -const Base = require('../base'); -const UserProxy = require('../../proxy/user'); -const ActionProxy = require('../../proxy/action'); -const TopicProxy = require('../../proxy/topic'); -const NoticeProxy = require('../../proxy/notice'); - -class User extends Base { - // 获取积分榜用户列表 - async getUserTop(ctx) { - const { count = 10 } = ctx.query; - const limit = parseInt(count); - const users = await UserProxy.get({}, 'nickname avatar score topic_count like_count collect_count follow_count', { limit, sort: '-score' }); - ctx.body = users; - } - - // 根据ID获取用户信息 - async getUserById(ctx) { - const { uid } = ctx.params; - const { user: current } = ctx.state; - - const user = await UserProxy.getById(uid, 'nickname avatar location signature score like_count collect_count follower_count following_count'); - - if (!user) { - ctx.throw(404, '用户不存在'); - } - - let follow; - - if (current) { - follow = await ActionProxy.getOne({ - type: 'follow', - author_id: current.id, - target_id: user.id - }); - } - - follow = (follow && !follow.is_un) || false; - - ctx.body = { - ...user.toObject({ virtuals: true }), - follow - }; - } - - // 获取用户动态 - async getUserAction(ctx) { - const { uid } = ctx.params; - - const actions = await ActionProxy.get({ author_id: uid, is_un: false }); - const result = await Promise.all(actions.map(item => { - return new Promise(resolve => { - if (item.type === 'follow') { - resolve(UserProxy.getById(item.target_id, 'id nickname signature avatar')); - } else { - resolve(TopicProxy.getById(item.target_id, 'id title')); - } - }); - })); - - const data = actions.map((item, i) => { - return { - ...result[i].toObject(), - type: item.type - }; - }); - - ctx.body = data; - } - - // 获取用户专栏的列表 - async getUserCreate(ctx) { - const { uid } = ctx.params; - - const actions = await ActionProxy.get({ type: 'create', author_id: uid, is_un: false }); - const data = await Promise.all(actions.map(item => { - return new Promise(resolve => { - resolve(TopicProxy.getById(item.target_id, 'id title like_count collect_count visit_count')); - }); - })); - - ctx.body = data; - } - - // 获取用户喜欢列表 - async getUserLike(ctx) { - const { uid } = ctx.params; - - const actions = await ActionProxy.get({ type: 'like', author_id: uid, is_un: false }); - const data = await Promise.all(actions.map(item => { - return new Promise(resolve => { - resolve(TopicProxy.getById(item.target_id, 'id title')); - }); - })); - - ctx.body = data.map(item => ({ ...item.toObject(), type: 'like' })); - } - - // 获取用户收藏列表 - async getUserCollect(ctx) { - const { uid } = ctx.params; - - const actions = await ActionProxy.get({ type: 'collect', author_id: uid, is_un: false }); - const data = await Promise.all(actions.map(item => { - return new Promise(resolve => { - resolve(TopicProxy.getById(item.target_id, 'id title')); - }); - })); - - ctx.body = data.map(item => ({ ...item.toObject(), type: 'collect' })); - } - - // 获取用户粉丝列表 - async getUserFollower(ctx) { - const { uid } = ctx.params; - - const actions = await ActionProxy.get({ type: 'follow', target_id: uid, is_un: false }); - const data = await Promise.all(actions.map(item => { - return new Promise(resolve => { - resolve(UserProxy.getById(item.author_id, 'id nickname avatar')); - }); - })); - - ctx.body = data; - } - - // 获取用户关注的人列表 - async getUserFollowing(ctx) { - const { uid } = ctx.params; - - const actions = await ActionProxy.get({ type: 'follow', author_id: uid, is_un: false }); - const data = await Promise.all(actions.map(item => { - return new Promise(resolve => { - resolve(UserProxy.getById(item.target_id, 'id nickname avatar')); - }); - })); - - ctx.body = data; - } - - // 关注或者取消关注某个用户 - async followOrUn(ctx) { - const { uid } = ctx.params; - const { id } = ctx.state.user; - - if (uid === id) { - ctx.throw(403, '不能关注你自己'); - } - - const targetUser = await UserProxy.getById(uid); - const authorUser = await UserProxy.getById(id); - - const actionParam = { - type: 'follow', - author_id: id, - target_id: uid - }; - - let action; - action = await ActionProxy.getOne(actionParam); - - if (action) { - action.is_un = !action.is_un; - await action.save(); - } else { - action = await ActionProxy.create(actionParam); - } - - if (action.is_un) { - targetUser.follower_count -= 1; - await targetUser.save(); - authorUser.following_count -= 1; - await authorUser.save(); - } else { - targetUser.follower_count += 1; - await targetUser.save(); - authorUser.following_count += 1; - await authorUser.save(); - await NoticeProxy.create({ - type: 'follow', - author_id: id, - target_id: uid - }); - } - - ctx.body = action.toObject({ virtuals: true }).actualType; - } -} - -module.exports = new User(); diff --git a/packages/server/controllers/v2/topic.js b/packages/server/controllers/v2/topic.js deleted file mode 100644 index e66df93..0000000 --- a/packages/server/controllers/v2/topic.js +++ /dev/null @@ -1,100 +0,0 @@ -const moment = require('moment'); -const TopicProxy = require('../../proxy/topic'); - -class Topic { - // 本周新增话题数 - async countTopicThisWeek(ctx) { - const start = moment().startOf('week'); - const end = moment().endOf('week'); - - const count = await TopicProxy.count({ - create_at: { $gte: start, $lt: end } - }); - - ctx.body = count; - } - - // 上周新增话题数 - async countTopicLastWeek(ctx) { - const start = moment().startOf('week').subtract(1, 'w'); - const end = moment().endOf('week').subtract(1, 'w'); - - const count = await TopicProxy.count({ - create_at: { $gte: start, $lt: end } - }); - - ctx.body = count; - } - - // 统计话题总数 - async countTopicTotal(ctx) { - const count = await TopicProxy.count(); - - ctx.body = count; - } - - // 删除话题(超管物理删除) - async deleteTopic(ctx) { - const { tid } = ctx.params; - - await TopicProxy.deleteById(tid); - - ctx.body = ''; - } - - // 话题置顶 - async topTopic(ctx) { - const { tid } = ctx.params; - const topic = await TopicProxy.getById(tid); - - if (topic.top) { - topic.top = false; - await topic.save(); - } else { - topic.top = true; - await topic.save(); - } - - const action = topic.top ? 'top' : 'un_top'; - - ctx.body = action; - } - - // 话题加精 - async goodTopic(ctx) { - const { tid } = ctx.params; - const topic = await TopicProxy.getById(tid); - - if (topic.good) { - topic.good = false; - await topic.save(); - } else { - topic.good = true; - await topic.save(); - } - - const action = topic.good ? 'good' : 'un_good'; - - ctx.body = action; - } - - // 话题锁定(封贴) - async lockTopic(ctx) { - const { tid } = ctx.params; - const topic = await TopicProxy.getById(tid); - - if (topic.lock) { - topic.lock = false; - await topic.save(); - } else { - topic.lock = true; - await topic.save(); - } - - const action = topic.lock ? 'lock' : 'un_lock'; - - ctx.body = action; - } -} - -module.exports = new Topic(); diff --git a/packages/server/controllers/v2/user.js b/packages/server/controllers/v2/user.js deleted file mode 100644 index a74bb61..0000000 --- a/packages/server/controllers/v2/user.js +++ /dev/null @@ -1,181 +0,0 @@ -const moment = require('moment'); -const Base = require('../base'); -const UserProxy = require('../../proxy/user'); - -class User extends Base { - constructor() { - super(); - this.createUser = this.createUser.bind(this); - } - - // 本周新增用户数 - async countUserThisWeek(ctx) { - const start = moment().startOf('week'); - const end = moment().endOf('week'); - - const count = await UserProxy.count({ - create_at: { $gte: start, $lt: end } - }); - - ctx.body = count; - } - - // 上周新增用户数 - async countUserLastWeek(ctx) { - const start = moment().startOf('week').subtract(1, 'w'); - const end = moment().endOf('week').subtract(1, 'w'); - - const count = await UserProxy.count({ - create_at: { $gte: start, $lt: end } - }); - - ctx.body = count; - } - - // 统计用户总数 - async countUserTotal(ctx) { - const count = await UserProxy.count(); - - ctx.body = count; - } - - // 用户列表 - async getUserList(ctx) { - const page = parseInt(ctx.query.page) || 1; - const size = parseInt(ctx.query.size) || 10; - - const option = { - skip: (page - 1) * size, - limit: size, - sort: 'create_at' - }; - - const total = await UserProxy.count(); - const users = await UserProxy.get({}, '-password', option); - - const list = users.map(item => { - return { - ...item.toObject(), - create_at: moment(item.create_at).format('YYYY-MM-DD HH:mm') - }; - }); - - ctx.body = { - users: list, - page, - size, - total - }; - } - - // 创建用户 - async createUser(ctx) { - const { user } = ctx.state; - const { email, password, nickname, role } = ctx.request.body; - - try { - if (!email || !/^([A-Za-z0-9_\-.])+@([A-Za-z0-9_\-.])+\.([A-Za-z]{2,4})$/.test(email)) { - throw new Error('邮箱格式不正确'); - } else if (!password || !/(?!^(\d+|[a-zA-Z]+|[~!@#$%^&*?]+)$)^[\w~!@#$%^&*?].{6,18}/.test(password)) { - throw new Error('密码必须为数字、字母和特殊字符其中两种组成并且在6至18位之间'); - } else if (!nickname || nickname.length > 8 || nickname.length < 2) { - throw new Error('昵称必须在2至8位之间'); - } else if (user.role < role || role < 0 || role > 100) { - throw new Error('权限值必须在0至100之间、且不能大于当前用户的权限值'); - } - } catch(err) { - ctx.throw(400, err.message); - } - - let existUser; - - existUser = await UserProxy.getOne({ email }); - if (existUser) { - ctx.throw(409, '手机号已经存在'); - } - - existUser = await UserProxy.getOne({ nickname }); - if (existUser) { - ctx.throw(409, '昵称已经存在'); - } - - const bcryptPassword = await this._encryption(password); - - await UserProxy.create({ - email, - password: bcryptPassword, - nickname, - role - }); - - ctx.body = ''; - } - - // 删除用户(超管物理删除) - async deleteUser(ctx) { - const { uid } = ctx.params; - const currentUser = await UserProxy.getById(uid); - - if (currentUser.role > 100) { - ctx.throw(401, '无法删除超级管理员'); - } - - await UserProxy.deleteById(uid); - - ctx.body = ''; - } - - // 设为星标用户 - async starUser(ctx) { - const { uid } = ctx.params; - const { user: currentUser } = ctx.state; - - if (currentUser.id === uid) { - ctx.throw(403, '不能操作自身'); - } - - const user = await UserProxy.getById(uid); - - if (user.star) { - user.star = false; - await user.save(); - } else { - user.star = true; - await user.save(); - } - - const action = user.star ? 'star' : 'un_star'; - - ctx.body = action; - } - - // 锁定用户(封号) - async lockUser(ctx) { - const { uid } = ctx.params; - const { user: currentUser } = ctx.state; - - if (currentUser.id === uid) { - ctx.throw(403, '不能操作自身'); - } - - const user = await UserProxy.getById(uid); - - if (currentUser.role < user.role) { - ctx.throw(403, '不能操作权限值高于自身的用户'); - } - - if (user.lock) { - user.lock = false; - await user.save(); - } else { - user.lock = true; - await user.save(); - } - - const action = user.lock ? 'lock' : 'un_lock'; - - ctx.body = action; - } -} - -module.exports = new User(); diff --git a/packages/server/package.json b/packages/server/package.json index 421112b..8fedd0d 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "license": "MIT", "scripts": { - "start": "cross-env NODE_ENV=development nodemon app.js", + "dev": "cross-env NODE_ENV=development nodemon app.js", "test": "cross-env NODE_ENV=test nyc --reporter=html mocha __test__ --recursive --exit", "coverage": "cross-env NODE_ENV=test nyc report --reporter=text-lcov > coverage.lcov && codecov" }, diff --git a/packages/server/router.js b/packages/server/router.js index f4a28fd..d44af33 100644 --- a/packages/server/router.js +++ b/packages/server/router.js @@ -1,86 +1,64 @@ const Router = require('koa-router'); -const Auth = require('./middlewares/auth'); -const User = require('./controllers/user'); -const Aider = require('./controllers/aider'); -const UserV1 = require('./controllers/v1/user'); -const UserV2 = require('./controllers/v2/user'); -const TopicV1 = require('./controllers/v1/topic'); -const TopicV2 = require('./controllers/v2/topic'); -const ReplyV1 = require('./controllers/v1/reply'); -const NoticeV1 = require('./controllers/v1/notice'); -const { ALLOW_SIGNUP } = require('../../config'); +const Aider = require('./controller/aider'); +const User = require('./controller/user'); +const Topic = require('./controller/topic'); +const auth = require('./middleware/auth'); const router = new Router(); -router.get('/', ctx => { ctx.body = 'Welcome to mints api'; }); // API 测试 +router + .get('/captcha', Aider.getCaptcha) // 获取图形验证码 + .post('/upload', auth(), Aider.upload) // 上传文件 + .get('/upload/:filename', Aider.getFile) // 获取文件 + .post('/signup', User.signup) // 注册 + .post('/signin', User.signin) // 登录 + .post('/forget-pass', User.forgetPass) // 忘记密码 + .post('/reset-pass', User.resetPass) // 重置密码 + .get('/info', auth(), User.getUser) // 获取当前登录用户信息 + .put('/setting', auth(), User.updateSetting) // 更新个人信息\ + .put('/password', auth(), User.updatePass) // 修改密码 + .get('/notice/user', auth(), User.getUserNotice) // 获取用户消息 + .get('/notice/system', auth(), User.getSystemNotice) // 获取系统消息 + .get('/users/top', User.getUserTop) // 获取积分榜用户列表 + .get('/user/:uid', User.getUserById) // 根据ID获取用户信息 + .get('/user/:uid/action', User.getUserAction) // 获取用户动态 + .get('/user/:uid/create', User.getUserCreate) // 获取用户专栏列表 + .get('/user/:uid/like', User.getUserLike) // 获取用户喜欢列表 + .get('/user/:uid/collect', User.getUserCollect) // 获取用户收藏列表 + .get('/user/:uid/follower', User.getUserFollower) // 获取用户粉丝列表 + .get('/user/:uid/following', User.getUserFollowing) // 获取用户关注列表 + .put('/user/:uid/follow', auth(), User.followUser) // 关注或者取消关注用户 + .get('/topics', Topic.getTopicList) // 获取话题列表 + .get('/topics/search', Topic.searchTopic) // 搜索话题列表 + .get('/topics/no-reply', Topic.getNoReplyTopic) // 获取无人回复的话题 + .post('/topic', auth(), Topic.createTopic) // 创建话题 + .delete('/topic/:tid', auth(), Topic.deleteTopic) // 删除话题 + .put('/topic/:tid', auth(), Topic.updateTopic) // 编辑话题 + .get('/topic/:tid', Topic.getTopicById) // 根据ID获取话题详情 + .put('/topic/:tid/like', auth(), Topic.liekTopic) // 喜欢或者取消喜欢话题 + .put('/topic/:tid/collect', auth(), Topic.collectTopic) // 收藏或者取消收藏话题 + .post('/topic/:tid/reply', auth(), Topic.createReply) // 创建回复 + .put('/reply/:rid/up', auth(), Topic.upReply); // 点赞回复 -if (ALLOW_SIGNUP || process.env.NODE_ENV === 'test') { - router.post('/signup', User.signup); // 注册 - router.get('/set_active', User.setActive); // 账户激活 -} else { - router.post('/github', User.github); // Github 登录 -} +const routerBe = new Router({ prefix: '/backend' }); -router.get('/captcha', Aider.getCaptcha); // 获取图形验证码 -router.get('/send_mail', User.sendMail); // 发送验证邮件 -router.post('/signin', User.signin); // 登录 -router.post('/forget_pass', User.forgetPass); // 忘记密码 -router.post('/reset_pass', User.resetPass); // 重置密码 -router.post('/upload_avatar', Auth.userRequired, User.uploadAvatar); // 头像上传 -router.put('/setting', Auth.userRequired, User.updateSetting); // 更新个人信息 -router.patch('/update_pass', Auth.userRequired, User.updatePass); // 修改密码 -router.get('/info', Auth.userRequired, User.getCurrentUser); // 获取当前用户信息 - -const routerV1 = new Router({ prefix: '/v1' }); - -routerV1 - .get('/users/top', UserV1.getUserTop) // 获取积分榜用户列表 - .get('/user/:uid', UserV1.getUserById) // 根据ID获取用户信息 - .get('/user/:uid/action', UserV1.getUserAction) // 获取用户动态 - .get('/user/:uid/create', UserV1.getUserCreate) // 获取用户专栏列表 - .get('/user/:uid/like', UserV1.getUserLike) // 获取用户喜欢列表 - .get('/user/:uid/collect', UserV1.getUserCollect) // 获取用户收藏列表 - .get('/user/:uid/follower', UserV1.getUserFollower) // 获取用户粉丝列表 - .get('/user/:uid/following', UserV1.getUserFollowing) // 获取用户关注列表 - .patch('/user/:uid/follow_or_un', Auth.userRequired, UserV1.followOrUn) // 关注或者取消关注用户 - .post('/create', Auth.userRequired, TopicV1.createTopic) // 创建话题 - .delete('/topic/:tid/delete', Auth.userRequired, TopicV1.deleteTopic) // 删除话题 - .put('/topic/:tid/update', Auth.userRequired, TopicV1.updateTopic) // 编辑话题 - .get('/topics/list', TopicV1.getTopicList) // 获取话题列表 - .get('/topics/search', TopicV1.searchTopic) // 搜索话题列表 - .get('/topics/no_reply', TopicV1.getNoReplyTopic) // 获取无人回复的话题 - .get('/topic/:tid', TopicV1.getTopicById) // 根据ID获取话题详情 - .patch('/topic/:tid/like_or_un', Auth.userRequired, TopicV1.likeOrUnLike) // 喜欢或者取消喜欢话题 - .patch('/topic/:tid/collect_or_un', Auth.userRequired, TopicV1.collectOrUnCollect) // 收藏或者取消收藏话题 - .post('/topic/:tid/reply', Auth.userRequired, ReplyV1.createReply) // 创建回复 - .delete('/reply/:rid/delete', Auth.userRequired, ReplyV1.deleteReply) // 删除回复 - .put('/reply/:rid/update', Auth.userRequired, ReplyV1.updateReply) // 编辑回复 - .patch('/reply/:rid/up_or_down', Auth.userRequired, ReplyV1.upOrDownReply) // 回复点赞或者取消点赞 - .get('/notice/user', Auth.userRequired, NoticeV1.getUserNotice) // 获取用户消息 - .get('/notice/system', Auth.userRequired, NoticeV1.getSystemNotice); // 获取系统消息 - -const routerV2 = new Router({ prefix: '/v2' }); - -routerV2 - .get('/', ctx => { ctx.body = 'Version_2 API'; }) - .get('/users/new_this_week', Auth.adminRequired, UserV2.countUserThisWeek) // 获取本周新增用户数 - .get('/users/new_last_week', Auth.adminRequired, UserV2.countUserLastWeek) // 获取上周新增用户数 - .get('/users/total', Auth.adminRequired, UserV2.countUserTotal) // 获取用户总数 - .get('/users/list', Auth.adminRequired, UserV2.getUserList) // 获取用户列表 - .post('/users/create', Auth.adminRequired, UserV2.createUser) // 新增用户 - .delete('/user/:uid/delete', Auth.rootRequired, UserV2.deleteUser) // 删除用户(超管物理删除) - .patch('/user/:uid/star', Auth.rootRequired, UserV2.starUser) // 设为星标用户 - .patch('/user/:uid/lock', Auth.adminRequired, UserV2.lockUser) // 锁定用户(封号) - .get('/topics/new_this_week', Auth.adminRequired, TopicV2.countTopicThisWeek) // 获取本周新增话题数 - .get('/topics/new_last_week', Auth.adminRequired, TopicV2.countTopicLastWeek) // 获取上周新增话题数 - .get('/topics/total', Auth.adminRequired, TopicV2.countTopicTotal) // 获取话题总数 - .delete('/topic/:tid/delete', Auth.rootRequired, TopicV2.deleteTopic) // 删除话题(超管物理删除) - .patch('/topic/:tid/top', Auth.adminRequired, TopicV2.topTopic) // 话题置顶 - .patch('/topic/:tid/good', Auth.adminRequired, TopicV2.goodTopic) // 话题加精 - .patch('/topic/:tid/lock', Auth.adminRequired, TopicV2.lockTopic); // 话题锁定(封贴) +routerBe + .get('/dashboard', auth(1), Aider.dashboard) // 获取系统概览 + .get('/users', auth(1), User.roleGetUserList) // 获取用户列表 + .post('/user', auth(1), User.roleCreateUser) // 新增用户 + .delete('/user/:uid', auth(100), User.roleDeleteUser) // 删除用户(超管物理删除) + .put('/user/:uid', auth(1), User.roleUpdateUser) // 更新用户 + .put('/user/:uid/star', auth(100), User.roleStarUser) // 设为星标用户 + .put('/user/:uid/lock', auth(1), User.roleLockUser) // 锁定用户(封号) + .get('/topics', auth(1), Topic.roleGetTopicList) // 获取话题列表 + .post('/topic', auth(1), Topic.roleCreateTopic) // 创建话题 + .delete('/topic/:tid', auth(100), Topic.roleDeleteTopic) // 删除话题(超管物理删除) + .put('/topic/:tid', auth(1), Topic.roleUpdateTopic) // 更新话题 + .put('/topic/:tid/top', auth(100), Topic.roleTopTopic) // 话题置顶 + .put('/topic/:tid/good', auth(1), Topic.roleGoodTopic) // 话题加精 + .put('/topic/:tid/lock', auth(1), Topic.roleLockTopic); // 话题锁定(封贴) module.exports = { rt: router.routes(), - v1: routerV1.routes(), - v2: routerV2.routes() + be: routerBe.routes(), };