import { BadRequestException, Body, Controller, Post, Req, Res, UseGuards, } from '@nestjs/common'; import { LoginByPasswordDto } from './dto/login.dto'; import { AuthService } from './auth.service'; import { UserSessionService } from 'src/auth/service/user-session.service'; import { Throttle, ThrottlerGuard } from '@nestjs/throttler'; import { Request, Response } from 'express'; import { UserService } from 'src/user/user.service'; import { AuthGuard } from './guards/auth.guard'; import { SmsLoginDto } from './dto/sms-login.dto'; import { SmsService } from 'src/sms/sms.service'; import { UserSession } from 'src/auth/entity/user-session.entity'; import { PasskeyService } from './service/passkey.service'; import { v4 as uuidv4 } from 'uuid'; import { PasskeyLoginDto } from './dto/passkey-login.dto'; import { AuthUser, CurrentUser } from './decorator/current-user.decorator'; import { PasskeyRegisterDto } from './dto/passkey-register.dto'; @Controller('auth') export class AuthController { constructor( private readonly authService: AuthService, private readonly userService: UserService, private readonly userSessionService: UserSessionService, private readonly smsService: SmsService, private readonly passkeyService: PasskeyService, ) { } private setUserSession(res: Response, session: UserSession) { res.cookie('session', session.sessionId, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', // 永不过期,不用设置maxAge path: '/', }) } @Post('login/password') @UseGuards(ThrottlerGuard) @Throttle({ 'min': { limit: 5, ttl: 60 * 1000 }, 'hour': { limit: 20, ttl: 60 * 60 * 1000 }, 'day': { limit: 50, ttl: 24 * 60 * 60 * 1000 } }) async loginByPassword( @Body() loginDto: LoginByPasswordDto, @Res({ passthrough: true }) res: Response, ) { const { identifier, password } = loginDto; const session = await this.authService.loginWithPassword(identifier, password); this.setUserSession(res, session); return { user: await this.userService.findById(session.userId), }; } @Post('login/sms') @UseGuards(ThrottlerGuard) @Throttle({ 'day': { limit: 50, ttl: 24 * 60 * 60 * 1000 } }) async loginBySms( @Body() dto: SmsLoginDto, @Res({ passthrough: true }) res: Response, ) { const { phone, code } = dto; await this.smsService.checkSms(phone, 'login', code); // 验证通过,(注册并)登陆 const session = await this.authService.loginWithPhone(phone); this.setUserSession(res, session); return { user: await this.userService.findById(session.userId), } } @Post('passkey/login/options') @UseGuards(ThrottlerGuard) @Throttle({ 'day': { limit: 20, ttl: 24 * 60 * 60 * 1000 } }) async loginByPasskeyOptions( @Res({ passthrough: true }) res: Response, ) { const tempSessionId = uuidv4(); const options = await this.passkeyService.getAuthenticationOptions(tempSessionId); res.cookie('passkey_temp_session', tempSessionId, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/api/auth/passkey/login', maxAge: 1 * 60 * 1000, }); return options; } @Post('passkey/login') @UseGuards(ThrottlerGuard) @Throttle({ 'day': { limit: 20, ttl: 24 * 60 * 60 * 1000 } }) async loginByPasskey( @Req() req: Request, @Body() body: PasskeyLoginDto, @Res({ passthrough: true }) res: Response, ) { const tempSessionId = req.cookies?.passkey_temp_session; if (!tempSessionId) { throw new BadRequestException('登录失败,请重试'); } try { const user = await this.passkeyService.login(tempSessionId, body.credentialResponse); const session = await this.userSessionService.createSession(user.userId); this.setUserSession(res, session); return { user: await this.userService.findById(user.userId), }; } catch (error) { throw error; } finally { res.clearCookie('passkey_temp_session', { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/api/auth/passkey/login', }); } } @UseGuards(AuthGuard) @Post('passkey/register/options') async getPasskeyRegisterOptions( @CurrentUser() user: AuthUser, ) { const { userId } = user; return this.passkeyService.getRegistrationOptions(userId); } @UseGuards(AuthGuard) @Post('passkey/register') async registerPasskey( @CurrentUser() user: AuthUser, @Body() dto: PasskeyRegisterDto, ) { const { userId } = user; const { credentialResponse, name } = dto; const passkey = await this.passkeyService.register(userId, credentialResponse, name.trim()); return { id: passkey.id, name: passkey.name, createdAt: passkey.createdAt, }; } @UseGuards(AuthGuard) @Post('logout') async logout(@CurrentUser() user: AuthUser, @Res({ passthrough: true }) res: Response) { const { sessionId } = user; await this.userSessionService.invalidateSession(sessionId, '用户主动登出'); res.clearCookie('session', { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', }) return true; } }