mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-14 16:51:33 +10:00
feat(resume): ✨ implement resume locking feature
This commit is contained in:
@ -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({
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user