feat: 后端调整登陆逻辑
This commit is contained in:
@@ -2,37 +2,86 @@ import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
Controller,
|
||||
NotImplementedException,
|
||||
Post,
|
||||
Request,
|
||||
Res,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
import { LoginByPasswordDto } from './dto/login.dto';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { UserSessionService } from 'src/user/services/user-session.service';
|
||||
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
|
||||
import { Response } from 'express';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly userService: UserService,
|
||||
private readonly userSessionService: UserSessionService,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
@Post('login')
|
||||
@UseGuards(ThrottlerGuard)
|
||||
@Throttle({ default: { limit: 20, ttl: 60000 } })
|
||||
async login(@Body() loginDto: LoginDto) {
|
||||
switch (loginDto.type) {
|
||||
case 'password':
|
||||
return this.authService.loginWithPassword(loginDto);
|
||||
case 'phone':
|
||||
return this.authService.loginWithPhone(loginDto);
|
||||
case 'email':
|
||||
return this.authService.loginWithEmail(loginDto);
|
||||
default:
|
||||
throw new BadRequestException('服务器错误');
|
||||
}
|
||||
// @Post('login')
|
||||
// @UseGuards(ThrottlerGuard)
|
||||
// @Throttle({ default: { limit: 20, ttl: 60000 } })
|
||||
// async login(@Body() loginDto: LoginDto) {
|
||||
// switch (loginDto.type) {
|
||||
// case 'password':
|
||||
// return this.authService.loginWithPassword(loginDto);
|
||||
// case 'phone':
|
||||
// return this.authService.loginWithPhone(loginDto);
|
||||
// case 'email':
|
||||
// return this.authService.loginWithEmail(loginDto);
|
||||
// default:
|
||||
// throw new BadRequestException('服务器错误');
|
||||
// }
|
||||
// }
|
||||
|
||||
private setUserToken(res: Response, token: string) {
|
||||
res.cookie('token', token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
// 永不过期,不用设置maxAge
|
||||
path: '/',
|
||||
})
|
||||
}
|
||||
|
||||
@Post('login/password')
|
||||
async loginByPassword(
|
||||
@Body() loginDto: LoginByPasswordDto,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
) {
|
||||
const { identifier, password } = loginDto;
|
||||
const loginRes = await this.authService.loginWithPassword(identifier, password);
|
||||
const { userId, token } = loginRes;
|
||||
this.setUserToken(res, token);
|
||||
return {
|
||||
user: await this.userService.findById(userId),
|
||||
};
|
||||
}
|
||||
|
||||
@Post('sms/send')
|
||||
async sendSms() {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Post('login/sms')
|
||||
async loginBySms() {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Post('passkey/login/options')
|
||||
async loginByPasskeyOptions() {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Post('passkey/login')
|
||||
async loginByPasskey() {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { createHash } from 'crypto';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
import { User } from 'src/user/entities/user.entity';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { UserSessionService } from 'src/user/services/user-session.service';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { VerificationService } from 'src/verification/verification.service';
|
||||
import { BusinessException } from 'src/common/exceptions/business.exception';
|
||||
import { ErrorCode } from 'src/common/constants/error-codes';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@@ -15,40 +15,49 @@ export class AuthService {
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly userSessionService: UserSessionService,
|
||||
private readonly verificationService: VerificationService,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
async loginWithPassword(loginDto: LoginDto) {
|
||||
const { account, password } = loginDto;
|
||||
// 依次使用邮箱登录、手机号、账号
|
||||
async loginWithPassword(identifier: string, password: string) {
|
||||
// 依次使用邮箱、手机号、账号登陆(防止有大聪明给账号改成别人的邮箱或手机号)
|
||||
const user = await this.userService.findOne(
|
||||
[{ email: account }, { phone: account }, { username: account }],
|
||||
[{ email: identifier }, { phone: identifier }, { username: identifier }],
|
||||
{
|
||||
withDeleted: true,
|
||||
},
|
||||
);
|
||||
|
||||
if (user && user.deletedAt !== null) {
|
||||
throw new BadRequestException('该账号注销中');
|
||||
throw new BusinessException({
|
||||
message: '该账号注销中',
|
||||
code: ErrorCode.USER_ACCOUNT_DEACTIVATED,
|
||||
});
|
||||
}
|
||||
|
||||
if (user === null || !user.password_hash || !user.salt) {
|
||||
throw new BadRequestException('账户或密码错误');
|
||||
throw new BusinessException({
|
||||
message: '账户或密码错误',
|
||||
code: ErrorCode.AUTH_INVALID_CREDENTIALS
|
||||
});
|
||||
}
|
||||
|
||||
// 判断密码是否正确
|
||||
const hashedPassword = this.hashPassword(password, user.salt);
|
||||
if (hashedPassword !== user.password_hash) {
|
||||
throw new BadRequestException('账户或密码错误');
|
||||
throw new BusinessException({
|
||||
message: '账户或密码错误',
|
||||
code: ErrorCode.AUTH_INVALID_CREDENTIALS
|
||||
});
|
||||
}
|
||||
|
||||
// 登录成功,颁发token
|
||||
return {
|
||||
token: await this.generateToken(user),
|
||||
userId: user.userId,
|
||||
};
|
||||
}
|
||||
|
||||
async loginWithPhone(loginDto: LoginDto) {
|
||||
const { phone, code } = loginDto;
|
||||
async loginWithPhone(data: { phone: string; code: string; }) {
|
||||
const { phone, code } = data;
|
||||
// 先判断验证码是否正确
|
||||
const isValid = this.verificationService.verifyPhoneCode(
|
||||
phone,
|
||||
@@ -90,65 +99,21 @@ export class AuthService {
|
||||
};
|
||||
}
|
||||
|
||||
async loginWithEmail(loginDto: LoginDto) {
|
||||
const { email, code } = loginDto;
|
||||
// 先判断验证码是否正确
|
||||
const isValid = this.verificationService.verifyEmailCode(
|
||||
email,
|
||||
code,
|
||||
'login',
|
||||
);
|
||||
switch (isValid) {
|
||||
case 0:
|
||||
break;
|
||||
case -1:
|
||||
throw new BadRequestException('验证码已过期,请重新获取');
|
||||
case -2:
|
||||
throw new BadRequestException('验证码错误');
|
||||
case -3:
|
||||
throw new BadRequestException('验证码已失效,请重新获取');
|
||||
default:
|
||||
throw new BadRequestException('验证码错误,请稍后再试');
|
||||
}
|
||||
|
||||
// 判断用户是否存在,若不存在则进行注册
|
||||
let user = await this.userService.findOne({ email }, { withDeleted: true });
|
||||
if (user && user.deletedAt !== null) {
|
||||
throw new BadRequestException('该账号注销中,请使用其他邮箱');
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
// 执行注册操作
|
||||
user = await this.userService.create({ email: email });
|
||||
}
|
||||
|
||||
if (!user || !user.userId) {
|
||||
// 注册失败或用户信息错误
|
||||
throw new BadRequestException('请求失败,请稍后再试');
|
||||
}
|
||||
|
||||
// 登录,颁发token
|
||||
return {
|
||||
token: await this.generateToken(user),
|
||||
};
|
||||
}
|
||||
|
||||
private hashPassword(password: string, salt: string): string {
|
||||
return createHash('sha256').update(`${password}${salt}`).digest('hex');
|
||||
}
|
||||
|
||||
private async generateToken(user: User) {
|
||||
// 存储
|
||||
const sessionRes = await this.userSessionService.createSession(
|
||||
user.userId,
|
||||
);
|
||||
|
||||
const payload = {
|
||||
userId: user.userId,
|
||||
sessionId: uuidv4(),
|
||||
sessionId: sessionRes.sessionId,
|
||||
};
|
||||
|
||||
// 存储
|
||||
await this.userSessionService.createSession(
|
||||
payload.userId,
|
||||
payload.sessionId,
|
||||
);
|
||||
|
||||
// 颁发token
|
||||
return this.jwtService.sign(payload);
|
||||
}
|
||||
|
||||
@@ -1,31 +1,37 @@
|
||||
import { IsEnum, IsString, Length, ValidateIf } from 'class-validator';
|
||||
|
||||
export class LoginDto {
|
||||
@IsEnum(['password', 'phone', 'email'], { message: '请求类型错误' })
|
||||
type: 'password' | 'phone' | 'email';
|
||||
// export class LoginDto {
|
||||
// @IsEnum(['password', 'phone', 'email'], { message: '请求类型错误' })
|
||||
// type: 'password' | 'phone' | 'email';
|
||||
|
||||
@ValidateIf((o) => o.type === 'password')
|
||||
// @ValidateIf((o) => o.type === 'password')
|
||||
|
||||
// account?: string;
|
||||
|
||||
|
||||
|
||||
// @ValidateIf((o) => o.type === 'phone')
|
||||
// @IsString({ message: '手机号必须输入' })
|
||||
// @Length(11, 11, { message: '手机号异常' }) // 中国大陆,11位数字
|
||||
// phone?: string;
|
||||
|
||||
// @ValidateIf((o) => o.type === 'email')
|
||||
// @IsString({ message: '邮箱必须输入' })
|
||||
// @Length(6, 254, { message: '邮箱异常' }) // RFC 5321
|
||||
// email?: string;
|
||||
|
||||
// @ValidateIf((o) => o.type === 'phone' || o.type === 'email')
|
||||
// @IsString({ message: '验证码必须输入' })
|
||||
// @Length(6, 6, { message: '验证码异常' }) // 6位数字
|
||||
// code?: string;
|
||||
// }
|
||||
|
||||
export class LoginByPasswordDto {
|
||||
@IsString({ message: '账户必须输入' })
|
||||
@Length(1, 254, { message: '账户异常' }) // 用户名、邮箱、手机号
|
||||
account?: string;
|
||||
identifier: string;
|
||||
|
||||
@ValidateIf((o) => o.type === 'password')
|
||||
@IsString({ message: '密码必须输入' })
|
||||
@Length(6, 32, { message: '密码异常' }) // 6-32位
|
||||
password?: string;
|
||||
|
||||
@ValidateIf((o) => o.type === 'phone')
|
||||
@IsString({ message: '手机号必须输入' })
|
||||
@Length(11, 11, { message: '手机号异常' }) // 中国大陆,11位数字
|
||||
phone?: string;
|
||||
|
||||
@ValidateIf((o) => o.type === 'email')
|
||||
@IsString({ message: '邮箱必须输入' })
|
||||
@Length(6, 254, { message: '邮箱异常' }) // RFC 5321
|
||||
email?: string;
|
||||
|
||||
@ValidateIf((o) => o.type === 'phone' || o.type === 'email')
|
||||
@IsString({ message: '验证码必须输入' })
|
||||
@Length(6, 6, { message: '验证码异常' }) // 6位数字
|
||||
code?: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
@@ -26,15 +26,14 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
async validate(payload: any) {
|
||||
const { userId, sessionId } = payload ?? {};
|
||||
|
||||
const isValidSession = await this.userSessionService.isSessionValid(
|
||||
await this.userSessionService.isSessionValid(
|
||||
userId,
|
||||
sessionId,
|
||||
);
|
||||
if (!isValidSession) {
|
||||
throw new UnauthorizedException('登录凭证已过期,请重新登录');
|
||||
}
|
||||
).catch((e) => {
|
||||
throw new UnauthorizedException(`${e}`);
|
||||
});
|
||||
|
||||
const user = await this.userService.findById(userId);
|
||||
const user = await this.userService.findOne({ userId });
|
||||
if (!user) {
|
||||
throw new BadRequestException('用户不存在');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user