mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-13 16:22:59 +10:00
[Feature] Implement Self-Serve Account Deletion
This commit is contained in:
@ -53,13 +53,12 @@ const Header = () => {
|
|||||||
|
|
||||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
const { mutateAsync: duplicateMutation } = useMutation<Resume, ServerError, DuplicateResumeParams>(duplicateResume);
|
|
||||||
|
|
||||||
const { mutateAsync: deleteMutation } = useMutation<void, ServerError, DeleteResumeParams>(deleteResume);
|
|
||||||
|
|
||||||
const resume = useAppSelector((state) => state.resume.present);
|
const resume = useAppSelector((state) => state.resume.present);
|
||||||
const { left, right } = useAppSelector((state) => state.build.sidebar);
|
const { left, right } = useAppSelector((state) => state.build.sidebar);
|
||||||
|
|
||||||
|
const { mutateAsync: deleteMutation } = useMutation<void, ServerError, DeleteResumeParams>(deleteResume);
|
||||||
|
const { mutateAsync: duplicateMutation } = useMutation<Resume, ServerError, DuplicateResumeParams>(duplicateResume);
|
||||||
|
|
||||||
const name = useMemo(() => get(resume, 'name'), [resume]);
|
const name = useMemo(() => get(resume, 'name'), [resume]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -6,15 +6,17 @@ import { useState } from 'react';
|
|||||||
|
|
||||||
import { logout } from '@/store/auth/authSlice';
|
import { logout } from '@/store/auth/authSlice';
|
||||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||||
|
import { setModalState } from '@/store/modal/modalSlice';
|
||||||
import getGravatarUrl from '@/utils/getGravatarUrl';
|
import getGravatarUrl from '@/utils/getGravatarUrl';
|
||||||
|
|
||||||
import styles from './Avatar.module.scss';
|
import styles from './Avatar.module.scss';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
size?: number;
|
size?: number;
|
||||||
|
interactive?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Avatar: React.FC<Props> = ({ size = 64 }) => {
|
const Avatar: React.FC<Props> = ({ size = 64, interactive = true }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -34,6 +36,11 @@ const Avatar: React.FC<Props> = ({ size = 64 }) => {
|
|||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenProfile = () => {
|
||||||
|
dispatch(setModalState({ modal: 'auth.profile', state: { open: true } }));
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
dispatch(logout());
|
dispatch(logout());
|
||||||
handleClose();
|
handleClose();
|
||||||
@ -43,7 +50,7 @@ const Avatar: React.FC<Props> = ({ size = 64 }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IconButton onClick={handleOpen}>
|
<IconButton onClick={handleOpen} disabled={!interactive}>
|
||||||
<Image
|
<Image
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
@ -54,9 +61,9 @@ const Avatar: React.FC<Props> = ({ size = 64 }) => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<Menu anchorEl={anchorEl} onClose={handleClose} open={Boolean(anchorEl)}>
|
<Menu anchorEl={anchorEl} onClose={handleClose} open={Boolean(anchorEl)}>
|
||||||
<MenuItem>
|
<MenuItem onClick={handleOpenProfile}>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-xs opacity-50">{t<string>('common.avatar.menu.greeting')}</span>
|
<span className="text-xs opacity-50">{t<string>('common.avatar.menu.greeting')},</span>
|
||||||
<p>{user?.name}</p>
|
<p>{user?.name}</p>
|
||||||
</div>
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|||||||
154
client/modals/auth/UserProfileModal.tsx
Normal file
154
client/modals/auth/UserProfileModal.tsx
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import { joiResolver } from '@hookform/resolvers/joi';
|
||||||
|
import { CrisisAlert, ManageAccounts } from '@mui/icons-material';
|
||||||
|
import { Button, Divider, TextField } from '@mui/material';
|
||||||
|
import Joi from 'joi';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { useMutation } from 'react-query';
|
||||||
|
|
||||||
|
import Avatar from '@/components/shared/Avatar';
|
||||||
|
import BaseModal from '@/components/shared/BaseModal';
|
||||||
|
import { deleteAccount, updateProfile, UpdateProfileParams } from '@/services/auth';
|
||||||
|
import { ServerError } from '@/services/axios';
|
||||||
|
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||||
|
import { setModalState } from '@/store/modal/modalSlice';
|
||||||
|
|
||||||
|
type FormData = {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultState: FormData = {
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const schema = Joi.object({
|
||||||
|
name: Joi.string().required(),
|
||||||
|
email: Joi.string()
|
||||||
|
.email({ tlds: { allow: false } })
|
||||||
|
.required(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const UserProfileModal = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const [deleteText, setDeleteText] = useState<string>('');
|
||||||
|
const isDeleteTextValid = useMemo(() => deleteText.toLowerCase() === 'delete', [deleteText]);
|
||||||
|
|
||||||
|
const user = useAppSelector((state) => state.auth.user);
|
||||||
|
const { open: isOpen } = useAppSelector((state) => state.modal['auth.profile']);
|
||||||
|
|
||||||
|
const { mutateAsync: deleteAccountMutation } = useMutation<void, ServerError>(deleteAccount);
|
||||||
|
const { mutateAsync: updateProfileMutation } = useMutation<void, ServerError, UpdateProfileParams>(updateProfile);
|
||||||
|
|
||||||
|
const { reset, getFieldState, control, handleSubmit } = useForm<FormData>({
|
||||||
|
defaultValues: defaultState,
|
||||||
|
resolver: joiResolver(schema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user && !getFieldState('name').isTouched && !getFieldState('email').isTouched) {
|
||||||
|
reset({ name: user.name, email: user.email });
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
dispatch(setModalState({ modal: 'auth.profile', state: { open: false } }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdate = handleSubmit(async (data) => {
|
||||||
|
handleClose();
|
||||||
|
await updateProfileMutation({ name: data.name });
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
await deleteAccountMutation();
|
||||||
|
handleClose();
|
||||||
|
|
||||||
|
router.push('/');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseModal isOpen={isOpen} handleClose={handleClose} heading="Your Account" icon={<ManageAccounts />}>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<form className="grid gap-4 xl:w-2/3">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Avatar interactive={false} />
|
||||||
|
|
||||||
|
<div className="grid flex-1 gap-1.5">
|
||||||
|
<Controller
|
||||||
|
name="name"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
label="Name"
|
||||||
|
error={!!fieldState.error}
|
||||||
|
helperText={fieldState.error?.message}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p className="pl-4 text-[10.5px] opacity-50">
|
||||||
|
You can update your profile picture on{' '}
|
||||||
|
<a href="https://gravatar.com/" target="_blank" rel="noreferrer">
|
||||||
|
Gravatar
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="email"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<TextField
|
||||||
|
disabled
|
||||||
|
label="Email"
|
||||||
|
error={!!fieldState.error}
|
||||||
|
helperText="It is not possible to update your email address at the moment, please create a new account instead."
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button onClick={handleUpdate}>Save Changes</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="my-2">
|
||||||
|
<Divider />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CrisisAlert />
|
||||||
|
<h5 className="font-medium">Danger Zone</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs opacity-75">
|
||||||
|
To delete your account, your data and all your resumes, type{' '}
|
||||||
|
<code className="font-bold">'delete'</code> into the text box below and click on the button. Please
|
||||||
|
note that this is an irreversible action and your data cannot be retrieved again.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex max-w-xs flex-col gap-4">
|
||||||
|
<TextField size="small" value={deleteText} onChange={(e) => setDeleteText(e.target.value)} />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button variant="contained" color="error" disabled={!isDeleteTextValid} onClick={handleDelete}>
|
||||||
|
Delete Your Data
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BaseModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserProfileModal;
|
||||||
@ -8,6 +8,7 @@ import ForgotPasswordModal from './auth/ForgotPasswordModal';
|
|||||||
import LoginModal from './auth/LoginModal';
|
import LoginModal from './auth/LoginModal';
|
||||||
import RegisterModal from './auth/RegisterModal';
|
import RegisterModal from './auth/RegisterModal';
|
||||||
import ResetPasswordModal from './auth/ResetPasswordModal';
|
import ResetPasswordModal from './auth/ResetPasswordModal';
|
||||||
|
import UserProfileModal from './auth/UserProfileModal';
|
||||||
import AwardModal from './builder/sections/AwardModal';
|
import AwardModal from './builder/sections/AwardModal';
|
||||||
import CertificateModal from './builder/sections/CertificateModal';
|
import CertificateModal from './builder/sections/CertificateModal';
|
||||||
import CustomModal from './builder/sections/CustomModal';
|
import CustomModal from './builder/sections/CustomModal';
|
||||||
@ -49,6 +50,7 @@ const ModalWrapper: React.FC = () => {
|
|||||||
<RegisterModal />
|
<RegisterModal />
|
||||||
<ForgotPasswordModal />
|
<ForgotPasswordModal />
|
||||||
<ResetPasswordModal />
|
<ResetPasswordModal />
|
||||||
|
<UserProfileModal />
|
||||||
|
|
||||||
{/* Dashboard */}
|
{/* Dashboard */}
|
||||||
<CreateResumeModal />
|
<CreateResumeModal />
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
# *
|
# *
|
||||||
User-agent: *
|
User-agent: *
|
||||||
Allow: /
|
Allow: /
|
||||||
|
Disallow: /*/*
|
||||||
|
|
||||||
# Host
|
# Host
|
||||||
Host: https://rxresu.me
|
Host: https://rxresu.me
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { User } from '@reactive-resume/schema';
|
|||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
import { setAccessToken, setUser } from '@/store/auth/authSlice';
|
import { logout, setAccessToken, setUser } from '@/store/auth/authSlice';
|
||||||
|
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
import axios from './axios';
|
import axios from './axios';
|
||||||
@ -37,6 +37,10 @@ export type ResetPasswordParams = {
|
|||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpdateProfileParams = {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const login = async (loginParams: LoginParams) => {
|
export const login = async (loginParams: LoginParams) => {
|
||||||
const {
|
const {
|
||||||
data: { user, accessToken },
|
data: { user, accessToken },
|
||||||
@ -75,3 +79,23 @@ export const resetPassword = async (resetPasswordParams: ResetPasswordParams) =>
|
|||||||
|
|
||||||
toast.success('Your password has been changed successfully, please login again.');
|
toast.success('Your password has been changed successfully, please login again.');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updateProfile = async (updateProfileParams: UpdateProfileParams) => {
|
||||||
|
const { data: user } = await axios.patch<User, AxiosResponse<User>, UpdateProfileParams>(
|
||||||
|
'/auth/update-profile',
|
||||||
|
updateProfileParams
|
||||||
|
);
|
||||||
|
|
||||||
|
store.dispatch(setUser(user));
|
||||||
|
|
||||||
|
toast.success('Your profile has been successfully updated.');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteAccount = async () => {
|
||||||
|
await axios.delete('/resume/all');
|
||||||
|
await axios.delete('/auth');
|
||||||
|
|
||||||
|
store.dispatch(logout());
|
||||||
|
|
||||||
|
toast.success('Your account has been deleted, hope to see you again soon.');
|
||||||
|
};
|
||||||
|
|||||||
@ -39,7 +39,7 @@ export const buildSlice = createSlice({
|
|||||||
name: 'build',
|
name: 'build',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setTheme: (state, action: PayloadAction<SetThemePayload>) => {
|
setTheme: (state: BuildState, action: PayloadAction<SetThemePayload>) => {
|
||||||
const { theme } = action.payload;
|
const { theme } = action.payload;
|
||||||
|
|
||||||
state.theme = theme;
|
state.theme = theme;
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export type ModalName =
|
|||||||
| 'auth.register'
|
| 'auth.register'
|
||||||
| 'auth.forgot'
|
| 'auth.forgot'
|
||||||
| 'auth.reset'
|
| 'auth.reset'
|
||||||
|
| 'auth.profile'
|
||||||
| 'dashboard.create-resume'
|
| 'dashboard.create-resume'
|
||||||
| 'dashboard.import-external'
|
| 'dashboard.import-external'
|
||||||
| 'dashboard.rename-resume'
|
| 'dashboard.rename-resume'
|
||||||
@ -24,6 +25,7 @@ const initialState: Record<ModalName, ModalState> = {
|
|||||||
'auth.register': { open: false },
|
'auth.register': { open: false },
|
||||||
'auth.forgot': { open: false },
|
'auth.forgot': { open: false },
|
||||||
'auth.reset': { open: false },
|
'auth.reset': { open: false },
|
||||||
|
'auth.profile': { open: false },
|
||||||
'dashboard.create-resume': { open: false },
|
'dashboard.create-resume': { open: false },
|
||||||
'dashboard.import-external': { open: false },
|
'dashboard.import-external': { open: false },
|
||||||
'dashboard.rename-resume': { open: false },
|
'dashboard.rename-resume': { open: false },
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Body, Controller, Delete, Get, HttpCode, Post, UseGuards } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, HttpCode, Patch, Post, UseGuards } from '@nestjs/common';
|
||||||
|
|
||||||
import { User } from '@/decorators/user.decorator';
|
import { User } from '@/decorators/user.decorator';
|
||||||
import { User as UserEntity } from '@/users/entities/user.entity';
|
import { User as UserEntity } from '@/users/entities/user.entity';
|
||||||
@ -7,6 +7,7 @@ import { AuthService } from './auth.service';
|
|||||||
import { ForgotPasswordDto } from './dto/forgot-password.dto';
|
import { ForgotPasswordDto } from './dto/forgot-password.dto';
|
||||||
import { RegisterDto } from './dto/register.dto';
|
import { RegisterDto } from './dto/register.dto';
|
||||||
import { ResetPasswordDto } from './dto/reset-password.dto';
|
import { ResetPasswordDto } from './dto/reset-password.dto';
|
||||||
|
import { UpdateProfileDto } from './dto/update-profile.dto';
|
||||||
import { JwtAuthGuard } from './guards/jwt.guard';
|
import { JwtAuthGuard } from './guards/jwt.guard';
|
||||||
import { LocalAuthGuard } from './guards/local.guard';
|
import { LocalAuthGuard } from './guards/local.guard';
|
||||||
|
|
||||||
@ -57,6 +58,14 @@ export class AuthController {
|
|||||||
return this.authService.resetPassword(resetPasswordDto);
|
return this.authService.resetPassword(resetPasswordDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@HttpCode(200)
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Patch('update-profile')
|
||||||
|
updateProfile(@User('id') userId: number, @Body() updateProfileDto: UpdateProfileDto) {
|
||||||
|
console.log({ userId, updateProfileDto });
|
||||||
|
return this.authService.updateProfile(userId, updateProfileDto);
|
||||||
|
}
|
||||||
|
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Delete()
|
@Delete()
|
||||||
|
|||||||
@ -6,12 +6,14 @@ import { compareSync, hashSync } from 'bcryptjs';
|
|||||||
import { OAuth2Client } from 'google-auth-library';
|
import { OAuth2Client } from 'google-auth-library';
|
||||||
|
|
||||||
import { PostgresErrorCode } from '@/database/errorCodes.enum';
|
import { PostgresErrorCode } from '@/database/errorCodes.enum';
|
||||||
|
import { ResumeService } from '@/resume/resume.service';
|
||||||
import { CreateGoogleUserDto } from '@/users/dto/create-google-user.dto';
|
import { CreateGoogleUserDto } from '@/users/dto/create-google-user.dto';
|
||||||
import { User } from '@/users/entities/user.entity';
|
import { User } from '@/users/entities/user.entity';
|
||||||
import { UsersService } from '@/users/users.service';
|
import { UsersService } from '@/users/users.service';
|
||||||
|
|
||||||
import { RegisterDto } from './dto/register.dto';
|
import { RegisterDto } from './dto/register.dto';
|
||||||
import { ResetPasswordDto } from './dto/reset-password.dto';
|
import { ResetPasswordDto } from './dto/reset-password.dto';
|
||||||
|
import { UpdateProfileDto } from './dto/update-profile.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
@ -68,6 +70,10 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateProfile(id: number, newData: UpdateProfileDto) {
|
||||||
|
return this.usersService.update(id, { name: newData.name });
|
||||||
|
}
|
||||||
|
|
||||||
forgotPassword(email: string) {
|
forgotPassword(email: string) {
|
||||||
return this.usersService.generateResetToken(email);
|
return this.usersService.generateResetToken(email);
|
||||||
}
|
}
|
||||||
|
|||||||
7
server/src/auth/dto/update-profile.dto.ts
Normal file
7
server/src/auth/dto/update-profile.dto.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { IsNotEmpty, IsString, MinLength } from 'class-validator';
|
||||||
|
|
||||||
|
export class UpdateProfileDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
@ -7,7 +7,7 @@ const sampleData: Partial<Resume> = {
|
|||||||
phone: '+1 800 1200 3820',
|
phone: '+1 800 1200 3820',
|
||||||
birthdate: '1995-08-06',
|
birthdate: '1995-08-06',
|
||||||
photo: {
|
photo: {
|
||||||
url: `https://i.imgur.com/40gTnCx.jpg`,
|
url: `https://i.imgur.com/O7iT9ke.jpg`,
|
||||||
filters: {
|
filters: {
|
||||||
size: 128,
|
size: 128,
|
||||||
shape: 'rounded-square',
|
shape: 'rounded-square',
|
||||||
|
|||||||
@ -71,6 +71,12 @@ export class ResumeController {
|
|||||||
return this.resumeService.update(+id, updateResumeDto, userId);
|
return this.resumeService.update(+id, updateResumeDto, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Delete('/all')
|
||||||
|
removeAllByUser(@User('id') userId: number) {
|
||||||
|
return this.resumeService.removeAllByUser(userId);
|
||||||
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
remove(@Param('id') id: string, @User('id') userId: number) {
|
remove(@Param('id') id: string, @User('id') userId: number) {
|
||||||
|
|||||||
@ -176,8 +176,12 @@ export class ResumeService {
|
|||||||
return this.resumeRepository.save<Resume>(updatedResume);
|
return this.resumeRepository.save<Resume>(updatedResume);
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(id: number, userId: number) {
|
remove(id: number, userId: number) {
|
||||||
await this.resumeRepository.delete({ id, user: { id: userId } });
|
return this.resumeRepository.delete({ id, user: { id: userId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAllByUser(userId: number) {
|
||||||
|
return this.resumeRepository.delete({ user: { id: userId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
async duplicate(id: number, userId: number) {
|
async duplicate(id: number, userId: number) {
|
||||||
|
|||||||
Reference in New Issue
Block a user