feat(resume): implement resume locking feature

This commit is contained in:
Amruth Pillai
2023-11-06 13:57:12 +01:00
parent 9a0402d525
commit 015e284318
23 changed files with 288 additions and 83 deletions

View File

@ -2,7 +2,7 @@ import { HttpException, Module } from "@nestjs/common";
import { APP_INTERCEPTOR, APP_PIPE } from "@nestjs/core";
import { ServeStaticModule } from "@nestjs/serve-static";
import { RavenInterceptor, RavenModule } from "nest-raven";
import { ZodSerializerInterceptor, ZodValidationPipe } from "nestjs-zod";
import { ZodValidationPipe } from "nestjs-zod";
import { join } from "path";
import { AuthModule } from "./auth/auth.module";
@ -44,10 +44,6 @@ import { UtilsModule } from "./utils/utils.module";
provide: APP_PIPE,
useClass: ZodValidationPipe,
},
{
provide: APP_INTERCEPTOR,
useClass: ZodSerializerInterceptor,
},
{
provide: APP_INTERCEPTOR,
useValue: new RavenInterceptor({

View File

@ -16,19 +16,16 @@ 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";
@ -151,7 +148,6 @@ export class AuthController {
@Patch("password")
@UseGuards(TwoFactorGuard)
@ZodSerializerDto(MessageDto)
async updatePassword(@User("email") email: string, @Body() { password }: UpdatePasswordDto) {
await this.authService.updatePassword(email, password);
@ -174,7 +170,6 @@ export class AuthController {
@ApiTags("Two-Factor Auth")
@Post("2fa/setup")
@UseGuards(JwtGuard)
@ZodSerializerDto(MessageDto)
async setup2FASecret(@User("email") email: string) {
return this.authService.setup2FASecret(email);
}
@ -204,7 +199,6 @@ export class AuthController {
@HttpCode(200)
@Post("2fa/disable")
@UseGuards(TwoFactorGuard)
@ZodSerializerDto(MessageDto)
async disable2FA(@User("email") email: string) {
await this.authService.disable2FA(email);
@ -215,7 +209,6 @@ export class AuthController {
@HttpCode(200)
@Post("2fa/verify")
@UseGuards(JwtGuard)
@ZodSerializerDto(UserDto)
async verify2FACode(
@User() user: UserWithSecrets,
@Body() { code }: TwoFactorDto,
@ -235,7 +228,6 @@ export class AuthController {
@HttpCode(200)
@Post("2fa/backup")
@UseGuards(JwtGuard)
@ZodSerializerDto(UserDto)
async useBackup2FACode(
@User("id") id: string,
@User("email") email: string,
@ -267,7 +259,6 @@ export class AuthController {
@ApiTags("Password Reset")
@HttpCode(200)
@Post("reset-password")
@ZodSerializerDto(MessageDto)
async resetPassword(@Body() { token, password }: ResetPasswordDto) {
try {
await this.authService.resetPassword(token, password);
@ -282,7 +273,6 @@ export class AuthController {
@ApiTags("Email Verification")
@Post("verify-email")
@UseGuards(TwoFactorGuard)
@ZodSerializerDto(MessageDto)
async verifyEmail(
@User("id") id: string,
@User("emailVerified") emailVerified: boolean,
@ -302,7 +292,6 @@ export class AuthController {
@ApiTags("Email Verification")
@Post("verify-email/resend")
@UseGuards(TwoFactorGuard)
@ZodSerializerDto(MessageDto)
async resendVerificationEmail(
@User("email") email: string,
@User("emailVerified") emailVerified: boolean,

View File

@ -21,6 +21,8 @@ export const ErrorMessage = {
ResumeSlugAlreadyExists:
"A resume with this slug already exists, please pick a different unique identifier.",
ResumeNotFound: "It looks like the resume you're looking for doesn't exist.",
ResumeLocked:
"The resume you want to update is locked, please unlock if you wish to make any changes to it.",
ResumePrinterError:
"Something went wrong while printing your resume. Please try again later or raise an issue on GitHub.",
ResumePreviewError:

View File

@ -16,16 +16,8 @@ import {
import { ApiTags } from "@nestjs/swagger";
import { User as UserEntity } from "@prisma/client";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
import {
CreateResumeDto,
ImportResumeDto,
ResumeDto,
StatisticsDto,
UpdateResumeDto,
UrlDto,
} from "@reactive-resume/dto";
import { CreateResumeDto, ImportResumeDto, ResumeDto, UpdateResumeDto } from "@reactive-resume/dto";
import { resumeDataSchema } from "@reactive-resume/schema";
import { ZodSerializerDto } from "nestjs-zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import { User } from "@/server/user/decorators/user.decorator";
@ -91,7 +83,6 @@ export class ResumeController {
@Get(":id/statistics")
@UseGuards(TwoFactorGuard)
@ZodSerializerDto(StatisticsDto)
findOneStatistics(@User("id") userId: string, @Param("id") id: string) {
return this.resumeService.findOneStatistics(userId, id);
}
@ -111,15 +102,20 @@ export class ResumeController {
return this.resumeService.update(user.id, id, updateResumeDto);
}
@Patch(":id/lock")
@UseGuards(TwoFactorGuard)
lock(@User() user: UserEntity, @Param("id") id: string, @Body("set") set: boolean = true) {
return this.resumeService.lock(user.id, id, set);
}
@Delete(":id")
@UseGuards(TwoFactorGuard)
remove(@User() user: UserEntity, @Param("id") id: string) {
return this.resumeService.remove(user.id, id);
async remove(@User() user: UserEntity, @Param("id") id: string) {
await this.resumeService.remove(user.id, id);
}
@Get("/print/:id")
@UseGuards(OptionalGuard, ResumeGuard)
@ZodSerializerDto(UrlDto)
async printResume(@Resume() resume: ResumeDto) {
try {
const url = await this.resumeService.printResume(resume);
@ -133,7 +129,6 @@ export class ResumeController {
@Get("/print/:id/preview")
@UseGuards(TwoFactorGuard, ResumeGuard)
@ZodSerializerDto(UrlDto)
async printPreview(@Resume() resume: ResumeDto) {
try {
const url = await this.resumeService.printPreview(resume);

View File

@ -1,5 +1,5 @@
import { CACHE_MANAGER } from "@nestjs/cache-manager";
import { Inject, Injectable, Logger } from "@nestjs/common";
import { BadRequestException, Inject, Injectable, Logger } from "@nestjs/common";
import { Prisma } from "@prisma/client";
import { CreateResumeDto, ImportResumeDto, ResumeDto, UpdateResumeDto } from "@reactive-resume/dto";
import { defaultResumeData, ResumeData } from "@reactive-resume/schema";
@ -13,6 +13,7 @@ import { PrismaService } from "nestjs-prisma";
import { PrinterService } from "@/server/printer/printer.service";
import { ErrorMessage } from "../constants/error-message";
import { StorageService } from "../storage/storage.service";
import { UtilsService } from "../utils/utils.service";
@ -129,22 +130,44 @@ export class ResumeService {
}
async update(userId: string, id: string, updateResumeDto: UpdateResumeDto) {
await Promise.all([
this.cache.set(`user:${userId}:resume:${id}`, updateResumeDto),
this.cache.del(`user:${userId}:resumes`),
this.cache.del(`user:${userId}:storage:resumes:${id}`),
this.cache.del(`user:${userId}:storage:previews:${id}`),
]);
try {
const resume = await this.prisma.resume.update({
data: {
title: updateResumeDto.title,
slug: updateResumeDto.slug,
visibility: updateResumeDto.visibility,
data: updateResumeDto.data as unknown as Prisma.JsonObject,
},
where: { userId_id: { userId, id }, locked: false },
});
return this.prisma.resume.update({
data: {
title: updateResumeDto.title,
slug: updateResumeDto.slug,
visibility: updateResumeDto.visibility,
data: updateResumeDto.data as unknown as Prisma.JsonObject,
},
await Promise.all([
this.cache.set(`user:${userId}:resume:${id}`, resume),
this.cache.del(`user:${userId}:resumes`),
this.cache.del(`user:${userId}:storage:resumes:${id}`),
this.cache.del(`user:${userId}:storage:previews:${id}`),
]);
return resume;
} catch (error) {
if (error.code === "P2025") {
throw new BadRequestException(ErrorMessage.ResumeLocked);
}
}
}
async lock(userId: string, id: string, set: boolean) {
const resume = await this.prisma.resume.update({
data: { locked: set },
where: { userId_id: { userId, id } },
});
await Promise.all([
this.cache.set(`user:${userId}:resume:${id}`, resume),
this.cache.del(`user:${userId}:resumes`),
]);
return resume;
}
async remove(userId: string, id: string) {
@ -156,9 +179,10 @@ export class ResumeService {
// Remove files in storage, and their cached keys
this.storageService.deleteObject(userId, "resumes", id),
this.storageService.deleteObject(userId, "previews", id),
]);
return this.prisma.resume.delete({ where: { userId_id: { userId, id } } });
// Remove resume from database
this.prisma.resume.delete({ where: { userId_id: { userId, id } } }),
]);
}
async printResume(resume: ResumeDto) {

View File

@ -1,8 +1,7 @@
import { Body, Controller, Delete, Get, Patch, Res, UseGuards } from "@nestjs/common";
import { ApiTags } from "@nestjs/swagger";
import { MessageDto, UpdateUserDto, UserDto } from "@reactive-resume/dto";
import { UpdateUserDto, UserDto } from "@reactive-resume/dto";
import type { Response } from "express";
import { ZodSerializerDto } from "nestjs-zod";
import { AuthService } from "../auth/auth.service";
import { TwoFactorGuard } from "../auth/guards/two-factor.guard";
@ -19,14 +18,12 @@ export class UserController {
@Get("me")
@UseGuards(TwoFactorGuard)
@ZodSerializerDto(UserDto)
fetch(@User() user: UserDto) {
return user;
}
@Patch("me")
@UseGuards(TwoFactorGuard)
@ZodSerializerDto(UserDto)
async update(@User("email") email: string, @Body() updateUserDto: UpdateUserDto) {
// If user is updating their email, send a verification email
if (updateUserDto.email && updateUserDto.email !== email) {
@ -50,7 +47,6 @@ export class UserController {
@Delete("me")
@UseGuards(TwoFactorGuard)
@ZodSerializerDto(MessageDto)
async delete(@User("id") id: string, @Res({ passthrough: true }) response: Response) {
await this.userService.deleteOneById(id);