feat: 后端调整登陆逻辑

This commit is contained in:
2025-12-16 22:48:51 +08:00
parent b235ca8a6e
commit 70517058ae
13 changed files with 305 additions and 194 deletions

View File

@@ -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'))

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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('用户不存在');
}