From da7442c3e1f41409bec1b899b82e0961c971cb7d Mon Sep 17 00:00:00 2001 From: expressgy Date: Mon, 27 Mar 2023 00:28:04 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E9=82=AE=E7=AE=B1=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=EF=BC=8C=E5=8D=8A=E6=AD=A5=E6=9D=83=E9=99=90=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=EF=BC=8C=E5=AD=A6=E4=B9=A0=E8=87=AA=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E8=A3=85=E9=A5=B0=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/development.ts | 17 +- config/production.ts | 15 + package.json | 2 + pnpm-lock.yaml | 44 +++ .../gdevinterceptor.interceptor.ts | 4 +- .../gresponseinterceptor.interceptor.ts | 6 +- src/Gservice/GDATABASE/gdatabase.service.ts | 2 +- src/Gservice/GEMAIL/gemail.module.ts | 8 + src/Gservice/GEMAIL/gemail.service.ts | 231 +++++++++++++ src/Gservice/GREDIS/gredis.service.ts | 111 ++++++- src/Gservice/GTOOLS/gtools.module.ts | 8 + src/Gservice/GTOOLS/gtools.service.ts | 137 ++++++++ src/app.module.ts | 4 + src/starlight/dto/register.dto.ts | 8 +- src/starlight/dto/signIn.dto.ts | 18 ++ src/starlight/starlight.controller.ts | 57 +++- src/starlight/starlight.service.ts | 305 ++++++++++++++++-- src/starlight/verify.guard.ts | 42 +++ 18 files changed, 987 insertions(+), 32 deletions(-) create mode 100644 src/Gservice/GEMAIL/gemail.module.ts create mode 100644 src/Gservice/GEMAIL/gemail.service.ts create mode 100644 src/Gservice/GTOOLS/gtools.module.ts create mode 100644 src/Gservice/GTOOLS/gtools.service.ts create mode 100644 src/starlight/dto/signIn.dto.ts create mode 100644 src/starlight/verify.guard.ts diff --git a/config/development.ts b/config/development.ts index aff5e94..4ff06ad 100644 --- a/config/development.ts +++ b/config/development.ts @@ -1,6 +1,7 @@ export default { // 主服务 master: { + systemName: '心曲Tune', host: '0.0.0.0', port: '3000', }, @@ -19,7 +20,7 @@ export default { host: 'localhost', port: 3306, username: 'root', - password: 'root', + password: 'Hxl1314521', database: 'Starlight', }, }, @@ -32,4 +33,18 @@ export default { dbNumber: 3, }, }, + email: { + host: 'smtp.qq.com', + user: 'togy.gc@qq.com', + pass: 'miidbsjeuqcxddbf', + }, + // 加密 + encryption: { + salt: 'время,вперёд!', // 盐 + secretKey: 'быть всегда готовым!', // 密钥 + }, + token: { + timeout: 1000 * 60 * 60 * 24 * 7, + secretKey: 'Missing You!(John waite)', + }, }; diff --git a/config/production.ts b/config/production.ts index c997b7c..622db12 100644 --- a/config/production.ts +++ b/config/production.ts @@ -1,6 +1,7 @@ export default { // 主服务 master: { + systemName: '心曲Tune', host: '127.0.0.1', port: '3000', }, @@ -32,4 +33,18 @@ export default { dbNumber: 3, }, }, + email: { + host: 'smtp.qq.com', + user: 'togy.gc@qq.com', + pass: 'miidbsjeuqcxddbf', + }, + // 加密 + encryption: { + salt: 'время,вперёд!', // 盐 + secretKey: 'быть всегда готовым!', // 密钥 + }, + token: { + timeout: 1000 * 60 * 60 * 24 * 7, + secretKey: 'Missing You!(John waite)', + }, }; diff --git a/package.json b/package.json index e6c5161..7154677 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,10 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "fastify-swagger": "^5.2.0", + "jsonwebtoken": "^9.0.0", "log4js": "^6.9.1", "mysql2": "^3.2.0", + "nodemailer": "^6.9.1", "redis": "^4.6.5", "reflect-metadata": "^0.1.13", "rxjs": "^7.2.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff27fbc..47850d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,8 +26,10 @@ specifiers: eslint-plugin-prettier: ^4.0.0 fastify-swagger: ^5.2.0 jest: 29.3.1 + jsonwebtoken: ^9.0.0 log4js: ^6.9.1 mysql2: ^3.2.0 + nodemailer: ^6.9.1 prettier: ^2.3.2 redis: ^4.6.5 reflect-metadata: ^0.1.13 @@ -52,8 +54,10 @@ dependencies: class-transformer: 0.5.1 class-validator: 0.14.0 fastify-swagger: 5.2.0 + jsonwebtoken: 9.0.0 log4js: 6.9.1 mysql2: 3.2.0 + nodemailer: 6.9.1 redis: 4.6.5 reflect-metadata: 0.1.13 rxjs: 7.8.0 @@ -2047,6 +2051,10 @@ packages: node-int64: 0.4.0 dev: true + /buffer-equal-constant-time/1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + dev: false + /buffer-from/1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} dev: true @@ -2434,6 +2442,12 @@ packages: engines: {node: '>=12'} dev: false + /ecdsa-sig-formatter/1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /ee-first/1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} dev: false @@ -3938,6 +3952,31 @@ packages: graceful-fs: 4.2.10 dev: true + /jsonwebtoken/9.0.0: + resolution: {integrity: sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==} + engines: {node: '>=12', npm: '>=6'} + dependencies: + jws: 3.2.2 + lodash: 4.17.21 + ms: 2.1.3 + semver: 7.3.8 + dev: false + + /jwa/1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + dev: false + + /jws/3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + dev: false + /kleur/3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -4242,6 +4281,11 @@ packages: resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==} dev: true + /nodemailer/6.9.1: + resolution: {integrity: sha512-qHw7dOiU5UKNnQpXktdgQ1d3OFgRAekuvbJLcdG5dnEo/GtcTHRYM7+UfJARdOFU9WUQO8OiIamgWPmiSFHYAA==} + engines: {node: '>=6.0.0'} + dev: false + /normalize-path/3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} diff --git a/src/Ginterceptor/gdevinterceptor.interceptor.ts b/src/Ginterceptor/gdevinterceptor.interceptor.ts index faf490a..470066b 100644 --- a/src/Ginterceptor/gdevinterceptor.interceptor.ts +++ b/src/Ginterceptor/gdevinterceptor.interceptor.ts @@ -17,12 +17,12 @@ export class GdevinterceptorInterceptor implements NestInterceptor { } intercept(context: ExecutionContext, next: CallHandler): Observable { const request = context.switchToHttp().getRequest(); - this.logger.error(request.method, request.url, '<=='); + this.logger.warn(request.method, request.url, '<=='); const now = Date.now(); return next.handle().pipe( tap((data) => { - this.logger.error( + this.logger.warn( request.method, request.url, '==>', diff --git a/src/Ginterceptor/gresponseinterceptor.interceptor.ts b/src/Ginterceptor/gresponseinterceptor.interceptor.ts index b15302e..6824877 100644 --- a/src/Ginterceptor/gresponseinterceptor.interceptor.ts +++ b/src/Ginterceptor/gresponseinterceptor.interceptor.ts @@ -28,7 +28,11 @@ export class GresponseinterceptorInterceptor implements NestInterceptor { ? data.data : data, statusCode: response.statusCode, - success: true, + success: data + ? data.success === false + ? false + : true + : true, message: typeof data == 'string' ? 'ok' diff --git a/src/Gservice/GDATABASE/gdatabase.service.ts b/src/Gservice/GDATABASE/gdatabase.service.ts index 325de17..84ad687 100644 --- a/src/Gservice/GDATABASE/gdatabase.service.ts +++ b/src/Gservice/GDATABASE/gdatabase.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import * as mysql from 'mysql2/promise'; -import config from '../../../config'; +import config from '@CFG/index'; @Injectable() export class GdatabaseService { diff --git a/src/Gservice/GEMAIL/gemail.module.ts b/src/Gservice/GEMAIL/gemail.module.ts new file mode 100644 index 0000000..d3689a8 --- /dev/null +++ b/src/Gservice/GEMAIL/gemail.module.ts @@ -0,0 +1,8 @@ +import { Global, Module } from '@nestjs/common'; +import { GemailService } from './gemail.service'; +@Global() +@Module({ + providers: [GemailService], + exports: [GemailService], +}) +export class GemailModule {} diff --git a/src/Gservice/GEMAIL/gemail.service.ts b/src/Gservice/GEMAIL/gemail.service.ts new file mode 100644 index 0000000..2084043 --- /dev/null +++ b/src/Gservice/GEMAIL/gemail.service.ts @@ -0,0 +1,231 @@ +import { Injectable } from '@nestjs/common'; +import * as nodemailer from 'nodemailer'; +import config from '@CFG/index'; + +@Injectable() +export class GemailService { + // @ 邮件服务 + private transporter; + // @ 邮箱配置 + private EMAILCONFIG; + // @ 系统名称,作用于邮件主题。 + private sysName: string; + + constructor() { + this.start(); + } + + // ? ? + // ? 函数名称: start + // ? 函数描述: 生成邮件服务 + // ? ? + private start() { + const EMAILCONFIG = config().email; + const transporter = nodemailer.createTransport({ + //node_modules/nodemailer/lib/well-known/services.json 查看相关的配置,如果使用qq邮箱,就查看qq邮箱的相关配置 + host: EMAILCONFIG.host, + // secureConnection:true, + service: 'qq', //类型qq邮箱 + // port: 465, + secure: true, // true for 465, false for other ports + auth: { + user: EMAILCONFIG.user, // 发送方的邮箱 + pass: EMAILCONFIG.pass, // smtp 的授权码 + }, + //pass 不是邮箱账户的密码而是stmp的授权码(必须是相应邮箱的stmp授权码) + //邮箱---设置--账户--POP3/SMTP服务---开启---获取stmp授权码 + }); + this.transporter = transporter; + this.EMAILCONFIG = EMAILCONFIG; + this.sysName = config().master.systemName; + } + + // ? ? + // ? 函数名称: senMail(邮件内容, 收件人邮箱) + // ? 函数描述: 发送邮件主函数 + // ? ? + private senMail(html, mail) { + return new Promise((res, rej) => { + const mailOptions = { + from: '"TOGY | 心曲Tune" ', // 发送方 + to: mail, //接收者邮箱,多个邮箱用逗号间隔 + subject: `${this.sysName}`, // 标题 + text: 'Hello world?', // 文本内容 + html, + }; + this.transporter.sendMail(mailOptions, (error, info) => { + if (error) { + rej([error, info]); + } else { + res(info); //因为是异步 所有需要回调函数通知成功结果 + } + }); + }); + } + // ? ? + // ? 函数名称: sendTestEmail + // ? 函数描述: 给指定邮箱发送测试验证码 + // ? ? + public sendTestEmail(email) { + return new Promise(async (res, rej) => { + const test = ` + +
+
+
我们欢迎您使用${this.sysName}
+
您在某些地方请求了邮箱的验证码,如果不是自己操作请修改账户的密码。
+
+
ABCDEF
+
+
此邮件作用于注册
截止邮件发送时间5分钟内使用有效。
+
+
+ `; + try { + const resd = await this.senMail(test, email); + res(resd); + } catch (e) { + rej(e); + } + }); + } + + // ? ? + // ? 函数名称: sendRegisterCodeMail(邮箱, 验证码) + // ? 函数描述: 给制定邮箱发送注册验证码 + // ? ? + public sendRegisterCodeMail(email, code) { + return new Promise(async (res, rej) => { + const test = ` + +
+
+
我们欢迎您使用${this.sysName}
+
您在某些地方请求了邮箱的验证码,如果不是自己操作请修改账户的密码。
+
+
${code}
+
+
此邮件作用于账户注册
截止邮件发送时间5分钟内使用有效。
+
+
+ `; + try { + console.time('EMAIL'); + const resd = await this.senMail(test, email); + console.timeEnd('EMAIL'); + res(resd); + } catch (e) { + rej({ + data: e, + message: '发送邮件失败!', + }); + } + }); + } +} diff --git a/src/Gservice/GREDIS/gredis.service.ts b/src/Gservice/GREDIS/gredis.service.ts index 6388e36..b0b6d70 100644 --- a/src/Gservice/GREDIS/gredis.service.ts +++ b/src/Gservice/GREDIS/gredis.service.ts @@ -1,16 +1,29 @@ import { Injectable } from '@nestjs/common'; import { createClient } from 'redis'; import config from '../../../config'; +import { GtoolsService } from '@/Gservice/GTOOLS/gtools.service'; + +export declare interface ISetRegisterEmailCode { + data: any; + message: string; + registerCode?: string; +} @Injectable() export class GredisService { public DB; private config; + // @ 专注于注册的数据库 + private readonly UserRegisterPoll = 1; - constructor() { + constructor(private readonly gtools: GtoolsService) { this.config = config().redis.starLight; this.start(); } + // ? ? + // ? 函数名称: start + // ? 函数描述: 连接redis + // ? ? private async start() { const client = createClient({ url: `redis://${this.config.username}:${this.config.password}@${this.config.host}:${this.config.port}/${this.config.dbNumber}`, @@ -18,4 +31,100 @@ export class GredisService { await client.connect(); this.DB = client; } + // ? ? + // ? 函数名称: setRegisterEmailCode(邮箱) + // ? 函数描述: 设置注册邮箱验证 + // ? ? + public setRegisterEmailCode(email): Promise { + return new Promise(async (res, rej) => { + this.DB.select(this.UserRegisterPoll); + try { + const key = 'Register-' + email; + const result = await this.DB.get(key); + if (result === null) { + const registerCode = this.gtools.getRandomString(6); + try { + const data = await this.DB.setEx( + key, + 300, + registerCode, + ); + res({ + data, + registerCode, + message: '生成验证码成功。', + }); + } catch (e) { + rej({ + data: e, + message: '生成验证码失败。', + }); + } + } else { + rej({ + data: null, + message: '已发送过邮件,请稍后再重试。', + }); + } + } catch (e) { + rej({ + data: e, + message: '查找注册码失败。', + }); + } + }); + } + + // ? ? + // ? 函数名称: getRegisterEmailCode(邮箱) + // ? 函数描述: 读取注册验证码 + // ? ? + public getRegisterEmailCode(email): Promise { + return new Promise(async (res, rej) => { + this.DB.select(this.UserRegisterPoll); + const key = 'Register-' + email; + try { + const result = await this.DB.get(key); + if (result === null) { + rej({ + data: {}, + message: '未找到相匹配的验证码!', + }); + } else { + res({ + data: {}, + message: '获取成功', + registerCode: result as string, + }); + } + } catch (e) { + rej({ + data: e, + message: '从Redis获取注册吗出现错误!', + }); + } + }); + } + // ? ? + // ? 函数名称: removeRegisterEmailCode + // ? 函数描述: + // ? ? + public removeRegisterEmailCode(email): Promise { + return new Promise(async (res, rej) => { + this.DB.select(this.UserRegisterPoll); + const key = 'Register-' + email; + try { + const result = await this.DB.del(key); + res({ + message: '清除注册验证码成功!', + data: {}, + }); + } catch (e) { + res({ + message: '清除注册验证码失败!', + data: e, + }); + } + }); + } } diff --git a/src/Gservice/GTOOLS/gtools.module.ts b/src/Gservice/GTOOLS/gtools.module.ts new file mode 100644 index 0000000..52736de --- /dev/null +++ b/src/Gservice/GTOOLS/gtools.module.ts @@ -0,0 +1,8 @@ +import { Global, Module } from '@nestjs/common'; +import { GtoolsService } from './gtools.service'; +@Global() +@Module({ + providers: [GtoolsService], + exports: [GtoolsService], +}) +export class GtoolsModule {} diff --git a/src/Gservice/GTOOLS/gtools.service.ts b/src/Gservice/GTOOLS/gtools.service.ts new file mode 100644 index 0000000..1d9847d --- /dev/null +++ b/src/Gservice/GTOOLS/gtools.service.ts @@ -0,0 +1,137 @@ +import { Injectable } from '@nestjs/common'; +import * as crypto from 'crypto'; +import config from '@CFG/index'; +import * as jwt from 'jsonwebtoken'; + +// @ 不可逆加密 +enum HASHT { + MD5 = 'md5', // 32位 + SHA256 = 'sha256', // 64位 + SHA512 = 'sha512', // 128位 +} +// @ 可逆加密 +enum AEST { + AES128 = 'aes-128-cbc', + AES256 = 'aes-256-gcm', +} + +@Injectable() +export class GtoolsService { + // @ 加解密配置文件 + private readonly encryptionCFG = config().encryption; + private readonly tokenCFG = config().token; + + // @ AES-128-cec加解密 iv和key + private AES128CEC_key; + private AES128CEC_iv; + + constructor() { + this.AES128CEC_key = Buffer.from( + this.makeHASH(this.encryptionCFG.secretKey, HASHT.MD5).slice(-16), + 'utf8', + ); + + this.AES128CEC_iv = Buffer.from( + this.makeHASH(this.encryptionCFG.salt, HASHT.MD5).slice(-16), + 'utf8', + ); + } + + // ? ? + // ? 函数名称: getRandomString(长度,大小写默认big) + // ? 函数描述: 获取制定长度的随机字符串 + // ? ? + public getRandomString(len: number, size = 'big'): string { + if (size == 'big') { + return Math.random() + .toString(36) + .slice(-len) + .toString() + .toUpperCase(); + } else { + return Math.random() + .toString(36) + .slice(-len) + .toString() + .toLowerCase(); + } + } + + // ? ? + // ? 函数名称: makeUUID + // ? 函数描述: 生成UUID + // ? ? + public makeUUID() { + return crypto.randomUUID().split('-').join('').toUpperCase(); + } + + // ? ? + // ? 函数名称: makeHASH(加密内容, 加密编码默认为SHA512) + // ? 函数描述: =加密字符串 + // ? ? + public makeHASH(plaintext, algorithm = HASHT.SHA512) { + const sha512 = crypto.createHash(algorithm); + const sha512Sum = sha512.update(plaintext + this.encryptionCFG.salt); + const ciphertext = sha512Sum.digest('hex'); + return ciphertext; + } + + //#region 可逆加密 + // ? ? + // ? 函数名称: encrypt + // ? 函数描述: 加密字符串 + // ? ? + encrypt(plaintext, algorithm = AEST.AES128) { + const cipher = crypto.createCipheriv( + algorithm, + this.AES128CEC_key, + this.AES128CEC_iv, + ); // 初始化加密算法 + let ciphertext = cipher.update(plaintext, 'utf8', 'hex'); + ciphertext += cipher.final('hex'); + return ciphertext; + } + + // ? ? + // ? 函数名称: decrypt + // ? 函数描述: 解密字符串 + // ? ? + decrypt(ciphertext, algorithm = AEST.AES128) { + let plaintext = ''; + const cipher = crypto.createDecipheriv( + algorithm, + this.AES128CEC_key, + this.AES128CEC_iv, + ); + plaintext += cipher.update(ciphertext, 'hex', 'utf8'); + plaintext += cipher.final('utf8'); + return plaintext; + } + //#endredion + + //#region jwt + createToken(data) { + const plaintextToken = jwt.sign(data, this.tokenCFG.secretKey, { + expiresIn: this.tokenCFG.timeout / 1000, + }); + const ciphertextToken = this.encrypt(plaintextToken); + return ciphertextToken; + } + resolveToken(ciphertextToken) { + try { + // 解析密文 + const plaintextToken = this.decrypt(ciphertextToken); + const data = jwt.verify(plaintextToken, this.tokenCFG.secretKey); + return { + token: true, + data, + }; + } catch (e) { + return { + token: false, + data: e, + }; + } + } + //#endredion +} diff --git a/src/app.module.ts b/src/app.module.ts index 1c028d1..fe8b7e1 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -3,6 +3,8 @@ import { ConfigModule } from '@nestjs/config'; import { GredisModule } from '@/Gservice/GREDIS/gredis.module'; import { GloggerModule } from '@/Gservice/GLOGGER/glogger.module'; import { GdatabaseModule } from '@/Gservice/GDATABASE/gdatabase.module'; +import { GemailModule } from '@/Gservice/GEMAIL/gemail.module'; +import { GtoolsModule } from '@/Gservice/GTOOLS/gtools.module'; import config from '@CFG/index'; import { StarlightModule } from '@/starlight/starlight.module'; @@ -16,6 +18,8 @@ import { StarlightModule } from '@/starlight/starlight.module'; StarlightModule, GdatabaseModule, GredisModule, + GemailModule, + GtoolsModule, ], controllers: [], providers: [], diff --git a/src/starlight/dto/register.dto.ts b/src/starlight/dto/register.dto.ts index dae3a50..9a0911e 100644 --- a/src/starlight/dto/register.dto.ts +++ b/src/starlight/dto/register.dto.ts @@ -40,6 +40,12 @@ export class RegisterEmailCheckoutEmailDto { @IsString({ message: '邮箱应为字符串格式!' }) email: string; } +export class RegisterEmailCheckoutUsernameDto { + // @ 用户名 + @Length(8, 128, { message: '请将用户名长度控制在8到128位之间!' }) + username: string; +} + export class RegisterEmailSignUpDto extends RegisterEmailCheckoutEmailDto { // @ 验证码 @Length(6, 6, { message: '验证码是6位字符!' }) @@ -51,7 +57,7 @@ export class RegisterEmailSignUpDto extends RegisterEmailCheckoutEmailDto { // @ 用户名 @Length(8, 128, { message: '请将用户名长度控制在8到128位之间!' }) - usernmae: string; + username: string; // @ 真实姓名 @IsOptional() diff --git a/src/starlight/dto/signIn.dto.ts b/src/starlight/dto/signIn.dto.ts new file mode 100644 index 0000000..8f1c574 --- /dev/null +++ b/src/starlight/dto/signIn.dto.ts @@ -0,0 +1,18 @@ +/* * + * * @name: signIn.dto.ts + * * @description: 登陆部分的数据传输验证 + * * @author: xi + * * @date: 2023/3/26 22:41 + * * */ + +import { Length } from 'class-validator'; + +export class SignInPasswdEntryDto { + // @ 用户名 + @Length(8, 128, { message: '请将用户名长度控制在8到128位之间!' }) + username: string; + + // @ 密码 + @Length(8, 128, { message: '请将密码长度控制在8到128位之间!' }) + password: string; +} diff --git a/src/starlight/starlight.controller.ts b/src/starlight/starlight.controller.ts index 6f0219a..f502866 100644 --- a/src/starlight/starlight.controller.ts +++ b/src/starlight/starlight.controller.ts @@ -15,6 +15,8 @@ import { UseGuards, Req, Inject, + ExecutionContext, + createParamDecorator, } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { ApiTags } from '@nestjs/swagger'; @@ -29,8 +31,18 @@ import { GdatabaseService } from '@/Gservice/GDATABASE/gdatabase.service'; import { GredisService } from '@/Gservice/GREDIS/gredis.service'; import { RegisterEmailCheckoutEmailDto, + RegisterEmailCheckoutUsernameDto, RegisterEmailSignUpDto, -} from './dto/register.dto'; +} from '@/starlight/dto/register.dto'; +import { SignInPasswdEntryDto } from '@/starlight/dto/signIn.dto'; +import { VerifyGuard } from '@/starlight/verify.guard'; + +// nest g d [name] +// 自定义装饰器 +const getUser = createParamDecorator((data: string, ctx: ExecutionContext) => { + const req = ctx.switchToHttp().getRequest(); + return req.user; +}); // C C // C 类名称: StarlightController @@ -77,16 +89,47 @@ export class StarlightController { } // ? ? - // ? 函数名称: registerEmailSignUp - // ? 函数描述: + // ? 函数名称: registerEmailCheckoutUsername + // ? 函数描述: 查重用户名 // ? ? - //#endregion - @Post('/register/email/signUp') - registerEmailSignUpDto( - @Body() body: RegisterEmailSignUpDto, + @Get('register/email/checkoutUsername/:username') + registerEmailCheckoutUsername( + @Param() param: RegisterEmailCheckoutUsernameDto, ): Promise { + return this.starlightService.registerEmailCheckoutUsername(param); + } + + // ? ? + // ? 函数名称: registerEmailSignUp + // ? 函数描述: 邮箱注册 + // ? ? + @Post('register/email/signUp') + registerEmailSignUp(@Body() body: RegisterEmailSignUpDto): Promise { return this.starlightService.registerEmailSignUpDto(body); } + //#endregion + + //#region 登陆 + // ? ? + // ? 函数名称: signInPasswdEntry + // ? 函数描述: 使用密码和用户名登陆 + // ? ? + @Post('/signIn/passwd/entry') + signInPasswdEntry(@Body() body: SignInPasswdEntryDto): Promise { + return this.starlightService.signInPasswdEntry(body); + } + + // ? ? + // ? 函数名称: + // ? 函数描述: 测试Token + // ? ? + @UseGuards(VerifyGuard) + @Get('/signIn/testToken') + testToken(@getUser() user) { + this.logger.info(user); + return {}; + } + //#endredion //#region 测试 @Post() diff --git a/src/starlight/starlight.service.ts b/src/starlight/starlight.service.ts index a146a11..16d9315 100644 --- a/src/starlight/starlight.service.ts +++ b/src/starlight/starlight.service.ts @@ -9,12 +9,17 @@ import { CreateStarlightDto } from './dto/create-starlight.dto'; import { UpdateStarlightDto } from './dto/update-starlight.dto'; import { RegisterEmailCheckoutEmailDto, + RegisterEmailCheckoutUsernameDto, RegisterEmailSignUpDto, + sex, } from './dto/register.dto'; import { ConfigService } from '@nestjs/config'; import { GloggerService } from '@/Gservice/GLOGGER/glogger.service'; import { GdatabaseService } from '@/Gservice/GDATABASE/gdatabase.service'; import { GredisService } from '@/Gservice/GREDIS/gredis.service'; +import { GemailService } from '@/Gservice/GEMAIL/gemail.service'; +import { GtoolsService } from '@/Gservice/GTOOLS/gtools.service'; +import { SignInPasswdEntryDto } from '@/starlight/dto/signIn.dto'; // C C // C 类名称: StarlightService @@ -31,45 +36,309 @@ export class StarlightService { private readonly database: GdatabaseService, // @ Redis服务 private readonly redis: GredisService, + // @ 邮件服务 + private readonly email: GemailService, + // @ 工具服务 + private readonly tools: GtoolsService, ) {} //#region 邮箱注册 // ? ? // ? 函数名称: registerEmailCheckoutEmail - // ? 函数描述: 查重邮箱注册 + // ? 函数描述: 查重邮箱注册邮箱 // ? ? // ? ? - async registerEmailCheckoutEmail(params: RegisterEmailCheckoutEmailDto) { - return { - data: { ok: 'ok' }, - message: '', - success: true, + public async registerEmailCheckoutEmail( + params: RegisterEmailCheckoutEmailDto, + ) { + // ! 从数据库用户身份表查询有没有已经使用的邮箱 + const [rows] = await this.database.DB.execute( + `SELECT * FROM user_info_verify WHERE email = ? AND state = 0`, + [params.email.trim()], + ); + // ! 判断是否存在此邮箱 + const resd = { + data: {}, + message: '此邮箱已被使用,请尝试使用其他邮箱注册!', + success: false, + }; + if (rows.length == 0) { + resd.message = '此邮箱未被使用,可以作为注册邮箱!'; + resd.success = true; + } + return resd; + } + + // ? ? + // ? 函数名称: registerEmailCheckoutUsername + // ? 函数描述: 查重邮箱注册用户名 + // ? ? + // ? ? + public async registerEmailCheckoutUsername( + params: RegisterEmailCheckoutUsernameDto, + ) { + // ! 从数据库用户身份表查询有没有已经使用的用户名 + const [rows] = await this.database.DB.execute( + `SELECT * FROM user_info_verify WHERE username = ? AND state = 0`, + [params.username.trim()], + ); + // ! 判断是否存在此用户名 + const resd = { + data: {}, + message: '此用户名已被使用,请尝试使用其他用户名注册!', + success: false, }; + if (rows.length == 0) { + resd.message = '此用户名未被使用,可以作为注册用户名!'; + resd.success = true; + } + return resd; } // ? 函数名称: registerEmailSendCode // ? 函数描述: 发送邮箱注册验证码 // ? ? - async registerEmailSendCode(params: RegisterEmailCheckoutEmailDto) { - return { - data: { ok: 'ok' }, - message: '', - success: true, + public async registerEmailSendCode(params: RegisterEmailCheckoutEmailDto) { + const { email } = params; + // ! 1. 验证是否存在已经注册的 + const checkoutEmail = await this.registerEmailCheckoutEmail({ + email, + }); + const resd = { + data: {}, + message: '此邮箱已被使用,请尝试使用其他邮箱注册!', + success: false, }; + if (!checkoutEmail.success) { + return resd; + } + // ! 2. 验证是否存在验证码 + let registerCode: any; + try { + const redisResd = await this.redis.setRegisterEmailCode(email); + registerCode = redisResd.registerCode as string; + } catch (e) { + this.logger.error(e); + resd.message = e.message; + return resd; + } + // ! 3. 发送验证码 + try { + const result = await this.email.sendRegisterCodeMail( + email, + registerCode, + ); + resd.success = true; + resd.message = '发送验证码成功,请注意查收!'; + return resd; + } catch (e) { + this.logger.error(e); + resd.message = e.message; + return resd; + } } // ? ? // ? 函数名称: registerEmailSignUpDto // ? 函数描述: 邮箱注册 // ? ? - async registerEmailSignUpDto(body: RegisterEmailSignUpDto) { - return { - data: { ok: 'ok' }, - message: '', - success: true, + public async registerEmailSignUpDto(body: RegisterEmailSignUpDto) { + const { email, code, username } = body; + // ! 1. 查重邮箱 + const checkoutEmail = await this.registerEmailCheckoutEmail({ + email, + }); + const resd = { + data: {}, + message: '此邮箱已被使用,请尝试使用其他邮箱注册!', + success: false, }; + if (!checkoutEmail.success) { + return resd; + } + // ! 2. 查询注册验证码是否存在,比对验证码 + try { + const { registerCode } = await this.redis.getRegisterEmailCode( + email, + ); + if (code.trim().toUpperCase() !== registerCode) { + resd.message = '验证码不匹配!'; + return resd; + } + } catch (e) { + resd.message = e.message; + return resd; + } + // ! 3. 查重用户名 + const checkoutUsername = await this.registerEmailCheckoutUsername({ + username, + }); + if (!checkoutUsername.success) { + resd.message = '此用户名已被使用,请尝试使用其他用户名注册!'; + return resd; + } + // ! 4. 清除空格,加密密码并插入信息 + const password = body.password.trim(); + const passwordHASH = this.tools.makeHASH(password); + const uuid = this.tools.makeUUID(); + const createTime = new Date(); + const s = { + email: email.trim(), + username: username.trim(), + realname: body.realname?.trim(), + nickname: body.nickname?.trim(), + birthday: new Date(body.birthday), + sex: sex[body.sex], + address: body.address?.trim(), + country: body.country?.trim(), + profile: body.profile?.trim(), + github_url: body.githubUrl?.trim(), + personal_url: body.personalUrl?.trim(), + alibaba_id: body.alibabaId?.trim(), + tiktok_id: body.tiktokId?.trim(), + weibo_id: body.weiboId?.trim(), + }; + Object.keys(s).map((item) => { + return s[item] != undefined ? '' : (s[item] = null); + }); + // ! -- 基础表 user_info_base + const userInfoBaseSQL = `INSERT INTO user_info_base (uuid, createtime) VALUES (?, ?);`; + try { + const [rows] = await this.database.DB.execute(userInfoBaseSQL, [ + uuid, + createTime, + ]); + } catch (e) { + const message = '插入基础表失败!'; + this.logger.error({ + data: e, + message, + }); + resd.message = message; + return resd; + } + // ! -- 验证表 user_info_verify + const userInfoVerifySQL = `INSERT INTO user_info_verify (uuid, email, username, createtime) VALUES (?, ?, ?, ?);`; + try { + const [rows] = await this.database.DB.execute(userInfoVerifySQL, [ + uuid, + s.email, + s.username, + createTime, + ]); + } catch (e) { + const message = '插入验证表失败!'; + this.logger.error({ + data: e, + message, + }); + resd.message = message; + return resd; + } + // ! -- 密码表 user_info_passwd + const userInfoPasswdSQL = `INSERT INTO user_info_passwd (uuid, passwd, createtime) VALUES (?, ?, ?);`; + try { + const [rows] = await this.database.DB.execute(userInfoPasswdSQL, [ + uuid, + passwordHASH, + createTime, + ]); + } catch (e) { + const message = '插入密码表失败!'; + this.logger.error({ + data: e, + message, + }); + // 删除用户验证表的这条记录 + const DELSQL = `DELETE user_info_verify WHERE email = ?`; + try { + const [rows] = await this.database.DB.execute(DELSQL, [ + s.email, + ]); + this.logger.info(rows); + } catch (e) { + this.logger.error({ + data: e, + message: + '删除用户验证表失败,此候补操作依然失效,系统错误!', + }); + } + resd.message = message; + return resd; + } + // ! -- 拓展表 user_info_extra + const userInfoExtraSQL = `INSERT INTO user_info_extra (uuid, realname, nickname, birthday, sex, address, country, profile, alibaba_id, tiktok_id, weibo_id, github_url, personal_url, createtime) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`; + try { + const [rows] = await this.database.DB.execute(userInfoExtraSQL, [ + uuid, + s.realname, + s.nickname, + s.birthday, + s.sex, + s.address, + s.country, + s.profile, + s.alibaba_id, + s.tiktok_id, + s.weibo_id, + s.github_url, + s.personal_url, + createTime, + ]); + } catch (e) { + const message = '插入用户拓展信息时出现错误!'; + this.logger.error({ + data: e, + message, + }); + resd.message = message; + return resd; + } + + this.logger.info(s); + + // ! 5. 清除验证码 + try { + const result = await this.redis.removeRegisterEmailCode(email); + } catch (e) { + this.logger.warn(e); + } + resd.message = '注册成功!'; + resd.success = true; + return resd; } //#endregion - //#region + + //#region 登陆 + // ? ? + // ? 函数名称: signInPasswdEntry + // ? 函数描述: 使用密码和用户名登陆 + // ? ? + public async signInPasswdEntry(body: SignInPasswdEntryDto) { + const { username, password } = body; + // ! 加密密码 + const passwordHash = this.tools.makeHASH(password); + // ! 验证密码 + // 查找最后一条 + // ! 生成token + const token = this.tools.createToken({ + username, + signInTime: new Date().getTime(), + }); + this.logger.info(token); + // ! Redis 登陆存储策略 + // ! 返回数据 + + return { + message: '登陆成功', + data: { + token: '', + }, + }; + } + + //#endredion + + //#region 测试啊 create(createStarlightDto: CreateStarlightDto) { return 'This action adds a new starlight'; @@ -94,5 +363,5 @@ export class StarlightService { remove(id: number) { return `This action removes a #${id} starlight`; } - //#endredion + //#endregion } diff --git a/src/starlight/verify.guard.ts b/src/starlight/verify.guard.ts new file mode 100644 index 0000000..39815de --- /dev/null +++ b/src/starlight/verify.guard.ts @@ -0,0 +1,42 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { GloggerService } from '@/Gservice/GLOGGER/glogger.service'; +import { GtoolsService } from '@/Gservice/GTOOLS/gtools.service'; +// 此文件为守卫 +@Injectable() +export class VerifyGuard implements CanActivate { + constructor( + private readonly logger: GloggerService, + private readonly tools: GtoolsService, + ) {} + canActivate( + context: ExecutionContext, + ): boolean | Promise | Observable { + const request = context.switchToHttp().getRequest(); + const url = request.url; + // this.logger.info(url); + const token = request.headers.authorization; + // this.logger.info(token); + const data = this.tools.resolveToken(token); + // this.logger.info(data); + if (data.token) { + context.switchToHttp().getRequest().user = '32'; + // this.logger.info(request.user); + } + // console.log(request); + // console.log(request.url);request.headers + // this.logger.info('GUARG', request.url, "TEST"); + // 自定义返回消息 + // throw new UnauthorizedException({ + // statusCode: 403, + // message: '你没有访问权限', + // error: 'Forbidden', + // }); + return true; + } +}