Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 52 additions & 13 deletions __tests__/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ import {
import { Company } from '../src/entity/Company';
import { UserCompany } from '../src/entity/UserCompany';
import { UserExperience } from '../src/entity/user/experiences/UserExperience';
import { UserExperienceWork } from '../src/entity/user/experiences/UserExperienceWork';
import { UserExperienceType } from '../src/entity/user/experiences/types';
import { SourceReport } from '../src/entity/sources/SourceReport';
import { SourceMemberRoles } from '../src/roles';
Expand Down Expand Up @@ -2524,20 +2525,35 @@ describe('user company', () => {
code: '654321',
userId: loggedUser,
});
return testQueryError(
client,
{ query: QUERY, variables: { email: 'u1@com3.com' } },
(errors) => {
expect(errors[0].extensions!.code).toEqual(
'GRAPHQL_VALIDATION_FAILED',
);
expect(errors[0].message).toEqual(
'This email has already been verified',
);
const res = await client.query(QUERY, {
variables: { email: 'u1@com3.com' },
});
expect(res.errors).toBeFalsy();
expect(res.data.addUserCompany._).toBe(true);
expect(sendEmail).not.toHaveBeenCalled();
});

expect(sendEmail).not.toHaveBeenCalled();
},
);
it('should re-assert work experience verification when email is already verified', async () => {
loggedUser = '1';
// beforeEach seeds a verified UserCompany (companyId 1) and an unverified
// Work experience for company 1. Re-submitting the already-verified email
// should idempotently mark that experience verified instead of erroring.
const before = await con
.getRepository(UserExperienceWork)
.findOneByOrFail({ userId: loggedUser, companyId: '1' });
expect(before.verified).toBe(false);

const res = await client.query(QUERY, {
variables: { email: 'u1@com1.com' },
});
expect(res.errors).toBeFalsy();
expect(res.data.addUserCompany._).toBe(true);
expect(sendEmail).not.toHaveBeenCalled();

const after = await con
.getRepository(UserExperienceWork)
.findOneByOrFail({ userId: loggedUser, companyId: '1' });
expect(after.verified).toBe(true);
});
});

Expand Down Expand Up @@ -2698,6 +2714,29 @@ describe('user company', () => {
});
expect(row.verified).toBeTruthy();
});

it('should mark the matching work experience as verified', async () => {
loggedUser = '1';
// Seed an unverified Work experience for company 3 (email u1@com3.com).
await con.getRepository(UserExperienceWork).save({
userId: loggedUser,
companyId: '3',
title: 'Staff Engineer',
type: UserExperienceType.Work,
startedAt: new Date('2024-01-01'),
endedAt: null,
});

const res = await client.query(QUERY, {
variables: { email: 'u1@com3.com', code: '123' },
});
expect(res.errors).toBeFalsy();

const experience = await con
.getRepository(UserExperienceWork)
.findOneByOrFail({ userId: loggedUser, companyId: '3' });
expect(experience.verified).toBe(true);
});
});
});

Expand Down
45 changes: 44 additions & 1 deletion src/schema/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ import {
} from '../entity/UserIntegration';
import { Company } from '../entity/Company';
import { UserCompany } from '../entity/UserCompany';
import { UserExperienceWork } from '../entity/user/experiences/UserExperienceWork';
import { generateVerifyCode } from '../ids';
import {
addClaimableItemsToUser,
Expand Down Expand Up @@ -2156,6 +2157,28 @@ const getUserCompanies = async (
);
};

/**
* Ensure the user's Work experiences for a given company are marked verified,
* mirroring a verified UserCompany record. This closes the gap where verifying
* a company email (a `verified` flip that does not change `companyId`) never
* propagated to `UserExperienceWork.verified` via CDC, leaving the profile UI
* showing the experience as unverified while the backend already considers the
* email verified.
*/
const syncVerifiedUserWorkExperiences = async (
con: DataSource,
userId: string,
companyId: string | null,
): Promise<void> => {
if (!companyId) {
return;
}

await con
.getRepository(UserExperienceWork)
.update({ userId, companyId, verified: false }, { verified: true });
};

interface ClearImagePreset {
con: ConnectionManager;
preset: UploadPreset;
Expand Down Expand Up @@ -3893,7 +3916,17 @@ export const resolvers: IResolvers<unknown, BaseContext> = {
}

if (existingUserCompanyEmail.verified) {
throw new ValidationError('This email has already been verified');
// The email is already verified for this user. Rather than throwing
// (which traps the user in a re-verify loop when the profile UI still
// shows the experience as unverified), re-assert the work-experience
// verification and return success idempotently.
await syncVerifiedUserWorkExperiences(
ctx.con,
ctx.userId,
existingUserCompanyEmail.companyId,
);

return { _: true };
}

const updatedRecord = { ...existingUserCompanyEmail, code };
Expand Down Expand Up @@ -3959,6 +3992,16 @@ export const resolvers: IResolvers<unknown, BaseContext> = {

await ctx.con.getRepository(UserCompany).save(updatedRecord);

// Propagate the verification to the matching Work experiences. The CDC
// worker only reacts to `companyId` changes, so a plain `verified` flip
// (companyId unchanged) would otherwise never mark the experience as
// verified.
await syncVerifiedUserWorkExperiences(
ctx.con,
ctx.userId,
updatedRecord.companyId,
);

return await graphorm.queryOneOrFail<GQLUserCompany>(
ctx,
info,
Expand Down
Loading