diff --git a/README.md b/README.md new file mode 100644 index 0000000..ec055ef --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +sd-pics-hub +=============== + + +
+ + + +This is an application that utilizes stable diffusion to create personalized AI portraits. You simply need to upload a clear selfie, and the application will generate AI portraits in various styles. + +## Copyright Information + +sd-pics-hub is released under the [Apache License 2.0]() open-source license and is available for free use. + +Copyright © 2023 by EMart diff --git a/README_ZH.md b/README_ZH.md new file mode 100644 index 0000000..2536a3d --- /dev/null +++ b/README_ZH.md @@ -0,0 +1,38 @@ +sd-pics-hub +=============== + + + + + +这是一个利用stable diffusion生成个人AI写真的应用,你只需要上传一张清晰的自拍照,就可以生成多种风格的AI写真 + +## 目录结构 + +总体目录结构如下 + +``` +├─sd-pics-hub +│ ├─api (数据接口部分,使用Egg.js框架) +│ ├─client 客户端,暂定使用hbuild+vue构建,支持多端发布) +│ ├─doc (文档、资料等) +│ │ ├─database.md (数据结构) +│ ├─worker (用于接收任务,调用stable-diffusion接口完成工作的工人模块) +│ ├─docker-compose.yml (用于一键启动docker-compose容器编排配置文件) +│ ├─README.md (项目说明文档) +│ ├─README_ZH.md (项目说明文档-中文) +``` +## 功能规划 + +**1.0 版本** + +1. + +## 版权信息 + +sd-pics-hub 遵循 [Apache License 2.0]() 开源协议发布,并提供免费使用。 + +Copyright © 2023 by EMart diff --git a/api/.env b/api/.env new file mode 100644 index 0000000..c5c9aa4 --- /dev/null +++ b/api/.env @@ -0,0 +1,44 @@ +# OpenAI API Key - https://platform.openai.com/overview +OPENAI_API_KEY= + +# change this to an `accessToken` extracted from the ChatGPT site's `https://chat.openai.com/api/auth/session` response +OPENAI_ACCESS_TOKEN= + +# OpenAI API Base URL - https://api.openai.com +OPENAI_API_BASE_URL= + +# OpenAI API Model - https://platform.openai.com/docs/models +OPENAI_API_MODEL= + +# set `true` to disable OpenAI API debug log +OPENAI_API_DISABLE_DEBUG= + +# Reverse Proxy - Available on accessToken +# Default: https://bypass.churchless.tech/api/conversation +# More: https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy +API_REVERSE_PROXY= + +# timeout +TIMEOUT_MS=100000 + +# Rate Limit +MAX_REQUEST_PER_HOUR= + +# Secret key +AUTH_SECRET_KEY= + +# Socks Proxy Host +SOCKS_PROXY_HOST=192.168.8.105 + +# Socks Proxy Port +SOCKS_PROXY_PORT=18991 + +# Socks Proxy Username +SOCKS_PROXY_USERNAME= + +# Socks Proxy Password +SOCKS_PROXY_PASSWORD= + +# HTTPS PROXY +HTTPS_PROXY= + diff --git a/api/.eslintignore b/api/.eslintignore new file mode 100644 index 0000000..4ebc8ae --- /dev/null +++ b/api/.eslintignore @@ -0,0 +1 @@ +coverage diff --git a/api/.eslintrc b/api/.eslintrc new file mode 100644 index 0000000..298f10d --- /dev/null +++ b/api/.eslintrc @@ -0,0 +1,9 @@ +{ + "overlay": { + "warnings": false, + "errors": false + }, + "lintOnSave": false, + "extends": "eslint-config-egg", + "root": false +} diff --git a/api/.github/workflows/nodejs.yml b/api/.github/workflows/nodejs.yml new file mode 100644 index 0000000..df76e78 --- /dev/null +++ b/api/.github/workflows/nodejs.yml @@ -0,0 +1,46 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: Node.js CI + +on: + push: + branches: + - main + - master + pull_request: + branches: + - main + - master + schedule: + - cron: '0 2 * * *' + +jobs: + build: + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + node-version: [16, 18] + os: [ubuntu-latest, windows-latest, macos-latest] + + steps: + - name: Checkout Git Source + uses: actions/checkout@v2 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + + - name: Install Dependencies + run: npm i + + - name: Continuous Integration + run: npm run ci + + - name: Code Coverage + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/api/.gitignore b/api/.gitignore new file mode 100644 index 0000000..652759b --- /dev/null +++ b/api/.gitignore @@ -0,0 +1,15 @@ +logs/ +npm-debug.log +yarn-error.log +node_modules/ +*-lock.json +*-lock.yaml +yarn.lock +coverage/ +.idea/ +run/ +.DS_Store +*.sw* +*.un~ +typings/ +.nyc_output/ diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..f065a74 --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,40 @@ +# 设置基础镜像,如果本地没有该镜像,会从Docker.io服务器pull镜像 +FROM node:18.14.2-alpine3.17 + +# # 设置时区 +# RUN apk --update add tzdata \ +# && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ +# && echo "Asia/Shanghai" > /etc/timezone \ +# && apk del tzdata + +# 创建app目录 +RUN mkdir -p /app + +# 设置工作目录 +WORKDIR /app + +# 拷贝package.json文件到工作目录 +# !!重要:package.json需要单独添加。 +# Docker在构建镜像的时候,是一层一层构建的,仅当这一层有变化时,重新构建对应的层。 +# 如果package.json和源代码一起添加到镜像,则每次修改源码都需要重新安装npm模块,这样木有必要。 +# 所以,正确的顺序是: 添加package.json;安装npm模块;添加源代码。 +COPY package.json /app/package.json + +# 安装npm依赖(使用淘宝的镜像源) +# 如果使用的境外服务器,无需使用淘宝的镜像源,即改为`RUN npm i`。 +RUN npm install -g npm@9.6.1 +RUN npm i --production --registry=https://registry.npmmirror.com + +# 拷贝所有源代码到工作目录 +COPY . /app + +RUN cd /app/lib/plugins/egg-lu-redis && npm i --registry=https://registry.npmmirror.com + + +WORKDIR /app + +# 暴露容器端口 +EXPOSE 7001 + +# 启动node应用 +CMD npm start \ No newline at end of file diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..e56fb8d --- /dev/null +++ b/api/README.md @@ -0,0 +1,33 @@ +# chat-egg-server + + + +## QuickStart + + + +see [egg docs][egg] for more detail. + +### Development + +```bash +$ npm i +$ npm run dev +$ open http://localhost:7001/ +``` + +### Deploy + +```bash +$ npm start +$ npm stop +``` + +### npm scripts + +- Use `npm run lint` to check code style. +- Use `npm test` to run unit test. +- Use `npm run autod` to auto detect dependencies upgrade, see [autod](https://www.npmjs.com/package/autod) for more detail. + + +[egg]: https://eggjs.org diff --git a/api/app/controller/account.js b/api/app/controller/account.js new file mode 100644 index 0000000..d0ecc25 --- /dev/null +++ b/api/app/controller/account.js @@ -0,0 +1,99 @@ +'use strict'; + +const { Controller } = require('egg'); + +class AccountController extends Controller { + + /** + * 验证码登录 + */ + async phoneLogin() { + + } + + /** + * 发送短信验证码 + */ + async sendSMS() { + + } + + /** + * 登录账号 + */ + async login() { + const { ctx, app, service } = this; + const input = ctx.request.body; + // 验证参数合法性 + const errors = app.validator.validate( + { + user_name: { type: 'string', required: true }, + timestamp: { type: 'number', required: true }, + random_number: { type: 'number', required: true }, + user_key: { type: 'string', max: 32, min: 32, required: true }, + }, + input + ); + if (errors && errors.length > 0) + throw ctx.ltool.err(`"${errors[0].field}"${errors[0].message}`, 4011); + + // 验证登录信息合法性 + const userinfo = await ctx.service.user.userKeyLogin(input, ctx.request.ip); + const token_3rd_session = ctx.ltool.nonceString(22); + + // 更新用户登录token + await ctx.service.user.updateSessionID( + userinfo.id, + token_3rd_session, + input.logintype + ); + + //返回信息 + ctx.body = { + token: token_3rd_session, + userid: userinfo.id, + logintype: input.logintype, + }; + } + /** + * 注册账号 + */ + async register() { + const { ctx, app } = this; + const { account, nick_name, pwd, logintype } = ctx.request.body; + const input = { account, nick_name, pwd ,logintype}; + + // 验证参数合法性 + const errors = app.validator.validate( + { + account: { type: 'string', required: true }, + nick_name: { type: 'string', required: true }, + pwd: { type: 'password', required: true }, + }, + input + ); + if (errors && errors.length > 0) + throw ctx.ltool.err(`"${errors[0].field}"${errors[0].message}`, 4011); + + // 尝试注册 + const userinfo = await ctx.service.user.register(input, ctx.request.ip); + + // 生成token + const token_3rd_session = ctx.ltool.nonceString(22); + // 更新用户登录token + await ctx.service.user.updateSessionID( + userinfo.id, + token_3rd_session, + input.logintype + ); + + //返回信息 + ctx.body = { + token: token_3rd_session, + userid: userinfo.id, + logintype: input.logintype, + }; + } +} + +module.exports = AccountController; diff --git a/api/app/controller/default.js b/api/app/controller/default.js new file mode 100644 index 0000000..c2bd2e1 --- /dev/null +++ b/api/app/controller/default.js @@ -0,0 +1,23 @@ +'use strict'; + +const { Controller } = require('egg'); + +class DefaultController extends Controller { + async getnowtime() { + this.ctx.body = new Date().getTime(); + } + async test() { + const { ctx , app} = this; + const user = await this.ctx.service.user.findByNameDB('284485094@qq.com'); + ctx.body = user?user:""; + } + + async pubChat() { + const { ctx, app } = this; + const { messages} = ctx.request.body; + const result = await ctx.service.chatgpt.SendMsg(messages); + ctx.body = result; + } +} + +module.exports = DefaultController; \ No newline at end of file diff --git a/api/app/controller/doppelganger.js b/api/app/controller/doppelganger.js new file mode 100644 index 0000000..73213de --- /dev/null +++ b/api/app/controller/doppelganger.js @@ -0,0 +1,14 @@ +/** + * 数字分身相关 + */ + +'use strict'; + +const { Controller } = require('egg'); + +class DoppelgangerController extends Controller { + async create(){} + async getList(){} +} + +module.exports = DoppelgangerController; diff --git a/api/app/controller/home.js b/api/app/controller/home.js new file mode 100644 index 0000000..2b2d8d1 --- /dev/null +++ b/api/app/controller/home.js @@ -0,0 +1,52 @@ +'use strict'; + +const { Controller } = require('egg'); +const { v4:uuid } = require('uuid'); + +class HomeController extends Controller { + async index() { + const { ctx , app} = this; + const nsp = app.io.of('/'); + // app.redis.sGet("lalal",100,async ()=>{ + // return (new Date()).valueOf(); + // }); + // const selfMark = await app.redis.sLock("test1",20); + // if(selfMark){ + // setTimeout(() => { + // app.redis.sUnLock("test1",selfMark) + // }, 2000); + // } + // console.log(app.cerror.g("dsfsfd",1111)) + // throw ctx.ltool.err("dsfsdf",100); + const clients = nsp.adapter.clients('default_room', (err, clients) => { + ctx.logger.warn('##########online_leave', clients.length); + + // 获取 client 信息 + // const clientsDetail = {}; + // clients.forEach(client => { + // const _client = app.io.sockets.sockets[client]; + // const _query = _client.handshake.query; + // clientsDetail[client] = _query; + // }); + + // // 更新在线用户列表 + // nsp.to(room).emit('online', { + // clients, + // action: 'leave', + // target: 'participator', + // message: `User(${id}) leaved.`, + // }); + }); + nsp.to('default_room').emit('welcome',"靓仔,现在是"+ Date.now()) + // throw new Error("test",{code:9808}) + // ctx.body = ctx.ltool.md5("dsfdsffs"); + // ctx.body = uuid(); + let numbers = []; + for (let index = 0; index < 100; index++) { + numbers.push(ctx.ltool.nonceString(22,false,"abc")); + } + ctx.body = numbers; + } +} + +module.exports = HomeController; diff --git a/api/app/controller/task.js b/api/app/controller/task.js new file mode 100644 index 0000000..0bf6aa2 --- /dev/null +++ b/api/app/controller/task.js @@ -0,0 +1,15 @@ +/** + * 任务相关(生成写真、高清修图等) + */ + +'use strict'; + +const { Controller } = require('egg'); + +class TaskController extends Controller { + async create(){} + async getList(){} + async getInfo(){} +} + +module.exports = TaskController; diff --git a/api/app/controller/template.js b/api/app/controller/template.js new file mode 100644 index 0000000..68ec61e --- /dev/null +++ b/api/app/controller/template.js @@ -0,0 +1,13 @@ +/** + * 写真模板相关 + */ + +'use strict'; +const { Controller } = require('egg'); + +class TemplateController extends Controller { + async getList(){} + async getInfo(){} +} + +module.exports = TemplateController; diff --git a/api/app/controller/user.js b/api/app/controller/user.js new file mode 100644 index 0000000..b1f6ada --- /dev/null +++ b/api/app/controller/user.js @@ -0,0 +1,48 @@ +'use strict'; + +const { Controller } = require('egg'); + +class UserController extends Controller { + /** + * 获得自己的信息 + */ + async getMyInfo() { + const { ctx, app } = this; + const user_id = ctx.user.id; + const user = await ctx.service.user.findUserbyId(user_id); + if (!user) throw ctx.ltool.err('用户不存在', 40004); + ctx.body = user; + } + + /** + * 根据用户id获得用户信息 + */ + async getUserInfoById() { + const { ctx, app } = this; + const { user_id } = ctx.request.query; + // 验证参数合法性 + const errors = app.validator.validate( + { + user_id: { type: 'string', required: true }, + }, + { user_id } + ); + + if (errors && errors.length > 0) + throw ctx.ltool.err(`"${errors[0].field}"${errors[0].message}`, 4011); + + const user = await ctx.service.user.findUserbyId(user_id); + if (!user) throw ctx.ltool.err('用户不存在', 40004); + + let result = { + _id: user._id, + nick_name:user.nick_name, + avatar:user.avatar, + contact_num:user.contact_num, + is_friend : await ctx.service.friend.isFriend(ctx.user.id, user._id) + } + ctx.body = result; + } +} + +module.exports = UserController; \ No newline at end of file diff --git a/api/app/extend/helper.js b/api/app/extend/helper.js new file mode 100644 index 0000000..c12d985 --- /dev/null +++ b/api/app/extend/helper.js @@ -0,0 +1,34 @@ +// 框架扩展用于封装数据格式 +module.exports = { + parseMsg(action, payload = {}, metadata = {}) { + const meta = Object.assign( + {}, + { + timestamp: Date.now(), + }, + metadata + ); + + return { + meta, + data: { + action, + payload, + }, + }; + }, +}; +/** + * Format: + { + data: { + action: 'exchange', // 'deny' || 'exchange' || 'broadcast' + payload: {}, + }, + meta:{ + timestamp: 1512116201597, + client: 'nNx88r1c5WuHf9XuAAAB', + target: 'nNx88r1c5WuHf9XuAAAB' + }, + } + */ \ No newline at end of file diff --git a/api/app/io/controller/nsp.js b/api/app/io/controller/nsp.js new file mode 100644 index 0000000..3b42591 --- /dev/null +++ b/api/app/io/controller/nsp.js @@ -0,0 +1,26 @@ + +const Controller = require('egg').Controller; + +class NspController extends Controller { + async exchange() { + const { ctx, app } = this; + const nsp = app.io.of('/'); + const message = ctx.args[0] || {}; + const socket = ctx.socket; + const client = socket.id; + + ctx.logger.debug(`[${socket.id}]exchange`,message); + // ctx.logger.debug("client handshake",socket.handshake); + // ctx.logger.debug("client rooms",socket.rooms); + try { + // const { target, payload } = message; + // if (!target) return; + // const msg = ctx.helper.parseMsg('exchange', payload, { client, target }); + // nsp.emit(target, msg); + } catch (error) { + app.logger.error(error); + } + } + } + + module.exports = NspController; \ No newline at end of file diff --git a/api/app/io/middleware/connection.js b/api/app/io/middleware/connection.js new file mode 100644 index 0000000..1077990 --- /dev/null +++ b/api/app/io/middleware/connection.js @@ -0,0 +1,103 @@ +const PREFIX = 'room'; +const ROOM = 'default_room'; +module.exports = () => { + return async (ctx, next) => { + const { app, socket, logger, helper } = ctx; + const id = socket.id; + // socket.to(id).emit("welcome","你好靓仔,我是EGG"); + + socket.emit('welcome', 'hello worker,i am shitian'); + socket.join(ROOM); + const query = socket.handshake.query; + logger.debug('#connent socket id', id); + logger.debug('#connent socket query', query); + + const nsp = app.io.of('/'); + const tick = (id, msg) => { + logger.debug('#tick', id, msg); + + // 踢出用户前发送消息 + socket.emit("message", helper.parseMsg('deny', msg)); + + // 调用 adapter 方法踢出用户,客户端触发 disconnect 事件 + nsp.adapter.remoteDisconnect(id, true, err => { + logger.error(err); + }); + }; + console.log("client query", query) + // 验证失败,踢出用户 + if (app.config.io.token!=query.token){ + tick(id, { + code: 401, + message: '请先登录', + data: null, + }); + return; + } + // 用户加入自己的房间 + socket.join(query.deviceid); + logger.debug('#join', query.deviceid); + // // 用户信息 + // const { room, userId } = query; + // const rooms = [room]; + + // logger.debug('#user_info', id, room, userId); + + // // 检查房间是否存在,不存在则踢出用户 + // // 备注:此处 app.redis 与插件无关,可用其他存储代替 + // const hasRoom = await app.redis.get(`${PREFIX}:${room}`); + + // logger.debug('#has_exist', hasRoom); + + // if (!hasRoom) { + // tick(id, { + // type: 'deleted', + // message: 'deleted, room has been deleted.', + // }); + // return; + // } + + // // 用户加入 + // logger.debug('#join', room); + // socket.join(room); + + // // 在线列表 + // nsp.adapter.clients(rooms, (err, clients) => { + // logger.debug('#online_join', clients); + + // // 更新在线用户列表 + // nsp.to(room).emit('online', { + // clients, + // action: 'join', + // target: 'participator', + // message: `User(${id}) joined.`, + // }); + // }); + + await next(); + + // 用户离开 + logger.debug('#leave', `socketid: ${id}, deviceid: ${query.deviceid}`); + + // // 在线列表 + // nsp.adapter.clients(rooms, (err, clients) => { + // logger.debug('#online_leave', clients); + + // // 获取 client 信息 + // // const clientsDetail = {}; + // // clients.forEach(client => { + // // const _client = app.io.sockets.sockets[client]; + // // const _query = _client.handshake.query; + // // clientsDetail[client] = _query; + // // }); + + // // 更新在线用户列表 + // nsp.to(room).emit('online', { + // clients, + // action: 'leave', + // target: 'participator', + // message: `User(${id}) leaved.`, + // }); + // }); + }; +}; diff --git a/api/app/io/middleware/packet.js b/api/app/io/middleware/packet.js new file mode 100644 index 0000000..a1c0572 --- /dev/null +++ b/api/app/io/middleware/packet.js @@ -0,0 +1,7 @@ +module.exports = app => { + return async (ctx, next) => { + // ctx.socket.emit('res', 'packet received!'); + // console.log('packet:', ctx.packet); + await next(); + }; +}; diff --git a/api/app/middleware/error_handler.js b/api/app/middleware/error_handler.js new file mode 100644 index 0000000..a7ba7e6 --- /dev/null +++ b/api/app/middleware/error_handler.js @@ -0,0 +1,2 @@ +// 暂不启用此中间件 +// module.exports = require('koa-json-error'); \ No newline at end of file diff --git a/api/app/middleware/response_handler.js b/api/app/middleware/response_handler.js new file mode 100644 index 0000000..94e7b70 --- /dev/null +++ b/api/app/middleware/response_handler.js @@ -0,0 +1,72 @@ +// 验证权限 +async function verifyPermission(ctx) { + let fag = false; + do { + // 控制器 account、default 下的接口不需要认证 + const ignore = ['/account/','/default/']; + console.log("ctx.request.url",ctx.request.url) + console.log("ctx.request.url",ctx.request.url == '/') + for (let index = 0; index < ignore.length; index++) { + const route = ignore[index]; + if (ctx.request.url.indexOf(route) == 0 || ctx.request.url == '/') { + fag = true; + break; + } + } + + if (fag == true) break; + + fag = await ctx.service.user.verifyPermission( + ctx.request.header.userid, + ctx.request.header.logintype || 'web', + ctx.request.header.timestamp, + ctx.request.header.signstr, + ctx.request.method == 'POST' ? ctx.request.body : ctx.request.query + ); + + if (!fag) break; + + ctx.user = { + id: ctx.request.header.userid, + logintype: ctx.request.header.logintype || 'web', + }; + } while (false); + return fag; +} + + +module.exports = options => { + return async function responseHandler(ctx, next) { + let code = 1; + try { + // 打印domain + console.warn(`[${ctx.request.method}]${ctx.request.url}`); + let auth_res = await verifyPermission(ctx); + if (!auth_res) { + code = 401; + throw new Error('请先登录'); + } + await next(); + if (ctx.status == 404) { + code = 404; + throw new Error('接口路径错误'); + } + + ctx.body = { + status: 'Success', + code: code, + message: '', + data: ctx.body ? ctx.body : '', + }; + } catch (err) { + ctx.body = { + status: 'Fail', + code: err.code || code || ctx.status || 500, + // code: 4001, + message: err.message, + data: null, + }; + ctx.status = 200; + } + }; +}; diff --git a/api/app/model/user.js b/api/app/model/user.js new file mode 100644 index 0000000..a3c5e1e --- /dev/null +++ b/api/app/model/user.js @@ -0,0 +1,39 @@ +module.exports = app => { + const mongoose = app.mongoose; + const Schema = mongoose.Schema; + + const UserSchema = new Schema({ + account: {type: String,}, + nick_name: {type: String,}, + avatar:{ type:String ,}, + pwd: {type: String,}, + contact_num :{ type:String}, + + reg_time: {type: Number,}, + reg_ip: {type: String,}, + + last_login_ip: { type: String,}, + last_login_time: { type: Number,}, + + // 好友列表 + friend_list: [], + // 黑名单 + black_list: [] + + }); + + + UserSchema.post('findOne', function (doc) { + if (doc) { + // 如果不存在avatar字段 + if (!('avatar' in doc) || !doc.avatar) { + doc.avatar = "default_avatar"; + } + } + }); + + let schema = mongoose.model('User', UserSchema); + + return schema; +}; + diff --git a/api/app/router.js b/api/app/router.js new file mode 100644 index 0000000..ce99a04 --- /dev/null +++ b/api/app/router.js @@ -0,0 +1,40 @@ +'use strict'; + +/** + * @param {Egg.Application} app - egg application + */ +module.exports = app => { + const { router, controller, io } = app; + router.get('/', controller.home.index); + // 手机号验证码登录 + router.post('/account/phone_login', controller.account.phoneLogin); + router.post('/account/send_sms', controller.account.sendSMS); + + /**---------------- 写真模板 ----------------**/ + router.get('/template/list', controller.template.getList); // 获取模板列表 + router.get('/template/info', controller.template.getInfo); // 获取模板详情 + + + /**---------------- 数字分身 ----------------**/ + router.post('/doppelganger/create', controller.doppelganger.create); // 创建数字分身 + router.post('/doppelganger/list', controller.doppelganger.getList); // 查看数字分身列表 + + /**---------------- 任务相关 ----------------**/ + router.post('/task/create', controller.task.create); // 创建任务 (生成写真、高清修图) + router.post('/task/list', controller.task.getList); // 查看任务列表 + router.post('/task/info', controller.task.getInfo); // 查看任务详情 + + + router.post('/account/register', controller.account.register); + router.post('/account/userkey_login', controller.account.login); + router.get('/default/getnowtime', controller.default.getnowtime); + router.get('/default/test', controller.default.test); + router.post('/default/test', controller.default.test); + + router.get('/user/myinfo', controller.user.getMyInfo); + router.get('/user/getuser', controller.user.getUserInfoById); + + + // socket.io + io.of('/').route('exchange', io.controller.nsp.exchange); +}; diff --git a/api/app/service/user.js b/api/app/service/user.js new file mode 100644 index 0000000..538946d --- /dev/null +++ b/api/app/service/user.js @@ -0,0 +1,245 @@ +const { Service } = require('egg'); + +class UserSevice extends Service { + /** + * 验证用户token是否有效 + * @param {string} userid 用户id + * @param {string} logintype 登录类型 + * @param {string} timestamp 时间戳 + * @param {string} signstr 签名 + * @param {object} data 签名的数据 + * @param {string} random 随机数 可不传 + * @returns + */ + async verifyPermission( + userid, + logintype, + timestamp, + signstr, + data, + random = null + ) { + let fag = false; + do { + // console.log("signstr:",signstr) + // console.log("timestamp:",timestamp) + if (!userid || !timestamp || !signstr) break; + // 获得token + const token = await this.getSessionID(userid, logintype); + if (!token) break; + const signstr_local = this.ctx.ltool.sign(timestamp, token, data, random); + if (signstr_local != signstr) break; + + fag = true; + } while (false); + return fag; + } + + /** + * 用户签名验证登陆 + * @param {object} input 登录信息 + * @param {string} ip 登录ip地址 + * @returns 登录是否成功 + */ + async userKeyLogin(input, ip) { + const { ctx, app } = this; + const nowtime = new Date().valueOf(); + + // 验证连续登录次数,防止暴力登录 + const userVa = await app.redis.sGet( + 'userKeyLoginCheck_' + input.user_name, + async () => { + return null; + } + ); + if (userVa && userVa.FirstVaDate + 60 >= nowtime && userVa.FailCount >= 5) + throw ctx.ltool.err('休息一会再试吧', 2001); + + // 前后端时间相差过大 + if (Math.abs(input.timestamp - nowtime) > 360000) + throw ctx.ltool.err('登录失败', 2002); + + // 随机数不能使用多次 + const isRe = await app.redis.sGet( + 'userKeyLogin_random_number_' + input.random_number, + async () => { + return -1; + }, + ctx.lconst.hour1 + ); + if (isRe > 0) throw ctx.ltool.err('登录失败', 2003); + await app.redis.sSet( + 'userKeyLogin_random_number_' + input.random_number, + 1, + 360 + ); + + // 获得用户信息 + const userinfo = await this.findByNameDB(input.user_name); + if (!userinfo) throw ctx.ltool.err('您的账号或密码不正确', 2004); + + console.warn('userinfo', userinfo); + const sign = ctx.ltool + .sign( + input.timestamp, + userinfo.pwd, + `username=${input.user_name}&random_number=${input.random_number}` + ) + .toLowerCase(); + if (sign != input.user_key) + throw ctx.ltool.err('您的账号或密码不正确', 2005); + + // 登陆成功后清空错误登陆的次数 + app.redis.del('userKeyLoginCheck_' + input.user_name); + + return userinfo; + } + + /** + * 注册账号 + * @param {*} input 用户信息 + * @param {string} ip ip地址 + * @returns 注册结果 + */ + async register(input, ip) { + const { ctx, app } = this; + + // 获得用户信息 + const userinfo = await this.findByNameDB(input.account); + if (userinfo) throw ctx.ltool.err('已经有这个账号了', 1004); + + let newuser = new ctx.model.User(); + + newuser.account = input.account; + newuser.nick_name = input.nick_name; + + // 处理一下密码 + newuser.pwd = ctx.ltool.md5(input.pwd).toLowerCase().substr(5, 23); + + // 设置注册时间和注册ip字段 + newuser.reg_time = new Date().valueOf(); + newuser.reg_ip = ip; + + // 生成一个随机的联系号码 + newuser.contact_num = await this.newContactNum(); + + // 添加用户 + await newuser.save(); + + return newuser; + } + + /** + * 获得一个新的联系号码(随机) + */ + async newContactNum() { + const { ctx } = this; + let exist = false; + let index = 0; + let contactNum = ''; + while (true) { + if (index >= 10) throw ctx.ltool.err('联系号码已用完,请修改此模块', 500); + contactNum = this.ctx.ltool.nonceNumer(6); + exist = await this.existContactNum(contactNum); + if (!exist) break; + index++; + } + return contactNum.toString(); + } + + /** + * 是否已存在此联系号码 + * @param {string} num 联系号码 + * @returns + */ + async existContactNum(num) { + const userinfo = await this.ctx.model.User.findOne({ contact_num: num }); + if (userinfo) return true; + else return false; + } + /** + * 更新用户的登录token + * @param {string} userid 用户id + * @param {string} token_3rd_session token + * @param {string} logintype 登录类型 + */ + async updateSessionID(userid, token_3rd_session, logintype) { + await this.app.redis.sSet( + `api_login_${logintype}_${userid}`, + token_3rd_session, + this.ctx.lconst.year1 + ); + } + + /** + * 获得用户的登录token + */ + async getSessionID(userid, logintype) { + return await this.app.redis.sGet( + `api_login_${logintype}_${userid}`, + async () => { + return ''; + } + ); + } + + /** + * 根据用户id获得用户信息 + * @param {string} userid 用户id + * @returns + */ + async findUserbyId(userid) { + return await this.app.redis.sGet('user:' + userid, async () => { + let user = await this.findUserByIdDB(userid); + // 删除不用存在redis中的字段 + if (user) { + user = user.toObject(); + delete user.friend_list; + } + return user; + }); + } + + /** + * 更新用户数据 + * @param {string} userid 用户id + * @param {object} data 要更新的数据 + */ + async updateUserById(userid, data) { + // 更新数据到数据库 + const result = await this.ctx.model.User.updateOne({ _id: userid }, data); + // 清除缓存 + await this.app.redis.del('user:' + userid); + if ('friend_list' in data) + await this.app.redis.del('userfriendlist:' + userid); + return result; + } + /** + * 根据用户id获得用户信息 + * @param {string} userid 用户id + * @returns + */ + async findUserByIdDB(userid) { + return await this.ctx.model.User.findOne({ _id: userid }); + } + + /** + * 根据账号查找用户 + * @param {string} account 账号 + * @returns + */ + async findByNameDB(account) { + return await this.ctx.model.User.findOne({ account: account }); + } + /** + * 根据联系号码查找用户 + * @param {string} num 联系号码 + * @returns + */ + async findByNumDB(num) { + return await this.ctx.model.User.findOne({ contact_num: num }); + } + +} + +module.exports = UserSevice; diff --git a/api/config/config.default.js b/api/config/config.default.js new file mode 100644 index 0000000..30c1535 --- /dev/null +++ b/api/config/config.default.js @@ -0,0 +1,110 @@ +/* eslint valid-jsdoc: "off" */ + +'use strict'; + +/** + * @param {Egg.EggAppInfo} appInfo app info + */ +module.exports = appInfo => { + /** + * built-in config + * @type {Egg.EggAppConfig} + **/ + const config = (exports = {}); + + // use for cookie sign key, should change to your own and keep security + config.keys = appInfo.name + '_1673064796027_4890'; + + // 中间件 + config.middleware = ["responseHandler"]; + + // socket模块 + config.io = { + // Egg Socket 内部默认使用 ws 引擎,uws 因为某些原因被废止。如坚持需要使用,请按照以下配置即可: + // init: { wsEngine: 'uws' }, // default: ws + init: {}, // passed to engine.io + namespace: { + '/': { + // 针对连接事件的授权处理 + connectionMiddleware: ['connection'], + // 针对消息的处理 (暂时不实现) + packetMiddleware: ['packet'], + }, + '/example': { + connectionMiddleware: [], + packetMiddleware: [], + }, + }, + redis: { + host: '192.168.1.175', + port: 6379, + auth_pass: '123456', + db: 0, + }, + token: '123456', + }; + + config.mongoose = { + client: { + url: `mongodb://192.168.1.175:27017/sd_pics_hub?authSource=admin`, + options: { + user: 'dbroot', + pass: '123456', + useUnifiedTopology:true + }, + }, + }; + + // 参数验证模块 + config.validate = { + // convert: false, + // validateRoot: false, + }; + + // 跨域访问配置 + config.cors = { + origin:'*', + allowMethods: 'GET,POST' + }; + config.security = { + csrf:{ + enable:false + } + } + + config.redis = { + client: { + port: 6379, // Redis port + host: "192.168.1.175", // Redis host + password: '123456', + db: 0, + prefix:"pics_hub_dev" + }, + } + + config.logger = { + consoleLevel: 'DEBUG', + }; + config.cluster = { + listen: { + port: 7002, + hostname: '0.0.0.0' + }, + } + + // add your user config here + const userConfig = { + + // myAppName: 'egg', + }; + + // // # 注意,开启此模式后,应用就默认自己处于反向代理之后, + // // # 会支持通过解析约定的请求头来获取用户真实的 IP,协议和域名。 + // // # 如果你的服务未部署在反向代理之后,请不要开启此配置,以防被恶意用户伪造请求 IP 等信息。 + // config.proxy = true; + + return { + ...config, + ...userConfig, + }; +}; diff --git a/api/config/config.prod.js b/api/config/config.prod.js new file mode 100644 index 0000000..5be8324 --- /dev/null +++ b/api/config/config.prod.js @@ -0,0 +1,110 @@ +/* eslint valid-jsdoc: "off" */ + +'use strict'; + +/** + * @param {Egg.EggAppInfo} appInfo app info + */ +module.exports = appInfo => { + /** + * built-in config + * @type {Egg.EggAppConfig} + **/ + const config = (exports = {}); + + // use for cookie sign key, should change to your own and keep security + config.keys = appInfo.name + '_1673064796027_4890'; + + // 中间件 + config.middleware = ["responseHandler"]; + + // socket模块 + config.io = { + // Egg Socket 内部默认使用 ws 引擎,uws 因为某些原因被废止。如坚持需要使用,请按照以下配置即可: + // init: { wsEngine: 'uws' }, // default: ws + init: {}, // passed to engine.io + namespace: { + '/': { + // 针对连接事件的授权处理 + connectionMiddleware: ['connection'], + // 针对消息的处理 (暂时不实现) + packetMiddleware: ['packet'], + }, + '/example': { + connectionMiddleware: [], + packetMiddleware: [], + }, + }, + redis: { + host: 'redis', + port: 6379, + auth_pass: 'lfluYk4reffZDjzzXfeNA2ub9odfJ1Ic', + db: 0, + }, + }; + + config.mongoose = { + client: { + url: `mongodb://mongodb:27017/sd_pics_hub?authSource=admin`, + options: { + user: 'dbroot', + pass: 'Gri42Mvyk3j2PXIiTdn6CCA8JDSST7yv', + useUnifiedTopology:true + }, + }, + }; + + + // 参数验证模块 + config.validate = { + // convert: false, + // validateRoot: false, + }; + + // 跨域访问配置 + config.cors = { + origin:'*', + allowMethods: 'GET,POST' + }; + config.security = { + csrf:{ + enable:false + } + } + + config.redis = { + client: { + port: 6379, // Redis port + host: "redis", // Redis host + password: 'lfluYk4reffZDjzzXfeNA2ub9odfJ1Ic', + db: 0, + }, + } + + config.logger = { + consoleLevel: 'DEBUG', + }; + config.cluster = { + listen: { + port: 7002, + hostname: '0.0.0.0' + }, + } + + + // add your user config here + const userConfig = { + + // myAppName: 'egg', + }; + + // // # 注意,开启此模式后,应用就默认自己处于反向代理之后, + // // # 会支持通过解析约定的请求头来获取用户真实的 IP,协议和域名。 + // // # 如果你的服务未部署在反向代理之后,请不要开启此配置,以防被恶意用户伪造请求 IP 等信息。 + config.proxy = true; + + return { + ...config, + ...userConfig, + }; +}; diff --git a/api/config/plugin.js b/api/config/plugin.js new file mode 100644 index 0000000..4fa17bb --- /dev/null +++ b/api/config/plugin.js @@ -0,0 +1,47 @@ +'use strict'; +const path = require('path') + +let egg_plugin = { + // had enabled by egg + // static: { + // enable: true, + // } +}; + +// 分布式缓存redis客户端 +egg_plugin.redis = { + enable: true, + path: path.join(__dirname,`../lib/plugins/egg-lu-redis`) +} + +// 通用工具类 +egg_plugin.ltool = { + enable: true, + path: path.join(__dirname, '../lib/plugins/egg-lu-tool'), +} + +// 开启 socket 插件 +egg_plugin.io = { + enable: true, + package: 'egg-socket.io', +}; + +// 开启mongo插件 +egg_plugin.mongoose = { + enable: true, // 开启插件 + package: 'egg-mongoose' +} + +// 开启验证插件 +egg_plugin.validate = { + enable: true, + package: 'egg-validate', +}; + +// 允许跨域插件 +egg_plugin.cors = { + enable: true, + package: 'egg-cors', +}; +/** @type Egg.EggPlugin */ +module.exports = egg_plugin; diff --git a/api/jsconfig.json b/api/jsconfig.json new file mode 100644 index 0000000..1bbed3f --- /dev/null +++ b/api/jsconfig.json @@ -0,0 +1,5 @@ +{ + "include": [ + "**/*" + ] +} \ No newline at end of file diff --git a/api/lib/plugins/egg-lu-redis/.autod.conf.js b/api/lib/plugins/egg-lu-redis/.autod.conf.js new file mode 100644 index 0000000..aba2efd --- /dev/null +++ b/api/lib/plugins/egg-lu-redis/.autod.conf.js @@ -0,0 +1,26 @@ +'use strict'; + +module.exports = { + write: true, + prefix: '^', + test: [ + 'test', + 'benchmark', + ], + devdep: [ + 'egg', + 'egg-bin', + 'egg-ci', + 'autod', + 'eslint', + 'eslint-config-egg', + 'supertest', + 'typescript', + ], + dep: [ + '@types/ioredis' + ], + exclude: [ + './test/fixtures', + ], +}; diff --git a/api/lib/plugins/egg-lu-redis/.eslintignore b/api/lib/plugins/egg-lu-redis/.eslintignore new file mode 100644 index 0000000..a24e501 --- /dev/null +++ b/api/lib/plugins/egg-lu-redis/.eslintignore @@ -0,0 +1,2 @@ +test/fixtures +coverage diff --git a/api/lib/plugins/egg-lu-redis/.eslintrc b/api/lib/plugins/egg-lu-redis/.eslintrc new file mode 100644 index 0000000..c799fe5 --- /dev/null +++ b/api/lib/plugins/egg-lu-redis/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": "eslint-config-egg" +} diff --git a/api/lib/plugins/egg-lu-redis/.gitignore b/api/lib/plugins/egg-lu-redis/.gitignore new file mode 100644 index 0000000..6575f21 --- /dev/null +++ b/api/lib/plugins/egg-lu-redis/.gitignore @@ -0,0 +1,9 @@ +logs/ +npm-debug.log +node_modules/ +coverage/ +.idea/ +run/ +.DS_Store +*.swp +.nyc_output/ diff --git a/api/lib/plugins/egg-lu-redis/.travis.yml b/api/lib/plugins/egg-lu-redis/.travis.yml new file mode 100644 index 0000000..ae0da72 --- /dev/null +++ b/api/lib/plugins/egg-lu-redis/.travis.yml @@ -0,0 +1,16 @@ + +language: node_js +node_js: + - '8' + - '10' + - '11' +before_install: + - npm i npminstall@5 -g +install: + - npminstall +script: + - npm run ci +after_script: + - npminstall codecov && codecov +services: + - redis-server diff --git a/api/lib/plugins/egg-lu-redis/History.md b/api/lib/plugins/egg-lu-redis/History.md new file mode 100644 index 0000000..41eacfd --- /dev/null +++ b/api/lib/plugins/egg-lu-redis/History.md @@ -0,0 +1,75 @@ + +2.4.0 / 2019-06-13 +================== + +**features** + * [[`5fbd718`](http://github.com/eggjs/egg-redis/commit/5fbd718c60e4c82a94f34a96583ed877d8e4fa5f)] - feat: add config.client.weakDependent (#30) (Yiyu He <