Skip to content

Commit

Permalink
new API to request password reset
Browse files Browse the repository at this point in the history
  • Loading branch information
javtran committed Aug 21, 2024
1 parent 35ace79 commit bd79dcb
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 0 deletions.
14 changes: 14 additions & 0 deletions server/emails/passwordReset/html.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<%- include('../_header.html.ejs') %>

<p>Dear <%= firstName %>,</p>

<p>We have received a request to reset your password for your SF Lifeline account.</p>

<%- include('../_button.html.ejs', { label: 'Reset Password', url }) %>

<p>This link will expire in 30 minutes. If you did not request a password reset, please ignore this email or contact
our support team if you have any concerns.</p>

<p>Sincerely,<br />The SF Life Line Team</p>

<%- include('../_footer.html.ejs') %>
1 change: 1 addition & 0 deletions server/emails/passwordReset/subject.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Reset your password for your SF Lifeline account
15 changes: 15 additions & 0 deletions server/emails/passwordReset/text.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Dear <%= firstName %>,

We have received a request to reset your password for your SF Lifeline account.

Click on the following link to reset your:

<%= url %>

This link will expire in 30 minutes. If you did not request a password reset, please ignore this email or contact
our support team if you have any concerns.

Sincerely,
The SF Life Line Team

<%- include('../_footer.text.ejs') %>
19 changes: 19 additions & 0 deletions server/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,25 @@ class User extends Base {
});
}

generatePasswordResetToken() {
this.passwordResetToken = crypto.randomUUID();
}

async sendPasswordResetEmail() {
const { firstName } = this;
const url = `${process.env.BASE_URL}/verify/${this.passwordResetToken}`;
return mailer.send({
message: {
to: this.fullNameAndEmail,
},
template: 'passwordReset',
locals: {
firstName,
url,
},
});
}

async setPassword(password) {
this.hashedPassword = await bcrypt.hash(password, 10);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ CREATE TABLE "User" (
"email" CITEXT NOT NULL,
"emailVerificationToken" TEXT,
"emailVerifiedAt" TIMESTAMP(3),
"passwordResetToken" TEXT,
"passwordResetExpires" TIMESTAMP(3),
"role" "Role" NOT NULL,
"hashedPassword" TEXT NOT NULL,
"licenseNumber" TEXT,
Expand Down Expand Up @@ -167,6 +169,9 @@ CREATE TABLE "_HospitalToPhysician" (
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");

-- CreateIndex
CREATE UNIQUE INDEX "User_passwordResetToken_key" ON "User"("passwordResetToken");

-- CreateIndex
CREATE UNIQUE INDEX "User_licenseNumber_key" ON "User"("licenseNumber");

Expand Down
2 changes: 2 additions & 0 deletions server/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ model User {
email String @unique @db.Citext
emailVerificationToken String?
emailVerifiedAt DateTime?
passwordResetToken String? @unique
passwordResetExpires DateTime?
role Role
hashedPassword String
licenseNumber String? @unique
Expand Down
58 changes: 58 additions & 0 deletions server/routes/api/v1/users/request-password-reset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { StatusCodes } from 'http-status-codes';
import User from '../../../../models/user.js';

export default async function (fastify, _opts) {
fastify.post(
'/request-password-reset',
{
schema: {
body: {
type: 'object',
required: ['email'],
properties: {
email: { type: 'string' },
},
},
response: {
[StatusCodes.OK]: {
type: 'object',
properties: {
message: { type: 'string' },
},
},
[StatusCodes.NOT_FOUND]: {
type: 'object',
properties: {
message: { type: 'string' },
},
},
},
},
},
async (request, reply) => {
const { email } = request.body;

const data = await fastify.prisma.user.findUnique({
where: { email },
});

if (!data) {
return reply.notFound('User not Found');
}

const user = new User(data);

user.generatePasswordResetToken();
await user.sendPasswordResetEmail();

await fastify.prisma.user.update({
where: { email },
data: {
passwordResetToken: user.passwordResetToken,
},
});

reply.send('Yes');
},
);
}
44 changes: 44 additions & 0 deletions server/test/routes/api/v1/users.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -695,4 +695,48 @@ describe('/api/v1/users', () => {
assert.deepStrictEqual(user.disabledAt, null);
});
});

describe('POST /request-password-reset', () => {
it('should allow user to request a password reset', async (t) => {
const app = await build(t);
await t.loadFixtures();

const res = await app
.inject()
.post('/api/v1/users/request-password-reset')
.payload({
email: '[email protected]',
});

assert.deepStrictEqual(res.statusCode, StatusCodes.OK);

const sentMails = nodemailerMock.mock.getSentMail();

assert.notDeepStrictEqual(sentMails.length, 0);
assert.deepStrictEqual(
sentMails[1].to,
'Volunteer User <[email protected]>',
);
assert.deepStrictEqual(
sentMails[1].subject,
'Reset your password for your SF Lifeline account',
);
});

it('should return not found if email does not exist', async (t) => {
const app = await build(t);
await t.loadFixtures();

const res = await app
.inject()
.post('/api/v1/users/request-password-reset')
.payload({
email: '[email protected]',
});

assert.deepStrictEqual(res.statusCode, StatusCodes.NOT_FOUND);
const { message } = JSON.parse(res.body);
assert.deepStrictEqual(message, 'User not Found');
});
});
});

0 comments on commit bd79dcb

Please sign in to comment.