Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
13 changes: 10 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 @@ -60,11 +64,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>,
}
`;
117 changes: 117 additions & 0 deletions api/routes/auth/tests/_forgotPassword.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
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." });

});


});
});

166 changes: 166 additions & 0 deletions api/routes/auth/tests/_login.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { describe, expect, it } from "vitest";
import prisma from "#prisma";
import { LogType } from "@prisma/client";
import request from "supertest";
import { app } from "#index";
import { tc } from "#setup";

describe("/api/auth/login", () => {
describe("POST", () => {
it("enables a user to login", async () => { //test standard login
const res = await request(app)
.post("/api/auth/login")
.send({
email: tc.globalLocalUser.email,
password: "TestPassword",
});

expect(res.status).toBe(200);
expect(res.body).toHaveProperty("token");
const log = await prisma.logs.findFirst({
where: {
type: LogType.USER_LOGIN_LOCAL,
userId: tc.globalLocalUser.id,
},
});

expect(log).toBeDefined();
});
it("returns status 401, no password field exists in the db", async () => { //tests the fact that user doesnt have a password field i.e (SSO)
const res = await request(app)
.post("/api/auth/login")
.send({
email: tc.globalLocalUser.email, //we can just use the globalUser because this has no password attribute and should throw an error.
password: "password",
});


const log = await prisma.logs.findFirst({
where: {
type: LogType.USER_LOGIN_FAILURE,
},
orderBy: { createdAt: "desc" },
});

expect(log).toBeDefined();
expect(log.type).toBe(LogType.USER_LOGIN_FAILURE);
expect(res.status).toBe(401);
expect(res.body).toEqual({ error: "Invalid credentials or SSO required" });

});
it("returns status 401, no user exists in the db", async () => { //tests a random email or person not in the db
const res = await request(app)
.post("/api/auth/login")
.send({
email: "[email protected]", //user a random email
password: "password",
});


const log = await prisma.logs.findFirst({
where: {
type: LogType.USER_LOGIN_FAILURE,
},
orderBy: { createdAt: "desc" },
});

expect(log).toBeDefined();
expect(log.type).toBe(LogType.USER_LOGIN_FAILURE);
expect(res.status).toBe(401);
expect(res.body).toEqual({ error: "Invalid credentials or SSO required" });

});

it("returns status 401, passwords dont match", async () => { //tests a password doesnt match what is in the db
const res = await request(app)
.post("/api/auth/login")
.send({
email: tc.globalLocalUser.email,
password: "passwordNotMatch",
});


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

expect(log).toBeDefined();
expect(log.type).toBe(LogType.USER_LOGIN_FAILURE);
expect(res.status).toBe(401);
expect(res.body).toEqual({ error: "Invalid credentials or SSO required" });

});

});



describe("PUT", () => {
it("resets a users password", async () => { //tests the fact that there is a password in the globalLocalUser
const res = await request(app)
.put("/api/auth/login")
.send({
userId: tc.globalLocalUser.id,
password: "newPassword",
});
expect(res.status).toBe(200);
expect(res.body).toEqual({ success: true });

//get the user
const User = await prisma.user.findUnique({
where: { id: tc.globalLocalUser.id },
});

expect(User.password).toBeDefined();

//check the logs
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, no user exists", async () => { //tests the fact that we dont have the user in our db
const res = await request(app)
.put("/api/auth/login")
.send({
userId: "nonExistentUser",
password: "newPassword",
});

expect(res.status).toBe(401);
expect(res.body).toEqual({ error: "Invalid credentials or SSO required" });

});




it("returns status 401, no password field exisits in the db", async () => { //tests the fact that user doesnt have a password field i.e (SSO)
const res = await request(app)
.put("/api/auth/login")
.send({
userId: tc.globalUser.id, //we can just use the globalUser because this has no password attribute and should throw an error.
password: "newPassword",
});

expect(res.status).toBe(401);
expect(res.body).toEqual({ error: "Invalid credentials or SSO required" });

});

});




});
Loading
Loading