[offers][chore] Validate edit token before allowing deletion of offer profile

pull/369/head
BryannYeap 2 years ago
parent 0eb4f3fc5b
commit 335413fdcd

@ -137,7 +137,7 @@ export default function OfferProfile() {
if (isEditable) { if (isEditable) {
deleteMutation.mutate({ deleteMutation.mutate({
id: offerProfileId as string, id: offerProfileId as string,
// TODO: token: token as string, token: 'CHANGE THIS PART TO URL PARAM @ ZIQING', // TODO: token: token as string,
}); });
trpcContext.invalidateQueries(['offers.profile.listOne']); trpcContext.invalidateQueries(['offers.profile.listOne']);
router.push('/offers'); router.push('/offers');

@ -126,7 +126,7 @@ function Test() {
}); });
}; };
const profileId = 'cl93tvejz00bei9qinzmjgy75'; // Remember to change this filed after testing deleting const profileId = 'cl93vo4w9009ow35mx7gemrzb'; // Remember to change this filed after testing deleting
const data = trpc.useQuery([ const data = trpc.useQuery([
`offers.profile.listOne`, `offers.profile.listOne`,
{ {
@ -138,7 +138,10 @@ function Test() {
const deleteMutation = trpc.useMutation(['offers.profile.delete']); const deleteMutation = trpc.useMutation(['offers.profile.delete']);
const handleDelete = (id: string) => { const handleDelete = (id: string) => {
deleteMutation.mutate({ id }); deleteMutation.mutate({
id,
token: '6c8d53530163bb765c42bd9f441aa7e345f607c4e1892edbc64e5bbbbe7ee916',
});
}; };
return ( return (

@ -17,7 +17,7 @@ function Test() {
const deleteMutation = trpc.useMutation(['offers.profile.delete']); const deleteMutation = trpc.useMutation(['offers.profile.delete']);
const handleDelete = (id: string) => { const handleDelete = (id: string) => {
deleteMutation.mutate({ id }); deleteMutation.mutate({ id, token: ' dadaadad' });
}; };
return ( return (

@ -2,8 +2,8 @@ import superjson from 'superjson';
import { companiesRouter } from './companies-router'; import { companiesRouter } from './companies-router';
import { createRouter } from './context'; import { createRouter } from './context';
import { offersRouter } from './offers'; import { offersRouter } from './offers/offers';
import { offersProfileRouter } from './offers-profile-router'; import { offersProfileRouter } from './offers/offers-profile-router';
import { protectedExampleRouter } from './protected-example-router'; import { protectedExampleRouter } from './protected-example-router';
import { questionsAnswerCommentRouter } from './questions-answer-comment-router'; import { questionsAnswerCommentRouter } from './questions-answer-comment-router';
import { questionsAnswerRouter } from './questions-answer-router'; import { questionsAnswerRouter } from './questions-answer-router';

@ -1,412 +0,0 @@
import crypto, { randomUUID } from 'crypto';
import { z } from 'zod';
import { Prisma } from '@prisma/client';
import { createRouter } from './context';
import type { offersProfile } from '~/types/offers-profile';
const valuation = z.object({
currency: z.string(),
value: z.number(),
});
// TODO: handle both full time and intern
const offer = z.object({
comments: z.string().optional(),
companyId: z.string(),
job: z.object({
base: valuation.optional(), // Full time
bonus: valuation.optional(), // Full time
internshipCycle: z.string().optional(), // Intern
level: z.string().optional(), // Full time
monthlySalary: valuation.optional(), // Intern
specialization: z.string(),
startYear: z.number().optional(), // Intern
stocks: valuation.optional(), // Full time
title: z.string(),
totalCompensation: valuation.optional(), // Full time
}),
jobType: z.string(),
location: z.string(),
monthYearReceived: z.date(),
negotiationStrategy: z.string().optional(),
});
const experience = z.object({
companyId: z.string().optional(),
durationInMonths: z.number().optional(),
jobType: z.string().optional(),
level: z.string().optional(),
monthlySalary: valuation.optional(),
specialization: z.string().optional(),
title: z.string().optional(),
totalCompensation: valuation.optional(),
});
const education = z.object({
endDate: z.date().optional(),
field: z.string().optional(),
school: z.string().optional(),
startDate: z.date().optional(),
type: z.string().optional(),
});
type WithIsEditable<T> = T & {
isEditable: boolean
}
function computeIsEditable(
profileInput: offersProfile,
editToken?: string
): WithIsEditable<offersProfile> {
return {
...profileInput,
isEditable: profileInput.editToken === editToken,
}
}
function exclude<Key extends keyof WithIsEditable<offersProfile>>(
profile: WithIsEditable<offersProfile>,
...keys: Array<Key>
): Omit<WithIsEditable<offersProfile>, Key> {
for (const key of keys) {
delete profile[key]
}
return profile
}
export const offersProfileRouter = createRouter()
.query('listOne', {
input: z.object({
profileId: z.string(),
token: z.string().optional()
}),
async resolve({ ctx, input }) {
const result = await ctx.prisma.offersProfile.findFirst({
include: {
background: {
include: {
educations: true,
experiences: {
include: {
company: true,
monthlySalary: true,
totalCompensation: true,
},
},
specificYoes: true,
},
},
discussion: {
include: {
replies: true,
replyingTo: true,
},
},
offers: {
include: {
OffersFullTime: {
include: {
baseSalary: true,
bonus: true,
stocks: true,
totalCompensation: true,
},
},
OffersIntern: {
include: {
monthlySalary: true,
},
},
company: true,
},
},
},
where: {
id: input.profileId,
}
});
return result ? exclude(computeIsEditable(result, input.token), 'editToken') : result;
},
})
.mutation('create', {
input: z.object({
background: z.object({
educations: z.array(education),
experiences: z.array(experience),
specificYoes: z.array(
z.object({
domain: z.string(),
yoe: z.number(),
}),
),
totalYoe: z.number().optional(),
}),
offers: z.array(offer),
}),
async resolve({ ctx, input }) {
// TODO: add more
const token = crypto
.createHash('sha256')
.update(Date.now().toString())
.digest('hex');
const profile = await ctx.prisma.offersProfile.create({
data: {
background: {
create: {
educations: {
create: input.background.educations.map((x) => ({
endDate: x.endDate,
field: x.field,
school: x.school,
startDate: x.startDate,
type: x.type,
})),
},
experiences: {
create: input.background.experiences.map((x) => {
if (
x.jobType === 'FULLTIME' &&
x.totalCompensation?.currency !== undefined &&
x.totalCompensation.value !== undefined
) {
if (x.companyId) {
return {
company: {
connect: {
id: x.companyId,
},
},
durationInMonths: x.durationInMonths,
jobType: x.jobType,
level: x.level,
specialization: x.specialization,
title: x.title,
totalCompensation: {
create: {
currency: x.totalCompensation?.currency,
value: x.totalCompensation?.value,
},
},
};
}
return {
durationInMonths: x.durationInMonths,
jobType: x.jobType,
level: x.level,
specialization: x.specialization,
title: x.title,
totalCompensation: {
create: {
currency: x.totalCompensation?.currency,
value: x.totalCompensation?.value,
},
},
};
}
if (
x.jobType === 'INTERN' &&
x.monthlySalary?.currency !== undefined &&
x.monthlySalary.value !== undefined
) {
if (x.companyId) {
return {
company: {
connect: {
id: x.companyId,
},
},
durationInMonths: x.durationInMonths,
jobType: x.jobType,
monthlySalary: {
create: {
currency: x.monthlySalary?.currency,
value: x.monthlySalary?.value,
},
},
specialization: x.specialization,
title: x.title,
};
}
return {
durationInMonths: x.durationInMonths,
jobType: x.jobType,
monthlySalary: {
create: {
currency: x.monthlySalary?.currency,
value: x.monthlySalary?.value,
},
},
specialization: x.specialization,
title: x.title,
};
}
throw Prisma.PrismaClientKnownRequestError;
}),
},
specificYoes: {
create: input.background.specificYoes.map((x) => {
return {
domain: x.domain,
yoe: x.yoe,
};
}),
},
totalYoe: input.background.totalYoe,
},
},
editToken: token,
offers: {
create: input.offers.map((x) => {
if (
x.jobType === 'INTERN' &&
x.job.internshipCycle !== undefined &&
x.job.monthlySalary?.currency !== undefined &&
x.job.monthlySalary.value !== undefined &&
x.job.startYear !== undefined
) {
return {
OffersIntern: {
create: {
internshipCycle: x.job.internshipCycle,
monthlySalary: {
create: {
currency: x.job.monthlySalary?.currency,
value: x.job.monthlySalary?.value,
},
},
specialization: x.job.specialization,
startYear: x.job.startYear,
title: x.job.title,
},
},
comments: x.comments,
company: {
connect: {
id: x.companyId,
},
},
jobType: x.jobType,
location: x.location,
monthYearReceived: x.monthYearReceived,
negotiationStrategy: x.negotiationStrategy,
};
}
if (
x.jobType === 'FULLTIME' &&
x.job.base?.currency !== undefined &&
x.job.base?.value !== undefined &&
x.job.bonus?.currency !== undefined &&
x.job.bonus?.value !== undefined &&
x.job.stocks?.currency !== undefined &&
x.job.stocks?.value !== undefined &&
x.job.totalCompensation?.currency !== undefined &&
x.job.totalCompensation?.value !== undefined &&
x.job.level !== undefined
) {
return {
OffersFullTime: {
create: {
baseSalary: {
create: {
currency: x.job.base?.currency,
value: x.job.base?.value,
},
},
bonus: {
create: {
currency: x.job.bonus?.currency,
value: x.job.bonus?.value,
},
},
level: x.job.level,
specialization: x.job.specialization,
stocks: {
create: {
currency: x.job.stocks?.currency,
value: x.job.stocks?.value,
},
},
title: x.job.title,
totalCompensation: {
create: {
currency: x.job.totalCompensation?.currency,
value: x.job.totalCompensation?.value,
},
},
},
},
comments: x.comments,
company: {
connect: {
id: x.companyId,
},
},
jobType: x.jobType,
location: x.location,
monthYearReceived: x.monthYearReceived,
negotiationStrategy: x.negotiationStrategy,
};
}
// Throw error
throw Prisma.PrismaClientKnownRequestError;
}),
},
profileName: randomUUID().substring(0, 10),
},
include: {
background: {
include: {
educations: true,
experiences: {
include: {
company: true,
monthlySalary: true,
totalCompensation: true,
},
},
specificYoes: true,
},
},
offers: {
include: {
OffersFullTime: {
include: {
baseSalary: true,
bonus: true,
stocks: true,
totalCompensation: true,
},
},
OffersIntern: {
include: {
monthlySalary: true,
},
},
},
},
},
});
// TODO: add analysis to profile object then return
return profile;
},
})
.mutation('delete', {
input: z.object({
id: z.string(),
}),
async resolve({ ctx, input }) {
return await ctx.prisma.offersProfile.delete({
where: {
id: input.id,
},
});
},
});

@ -1,342 +0,0 @@
import assert from 'assert';
import { z } from 'zod';
import { createRouter } from './context';
const yoeCategoryMap: Record<number, string> = {
0: 'Internship',
1: 'Fresh Grad',
2: 'Mid',
3: 'Senior',
};
const getYoeRange = (yoeCategory: number) => {
return yoeCategoryMap[yoeCategory] === 'Fresh Grad'
? { maxYoe: 3, minYoe: 0 }
: yoeCategoryMap[yoeCategory] === 'Mid'
? { maxYoe: 7, minYoe: 4 }
: yoeCategoryMap[yoeCategory] === 'Senior'
? { maxYoe: null, minYoe: 8 }
: null;
};
const ascOrder = '+';
const descOrder = '-';
const sortingKeys = ['monthYearReceived', 'totalCompensation', 'totalYoe'];
const createSortByValidationRegex = () => {
const startsWithPlusOrMinusOnly = '^[+-]{1}';
const sortingKeysRegex = sortingKeys.join('|');
return new RegExp(startsWithPlusOrMinusOnly + '(' + sortingKeysRegex + ')');
};
export const offersRouter = createRouter().query('list', {
input: z.object({
company: z.string().nullish(),
dateEnd: z.date().nullish(),
dateStart: z.date().nullish(),
limit: z.number().nonnegative(),
location: z.string(),
offset: z.number().nonnegative(),
salaryMax: z.number().nullish(),
salaryMin: z.number().nonnegative().nullish(),
sortBy: z.string().regex(createSortByValidationRegex()).nullish(),
title: z.string().nullish(),
yoeCategory: z.number().min(0).max(3),
}),
async resolve({ ctx, input }) {
const yoeRange = getYoeRange(input.yoeCategory);
let data = !yoeRange
? await ctx.prisma.offersOffer.findMany({
// Internship
include: {
OffersFullTime: {
include: {
baseSalary: true,
bonus: true,
stocks: true,
totalCompensation: true,
},
},
OffersIntern: {
include: {
monthlySalary: true,
},
},
company: true,
profile: {
include: {
background: true,
},
},
},
where: {
AND: [
{
location: input.location,
},
{
OffersIntern: {
isNot: null,
},
},
{
OffersFullTime: {
is: null,
},
},
],
},
})
: yoeRange.maxYoe
? await ctx.prisma.offersOffer.findMany({
include: {
OffersFullTime: {
include: {
baseSalary: true,
bonus: true,
stocks: true,
totalCompensation: true,
},
},
OffersIntern: {
include: {
monthlySalary: true,
},
},
company: true,
profile: {
include: {
background: true,
},
},
},
// Junior, Mid
where: {
AND: [
{
location: input.location,
},
{
OffersIntern: {
is: null,
},
},
{
OffersFullTime: {
isNot: null,
},
},
{
profile: {
background: {
totalYoe: {
gte: yoeRange.minYoe,
},
},
},
},
{
profile: {
background: {
totalYoe: {
gte: yoeRange.maxYoe,
},
},
},
},
],
},
})
: await ctx.prisma.offersOffer.findMany({
// Senior
include: {
OffersFullTime: {
include: {
baseSalary: true,
bonus: true,
stocks: true,
totalCompensation: true,
},
},
OffersIntern: {
include: {
monthlySalary: true,
},
},
company: true,
profile: {
include: {
background: true,
},
},
},
where: {
AND: [
{
location: input.location,
},
{
OffersIntern: {
is: null,
},
},
{
OffersFullTime: {
isNot: null,
},
},
{
profile: {
background: {
totalYoe: {
gte: yoeRange.minYoe,
},
},
},
},
],
},
});
// FILTERING
data = data.filter((offer) => {
let validRecord = true;
if (input.company) {
validRecord = validRecord && offer.company.name === input.company;
}
if (input.title) {
validRecord =
validRecord &&
(offer.OffersFullTime?.title === input.title ||
offer.OffersIntern?.title === input.title);
}
if (input.dateStart && input.dateEnd) {
validRecord =
validRecord &&
offer.monthYearReceived.getTime() >= input.dateStart.getTime() &&
offer.monthYearReceived.getTime() <= input.dateEnd.getTime();
}
if (input.salaryMin && input.salaryMax) {
const salary = offer.OffersFullTime?.totalCompensation.value
? offer.OffersFullTime?.totalCompensation.value
: offer.OffersIntern?.monthlySalary.value;
assert(salary);
validRecord =
validRecord && salary >= input.salaryMin && salary <= input.salaryMax;
}
return validRecord;
});
// SORTING
data = data.sort((offer1, offer2) => {
const defaultReturn =
offer2.monthYearReceived.getTime() - offer1.monthYearReceived.getTime();
if (!input.sortBy) {
return defaultReturn;
}
const order = input.sortBy.charAt(0);
const sortingKey = input.sortBy.substring(1);
if (order === ascOrder) {
return (() => {
if (sortingKey === 'monthYearReceived') {
return (
offer1.monthYearReceived.getTime() -
offer2.monthYearReceived.getTime()
);
}
if (sortingKey === 'totalCompensation') {
const salary1 = offer1.OffersFullTime?.totalCompensation.value
? offer1.OffersFullTime?.totalCompensation.value
: offer1.OffersIntern?.monthlySalary.value;
const salary2 = offer2.OffersFullTime?.totalCompensation.value
? offer2.OffersFullTime?.totalCompensation.value
: offer2.OffersIntern?.monthlySalary.value;
if (salary1 && salary2) {
return salary1 - salary2;
}
}
if (sortingKey === 'totalYoe') {
const yoe1 = offer1.profile.background?.totalYoe;
const yoe2 = offer2.profile.background?.totalYoe;
if (yoe1 && yoe2) {
return yoe1 - yoe2;
}
}
return defaultReturn;
})();
}
if (order === descOrder) {
return (() => {
if (sortingKey === 'monthYearReceived') {
return (
offer2.monthYearReceived.getTime() -
offer1.monthYearReceived.getTime()
);
}
if (sortingKey === 'totalCompensation') {
const salary1 = offer1.OffersFullTime?.totalCompensation.value
? offer1.OffersFullTime?.totalCompensation.value
: offer1.OffersIntern?.monthlySalary.value;
const salary2 = offer2.OffersFullTime?.totalCompensation.value
? offer2.OffersFullTime?.totalCompensation.value
: offer2.OffersIntern?.monthlySalary.value;
if (salary1 && salary2) {
return salary2 - salary1;
}
}
if (sortingKey === 'totalYoe') {
const yoe1 = offer1.profile.background?.totalYoe;
const yoe2 = offer2.profile.background?.totalYoe;
if (yoe1 && yoe2) {
return yoe2 - yoe1;
}
}
return defaultReturn;
})();
}
return defaultReturn;
});
const startRecordIndex: number = input.limit * input.offset;
const endRecordIndex: number =
startRecordIndex + input.limit <= data.length
? startRecordIndex + input.limit
: data.length;
const paginatedData = data.slice(startRecordIndex, endRecordIndex);
return {
data: paginatedData,
paging: {
currPage: input.offset,
numOfItemsInPage: paginatedData.length,
numOfPages: Math.ceil(data.length / input.limit),
totalNumberOfOffers: data.length,
},
};
},
});

@ -4,6 +4,8 @@ import { Prisma } from '@prisma/client';
import { createRouter } from '../context'; import { createRouter } from '../context';
import type { offersProfile } from '~/types/offers-profile';
const valuation = z.object({ const valuation = z.object({
currency: z.string(), currency: z.string(),
value: z.number(), value: z.number(),
@ -11,7 +13,7 @@ const valuation = z.object({
// TODO: handle both full time and intern // TODO: handle both full time and intern
const offer = z.object({ const offer = z.object({
comments: z.string(), comments: z.string().optional(),
companyId: z.string(), companyId: z.string(),
job: z.object({ job: z.object({
base: valuation.optional(), // Full time base: valuation.optional(), // Full time
@ -28,7 +30,7 @@ const offer = z.object({
jobType: z.string(), jobType: z.string(),
location: z.string(), location: z.string(),
monthYearReceived: z.date(), monthYearReceived: z.date(),
negotiationStrategy: z.string(), negotiationStrategy: z.string().optional(),
}); });
const experience = z.object({ const experience = z.object({
@ -50,13 +52,38 @@ const education = z.object({
type: z.string().optional(), type: z.string().optional(),
}); });
type WithIsEditable<T> = T & {
isEditable: boolean;
};
function computeIsEditable(
profileInput: offersProfile,
editToken?: string,
): WithIsEditable<offersProfile> {
return {
...profileInput,
isEditable: profileInput.editToken === editToken,
};
}
function exclude<Key extends keyof WithIsEditable<offersProfile>>(
profile: WithIsEditable<offersProfile>,
...keys: Array<Key>
): Omit<WithIsEditable<offersProfile>, Key> {
for (const key of keys) {
delete profile[key];
}
return profile;
}
export const offersProfileRouter = createRouter() export const offersProfileRouter = createRouter()
.query('listOne', { .query('listOne', {
input: z.object({ input: z.object({
profileId: z.string(), profileId: z.string(),
token: z.string().optional(),
}), }),
async resolve({ ctx, input }) { async resolve({ ctx, input }) {
return await ctx.prisma.offersProfile.findFirst({ const result = await ctx.prisma.offersProfile.findFirst({
include: { include: {
background: { background: {
include: { include: {
@ -100,6 +127,10 @@ export const offersProfileRouter = createRouter()
id: input.profileId, id: input.profileId,
}, },
}); });
return result
? exclude(computeIsEditable(result, input.token), 'editToken')
: result;
}, },
}) })
.mutation('create', { .mutation('create', {
@ -144,6 +175,7 @@ export const offersProfileRouter = createRouter()
x.totalCompensation?.currency !== undefined && x.totalCompensation?.currency !== undefined &&
x.totalCompensation.value !== undefined x.totalCompensation.value !== undefined
) { ) {
if (x.companyId) {
return { return {
company: { company: {
connect: { connect: {
@ -163,11 +195,26 @@ export const offersProfileRouter = createRouter()
}, },
}; };
} }
return {
durationInMonths: x.durationInMonths,
jobType: x.jobType,
level: x.level,
specialization: x.specialization,
title: x.title,
totalCompensation: {
create: {
currency: x.totalCompensation?.currency,
value: x.totalCompensation?.value,
},
},
};
}
if ( if (
x.jobType === 'INTERN' && x.jobType === 'INTERN' &&
x.monthlySalary?.currency !== undefined && x.monthlySalary?.currency !== undefined &&
x.monthlySalary.value !== undefined x.monthlySalary.value !== undefined
) { ) {
if (x.companyId) {
return { return {
company: { company: {
connect: { connect: {
@ -186,6 +233,19 @@ export const offersProfileRouter = createRouter()
title: x.title, title: x.title,
}; };
} }
return {
durationInMonths: x.durationInMonths,
jobType: x.jobType,
monthlySalary: {
create: {
currency: x.monthlySalary?.currency,
value: x.monthlySalary?.value,
},
},
specialization: x.specialization,
title: x.title,
};
}
throw Prisma.PrismaClientKnownRequestError; throw Prisma.PrismaClientKnownRequestError;
}), }),
@ -334,7 +394,6 @@ export const offersProfileRouter = createRouter()
}, },
}, },
}); });
// TODO: add analysis to profile object then return // TODO: add analysis to profile object then return
return profile; return profile;
}, },
@ -342,12 +401,23 @@ export const offersProfileRouter = createRouter()
.mutation('delete', { .mutation('delete', {
input: z.object({ input: z.object({
id: z.string(), id: z.string(),
token: z.string(),
}), }),
async resolve({ ctx, input }) { async resolve({ ctx, input }) {
const profileToDelete = await ctx.prisma.offersProfile.findFirst({
where: {
id: input.id,
},
});
const profileEditToken = profileToDelete?.editToken;
if (profileEditToken === input.token) {
return await ctx.prisma.offersProfile.delete({ return await ctx.prisma.offersProfile.delete({
where: { where: {
id: input.id, id: input.id,
}, },
}); });
}
// TODO: Throw 401
}, },
}); });

Loading…
Cancel
Save