diff --git a/api/src/services/user.service.ts b/api/src/services/user.service.ts index 030625e189..da2fcb93e3 100644 --- a/api/src/services/user.service.ts +++ b/api/src/services/user.service.ts @@ -401,6 +401,17 @@ export class UserService { UserViews.full, ); + const isPartnerPortalUser = + storedUser.userRoles?.isAdmin || + storedUser.userRoles?.isJurisdictionalAdmin || + storedUser.userRoles?.isPartner; + const isUserSiteMatch = + (isPartnerPortalUser && dto.appUrl === process.env.PARTNERS_PORTAL_URL) || + (!isPartnerPortalUser && + dto.appUrl === storedUser.jurisdictions?.[0]?.publicUrl); + // user on wrong site, return neutral message and don't send email + if (!isUserSiteMatch) return { success: true }; + const payload = { id: storedUser.id, exp: Number.parseInt(dayjs().add(1, 'hour').format('X')), diff --git a/api/test/integration/user.e2e-spec.ts b/api/test/integration/user.e2e-spec.ts index 39c7be979c..a60d54e2b6 100644 --- a/api/test/integration/user.e2e-spec.ts +++ b/api/test/integration/user.e2e-spec.ts @@ -533,9 +533,13 @@ describe('User Controller Tests', () => { expect(userPostResend.hitConfirmationUrl).toBeNull(); }); - it('should set resetToken when forgot-password is called', async () => { + it('should set resetToken when forgot-password is called by public user on the public site', async () => { + const juris = await prisma.jurisdictions.create({ + data: jurisdictionFactory(), + }); + const userA = await prisma.userAccounts.create({ - data: await userFactory(), + data: await userFactory({ jurisdictionIds: [juris.id] }), }); const mockforgotPassword = jest.spyOn(testEmailService, 'forgotPassword'); @@ -544,6 +548,7 @@ describe('User Controller Tests', () => { .set({ passkey: process.env.API_PASS_KEY || '' }) .send({ email: userA.email, + appUrl: juris.publicUrl, } as EmailAndAppUrl) .expect(200); @@ -559,6 +564,105 @@ describe('User Controller Tests', () => { expect(mockforgotPassword.mock.calls.length).toBe(1); }); + it('should not set resetToken when forgot-password is called by public user on the partners site', async () => { + const juris = await prisma.jurisdictions.create({ + data: jurisdictionFactory(), + }); + + const userA = await prisma.userAccounts.create({ + data: await userFactory({ jurisdictionIds: [juris.id] }), + }); + + const mockforgotPassword = jest.spyOn(testEmailService, 'forgotPassword'); + const res = await request(app.getHttpServer()) + .put(`/user/forgot-password/`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .send({ + email: userA.email, + appUrl: process.env.PARTNERS_PORTAL_URL, + } as EmailAndAppUrl) + .expect(200); + + expect(res.body.success).toBe(true); + + const userPostResend = await prisma.userAccounts.findUnique({ + where: { + id: userA.id, + }, + }); + + expect(userPostResend.resetToken).toBeNull(); + expect(mockforgotPassword.mock.calls.length).toBe(0); + }); + + it('should set resetToken when forgot-password is called by partner user on the partners site', async () => { + const juris = await prisma.jurisdictions.create({ + data: jurisdictionFactory(), + }); + + const userA = await prisma.userAccounts.create({ + data: await userFactory({ + roles: { isAdmin: true }, + jurisdictionIds: [juris.id], + }), + }); + + const mockforgotPassword = jest.spyOn(testEmailService, 'forgotPassword'); + const res = await request(app.getHttpServer()) + .put(`/user/forgot-password/`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .send({ + email: userA.email, + appUrl: process.env.PARTNERS_PORTAL_URL, + } as EmailAndAppUrl) + .expect(200); + + expect(res.body.success).toBe(true); + + const userPostResend = await prisma.userAccounts.findUnique({ + where: { + id: userA.id, + }, + }); + + expect(userPostResend.resetToken).not.toBeNull(); + expect(mockforgotPassword.mock.calls.length).toBe(1); + }); + + it('should not set resetToken when forgot-password is called by partner user on the public site', async () => { + const juris = await prisma.jurisdictions.create({ + data: jurisdictionFactory(), + }); + + const userA = await prisma.userAccounts.create({ + data: await userFactory({ + roles: { isAdmin: true }, + jurisdictionIds: [juris.id], + }), + }); + + const mockforgotPassword = jest.spyOn(testEmailService, 'forgotPassword'); + const res = await request(app.getHttpServer()) + .put(`/user/forgot-password/`) + .set({ passkey: process.env.API_PASS_KEY || '' }) + .send({ + email: userA.email, + appUrl: juris.publicUrl, + } as EmailAndAppUrl) + .expect(200); + + expect(res.body.success).toBe(true); + + const userPostResend = await prisma.userAccounts.findUnique({ + where: { + id: userA.id, + }, + }); + + expect(userPostResend.resetToken).toBeNull(); + expect(mockforgotPassword.mock.calls.length).toBe(0); + }); + it('should create public user', async () => { const juris = await prisma.jurisdictions.create({ data: jurisdictionFactory(), diff --git a/api/test/unit/services/user.service.spec.ts b/api/test/unit/services/user.service.spec.ts index f19199236e..4a6a905249 100644 --- a/api/test/unit/services/user.service.spec.ts +++ b/api/test/unit/services/user.service.spec.ts @@ -664,12 +664,13 @@ describe('Testing user service', () => { }); describe('forgotPassword', () => { - it('should set resetToken', async () => { + it('should set resetToken when public user on public site', async () => { const id = randomUUID(); const email = 'email@example.com'; prisma.userAccounts.findUnique = jest.fn().mockResolvedValue({ id, + jurisdictions: [{ publicUrl: 'http://localhost:3000' }], }); prisma.userAccounts.update = jest.fn().mockResolvedValue({ id, @@ -699,6 +700,109 @@ describe('Testing user service', () => { }); }); + it('should not set resetToken when public user on partner site', async () => { + const id = randomUUID(); + const email = 'email@example.com'; + + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue({ + id, + jurisdictions: [{ publicUrl: 'http://localhost:3000' }], + }); + prisma.userAccounts.update = jest.fn().mockResolvedValue({ + id, + resetToken: 'example reset token', + }); + emailService.forgotPassword = jest.fn(); + + await service.forgotPassword({ + email, + appUrl: process.env.PARTNERS_PORTAL_URL, + }); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + listings: true, + userRoles: true, + }, + where: { + email, + id: undefined, + }, + }); + expect(prisma.userAccounts.update).not.toHaveBeenCalled(); + }); + + it('should set resetToken when partner user on partner site', async () => { + const id = randomUUID(); + const email = 'email@example.com'; + + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue({ + id, + userRoles: { isAdmin: true }, + }); + prisma.userAccounts.update = jest.fn().mockResolvedValue({ + id, + resetToken: 'example reset token', + }); + emailService.forgotPassword = jest.fn(); + + await service.forgotPassword({ + email, + appUrl: process.env.PARTNERS_PORTAL_URL, + }); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + listings: true, + userRoles: true, + }, + where: { + email, + id: undefined, + }, + }); + expect(prisma.userAccounts.update).toHaveBeenCalledWith({ + data: { + resetToken: expect.anything(), + }, + where: { + id, + }, + }); + }); + + it('should not set resetToken when partner user on public site', async () => { + const id = randomUUID(); + const email = 'email@example.com'; + + prisma.userAccounts.findUnique = jest.fn().mockResolvedValue({ + id, + userRoles: { isAdmin: true }, + }); + prisma.userAccounts.update = jest.fn().mockResolvedValue({ + id, + resetToken: 'example reset token', + }); + emailService.forgotPassword = jest.fn(); + + await service.forgotPassword({ + email, + appUrl: 'http://localhost:3000', + }); + expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({ + include: { + jurisdictions: true, + listings: true, + userRoles: true, + }, + where: { + email, + id: undefined, + }, + }); + expect(prisma.userAccounts.update).not.toHaveBeenCalled(); + }); + it('should error when trying to set resetToken on nonexistent user', async () => { const email = 'email@example.com';