Files
Reactive-Resume/apps/server/src/auth/auth.controller.ts
2023-11-05 12:31:42 +01:00

321 lines
9.2 KiB
TypeScript

import {
BadRequestException,
Body,
Controller,
Get,
HttpCode,
InternalServerErrorException,
Patch,
Post,
Query,
Res,
UseGuards,
} from "@nestjs/common";
import { ApiTags } from "@nestjs/swagger";
import {
authResponseSchema,
backupCodesSchema,
ForgotPasswordDto,
MessageDto,
messageSchema,
RegisterDto,
ResetPasswordDto,
TwoFactorBackupDto,
TwoFactorDto,
UpdatePasswordDto,
UserDto,
userSchema,
UserWithSecrets,
} from "@reactive-resume/dto";
import type { Response } from "express";
import { ZodSerializerDto } from "nestjs-zod";
import { ErrorMessage } from "../constants/error-message";
import { User } from "../user/decorators/user.decorator";
import { UtilsService } from "../utils/utils.service";
import { AuthService } from "./auth.service";
import { GitHubGuard } from "./guards/github.guard";
import { GoogleGuard } from "./guards/google.guard";
import { JwtGuard } from "./guards/jwt.guard";
import { LocalGuard } from "./guards/local.guard";
import { RefreshGuard } from "./guards/refresh.guard";
import { TwoFactorGuard } from "./guards/two-factor.guard";
import { getCookieOptions } from "./utils/cookie";
import { payloadSchema } from "./utils/payload";
@ApiTags("Authentication")
@Controller("auth")
export class AuthController {
constructor(
private readonly authService: AuthService,
private readonly utils: UtilsService,
) {}
private async exchangeToken(id: string, email: string, isTwoFactorAuth: boolean = false) {
try {
const payload = payloadSchema.parse({ id, isTwoFactorAuth });
const accessToken = this.authService.generateToken("access", payload);
const refreshToken = this.authService.generateToken("refresh", payload);
// Set Refresh Token in Database
await this.authService.setRefreshToken(email, refreshToken);
return { accessToken, refreshToken };
} catch (error) {
throw new InternalServerErrorException(error, ErrorMessage.SomethingWentWrong);
}
}
private async handleAuthenticationResponse(
user: UserWithSecrets,
response: Response,
isTwoFactorAuth: boolean = false,
redirect: boolean = false,
) {
let status = "authenticated";
const redirectUrl = new URL(`${this.utils.getUrl()}/auth/callback`);
const { accessToken, refreshToken } = await this.exchangeToken(
user.id,
user.email,
isTwoFactorAuth,
);
response.cookie("Authentication", accessToken, getCookieOptions("access"));
response.cookie("Refresh", refreshToken, getCookieOptions("refresh"));
if (user.twoFactorEnabled && !isTwoFactorAuth) status = "2fa_required";
const responseData = authResponseSchema.parse({ status, user });
redirectUrl.searchParams.set("status", status);
if (redirect) response.redirect(redirectUrl.toString());
else response.status(200).send(responseData);
}
@Post("register")
async register(@Body() registerDto: RegisterDto, @Res({ passthrough: true }) response: Response) {
const user = await this.authService.register(registerDto);
return this.handleAuthenticationResponse(user, response);
}
@Post("login")
@UseGuards(LocalGuard)
async login(@User() user: UserWithSecrets, @Res({ passthrough: true }) response: Response) {
return this.handleAuthenticationResponse(user, response);
}
@ApiTags("OAuth", "GitHub")
@Get("github")
@UseGuards(GitHubGuard)
githubLogin() {
return;
}
@ApiTags("OAuth", "GitHub")
@Get("github/callback")
@UseGuards(GitHubGuard)
async githubCallback(
@User() user: UserWithSecrets,
@Res({ passthrough: true }) response: Response,
) {
return this.handleAuthenticationResponse(user, response, false, true);
}
@ApiTags("OAuth", "Google")
@Get("google")
@UseGuards(GoogleGuard)
googleLogin() {
return;
}
@ApiTags("OAuth", "Google")
@Get("google/callback")
@UseGuards(GoogleGuard)
async googleCallback(
@User() user: UserWithSecrets,
@Res({ passthrough: true }) response: Response,
) {
return this.handleAuthenticationResponse(user, response, false, true);
}
@Post("refresh")
@UseGuards(RefreshGuard)
async refresh(@User() user: UserWithSecrets, @Res({ passthrough: true }) response: Response) {
return this.handleAuthenticationResponse(user, response, true);
}
@Patch("password")
@UseGuards(TwoFactorGuard)
@ZodSerializerDto(MessageDto)
async updatePassword(@User("email") email: string, @Body() { password }: UpdatePasswordDto) {
await this.authService.updatePassword(email, password);
return { message: "Your password has been successfully updated." };
}
@Post("logout")
@UseGuards(TwoFactorGuard)
async logout(@User() user: UserWithSecrets, @Res({ passthrough: true }) response: Response) {
await this.authService.setRefreshToken(user.email, null);
response.clearCookie("Authentication");
response.clearCookie("Refresh");
const data = messageSchema.parse({ message: "You have been logged out, tschüss!" });
response.status(200).send(data);
}
// Two Factor Authentication Flows
@ApiTags("Two-Factor Auth")
@Post("2fa/setup")
@UseGuards(JwtGuard)
@ZodSerializerDto(MessageDto)
async setup2FASecret(@User("email") email: string) {
return this.authService.setup2FASecret(email);
}
@ApiTags("Two-Factor Auth")
@HttpCode(200)
@Post("2fa/enable")
@UseGuards(JwtGuard)
async enable2FA(
@User("id") id: string,
@User("email") email: string,
@Body() { code }: TwoFactorDto,
@Res({ passthrough: true }) response: Response,
) {
const { backupCodes } = await this.authService.enable2FA(email, code);
const { accessToken, refreshToken } = await this.exchangeToken(id, email, true);
response.cookie("Authentication", accessToken, getCookieOptions("access"));
response.cookie("Refresh", refreshToken, getCookieOptions("refresh"));
const data = backupCodesSchema.parse({ backupCodes });
response.status(200).send(data);
}
@ApiTags("Two-Factor Auth")
@HttpCode(200)
@Post("2fa/disable")
@UseGuards(TwoFactorGuard)
@ZodSerializerDto(MessageDto)
async disable2FA(@User("email") email: string) {
await this.authService.disable2FA(email);
return { message: "Two-factor authentication has been successfully disabled on your account." };
}
@ApiTags("Two-Factor Auth")
@HttpCode(200)
@Post("2fa/verify")
@UseGuards(JwtGuard)
@ZodSerializerDto(UserDto)
async verify2FACode(
@User() user: UserWithSecrets,
@Body() { code }: TwoFactorDto,
@Res({ passthrough: true }) response: Response,
) {
await this.authService.verify2FACode(user.email, code);
const { accessToken, refreshToken } = await this.exchangeToken(user.id, user.email, true);
response.cookie("Authentication", accessToken, getCookieOptions("access"));
response.cookie("Refresh", refreshToken, getCookieOptions("refresh"));
response.status(200).send(userSchema.parse(user));
}
@ApiTags("Two-Factor Auth")
@HttpCode(200)
@Post("2fa/backup")
@UseGuards(JwtGuard)
@ZodSerializerDto(UserDto)
async useBackup2FACode(
@User("id") id: string,
@User("email") email: string,
@Body() { code }: TwoFactorBackupDto,
@Res({ passthrough: true }) response: Response,
) {
const user = await this.authService.useBackup2FACode(email, code);
return this.handleAuthenticationResponse(user, response, true);
}
// Password Recovery Flows
@ApiTags("Password Reset")
@HttpCode(200)
@Post("forgot-password")
async forgotPassword(@Body() { email }: ForgotPasswordDto) {
try {
await this.authService.forgotPassword(email);
} catch (error) {
// pass
}
return {
message:
"A password reset link should have been sent to your inbox, if an account existed with the email you provided.",
};
}
@ApiTags("Password Reset")
@HttpCode(200)
@Post("reset-password")
@ZodSerializerDto(MessageDto)
async resetPassword(@Body() { token, password }: ResetPasswordDto) {
try {
await this.authService.resetPassword(token, password);
return { message: "Your password has been successfully reset." };
} catch (error) {
throw new BadRequestException(ErrorMessage.InvalidResetToken);
}
}
// Email Verification Flows
@ApiTags("Email Verification")
@Post("verify-email")
@UseGuards(TwoFactorGuard)
@ZodSerializerDto(MessageDto)
async verifyEmail(
@User("id") id: string,
@User("emailVerified") emailVerified: boolean,
@Query("token") token: string,
) {
if (!token) throw new BadRequestException(ErrorMessage.InvalidVerificationToken);
if (emailVerified) {
throw new BadRequestException(ErrorMessage.EmailAlreadyVerified);
}
await this.authService.verifyEmail(id, token);
return { message: "Your email has been successfully verified." };
}
@ApiTags("Email Verification")
@Post("verify-email/resend")
@UseGuards(TwoFactorGuard)
@ZodSerializerDto(MessageDto)
async resendVerificationEmail(
@User("email") email: string,
@User("emailVerified") emailVerified: boolean,
) {
if (emailVerified) {
throw new BadRequestException(ErrorMessage.EmailAlreadyVerified);
}
await this.authService.sendVerificationEmail(email);
return {
message: "You should have received a new email with a link to verify your email address.",
};
}
}