184 lines
5.3 KiB
TypeScript
184 lines
5.3 KiB
TypeScript
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;
|
||
}
|
||
}
|