Skip to content

Commit

Permalink
feat: 3909/add redirect url prisma (#3938)
Browse files Browse the repository at this point in the history
* feat: get email url from getPublicEmailURL

* fix: handle undefined url case and simplify parsing

* fix: use only baseUrl in welcome and password emails

* fix: fix test
  • Loading branch information
cade-exygy authored Mar 14, 2024
1 parent dfd580c commit d54b9a6
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 17 deletions.
9 changes: 6 additions & 3 deletions api/src/services/email.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Listing } from '../dtos/listings/listing.dto';
import { SendGridService } from './sendgrid.service';
import { ApplicationCreate } from '../dtos/applications/application-create.dto';
import { User } from '../dtos/users/user.dto';
import { getPublicEmailURL } from '../utilities/get-public-email-url';
dayjs.extend(utc);
dayjs.extend(tz);
dayjs.extend(advanced);
Expand Down Expand Up @@ -194,6 +195,7 @@ export class EmailService {
confirmationUrl: string,
) {
const jurisdiction = await this.getJurisdiction(null, jurisdictionName);
const baseUrl = appUrl ? new URL(appUrl).origin : undefined;
await this.loadTranslations(jurisdiction, user.language);
await this.send(
user.email,
Expand All @@ -202,7 +204,7 @@ export class EmailService {
this.template('register-email')({
user: user,
confirmationUrl: confirmationUrl,
appOptions: { appUrl: appUrl },
appOptions: { appUrl: baseUrl },
}),
);
}
Expand Down Expand Up @@ -287,7 +289,8 @@ export class EmailService {
const jurisdiction = await this.getJurisdiction(jurisdictionIds);
void (await this.loadTranslations(jurisdiction, user.language));
const compiledTemplate = this.template('forgot-password');
const resetUrl = `${appUrl}/reset-password?token=${resetToken}`;
const resetUrl = getPublicEmailURL(appUrl, resetToken, '/reset-password');
const baseUrl = appUrl ? new URL(appUrl).origin : undefined;
const emailFromAddress = await this.getEmailToSendFrom(
jurisdictionIds,
jurisdiction,
Expand All @@ -299,7 +302,7 @@ export class EmailService {
this.polyglot.t('forgotPassword.subject'),
compiledTemplate({
resetUrl: resetUrl,
resetOptions: { appUrl: appUrl },
resetOptions: { appUrl: baseUrl },
user: user,
}),
);
Expand Down
3 changes: 2 additions & 1 deletion api/src/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { EmailService } from './email.service';
import { PermissionService } from './permission.service';
import { permissionActions } from '../enums/permissions/permission-actions-enum';
import { buildWhereClause } from '../utilities/build-user-where';
import { getPublicEmailURL } from '../utilities/get-public-email-url';
import { UserRole } from '../dtos/users/user-role.dto';

/*
Expand Down Expand Up @@ -815,7 +816,7 @@ export class UserService {
constructs the url to confirm a public site user
*/
getPublicConfirmationUrl(appUrl: string, confirmationToken: string) {
return `${appUrl}?token=${confirmationToken}`;
return getPublicEmailURL(appUrl, confirmationToken);
}

/*
Expand Down
28 changes: 28 additions & 0 deletions api/src/utilities/get-public-email-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Creates a email URL object from passed url applies redirectUrl and listingId query params if they exist
* If they do not exist, the return value will be the email url with just the necessary token
*/

export const getPublicEmailURL = (
url: string,
token: string,
actionPath?: string,
): string => {
if (!url) {
return;
}
const urlObj = new URL(url);
const redirectUrl = urlObj.searchParams.get('redirectUrl');
const listingId = urlObj.searchParams.get('listingId');

let emailUrl = `${urlObj.origin}${
actionPath ? actionPath : ''
}?token=${token}`;

if (!!redirectUrl && !!listingId) {
emailUrl = emailUrl.concat(
`&redirectUrl=${redirectUrl}&listingId=${listingId}`,
);
}
return emailUrl;
};
28 changes: 28 additions & 0 deletions api/test/unit/services/email.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,34 @@ describe('Testing email service', () => {
);
});

it('testing forgot password with query params', async () => {
await service.forgotPassword(
[
{ name: 'test', id: '1234' },
{ name: 'second', id: '1234' },
{ name: 'third', id: '1234' },
],
user,
'http://localhost:3001?redirectUrl=redirect&listingId=123',
'resetToken',
);
expect(sendMock).toHaveBeenCalled();
expect(sendMock.mock.calls[0][0].to).toEqual(user.email);
expect(sendMock.mock.calls[0][0].subject).toEqual('Forgot your password?');
expect(sendMock.mock.calls[0][0].html).toContain(
'A request to reset your Bloom Housing Portal website password for http://localhost:3001 has recently been made.',
);
expect(sendMock.mock.calls[0][0].html).toContain(
'If you did make this request, please click on the link below to reset your password:',
);
expect(sendMock.mock.calls[0][0].html).toContain(
'<a href="http://localhost:3001/reset-password?token&#x3D;resetToken&amp;redirectUrl&#x3D;redirect&amp;listingId&#x3D;123">Change my password</a>',
);
expect(sendMock.mock.calls[0][0].html).toContain(
'Your password won&#x27;t change until you access the link above and create a new one.',
);
});

it('should send csv data email', async () => {
await service.sendCSV(
[{ name: 'test', id: '1234' }],
Expand Down
45 changes: 33 additions & 12 deletions api/test/unit/services/user.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,15 +311,36 @@ describe('Testing user service', () => {

describe('getPublicConfirmationUrl', () => {
it('should build public confirmation url', () => {
const res = service.getPublicConfirmationUrl('url', 'tokenExample');
expect(res).toEqual('url?token=tokenExample');
const res = service.getPublicConfirmationUrl(
'https://www.example.com',
'tokenExample',
);
expect(res).toEqual('https://www.example.com?token=tokenExample');
});
it('should build public confirmation url with query params', () => {
const res = service.getPublicConfirmationUrl(
'https://www.example.com?redirectUrl=redirect&listingId=123',
'tokenExample',
);
expect(res).toEqual(
'https://www.example.com?token=tokenExample&redirectUrl=redirect&listingId=123',
);
});
it('should return undefined when url is undefined', () => {
const res = service.getPublicConfirmationUrl(undefined, 'tokenExample');
expect(res).toEqual(undefined);
});
});

describe('getPartnersConfirmationUrl', () => {
it('should build partner confirmation url', () => {
const res = service.getPartnersConfirmationUrl('url', 'tokenExample');
expect(res).toEqual('url/users/confirm?token=tokenExample');
const res = service.getPartnersConfirmationUrl(
'https://www.example.com',
'tokenExample',
);
expect(res).toEqual(
'https://www.example.com/users/confirm?token=tokenExample',
);
});
});

Expand Down Expand Up @@ -910,7 +931,7 @@ describe('Testing user service', () => {
id: 'requestingUser id',
userRoles: { isAdmin: true },
} as unknown as User,
'juris name',
'jurisdictionName',
);
expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({
include: {
Expand Down Expand Up @@ -981,7 +1002,7 @@ describe('Testing user service', () => {
id: 'requestingUser id',
userRoles: { isAdmin: true },
} as unknown as User,
'juris name',
'jurisdictionName',
);
expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({
include: {
Expand Down Expand Up @@ -1055,7 +1076,7 @@ describe('Testing user service', () => {
id: 'requestingUser id',
userRoles: { isAdmin: true },
} as unknown as User,
'juris name',
'jurisdictionName',
),
).rejects.toThrowError(`userID ${id}: request missing currentPassword`);
expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({
Expand Down Expand Up @@ -1112,7 +1133,7 @@ describe('Testing user service', () => {
id: 'requestingUser id',
userRoles: { isAdmin: true },
} as unknown as User,
'juris name',
'jurisdictionName',
),
).rejects.toThrowError(
`userID ${id}: incoming password doesn't match stored password`,
Expand Down Expand Up @@ -1161,14 +1182,14 @@ describe('Testing user service', () => {
lastName: 'last name',
jurisdictions: [{ id: jurisId }],
newEmail: '[email protected]',
appUrl: 'www.example.com',
appUrl: 'https://www.example.com',
agreedToTermsOfService: true,
},
{
id: 'requestingUser id',
userRoles: { isAdmin: true },
} as unknown as User,
'juris name',
'jurisdictionName',
);
expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({
include: {
Expand Down Expand Up @@ -1242,7 +1263,7 @@ describe('Testing user service', () => {
id: 'requestingUser id',
userRoles: { isAdmin: true },
} as unknown as User,
'juris name',
'jurisdictionName',
);
expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({
include: {
Expand Down Expand Up @@ -1334,7 +1355,7 @@ describe('Testing user service', () => {
id: 'requestingUser id',
userRoles: { isAdmin: true },
} as unknown as User,
'juris name',
'jurisdictionName',
),
).rejects.toThrowError(`user id: ${id} was requested but not found`);
expect(prisma.userAccounts.findUnique).toHaveBeenCalledWith({
Expand Down
5 changes: 4 additions & 1 deletion backend/core/src/shared/utils/get-public-email-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
*/

export const getPublicEmailURL = (url: string, token: string, actionPath?: string): string => {
if (!url) {
return
}
const urlObj = new URL(url)

const redirectUrl = urlObj.searchParams.get("redirectUrl")
const listingId = urlObj.searchParams.get("listingId")

let emailUrl = `${urlObj.origin}${urlObj.pathname}/${actionPath ? actionPath : ""}?token=${token}`
let emailUrl = `${urlObj.origin}/${actionPath ? actionPath : ""}?token=${token}`

if (!!redirectUrl && !!listingId) {
emailUrl = emailUrl.concat(`&redirectUrl=${redirectUrl}&listingId=${listingId}`)
Expand Down

0 comments on commit d54b9a6

Please sign in to comment.