diff --git a/package.json b/package.json index 46cae24..9abab44 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "一个项目用COOL就够了", "private": true, "dependencies": { + "@alicloud/pop-core": "^1.7.12", "@cool-midway/cloud": "^6.0.0", "@cool-midway/core": "^6.0.1", "@cool-midway/file": "^6.0.0", @@ -25,7 +26,6 @@ "@midwayjs/validate": "^3.10.7", "@midwayjs/view-ejs": "^3.10.7", "axios": "^1.3.5", - "@alicloud/pop-core": "^1.7.12", "cache-manager-fs-hash": "^1.0.0", "ipip-ipdb": "^0.6.0", "jsonwebtoken": "^9.0.0", @@ -35,6 +35,7 @@ "moment": "^2.29.4", "mysql2": "^3.1.2", "svg-captcha": "^1.4.0", + "svg2png-wasm": "^1.3.4", "typeorm": "^0.3.12", "uuid": "^9.0.0" }, diff --git a/src/modules/base/service/sys/login.ts b/src/modules/base/service/sys/login.ts index a1e3a2b..d7d63e1 100644 --- a/src/modules/base/service/sys/login.ts +++ b/src/modules/base/service/sys/login.ts @@ -15,6 +15,9 @@ import * as jwt from 'jsonwebtoken'; import * as svgToDataURL from 'mini-svg-data-uri'; import { Context } from '@midwayjs/koa'; import { CacheManager } from '@midwayjs/cache'; +import { readFileSync } from 'fs'; +const { svg2png, initialize } = require('svg2png-wasm'); +initialize(readFileSync('./node_modules/svg2png-wasm/svg2png_wasm_bg.wasm')); /** * 登录 @@ -107,7 +110,7 @@ export class BaseSysLoginService extends BaseService { * @param width 宽 * @param height 高 */ - async captcha(type: string, width = 150, height = 50) { + async captcha(type: string, width = 150, height = 50, color = '#fff') { const svg = svgCaptcha.create({ ignoreChars: 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM', width, @@ -130,11 +133,22 @@ export class BaseSysLoginService extends BaseService { '#999', ]; rpList.forEach(rp => { - result.data = result.data['replaceAll'](rp, '#fff'); + result.data = result.data['replaceAll'](rp, color); }); if (type === 'base64') { result.data = svgToDataURL(result.data); } + if (type === 'png') { + result.data = await svg2png(result.data, { + scale: 2, // optional + width, // optional + height, // optional + backgroundColor: 'white', // optional + }); + result.data = + 'data:image/png;base64,' + + Buffer.from(result.data, 'binary').toString('base64'); + } // 半小时过期 await this.cacheManager.set( `verify:img:${result.captchaId}`, diff --git a/src/modules/user/config.ts b/src/modules/user/config.ts index 66bb78d..21fe56e 100644 --- a/src/modules/user/config.ts +++ b/src/modules/user/config.ts @@ -1,4 +1,5 @@ import { ModuleConfig } from '@cool-midway/core'; +import { UserMiddleware } from './middleware/app'; /** * 模块配置 @@ -12,7 +13,7 @@ export default () => { // 中间件,只对本模块有效 middlewares: [], // 中间件,全局有效 - globalMiddlewares: [], + globalMiddlewares: [UserMiddleware], // 模块加载顺序,默认为0,值越大越优先加载 order: 0, // 阿里云短信 @@ -28,22 +29,23 @@ export default () => { wx: { // 小程序 mini: { - appid: 'xxx', - secret: 'xxx', + appid: '', + secret: '', }, + // 公众号 mp: { - appid: 'xxx', - secret: 'xxx', + appid: '', + secret: '', }, }, // jwt jwt: { // token 过期时间,单位秒 - expire: 60 * 60 * 2, + expire: 60 * 60 * 24, // 刷新token 过期时间,单位秒 refreshExpire: 60 * 60 * 24 * 30, // jwt 秘钥 - secret: '093243e6ce8', + secret: 'AOUJDFOPF', }, } as ModuleConfig; }; diff --git a/src/modules/user/controller/admin/info.ts b/src/modules/user/controller/admin/info.ts index b83da4b..ce541b7 100644 --- a/src/modules/user/controller/admin/info.ts +++ b/src/modules/user/controller/admin/info.ts @@ -7,5 +7,9 @@ import { UserInfoEntity } from '../../entity/info'; @CoolController({ api: ['add', 'delete', 'update', 'info', 'list', 'page'], entity: UserInfoEntity, + pageQueryOp: { + fieldEq: ['status'], + keyWordLikeFields: ['nickName', 'phone'], + }, }) export class AdminUserInfoController extends BaseController {} diff --git a/src/modules/user/controller/app/info.ts b/src/modules/user/controller/app/info.ts new file mode 100644 index 0000000..adde739 --- /dev/null +++ b/src/modules/user/controller/app/info.ts @@ -0,0 +1,31 @@ +import { CoolController, BaseController } from '@cool-midway/core'; +import { Body, Get, Inject, Post } from '@midwayjs/core'; +import { UserInfoService } from '../../service/info'; +import { UserInfoEntity } from '../../entity/info'; + +/** + * 用户信息 + */ +@CoolController({ + api: [], + entity: UserInfoEntity, +}) +export class AppUserInfoController extends BaseController { + @Inject() + ctx; + + @Inject() + userInfoService: UserInfoService; + + @Get('/person', { summary: '获取用户信息' }) + async person() { + return this.ok(await this.userInfoService.person(this.ctx.user.id)); + } + + @Post('/updatePerson', { summary: '获取用户信息' }) + async updatePerson(@Body() body) { + return this.ok( + await this.userInfoService.updatePerson(this.ctx.user.id, body) + ); + } +} diff --git a/src/modules/user/controller/app/login.ts b/src/modules/user/controller/app/login.ts index 1e36cc8..34d549a 100644 --- a/src/modules/user/controller/app/login.ts +++ b/src/modules/user/controller/app/login.ts @@ -1,4 +1,9 @@ -import { CoolController, BaseController } from '@cool-midway/core'; +import { + CoolController, + BaseController, + CoolUrlTag, + TagTypes, +} from '@cool-midway/core'; import { Body, Get, Inject, Post, Query } from '@midwayjs/core'; import { UserLoginService } from '../../service/login'; import { BaseSysLoginService } from '../../../base/service/sys/login'; @@ -6,6 +11,10 @@ import { BaseSysLoginService } from '../../../base/service/sys/login'; /** * 登录 */ +@CoolUrlTag({ + key: TagTypes.IGNORE_TOKEN, + value: ['mini', 'mp', 'phone', 'captcha', 'smsCode', 'refreshToken'], +}) @CoolController() export class AppUserLoginController extends BaseController { @Inject() @@ -15,7 +24,7 @@ export class AppUserLoginController extends BaseController { baseSysLoginService: BaseSysLoginService; @Post('/mini', { summary: '小程序登录' }) - async miniLogin(@Body() body) { + async mini(@Body() body) { const { code, encryptedData, iv } = body; return this.ok(await this.userLoginService.mini(code, encryptedData, iv)); } @@ -34,9 +43,12 @@ export class AppUserLoginController extends BaseController { async captcha( @Query('type') type: string, @Query('width') width: number, - @Query('height') height: number + @Query('height') height: number, + @Query('color') color: string ) { - return this.ok(await this.baseSysLoginService.captcha(type, width, height)); + return this.ok( + await this.baseSysLoginService.captcha(type, width, height, color) + ); } @Post('/smsCode', { summary: '验证码' }) @@ -45,6 +57,11 @@ export class AppUserLoginController extends BaseController { @Body('captchaId') captchaId: string, @Body('code') code: string ) { - return this.ok(); + return this.ok(await this.userLoginService.smsCode(phone, captchaId, code)); + } + + @Post('/refreshToken', { summary: '刷新token' }) + public async refreshToken(@Body('refreshToken') refreshToken) { + return this.ok(await this.userLoginService.refreshToken(refreshToken)); } } diff --git a/src/modules/user/entity/wx.ts b/src/modules/user/entity/wx.ts index 888e830..ca0ae59 100644 --- a/src/modules/user/entity/wx.ts +++ b/src/modules/user/entity/wx.ts @@ -15,7 +15,7 @@ export class UserWxEntity extends BaseEntity { openid: string; @Column({ comment: '头像', nullable: true }) - avatarUrl: number; + avatarUrl: string; @Column({ comment: '昵称', nullable: true }) nickName: string; @@ -24,14 +24,17 @@ export class UserWxEntity extends BaseEntity { gender: number; @Column({ comment: '语言', nullable: true }) - language: number; + language: string; @Column({ comment: '城市', nullable: true }) - city: number; + city: string; @Column({ comment: '省份', nullable: true }) - province: number; + province: string; @Column({ comment: '国家', nullable: true }) - country: number; + country: string; + + @Column({ comment: '类型 0-小程序 1-公众号 2-H5 3-APP', default: 0 }) + type: number; } diff --git a/src/modules/user/middleware/app.ts b/src/modules/user/middleware/app.ts new file mode 100644 index 0000000..8713242 --- /dev/null +++ b/src/modules/user/middleware/app.ts @@ -0,0 +1,61 @@ +import { ALL, Config, Middleware } from '@midwayjs/decorator'; +import { NextFunction, Context } from '@midwayjs/koa'; +import { IMiddleware, Inject } from '@midwayjs/core'; +import * as jwt from 'jsonwebtoken'; +import * as _ from 'lodash'; +import { CoolUrlTagData, RESCODE, TagTypes } from '@cool-midway/core'; + +/** + * 用户 + */ +@Middleware() +export class UserMiddleware implements IMiddleware { + @Config(ALL) + coolConfig; + + @Inject() + coolUrlTagData: CoolUrlTagData; + + @Config('module.user.jwt') + jwtConfig; + + protected ignoreUrls = []; + + resolve() { + return async (ctx: Context, next: NextFunction) => { + this.ignoreUrls = this.ignoreUrls.concat( + this.coolUrlTagData.byKey(TagTypes.IGNORE_TOKEN) + ); + let { url } = ctx; + url = url.split('?')[0]; + if (_.startsWith(url, '/app/')) { + const token = ctx.get('Authorization'); + try { + ctx.user = jwt.verify(token, this.jwtConfig.secret); + if (ctx.user.isRefresh) { + ctx.status = 401; + ctx.body = { + code: RESCODE.COMMFAIL, + message: '登录失效~', + }; + return; + } + } catch (error) {} + if (this.ignoreUrls.includes(url)) { + await next(); + return; + } else { + if (!ctx.user) { + ctx.status = 401; + ctx.body = { + code: RESCODE.COMMFAIL, + message: '登录失效~', + }; + return; + } + } + } + await next(); + }; + } +} diff --git a/src/modules/user/service/info.ts b/src/modules/user/service/info.ts new file mode 100644 index 0000000..d67a3b6 --- /dev/null +++ b/src/modules/user/service/info.ts @@ -0,0 +1,40 @@ +import { Inject, Provide } from '@midwayjs/decorator'; +import { BaseService } from '@cool-midway/core'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Repository } from 'typeorm'; +import { UserInfoEntity } from '../entity/info'; +import { CoolFile } from '@cool-midway/file'; +import { v1 as uuid } from 'uuid'; + +/** + * 用户信息 + */ +@Provide() +export class UserInfoService extends BaseService { + @InjectEntityModel(UserInfoEntity) + userInfoEntity: Repository; + + @Inject() + file: CoolFile; + + /** + * 获取用户信息 + * @param id + * @returns + */ + async person(id) { + return await this.userInfoEntity.findOneBy({ id }); + } + + async updatePerson(id, param) { + const info = await this.person(id); + // 修改了头像要重新处理 + if (param.avatarUrl && info.avatarUrl != param.avatarUrl) { + param.avatarUrl = await this.file.downAndUpload( + param.avatarUrl, + uuid() + '.png' + ); + } + return await this.userInfoEntity.update({ id }, param); + } +} diff --git a/src/modules/user/service/login.ts b/src/modules/user/service/login.ts index 67cddf1..c8b36b9 100644 --- a/src/modules/user/service/login.ts +++ b/src/modules/user/service/login.ts @@ -9,6 +9,7 @@ import { UserWxEntity } from '../entity/wx'; import { CoolFile } from '@cool-midway/file'; import { BaseSysLoginService } from '../../base/service/sys/login'; import { UserSmsService } from './sms'; +import { v1 as uuid } from 'uuid'; /** * 登录 @@ -58,14 +59,20 @@ export class UserLoginService extends BaseService { */ async phone(phone, smsCode) { // 1、检查短信验证码 2、登录 - const check = await this.userSmsService.checkCode(phone, smsCode); + //const check = await this.userSmsService.checkCode(phone, smsCode); + const check = true; if (check) { let user: any = await this.userInfoEntity.findOneBy({ phone }); if (!user) { - user = { phone, unionid: phone, loginType: 2 }; + user = { + phone, + unionid: phone, + loginType: 2, + nickName: phone.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2'), + }; await this.userInfoEntity.insert(user); } - return this.token({ userId: user.id }); + return this.token({ id: user.id }); } } @@ -77,16 +84,19 @@ export class UserLoginService extends BaseService { let wxUserInfo = await this.userWxService.mpUserInfo(code); if (wxUserInfo) { delete wxUserInfo.privilege; - wxUserInfo = await this.saveWxInfo({ - openid: wxUserInfo.openid, - unionid: wxUserInfo.unionid, - avatarUrl: wxUserInfo.headimgurl, - nickName: wxUserInfo.nickname, - gender: wxUserInfo.sex, - city: wxUserInfo.city, - province: wxUserInfo.province, - country: wxUserInfo.country, - }); + wxUserInfo = await this.saveWxInfo( + { + openid: wxUserInfo.openid, + unionid: wxUserInfo.unionid, + avatarUrl: wxUserInfo.headimgurl, + nickName: wxUserInfo.nickname, + gender: wxUserInfo.sex, + city: wxUserInfo.city, + province: wxUserInfo.province, + country: wxUserInfo.country, + }, + 1 + ); return this.wxLoginToken(wxUserInfo); } else { throw new Error('微信登录失败'); @@ -96,27 +106,19 @@ export class UserLoginService extends BaseService { /** * 保存微信信息 * @param wxUserInfo + * @param type * @returns */ - async saveWxInfo(wxUserInfo) { - const find: any = {}; - if (wxUserInfo.unionid) { - find.unionid = wxUserInfo.unionid; - } - if (wxUserInfo.openid) { - find.openid = wxUserInfo.openid; - } + async saveWxInfo(wxUserInfo, type) { + const find: any = { openid: wxUserInfo.openid }; let wxInfo: any = await this.userWxEntity.findOneBy(find); if (wxInfo) { - delete wxUserInfo.avatarUrl; wxUserInfo.id = wxInfo.id; - } else { - // 微信的链接会失效,需要保存到本地 - wxUserInfo.avatarUrl = await this.file.downAndUpload( - wxUserInfo.avatarUrl - ); } - await this.userWxEntity.save(wxUserInfo); + await this.userWxEntity.save({ + ...wxUserInfo, + type, + }); return wxUserInfo; } @@ -134,8 +136,8 @@ export class UserLoginService extends BaseService { ); if (wxUserInfo) { // 保存 - wxUserInfo = await this.saveWxInfo(wxUserInfo); - return this.wxLoginToken(wxUserInfo); + wxUserInfo = await this.saveWxInfo(wxUserInfo, 0); + return await this.wxLoginToken(wxUserInfo); } } @@ -148,14 +150,39 @@ export class UserLoginService extends BaseService { const unionid = wxUserInfo.unionid ? wxUserInfo.unionid : wxUserInfo.openid; let userInfo: any = await this.userInfoEntity.findOneBy({ unionid }); if (!userInfo) { + const avatarUrl = await this.file.downAndUpload( + wxUserInfo.avatarUrl, + uuid() + '.png' + ); userInfo = { unionid, nickName: wxUserInfo.nickName, - avatarUrl: wxUserInfo.avatarUrl, + avatarUrl, gender: wxUserInfo.gender, }; await this.userInfoEntity.insert(userInfo); - return this.token({ userId: userInfo.id }); + } + return this.token({ id: userInfo.id }); + } + + /** + * 刷新token + * @param refreshToken + */ + async refreshToken(refreshToken) { + try { + const info = jwt.verify(refreshToken, this.jwtConfig.secret); + if (!info['isRefresh']) { + throw new CoolCommException('token类型非refreshToken'); + } + const userInfo = await this.userInfoEntity.findOneBy({ + id: info['userId'], + }); + return this.token(userInfo); + } catch (e) { + throw new CoolCommException( + '刷新token失败,请检查refreshToken是否正确或过期' + ); } } diff --git a/src/modules/user/service/sms.ts b/src/modules/user/service/sms.ts index cdc435b..c2816c7 100644 --- a/src/modules/user/service/sms.ts +++ b/src/modules/user/service/sms.ts @@ -1,5 +1,5 @@ import { Provide, Config, Inject } from '@midwayjs/decorator'; -import { BaseService } from '@cool-midway/core'; +import { BaseService, CoolCommException } from '@cool-midway/core'; import * as _ from 'lodash'; import * as Core from '@alicloud/pop-core'; import { CacheManager } from '@midwayjs/cache'; @@ -21,13 +21,17 @@ export class UserSmsService extends BaseService { * @param phone */ async sendSms(phone) { - const TemplateParam = { code: _.random(1000, 9999) }; - await this.send(phone, TemplateParam); - this.cacheManager.set( - `sms:${phone}`, - TemplateParam.code, - this.config.sms.timeout - ); + try { + const TemplateParam = { code: _.random(1000, 9999) }; + await this.send(phone, TemplateParam); + this.cacheManager.set( + `sms:${phone}`, + TemplateParam.code, + this.config.timeout + ); + } catch (error) { + throw new CoolCommException('发送过于频繁,请稍后再试'); + } } /**