From 971b165be8ce225b7205389836e81c12b1c4219c Mon Sep 17 00:00:00 2001 From: andres Date: Sun, 16 Jul 2023 20:40:58 +0200 Subject: [PATCH] add more auth docs --- src/modules/auth/auth.controller.ts | 70 +++++++++++++++---- src/modules/auth/dto/reset-password.dto.ts | 6 ++ src/modules/auth/use-cases/logout-use-case.ts | 4 +- 3 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 src/modules/auth/dto/reset-password.dto.ts diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 0a0394c..12c8a00 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -13,7 +13,15 @@ import { UseGuards, } from '@nestjs/common' import { CommandBus } from '@nestjs/cqrs' -import { ApiBody, ApiTags } from '@nestjs/swagger' +import { + ApiBadRequestResponse, + ApiBody, + ApiNoContentResponse, + ApiNotFoundResponse, + ApiOperation, + ApiTags, + ApiUnauthorizedResponse, +} from '@nestjs/swagger' import { Response as ExpressResponse } from 'express' import { Cookies } from '../../infrastructure/decorators' @@ -25,6 +33,7 @@ import { RegistrationDto, ResendVerificationEmailDto, } from './dto' +import { ResetPasswordDto } from './dto/reset-password.dto' import { LoginResponse, UserEntity } from './entities/auth.entity' import { JwtAuthGuard, JwtRefreshGuard, LocalAuthGuard } from './guards' import { @@ -43,6 +52,9 @@ import { export class AuthController { constructor(private commandBus: CommandBus) {} + @ApiOperation({ description: 'Retrieve current user data.' }) + @ApiUnauthorizedResponse({ description: 'Not logged in' }) + @ApiBadRequestResponse({ description: 'User not found' }) @UseGuards(JwtAuthGuard) @Get('me') async getUserData(@Request() req): Promise { @@ -51,8 +63,10 @@ export class AuthController { return await this.commandBus.execute(new GetCurrentUserDataCommand(userId)) } - @HttpCode(HttpStatus.OK) + @ApiOperation({ description: 'Sign in using email and password. Must have an account to do so.' }) + @ApiUnauthorizedResponse({ description: 'Invalid credentials' }) @ApiBody({ type: LoginDto }) + @HttpCode(HttpStatus.OK) @UseGuards(LocalAuthGuard) @Post('login') async login( @@ -76,73 +90,101 @@ export class AuthController { return { accessToken: req.user.data.accessToken } } + @ApiOperation({ description: 'Create a new user account' }) + @ApiBadRequestResponse({ description: 'Email already exists' }) @HttpCode(HttpStatus.CREATED) @Post('sign-up') async registration(@Body() registrationData: RegistrationDto): Promise { return await this.commandBus.execute(new CreateUserCommand(registrationData)) } + @ApiOperation({ description: 'Verify user email' }) + @ApiBadRequestResponse({ description: 'Email has already been verified' }) + @ApiNotFoundResponse({ description: 'User not found' }) + @ApiNoContentResponse({ description: 'Email verified successfully' }) @HttpCode(HttpStatus.NO_CONTENT) @Post('verify-email') async confirmRegistration(@Body() body: EmailVerificationDto): Promise { return await this.commandBus.execute(new VerifyEmailCommand(body.code)) } + @ApiOperation({ description: 'Send verification email again' }) + @ApiBadRequestResponse({ description: 'Email has already been verified' }) + @ApiNotFoundResponse({ description: 'User not found' }) + @ApiNoContentResponse({ description: 'Verification email sent successfully' }) @HttpCode(HttpStatus.NO_CONTENT) @Post('resend-verification-email') async resendVerificationEmail(@Body() body: ResendVerificationEmailDto): Promise { return await this.commandBus.execute(new ResendVerificationEmailCommand(body.userId)) } + @ApiOperation({ description: 'Sign current user out' }) + @ApiUnauthorizedResponse({ description: 'Not logged in' }) + @ApiNoContentResponse({ description: 'Logged out successfully' }) @HttpCode(HttpStatus.NO_CONTENT) @UseGuards(JwtAuthGuard) @Post('logout') async logout( - @Cookies('refreshToken') refreshToken: string, + @Cookies('accessToken') accessToken: string, @Res({ passthrough: true }) res: ExpressResponse ): Promise { - if (!refreshToken) throw new UnauthorizedException() - await this.commandBus.execute(new LogoutCommand(refreshToken)) + if (!accessToken) throw new UnauthorizedException() + await this.commandBus.execute(new LogoutCommand(accessToken)) + res.clearCookie('accessToken') res.clearCookie('refreshToken') return null } - @HttpCode(HttpStatus.OK) + @ApiOperation({ description: 'Get new access token using refresh token' }) + @ApiUnauthorizedResponse({ description: 'Invalid or missing refreshToken' }) + @ApiNoContentResponse({ description: 'New tokens generated successfully' }) + @HttpCode(HttpStatus.NO_CONTENT) @UseGuards(JwtRefreshGuard) @Post('refresh-token') async refreshToken( @Request() req, @Response({ passthrough: true }) res: ExpressResponse - ): Promise { + ): Promise { if (!req.cookies?.refreshToken) throw new UnauthorizedException() const userId = req.user.id const newTokens = await this.commandBus.execute(new RefreshTokenCommand(userId)) res.cookie('refreshToken', newTokens.refreshToken, { httpOnly: true, - // secure: true, + sameSite: 'none', path: '/v1/auth/refresh-token', - expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), + secure: true, + }) + res.cookie('accessToken', newTokens.accessToken, { + httpOnly: true, + sameSite: 'none', + secure: true, }) - return { - accessToken: newTokens.accessToken, - } + return null } + @ApiOperation({ description: 'Send password recovery email' }) + @ApiBadRequestResponse({ description: 'Email has already been verified' }) + @ApiNotFoundResponse({ description: 'User not found' }) + @ApiNoContentResponse({ description: 'Password recovery email sent successfully' }) @HttpCode(HttpStatus.NO_CONTENT) @Post('recover-password') async recoverPassword(@Body() body: RecoverPasswordDto): Promise { return await this.commandBus.execute(new SendPasswordRecoveryEmailCommand(body.email)) } + @ApiOperation({ description: 'Reset password' }) + @ApiBadRequestResponse({ description: 'Password is required' }) + @ApiNotFoundResponse({ description: 'Incorrect or expired password reset token' }) + @ApiNoContentResponse({ description: 'Password reset successfully' }) @HttpCode(HttpStatus.NO_CONTENT) @Post('reset-password/:token') async resetPassword( - @Body('password') password: string, + @Body() body: ResetPasswordDto, @Param('token') token: string ): Promise { - return await this.commandBus.execute(new ResetPasswordCommand(token, password)) + return await this.commandBus.execute(new ResetPasswordCommand(token, body.password)) } } diff --git a/src/modules/auth/dto/reset-password.dto.ts b/src/modules/auth/dto/reset-password.dto.ts new file mode 100644 index 0000000..cb3ddc3 --- /dev/null +++ b/src/modules/auth/dto/reset-password.dto.ts @@ -0,0 +1,6 @@ +import { Length } from 'class-validator' + +export class ResetPasswordDto { + @Length(3, 30) + password: string +} diff --git a/src/modules/auth/use-cases/logout-use-case.ts b/src/modules/auth/use-cases/logout-use-case.ts index fbfa7d4..a1ade2c 100644 --- a/src/modules/auth/use-cases/logout-use-case.ts +++ b/src/modules/auth/use-cases/logout-use-case.ts @@ -5,7 +5,7 @@ import jwt from 'jsonwebtoken' import { UsersRepository } from '../../users/infrastructure/users.repository' export class LogoutCommand { - constructor(public readonly refreshToken: string) {} + constructor(public readonly accessToken: string) {} } @CommandHandler(LogoutCommand) @@ -15,7 +15,7 @@ export class LogoutHandler implements ICommandHandler { private readonly logger = new Logger(LogoutHandler.name) async execute(command: LogoutCommand) { - const token = command.refreshToken + const token = command.accessToken const secretKey = process.env.JWT_SECRET_KEY