Skip to content

Commit

Permalink
password resets e2e
Browse files Browse the repository at this point in the history
  • Loading branch information
potts99 committed Dec 3, 2023
1 parent 93a7cb3 commit ab11360
Show file tree
Hide file tree
Showing 7 changed files with 501 additions and 22 deletions.
95 changes: 95 additions & 0 deletions apps/api/src/controllers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import bcrypt from "bcrypt";
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import jwt from "jsonwebtoken";
import { checkToken } from "../lib/jwt";
import { forgotPassword } from "../lib/nodemailer/auth/forgot-password";
import { prisma } from "../prisma";

export function authRoutes(fastify: FastifyInstance) {
Expand Down Expand Up @@ -58,6 +59,100 @@ export function authRoutes(fastify: FastifyInstance) {
}
);

// Forgot password
fastify.post(
"/api/v1/auth/password-reset",
async (request: FastifyRequest, reply: FastifyReply) => {
const { email, link } = request.body as { email: string; link: string };

let user = await prisma.user.findUnique({
where: { email },
});

if (!user) {
reply.code(401).send({
message: "Invalid email",
success: false,
});
}

function generateRandomCode() {
const min = 100000; // Minimum 6-digit number
const max = 999999; // Maximum 6-digit number
return Math.floor(Math.random() * (max - min + 1)) + min;
}

const code = generateRandomCode();

await prisma.passwordResetToken.create({
data: {
userId: user!.id,
code: String(code),
},
});

forgotPassword(email, String(code), link);

reply.send({
success: true,
});
}
);

fastify.post(
"/api/v1/auth/password-reset/code",
async (request: FastifyRequest, reply: FastifyReply) => {
const { code } = request.body as { code: string };

const reset = await prisma.passwordResetToken.findUnique({
where: { code: code },
});

if (!reset) {
reply.code(401).send({
message: "Invalid Code",
success: false,
});
} else {
reply.send({
success: true,
});
}
}
);

fastify.post(
"/api/v1/auth/password-reset/password",
async (request: FastifyRequest, reply: FastifyReply) => {
const { password, code } = request.body as {
password: string;
code: string;
};

const user = await prisma.passwordResetToken.findUnique({
where: { code: code },
});

if (!user) {
reply.code(401).send({
message: "Invalid Code",
success: false,
});
}

await prisma.user.update({
where: { id: user!.userId },
data: {
password: await bcrypt.hash(password, 10),
},
});

reply.send({
success: true,
});
}
);

// User password login route
fastify.post(
"/api/v1/auth/login",
Expand Down
97 changes: 97 additions & 0 deletions apps/api/src/lib/nodemailer/auth/forgot-password.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import nodeMailer from "nodemailer";
import { prisma } from "../../../prisma";

export async function forgotPassword(
email: string,
code: string,
link: string
) {
try {
let mail;

const emails = await prisma.email.findMany();

const resetlink = `${link}/auth/reset-password?code=${code}`;

if (emails.length > 0) {
if (process.env.ENVIRONMENT === "development") {
let testAccount = await nodeMailer.createTestAccount();
mail = nodeMailer.createTransport({
port: 1025,
secure: false, // true for 465, false for other ports
auth: {
user: testAccount.user, // generated ethereal user
pass: testAccount.pass, // generated ethereal password
},
});
} else {
const email = emails[0];
mail = nodeMailer.createTransport({
// @ts-ignore
host: email.host,
port: email.port,
secure: email.secure, // true for 465, false for other ports
auth: {
user: email.user, // generated ethereal user
pass: email.pass, // generated ethereal password
},
});
}

console.log("Sending email to: ", email);

let info = await mail.sendMail({
from: '"No reply 👻" noreply@peppermint.sh', // sender address
to: email, // list of receivers
subject: `Password Reset Request`, // Subject line
text: `Password Reset Code: ${code}, follow this link to reset your password ${link}`, // plain text body
html: `
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
</head>
<div id="" style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0">Ticket Created<div></div>
</div>
<body style="background-color:#ffffff;margin:0 auto;font-family:-apple-system, BlinkMacSystemFont, &#x27;Segoe UI&#x27;, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif">
<table align="center" role="presentation" cellSpacing="0" cellPadding="0" border="0" width="100%" style="max-width:600px;margin:0 auto">
<tr style="width:100%">
<td>
<table style="margin-top:8px" align="center" border="0" cellPadding="0" cellSpacing="0" role="presentation" width="100%">
</table>
<h1 style="color:#1d1c1d;font-size:16px;font-weight:700;margin:10px 0;padding:0;line-height:42px">Password Reset</h1>
<p style="font-size:20px;line-height:28px;margin:4px 0">
<p>Password code: ${code}</p>
<a href=${resetlink}>Reset Here</a>
<p style="font-size:14px;margin:16px 0;color:#000">
Kind regards,
<table align="center" border="0" cellPadding="0" cellSpacing="0" role="presentation" width="100%">
<tbody>
<tr>
<td>
<a target="_blank" style="color:#b7b7b7;text-decoration:underline" href="https://slackhq.com" rel="noopener noreferrer">Our blog</a> | <a target="_blank" style="color:#b7b7b7;text-decoration:underline" href="https://slack.com/legal" rel="noopener noreferrer">Documentation</a> | <a target="_blank" style="color:#b7b7b7;text-decoration:underline" href="https://slack.com/help" rel="noopener noreferrer">Discord</a> </a>
<p style="font-size:12px;line-height:15px;margin:16px 0;color:#b7b7b7;text-align:left">This was an automated message sent by peppermint.sh -> An open source helpdesk solution</p>
<p style="font-size:12px;line-height:15px;margin:16px 0;color:#b7b7b7;text-align:left;margin-bottom:50px">©2022 Peppermint Ticket Management, a Peppermint Labs product.<br />All rights reserved.</p>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</body>
</html>
`,
});

console.log("Message sent: %s", info.messageId);

// Preview only available when sending through an Ethereal account
console.log("Preview URL: %s", nodeMailer.getTestMessageUrl(info));
}
} catch (error) {
console.log(error);
}
}
11 changes: 11 additions & 0 deletions apps/api/src/prisma/migrations/20231203224035_code/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- CreateTable
CREATE TABLE "PasswordResetToken" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"userId" TEXT NOT NULL,

CONSTRAINT "PasswordResetToken_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "PasswordResetToken_code_key" ON "PasswordResetToken"("code");
6 changes: 6 additions & 0 deletions apps/api/src/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ model Session {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model PasswordResetToken {
id String @id @default(cuid())
code String @unique
userId String
}

model User {
id String @id @default(uuid())
createdAt DateTime @default(now())
Expand Down
113 changes: 113 additions & 0 deletions apps/client/pages/auth/forgot-password.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { notifications } from "@mantine/notifications";
import Link from "next/link";
import { useRouter } from "next/router";
import { useState } from "react";

export default function Login({}) {
const router = useRouter();

const [email, setEmail] = useState("");
const [view, setView] = useState("request");

async function postData() {
await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/auth/password-reset`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, link: window.location.origin }),
}
)
.then((res) => res.json())
.then((res) => {
if (res.success) {
notifications.show({
title: "Success",
message: "A password reset email is on its way.",
color: "green",
autoClose: 5000,
});
router.push("/auth/login");
} else {
notifications.show({
title: "Error",
message:
"There was an error with this request, please try again. If this issue persists, please contact support via the discord.",
color: "red",
autoClose: 5000,
});
}
});
}

return (
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<a target="_blank" href="https://peppermint.sh/">
<img
className="mx-auto h-36 w-auto"
src="/login.svg"
alt="peppermint.sh logo"
/>
</a>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Request Password Reset
</h2>
</div>

<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<div className="space-y-4">
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700"
>
Email address
</label>
<div className="mt-1">
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
onChange={(e) => setEmail(e.target.value)}
className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm"
/>
</div>
</div>

<div className="flex items-center justify-between">
<div className="text-sm">
<Link
href="/auth/login"
className="font-medium text-indigo-600 hover:text-indigo-500"
>
Remember your password?
</Link>
</div>
</div>

<div>
<button
type="submit"
onClick={postData}
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
Submit Request
</button>
</div>
</div>
</div>

<div className="mt-8 text-center flex flex-col space-y-2">
<span className="font-bold">Built with 💚 by Peppermint Labs</span>
<a href="https://docs.peppermint.sh/" target="_blank">
Documentation
</a>
</div>
</div>
</div>
);
}
30 changes: 8 additions & 22 deletions apps/client/pages/auth/login.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { notifications } from "@mantine/notifications";
import { setCookie } from "cookies-next";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";

Expand Down Expand Up @@ -96,7 +97,7 @@ export default function Login({}) {
<div className="text-center mr-4">{/* <Loader size={32} /> */}</div>
) : (
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<div className="space-y-6">
<div className="space-y-4">
<div>
<label
htmlFor="email"
Expand Down Expand Up @@ -139,31 +140,16 @@ export default function Login({}) {
</div>
)}

{/* <div className="flex items-center justify-between">
<div className="flex items-center">
<input
id="remember-me"
name="remember-me"
type="checkbox"
className="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
/>
<label
htmlFor="remember-me"
className="ml-2 block text-sm text-gray-900"
>
Remember me
</label>
</div>
<div className="text-sm">
<a
href="#"
<div className="flex items-center justify-between">
<div className="text-sm">
<Link
href="/auth/forgot-password"
className="font-medium text-indigo-600 hover:text-indigo-500"
>
Forgot your password?
</a>
</Link>
</div>
</div> */}
</div>

<div>
<button
Expand Down
Loading

0 comments on commit ab11360

Please sign in to comment.