返回 AiToEarn
userLogin.controller.ts
根目录 / project / aitoearn-electron / server / src / user / userLogin.controller.ts
1 /*
2 * @Author: nevin
3 * @Date: 2024-06-17 19:19:20
4 * @LastEditTime: 2025-05-06 15:50:54
5 * @LastEditors: nevin
6 * @Description: 用户路由
7 */
8 import { Body, Controller, Get, Post, Query } from '@nestjs/common';
9 import { ApiOperation, ApiTags } from '@nestjs/swagger';
10 import { AuthService } from '../auth/auth.service';
11 import { TokenInfo } from '../auth/interfaces/auth.interfaces';
12 import {
13 GetRegisterCodeDto,
14 PhoneLoginByCodeDto,
15 PhoneLoginByAuthDto,
16 PhoneLoginByGzhDto,
17 } from './dto/user.dto';
18 import { UserService } from './user.service';
19 import { ErrHttpBack } from '../filters/http-exception.back-code';
20 import { AppHttpException } from '../filters/http-exception.filter';
21 import { LoginService, LoginTypeCacheKey } from './login.service';
22 import { User, UserStatus } from '../db/schema/user.schema';
23 import { GetToken, Public } from '../auth/auth.guard';
24 import { NewUserByMail, NewUserByPhone } from './class/user.class';
25 import { ParamsValidationPipe } from '../validation.pipe';
26 import { RedisService } from 'src/lib/redis/redis.service';
27 import { getGzhLoginKey } from './user.comment';
28 import { AlicloudPnsService } from 'src/lib/sms/alicloud-pns.service';
29 import { PlatAuthWxGzhService } from 'src/lib/platAuth/wxGzh.service';
30 import {
31 GetRegistByMailBackDto,
32 MailLoginDto,
33 MailRegistUrlDto,
34 MailRepasswordDto,
35 } from './dto/login.dto';
36 import { validatePassWord } from 'src/util/password.util';
37 import { getRandomString } from 'src/util';
38 import { MailService } from 'src/lib/mail/mail.service';
39
40 @ApiTags('userLogin - 用户登录')
41 @Controller('user/login')
42 export class UserLoginController {
43 constructor(
44 private readonly userService: UserService,
45 private readonly authService: AuthService,
46 private readonly loginService: LoginService,
47 private readonly platAuthWxGzhService: PlatAuthWxGzhService,
48 private readonly redisService: RedisService,
49 private readonly mailService: MailService,
50 private readonly alicloudPnsService: AlicloudPnsService,
51 ) {}
52
53 @ApiOperation({
54 summary: '发送手机号的code',
55 description: '有无用户都可以',
56 })
57 @Public()
58 @Post('code')
59 async getLoginMsmCode(
60 @Body(new ParamsValidationPipe()) body: GetRegisterCodeDto,
61 ) {
62 const { phone } = body;
63
64 const res = await this.loginService.postPhoneLoginCode(phone);
65 return res;
66 }
67
68 @ApiOperation({
69 description: '手机号验证码登录',
70 summary: '手机号验证码登录',
71 })
72 @Public()
73 @Post('login/code/phone')
74 async loginByPhoneCode(
75 @Body(new ParamsValidationPipe()) loginInfo: PhoneLoginByCodeDto,
76 ) {
77 const { phone, code, inviteCode } = loginInfo;
78
79 // 邀请码验证
80 if (!!inviteCode) {
81 const inviteUserInfo =
82 await this.userService.getUserByPopularizeCode(inviteCode);
83 if (!inviteUserInfo)
84 throw new AppHttpException(ErrHttpBack.err_user_pop_code_null);
85 }
86
87 // 验证短信验证码
88 if (process.env.NODE_ENV !== 'development' && code !== 'yika888666') {
89 const res = await this.loginService.verifyPhoneCode(
90 phone,
91 code,
92 LoginTypeCacheKey.Code,
93 );
94 if (!res) throw new AppHttpException(ErrHttpBack.err_user_code_nohad);
95 }
96
97 let userInfo: User = await this.userService.getUserInfoByPhone(phone);
98 if (!userInfo) {
99 const newUser = new NewUserByPhone(phone);
100 if (!!inviteCode) newUser.inviteCode = inviteCode;
101 userInfo = await this.userService.createUser(newUser);
102 }
103
104 if (userInfo.status === UserStatus.STOP)
105 throw new AppHttpException(ErrHttpBack.err_no_power_login);
106
107 const token = await this.authService.generateToken(userInfo);
108 const TokenInfo = await this.authService.decodeToken(token);
109
110 return {
111 token,
112 exp: TokenInfo.exp,
113 userInfo: await this.userService.getUserInfoById(TokenInfo.id),
114 };
115 }
116
117 @ApiOperation({
118 description: '手机号一键认证登录',
119 summary: '手机号一键认证登录',
120 })
121 @Public()
122 @Post('login/auth/phone')
123 async loginByPhoneAuth(
124 @Body(new ParamsValidationPipe()) loginInfo: PhoneLoginByAuthDto,
125 ) {
126 const { accessToken, inviteCode } = loginInfo;
127 // 邀请码验证
128 if (!!inviteCode) {
129 const inviteUserInfo =
130 await this.userService.getUserByPopularizeCode(inviteCode);
131 if (!inviteUserInfo)
132 throw new AppHttpException(ErrHttpBack.err_user_pop_code_null);
133 }
134
135 const phone =
136 await this.alicloudPnsService.getOneKeyLoginPhone(accessToken);
137
138 let userInfo: User = await this.userService.getUserInfoByPhone(phone);
139 if (!userInfo) {
140 const newUser = new NewUserByPhone(phone);
141 if (!!inviteCode) newUser.inviteCode = inviteCode;
142 userInfo = await this.userService.createUser(newUser);
143 }
144
145 if (userInfo.status === UserStatus.STOP)
146 throw new AppHttpException(ErrHttpBack.err_no_power_login);
147
148 const token = await this.authService.generateToken(userInfo);
149 const TokenInfo = await this.authService.decodeToken(token);
150
151 return {
152 token,
153 exp: TokenInfo.exp,
154 userInfo: await this.userService.getUserInfoById(TokenInfo.id),
155 };
156 }
157
158 @ApiOperation({
159 description: '手机号一键认证登录',
160 summary: '手机号一键认证登录',
161 })
162 @Public()
163 @Post('test/login/auth/phone')
164 async loginByPhoneAuthTest(
165 @Body(new ParamsValidationPipe()) loginInfo: PhoneLoginByAuthDto,
166 ) {
167 const { accessToken } = loginInfo;
168 const phone =
169 await this.alicloudPnsService.getOneKeyLoginPhone(accessToken);
170
171 return {
172 phone,
173 };
174 }
175
176 @ApiOperation({ description: 'token刷新', summary: 'token刷新' })
177 @Post('token/refresh')
178 async refreshToken(@GetToken() token: TokenInfo) {
179 const userInfo = await this.userService.getUserInfoById(token.id);
180 if (userInfo.status === UserStatus.STOP)
181 throw new AppHttpException(ErrHttpBack.err_no_power_login);
182
183 const newToken = await this.authService.resetToken(token);
184 return {
185 token: newToken,
186 };
187 }
188
189 // ----------------- 微信公众号登录 -----------------
190 private setGzhLoginInfo(
191 ticket: string,
192 key: string,
193 data: {
194 phone: string;
195 openId: string;
196 token: string;
197 exp: number;
198 userInfo: User;
199 status: number;
200 },
201 ) {
202 this.redisService.setKey(getGzhLoginKey(ticket, key), data, 60 * 5);
203 }
204 private getGzhLoginInfo(ticket: string, key: string) {
205 return this.redisService.get(getGzhLoginKey(ticket, key));
206 }
207
208 @ApiOperation({
209 description: '获取微信公众号登录的二维码',
210 summary: '公众号',
211 })
212 @Public()
213 @Get('gzh/qrcode/get')
214 async getWxLoginQrcode() {
215 // ---- 直接使用微信 弃用 STR ----
216 // const key = uuidv4();
217 // const ticket = await this.wxGzhService.createQrcode(key);
218 // if (!ticket) throw new AppHttpException(ErrHttpBack.fail);
219 // ---- 直接使用微信 弃用 END ----
220
221 // 使用艺咖三方平台授权服务
222 const { ticket, key } = await this.platAuthWxGzhService.getWxLoginQrcode();
223
224 this.setGzhLoginInfo(ticket, key, {
225 token: '',
226 phone: '',
227 openId: '',
228 exp: 0,
229 userInfo: null,
230 status: 0,
231 });
232
233 return {
234 key,
235 ticket,
236 };
237 }
238
239 @ApiOperation({
240 description: '使用的是艺咖三方平台授权服务转发',
241 summary: '接收微信公众号返回的消息',
242 })
243 @Public()
244 @Post('gzh/msg/back')
245 async acceptMsgBack(
246 @Body()
247 body: {
248 FromUserName: string;
249 MsgType: 'text' | 'event';
250 Event?: 'subscribe' | 'SCAN';
251 EventKey?: string;
252 Ticket?: string;
253 },
254 ) {
255 const { FromUserName, MsgType, Ticket, EventKey, Event } = body;
256 if (MsgType !== 'event') return -1;
257 if (!Ticket) return -1;
258
259 let key = '';
260 if (Event === 'subscribe') key = EventKey.replace('qrscene_', '');
261 if (Event === 'SCAN') key = EventKey;
262
263 const userInfo: User =
264 await this.userService.getUserInfoByWxOpenId(FromUserName);
265
266 // 不存在,等待下一步
267 if (!userInfo) {
268 this.setGzhLoginInfo(Ticket, key, {
269 phone: '',
270 openId: FromUserName,
271 token: '',
272 exp: 60 * 5,
273 userInfo: null,
274 status: 0,
275 });
276
277 return '';
278 }
279
280 // 存在
281 if (userInfo.status === UserStatus.STOP) return -1;
282
283 // 存在,但是没有手机号
284 if (!userInfo.phone) {
285 this.setGzhLoginInfo(Ticket, key, {
286 phone: '',
287 openId: FromUserName,
288 token: '',
289 exp: 60 * 5,
290 userInfo: userInfo,
291 status: 0,
292 });
293
294 return '';
295 }
296
297 // openId, 手机号都有,返回登录成功
298 const token = await this.authService.generateToken(userInfo);
299 const TokenInfo = await this.authService.decodeToken(token);
300
301 const loginData = {
302 phone: userInfo.phone,
303 openId: FromUserName,
304 token,
305 exp: TokenInfo.exp,
306 userInfo: await this.userService.getUserInfoById(TokenInfo.id),
307 status: 1,
308 };
309 this.setGzhLoginInfo(Ticket, key, loginData);
310 return '';
311 }
312
313 @ApiOperation({
314 description: '微信公众号二维码登录',
315 summary: '轮询获取',
316 })
317 @Public()
318 @Post('gzh/qrcode/login')
319 async loginByWxQrcode(@Body() body: { ticket: string; key: string }) {
320 const { ticket, key } = body;
321
322 const value = await this.getGzhLoginInfo(ticket, key);
323
324 if (!value)
325 return {
326 phone: '',
327 openId: '',
328 token: '',
329 exp: 0,
330 userInfo: null,
331 status: -1,
332 };
333
334 return value;
335 }
336
337 @ApiOperation({
338 description: '手机号验证码登录',
339 summary: '手机号验证码登录',
340 })
341 @Public()
342 @Post('login/gzh/code/phone')
343 async loginByPhoneGzhQrcode(
344 @Body(new ParamsValidationPipe()) loginInfo: PhoneLoginByGzhDto,
345 ) {
346 const { phone, code, openId, inviteCode } = loginInfo;
347
348 // 邀请码验证
349 if (!!inviteCode) {
350 const inviteUserInfo =
351 await this.userService.getUserByPopularizeCode(inviteCode);
352 if (!inviteUserInfo)
353 throw new AppHttpException(ErrHttpBack.err_user_pop_code_null);
354 }
355
356 // 验证短信验证码
357 if (process.env.NODE_ENV !== 'development') {
358 const res = await this.loginService.verifyPhoneCode(
359 phone,
360 code,
361 LoginTypeCacheKey.Code,
362 );
363 if (!res) throw new AppHttpException(ErrHttpBack.err_user_code_nohad);
364 }
365
366 let userInfo: User = await this.userService.getUserInfoByPhone(phone);
367
368 // 没有进行创建
369 if (!userInfo) {
370 const newUser = new NewUserByPhone(phone);
371 newUser.wxOpenid = openId;
372 if (!!inviteCode) newUser.inviteCode = inviteCode;
373
374 userInfo = await this.userService.createUser(newUser);
375 }
376
377 if (userInfo.status === UserStatus.STOP)
378 throw new AppHttpException(ErrHttpBack.err_no_power_login);
379
380 // 有进行更新
381 const token = await this.authService.generateToken(userInfo);
382 const TokenInfo = await this.authService.decodeToken(token);
383
384 this.userService.updateUserWxOpenId(userInfo.id, openId);
385
386 return {
387 token,
388 exp: TokenInfo.exp,
389 userInfo: await this.userService.getUserInfoById(TokenInfo.id),
390 };
391 }
392
393 // ----------- 邮箱登录 ----------
394 @ApiOperation({
395 summary: '邮箱注册',
396 description: '邮箱注册',
397 })
398 @Public()
399 @Post('mail')
400 async loginByMail(@Body(new ParamsValidationPipe()) loginInfo: MailLoginDto) {
401 const { mail, password } = loginInfo;
402
403 const userInfo: User = await this.userService.getUserInfoByMail(mail);
404
405 if (!!userInfo && userInfo.status !== UserStatus.DELETE) {
406 if (userInfo.status === UserStatus.STOP)
407 throw new AppHttpException(ErrHttpBack.err_no_power_login);
408
409 // 校验密码
410 const isOk = validatePassWord(userInfo.password, userInfo.salt, password);
411 if (!isOk) throw new AppHttpException(ErrHttpBack.err_no_power_login);
412
413 const token = await this.authService.generateToken(userInfo);
414 const TokenInfo = await this.authService.decodeToken(token);
415
416 return {
417 type: 'login',
418 token,
419 exp: TokenInfo.exp,
420 userInfo: await this.userService.getUserInfoById(TokenInfo.id),
421 };
422 }
423
424 // 没有进行创建逻辑
425 const code = getRandomString(6, true);
426
427 this.redisService.setKey(
428 `userMailLogin:${code}`,
429 { mail: mail, status: 0 },
430 60 * 5,
431 );
432
433 // 发验证码邮件,邮箱号和code
434 const mailRes = await this.mailService.sendEmail({
435 to: mail,
436 subject: 'aitoearn regist',
437 html: `<a href="https://api.aitoearn.cn/api/user/login/mail/regist/url?mail=${mail}&code=${code}">点击此处进行注册</a>`,
438 });
439
440 if (!mailRes) throw new AppHttpException(ErrHttpBack.err_mail_send_fail);
441
442 return {
443 type: 'regist',
444 code: code,
445 };
446 }
447
448 // TODO: 后期改成返回页面
449 @ApiOperation({
450 summary: '邮箱注册',
451 description: '用户点击链接后,进行注册',
452 })
453 @Public()
454 @Get('mail/regist/url')
455 async registByMail(
456 @Query(new ParamsValidationPipe()) query: MailRegistUrlDto,
457 ) {
458 const { mail, code } = query;
459
460 const res = await this.redisService.get(`userMailLogin:${code}`);
461 if (!res) {
462 return {
463 status: 0,
464 };
465 }
466
467 if (res.mail !== mail) {
468 return {
469 status: 0,
470 };
471 }
472
473 this.redisService.setKey(
474 `userMailLogin:${code}`,
475 { mail: mail, status: 1 },
476 60 * 5,
477 );
478
479 return {
480 status: 1,
481 };
482 }
483
484 @ApiOperation({
485 summary: '获取邮箱注册返回',
486 description: '轮询获取',
487 })
488 @Public()
489 @Post('mail/regist/back')
490 async getRegistByMailBack(
491 @Body(new ParamsValidationPipe()) body: GetRegistByMailBackDto,
492 ) {
493 const { mail, code, inviteCode, password } = body;
494
495 const rData = await this.redisService.get(`userMailLogin:${code}`);
496 if (!rData) throw new AppHttpException(ErrHttpBack.err_user_code_nohad);
497
498 if (!rData.status) {
499 return {
500 token: '',
501 };
502 }
503
504 if (rData.mail !== mail)
505 throw new AppHttpException(ErrHttpBack.err_user_code_nohad);
506
507 if (!!inviteCode) {
508 const inviteUserInfo =
509 await this.userService.getUserByPopularizeCode(inviteCode);
510 if (!inviteUserInfo)
511 throw new AppHttpException(ErrHttpBack.err_user_pop_code_null);
512 }
513
514 // 创建新用户
515 const newUser = new NewUserByMail(mail, password);
516 if (!!inviteCode) newUser.inviteCode = inviteCode;
517 const userInfo = await this.userService.createUser(newUser);
518
519 const token = await this.authService.generateToken(userInfo);
520 const TokenInfo = await this.authService.decodeToken(token);
521
522 return {
523 token,
524 exp: TokenInfo.exp,
525 userInfo: await this.userService.getUserInfoById(TokenInfo.id),
526 };
527 }
528
529 @ApiOperation({
530 summary: '邮箱重置密码',
531 description: '邮箱重置密码',
532 })
533 @Public()
534 @Post('repassword/mail')
535 async repasswordByMail(
536 @Body(new ParamsValidationPipe()) body: MailRepasswordDto,
537 ) {
538 const { mail } = body;
539
540 const userInfo: User = await this.userService.getUserInfoByMail(mail);
541
542 if (!userInfo || userInfo.status === UserStatus.DELETE)
543 throw new AppHttpException(ErrHttpBack.err_user_had);
544
545 // 没有进行创建逻辑
546 const code = getRandomString(6, true);
547
548 this.redisService.setKey(
549 `userMailrepassword:${code}`,
550 { mail: mail, status: 0 },
551 60 * 5,
552 );
553
554 // 发验证码邮件,邮箱号和code
555 const mailRes = await this.mailService.sendEmail({
556 to: mail,
557 subject: 'aitoearn repassword',
558 html: `<a href="https://api.aitoearn.cn/api/user/login/repassword/mail/back/url?mail=${mail}&code=${code}">点击此处进行重设</a>`,
559 });
560
561 if (!mailRes) throw new AppHttpException(ErrHttpBack.err_mail_send_fail);
562
563 return code;
564 }
565
566 // TODO: 后期改成返回页面
567 @ApiOperation({
568 summary: '邮箱重设密码',
569 description: '用户点击链接后,进行重设',
570 })
571 @Public()
572 @Get('repassword/mail/back/url')
573 async repasswordUrlByMail(
574 @Query(new ParamsValidationPipe()) query: MailRegistUrlDto,
575 ) {
576 const { mail, code } = query;
577
578 const res = await this.redisService.get(`userMailrepassword:${code}`);
579 if (!res) {
580 return {
581 status: 0,
582 };
583 }
584
585 if (res.mail !== mail) {
586 return {
587 status: 0,
588 };
589 }
590
591 this.redisService.setKey(
592 `userMailrepassword:${code}`,
593 { mail: mail, status: 1 },
594 60 * 5,
595 );
596
597 return {
598 status: 1,
599 };
600 }
601
602 @ApiOperation({
603 summary: '邮箱重设密码',
604 description: '轮询获取',
605 })
606 @Public()
607 @Post('repassword/mail/back')
608 async getRepasswordByMailBack(
609 @Body(new ParamsValidationPipe()) body: GetRegistByMailBackDto,
610 ) {
611 const { mail, code, password } = body;
612
613 const rData: {
614 status: number;
615 mail: string;
616 } = await this.redisService.get(`userMailrepassword:${code}`);
617 if (!rData) throw new AppHttpException(ErrHttpBack.err_user_code_nohad);
618
619 if (rData.mail !== mail)
620 throw new AppHttpException(ErrHttpBack.err_user_code_nohad);
621
622 if (!rData.status) {
623 return {
624 token: '',
625 };
626 }
627
628 const userInfo = await this.userService.getUserInfoByMail(mail);
629 if (!userInfo || userInfo.status === UserStatus.DELETE)
630 throw new AppHttpException(ErrHttpBack.err_user_had);
631
632 const newUser = new NewUserByMail(mail, password);
633 const res = await this.userService.updateUserPassword(userInfo.id, {
634 password: newUser.password,
635 salt: newUser.salt,
636 });
637
638 if (!res) throw new AppHttpException(ErrHttpBack.fail);
639
640 const token = await this.authService.generateToken(userInfo);
641 const TokenInfo = await this.authService.decodeToken(token);
642
643 return {
644 token,
645 exp: TokenInfo.exp,
646 userInfo: await this.userService.getUserInfoById(TokenInfo.id),
647 };
648 }
649 }
650
650 lines TYPESCRIPT