Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
20 changes: 17 additions & 3 deletions api/routes/auth/forgotPassword.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { prisma } from "#prisma";
import postmark from "postmark";
import { LogType } from "@prisma/client";

export const post = [
async (req, res) => {
Expand All @@ -10,10 +11,13 @@ export const post = [
const user = await prisma.user.findUnique({
where: { email: email.toLowerCase() },
}); //emails need to be exact so when we create user, the email needs to be stored with lowercase.
if (!user) {
return res.status(401).json({ error: "User does not exist." });


if (!user) {
return res.status(404).json({ error: "User does not exist." });
}


const client = new postmark.ServerClient(process.env.POSTMARK_API_KEY);

const resetToken = jwt.sign({ id: user.id }, process.env.JWT_SECRET, {
Expand Down Expand Up @@ -43,7 +47,14 @@ export const put = [
async (req, res) => {
const { newPassword, token } = req.body;


try {

if (newPassword.length < 8){
return res.status(400).json({error : "Password must be at least 8 characters." });
}


const decodedToken = jwt.verify(token, process.env.JWT_SECRET);

const hashedPassword = await bcrypt.hash(newPassword, 10);
Expand All @@ -60,11 +71,14 @@ export const put = [
where: { id: decodedToken.id },
data: { password: hashedPassword },
});
await prisma.logs.create({
data: { type: LogType.USER_PASSWORD_CHANGE, userId: user.id },
});

return res.status(200).json({ success: true });
} catch (error) {
console.error(error);
return res.status(400).json({ error: "Invalid or expired link." });
return res.status(401).json({ error: "Invalid or expired link." });
}
},
];
13 changes: 7 additions & 6 deletions api/routes/auth/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,16 @@ export const put = [
async (req, res) => {
const { userId, password } = req.body;
try {
const user = await prisma.user.findUnique({ where: { userId } });
const user = await prisma.user.findUnique({ where: { id: userId } });

if (!user || !user.password) {
//if there is no user matching the userId

return res
.status(404)
.status(401)
.json({ error: "Invalid credentials or SSO required" });
}
const hashedPassword = bcrypt.hash(password, 10);
const hashedPassword = await bcrypt.hash(password, 10);

await prisma.user.update({
where: { id: userId },
Expand All @@ -88,11 +88,12 @@ export const put = [
await prisma.logs.create({
data: { type: LogType.USER_PASSWORD_CHANGE, userId: user.id },
});
return res.status(200);
return res.status(200).json({ success: true });
} catch (error) {
console.log("Error", error);

return res.status(500).json({ error: "Internal server error" });
return res
.status(401)
.json({ error: "Invalid credentials or SSO required" });
}
},
];
36 changes: 36 additions & 0 deletions api/routes/auth/tests/__snapshots__/_index.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`/auth/forgotPassword/ > GET > returns a user's shop information to admins 1`] = `
{
"accountTitle": null,
"accountType": "CUSTOMER",
"active": true,
"blacklisted": false,
"createdAt": Any<String>,
"id": Any<String>,
"shopId": Any<String>,
"updatedAt": Any<String>,
"user": {
"admin": true,
"balance": 0,
"createdAt": Any<String>,
"email": "[email protected]",
"firstName": "TestFirstName",
"id": Any<String>,
"isMe": true,
"jobCounts": {
"completedCount": 0,
"excludedCount": 0,
"inProgressCount": 0,
"notStartedCount": 0,
},
"jobs": [],
"lastName": "TestLastName",
"simple": true,
"suspended": false,
"suspensionReason": null,
"updatedAt": Any<String>,
},
"userId": Any<String>,
}
`;
138 changes: 138 additions & 0 deletions api/routes/auth/tests/_forgotPassword.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { describe, expect, it, vi } from "vitest";
import prisma from "#prisma";
import { LogType } from "@prisma/client";
import request from "supertest";
import { app } from "#index";
import { tc } from "#setup";
import postmark from "postmark";
import jwt from "jsonwebtoken";


const sendEmailMock = vi.fn().mockResolvedValue(true);

vi.mock("postmark", () => {
const ServerClient = vi.fn().mockImplementation(() => ({
sendEmail: sendEmailMock,
}));

return {
default: { ServerClient },
};
});


describe("/api/auth/forgotPassword", () => {
describe("POST", () => {
it("sends a user a reset link with postmark", async () => {
const res = await request(app)
.post("/api/auth/forgotPassword")
.send({
email: tc.globalLocalUser.email,
})

expect(res.status).toBe(200);
expect(res.body).toEqual({ success: true });
expect(postmark.ServerClient).toHaveBeenCalled();
expect(sendEmailMock).toHaveBeenCalled();
});


it("returns status 404, user does not exist", async () => { //token is invalid
const res = await request(app)
.post("/api/auth/forgotPassword")
.send({
email: "[email protected]",
});

expect(res.status).toBe(404);
expect(res.body).toEqual({ error: "User does not exist." });

});


});


describe("PUT", () => { //this route is very similar to the put request in login, but we are passing a token
it("resets a users password with postmark", async () => {
const res = await request(app)
.put("/api/auth/forgotPassword")
.send({
token: tc.token,
newPassword: "newPassword",
})

expect(res.status).toBe(200);
expect(res.body).toEqual({ success: true });

const log = await prisma.logs.findFirst({
where: {
type: LogType.USER_PASSWORD_CHANGE,
userId: tc.globalLocalUser.id,
},
});

expect(log).toBeDefined();
expect(log.type).toBe(LogType.USER_PASSWORD_CHANGE);
});



it("returns status 401, invalid token", async () => { //token is invalid
const res = await request(app)
.put("/api/auth/forgotPassword")
.send({
newPassword: "newPassword",
token: "fake-token",
});

expect(res.status).toBe(401);
expect(res.body).toEqual({ error: "Invalid or expired link." });

});



it("returns status 401, user not found", async () => { //token is valid, but the user doesnt exist with that token
const fakeUserId = "00000000-0000-0000-0000-000000000000";

const token = jwt.sign( //we need the jwt token to verify successfully
{ id: fakeUserId },
process.env.JWT_SECRET
);
const res = await request(app)
.put("/api/auth/forgotPassword")
.send({
token: token,
newPassword: "newPassword",
});

expect(res.status).toBe(401);
expect(res.body).toEqual({ error: "Invalid or expired link." });

});


it("returns status 400, Password must be at least 8 characters.", async () => { //passsword is not 8 charachters
const fakeUserId = "00000000-0000-0000-0000-000000000000";

const token = jwt.sign( //we need the jwt token to verify successfully
{ id: fakeUserId },
process.env.JWT_SECRET
);
const res = await request(app)
.put("/api/auth/forgotPassword")
.send({
token: token,
newPassword: "newPass",
});

expect(res.status).toBe(400);
expect(res.body).toEqual({ error: "Password must be at least 8 characters." });

});


});
});

Loading
Loading