Allow user to reset password
This commit is contained in:
parent
ed92451126
commit
ab1298e2ca
11 changed files with 672 additions and 44 deletions
19
lib/emails/buildMessageTemplate.ts
Normal file
19
lib/emails/buildMessageTemplate.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import Handlebars from "handlebars";
|
||||
|
||||
export const buildMessageTemplate = ({
|
||||
messageTemplate,
|
||||
subjectTemplate,
|
||||
vars,
|
||||
}): { subject: string; message: string } => {
|
||||
const buildMessage = Handlebars.compile(messageTemplate);
|
||||
const message = buildMessage(vars);
|
||||
|
||||
const buildSubject = Handlebars.compile(subjectTemplate);
|
||||
const subject = buildSubject(vars);
|
||||
return {
|
||||
subject,
|
||||
message,
|
||||
};
|
||||
};
|
||||
|
||||
export default buildMessageTemplate;
|
30
lib/emails/sendMail.ts
Normal file
30
lib/emails/sendMail.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { serverConfig } from "../serverConfig";
|
||||
import nodemailer, { SentMessageInfo } from "nodemailer";
|
||||
|
||||
const sendEmail = ({ to, subject, text, html = null }): Promise<string | SentMessageInfo> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const { transport, from } = serverConfig;
|
||||
|
||||
if (!to || !subject || (!text && !html)) {
|
||||
return reject("Missing required elements to send email.");
|
||||
}
|
||||
|
||||
nodemailer.createTransport(transport).sendMail(
|
||||
{
|
||||
from: `Calendso ${from}`,
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
},
|
||||
(error, info) => {
|
||||
if (error) {
|
||||
console.error("SEND_INVITATION_NOTIFICATION_ERROR", to, error);
|
||||
return reject(error.message);
|
||||
}
|
||||
return resolve(info);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export default sendEmail;
|
|
@ -23,6 +23,7 @@
|
|||
"googleapis": "^67.1.1",
|
||||
"handlebars": "^4.7.7",
|
||||
"ics": "^2.27.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"next": "^10.2.0",
|
||||
"next-auth": "^3.13.2",
|
||||
|
|
77
pages/api/auth/forgot-password.ts
Normal file
77
pages/api/auth/forgot-password.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import prisma from "../../../lib/prisma";
|
||||
import dayjs from "dayjs";
|
||||
import { User, ResetPasswordRequest } from "@prisma/client";
|
||||
import sendEmail from "../../../lib/emails/sendMail";
|
||||
import { buildForgotPasswordMessage } from "../../../src/forgot-password/messaging/forgot-password";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(400).json({ message: "" });
|
||||
}
|
||||
|
||||
try {
|
||||
const rawEmail = req.body?.email;
|
||||
|
||||
const maybeUser: User = await prisma.user.findUnique({
|
||||
where: {
|
||||
email: rawEmail,
|
||||
},
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!maybeUser) {
|
||||
return res.status(400).json({ message: "Couldn't find an account for this email" });
|
||||
}
|
||||
|
||||
const now = dayjs().toDate();
|
||||
const maybePreviousRequest = await prisma.resetPasswordRequest.findMany({
|
||||
where: {
|
||||
email: rawEmail,
|
||||
expires: {
|
||||
gt: now,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let passwordRequest: ResetPasswordRequest;
|
||||
|
||||
if (maybePreviousRequest && maybePreviousRequest?.length >= 1) {
|
||||
passwordRequest = maybePreviousRequest[0];
|
||||
} else {
|
||||
const expiry = dayjs().tz(maybeUser.timeZone).add(6, "hours").toDate();
|
||||
const createdResetPasswordRequest = await prisma.resetPasswordRequest.create({
|
||||
data: {
|
||||
email: rawEmail,
|
||||
expires: expiry,
|
||||
},
|
||||
});
|
||||
passwordRequest = createdResetPasswordRequest;
|
||||
}
|
||||
|
||||
const passwordResetLink = `${process.env.BASE_URL}/auth/reset-password/${passwordRequest.id}`;
|
||||
const { subject, message } = buildForgotPasswordMessage({
|
||||
user: {
|
||||
name: maybeUser.name,
|
||||
},
|
||||
link: passwordResetLink,
|
||||
});
|
||||
|
||||
await sendEmail({
|
||||
to: rawEmail,
|
||||
subject: subject,
|
||||
text: message,
|
||||
});
|
||||
|
||||
return res.status(201).json({ message: "Reset Requested", data: passwordRequest });
|
||||
} catch (reason) {
|
||||
console.error(reason);
|
||||
return res.status(500).json({ message: "Unable to create password reset request" });
|
||||
}
|
||||
}
|
60
pages/api/auth/reset-password.ts
Normal file
60
pages/api/auth/reset-password.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import prisma from "../../../lib/prisma";
|
||||
import dayjs from "dayjs";
|
||||
import { User, ResetPasswordRequest } from "@prisma/client";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
import { hashPassword } from "../../../lib/auth";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(400).json({ message: "" });
|
||||
}
|
||||
|
||||
try {
|
||||
const rawPassword = req.body?.password;
|
||||
const rawRequestId = req.body?.requestId;
|
||||
|
||||
if (!rawPassword || !rawRequestId) {
|
||||
return res.status(400).json({ message: "Couldn't find an account for this email" });
|
||||
}
|
||||
|
||||
const maybeRequest: ResetPasswordRequest = await prisma.resetPasswordRequest.findUnique({
|
||||
where: {
|
||||
id: rawRequestId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!maybeRequest) {
|
||||
return res.status(400).json({ message: "Couldn't find an account for this email" });
|
||||
}
|
||||
|
||||
const maybeUser: User = await prisma.user.findUnique({
|
||||
where: {
|
||||
email: maybeRequest.email,
|
||||
},
|
||||
});
|
||||
|
||||
if (!maybeUser) {
|
||||
return res.status(400).json({ message: "Couldn't find an account for this email" });
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(rawPassword);
|
||||
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: maybeUser.id,
|
||||
},
|
||||
data: {
|
||||
password: hashedPassword,
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(201).json({ message: "Password reset." });
|
||||
} catch (reason) {
|
||||
console.error(reason);
|
||||
return res.status(500).json({ message: "Unable to create password reset request" });
|
||||
}
|
||||
}
|
231
pages/auth/forgot-password/[id].tsx
Normal file
231
pages/auth/forgot-password/[id].tsx
Normal file
|
@ -0,0 +1,231 @@
|
|||
import { getCsrfToken } from "next-auth/client";
|
||||
import prisma from "../../../lib/prisma";
|
||||
|
||||
import Head from "next/head";
|
||||
import React from "react";
|
||||
import debounce from "lodash.debounce";
|
||||
import dayjs from "dayjs";
|
||||
import { ResetPasswordRequest } from "@prisma/client";
|
||||
import { useMemo } from "react";
|
||||
import Link from "next/link";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
resetPasswordRequest: ResetPasswordRequest;
|
||||
csrfToken: string;
|
||||
};
|
||||
|
||||
export default function Page({ resetPasswordRequest, csrfToken }: Props) {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [error, setError] = React.useState(null);
|
||||
const [success, setSuccess] = React.useState(false);
|
||||
|
||||
const [password, setPassword] = React.useState("");
|
||||
const handleChange = (e) => {
|
||||
setPassword(e.target.value);
|
||||
};
|
||||
|
||||
const submitChangePassword = async ({ password, requestId }) => {
|
||||
try {
|
||||
const res = await fetch("/api/auth/reset-password", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ requestId: requestId, password: password }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
setError(json);
|
||||
} else {
|
||||
setSuccess(true);
|
||||
}
|
||||
|
||||
return json;
|
||||
} catch (reason) {
|
||||
setError({ message: "An unexpected error occurred. Try again." });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedChangePassword = debounce(submitChangePassword, 250);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!password) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
await debouncedChangePassword({ password, requestId: resetPasswordRequest.id });
|
||||
};
|
||||
|
||||
const Success = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Success</h2>
|
||||
</div>
|
||||
<p>Your password has been reset. You can now login with your newly created password.</p>
|
||||
<Link href="/auth/login">
|
||||
<button
|
||||
type="button"
|
||||
className="w-full flex justify-center py-2 px-4 text-sm font-medium text-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Login
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Expired = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Whoops</h2>
|
||||
<h2 className="text-center text-3xl font-extrabold text-gray-900">That Request is Expired.</h2>
|
||||
</div>
|
||||
<p>
|
||||
That request is expired. You can back and enter the email associated with your account and we will
|
||||
you another link to reset your password.
|
||||
</p>
|
||||
<Link href="/auth/forgot-password">
|
||||
<button
|
||||
type="button"
|
||||
className="w-full flex justify-center py-2 px-4 text-sm font-medium text-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Try Again
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const isRequestExpired = useMemo(() => {
|
||||
const now = dayjs();
|
||||
return dayjs(resetPasswordRequest.expires).isBefore(now);
|
||||
}, [resetPasswordRequest]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
<Head>
|
||||
<title>Reset Password</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="bg-white py-8 px-4 mx-2 shadow rounded-lg sm:px-10 space-y-6">
|
||||
{isRequestExpired && <Expired />}
|
||||
{!isRequestExpired && !success && (
|
||||
<>
|
||||
<div className="space-y-6">
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Reset Password</h2>
|
||||
<p>Enter the new password you'd like for your account.</p>
|
||||
{error && <p className="text-red-600">{error.message}</p>}
|
||||
</div>
|
||||
<form className="space-y-6" onSubmit={handleSubmit} action="#">
|
||||
<input name="csrfToken" type="hidden" defaultValue={csrfToken} hidden />
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||
New Password
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
onChange={handleChange}
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="password"
|
||||
required
|
||||
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-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className={`w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${
|
||||
loading ? "cursor-not-allowed" : ""
|
||||
}`}>
|
||||
{loading && (
|
||||
<svg
|
||||
className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
)}
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
{!isRequestExpired && success && (
|
||||
<>
|
||||
<Success />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const id = context.params.id;
|
||||
|
||||
try {
|
||||
const resetPasswordRequest = await prisma.resetPasswordRequest.findUnique({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
expires: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
resetPasswordRequest: {
|
||||
...resetPasswordRequest,
|
||||
expires: resetPasswordRequest.expires.toString(),
|
||||
},
|
||||
id,
|
||||
csrfToken: await getCsrfToken({ req: context.req }),
|
||||
},
|
||||
};
|
||||
} catch (reason) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
}
|
153
pages/auth/forgot-password/index.tsx
Normal file
153
pages/auth/forgot-password/index.tsx
Normal file
|
@ -0,0 +1,153 @@
|
|||
import Head from "next/head";
|
||||
import React from "react";
|
||||
import { getCsrfToken } from "next-auth/client";
|
||||
import debounce from "lodash.debounce";
|
||||
|
||||
export default function Page({ csrfToken }) {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [error, setError] = React.useState(null);
|
||||
const [success, setSuccess] = React.useState(false);
|
||||
const [email, setEmail] = React.useState("");
|
||||
|
||||
const handleChange = (e) => {
|
||||
setEmail(e.target.value);
|
||||
};
|
||||
|
||||
const submitForgotPasswordRequest = async ({ email }) => {
|
||||
try {
|
||||
const res = await fetch("/api/auth/forgot-password", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email: email }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
if (!res.ok) {
|
||||
setError(json);
|
||||
} else {
|
||||
setSuccess(true);
|
||||
}
|
||||
|
||||
return json;
|
||||
} catch (reason) {
|
||||
setError({ message: "An unexpected error occurred. Try again." });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedHandleSubmitPasswordRequest = debounce(submitForgotPasswordRequest, 250);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!email) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
await debouncedHandleSubmitPasswordRequest({ email });
|
||||
};
|
||||
|
||||
const Success = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Done</h2>
|
||||
<p>Check your email. We sent you a link to reset your password.</p>
|
||||
{error && <p className="text-red-600">{error.message}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
<Head>
|
||||
<title>Forgot Password</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="bg-white py-8 px-4 mx-2 shadow rounded-lg sm:px-10 space-y-6">
|
||||
{success && <Success />}
|
||||
{!success && (
|
||||
<>
|
||||
<div className="space-y-6">
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Forgot Password</h2>
|
||||
<p>
|
||||
Enter the email address associated with your account and we will send you a link to reset
|
||||
your password.
|
||||
</p>
|
||||
{error && <p className="text-red-600">{error.message}</p>}
|
||||
</div>
|
||||
<form className="space-y-6" onSubmit={handleSubmit} action="#">
|
||||
<input name="csrfToken" type="hidden" defaultValue={csrfToken} hidden />
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||
Email address
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
onChange={handleChange}
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
placeholder="john.doe@example.com"
|
||||
required
|
||||
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-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className={`w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${
|
||||
loading ? "cursor-not-allowed" : ""
|
||||
}`}>
|
||||
{loading && (
|
||||
<svg
|
||||
className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
)}
|
||||
Request Password Reset
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Page.getInitialProps = async ({ req }) => {
|
||||
return {
|
||||
csrfToken: await getCsrfToken({ req }),
|
||||
};
|
||||
};
|
|
@ -1,5 +1,6 @@
|
|||
import Head from 'next/head';
|
||||
import { getCsrfToken } from 'next-auth/client';
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { getCsrfToken } from "next-auth/client";
|
||||
|
||||
export default function Login({ csrfToken }) {
|
||||
return (
|
||||
|
@ -9,21 +10,27 @@ export default function Login({ csrfToken }) {
|
|||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||
Sign in to your account
|
||||
</h2>
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Sign in to your account</h2>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="bg-white py-8 px-4 mx-2 shadow rounded-lg sm:px-10">
|
||||
<form className="space-y-6" method="post" action="/api/auth/callback/credentials">
|
||||
<input name='csrfToken' type='hidden' defaultValue={csrfToken} hidden/>
|
||||
<input name="csrfToken" type="hidden" defaultValue={csrfToken} hidden />
|
||||
<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" placeholder="john.doe@example.com" required 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-blue-500 focus:border-blue-500 sm:text-sm" />
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
placeholder="john.doe@example.com"
|
||||
required
|
||||
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-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -32,24 +39,41 @@ export default function Login({ csrfToken }) {
|
|||
Password
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input id="password" name="password" type="password" autoComplete="current-password" placeholder="•••••••••••••" required 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-blue-500 focus:border-blue-500 sm:text-sm" />
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
placeholder="•••••••••••••"
|
||||
required
|
||||
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-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit" className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Sign in
|
||||
</button>
|
||||
<Link href="/auth/forgot-password">
|
||||
<button
|
||||
type="button"
|
||||
className="w-full flex justify-center py-2 px-4 text-sm font-medium text-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Forgot Password?
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Login.getInitialProps = async ({ req, res }) => {
|
||||
Login.getInitialProps = async ({ req }) => {
|
||||
return {
|
||||
csrfToken: await getCsrfToken({ req })
|
||||
}
|
||||
}
|
||||
csrfToken: await getCsrfToken({ req }),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -142,3 +142,11 @@ model EventTypeCustomInput {
|
|||
required Boolean
|
||||
}
|
||||
|
||||
model ResetPasswordRequest {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
email String
|
||||
expires DateTime
|
||||
}
|
||||
|
||||
|
|
20
src/forgot-password/messaging/forgot-password.ts
Normal file
20
src/forgot-password/messaging/forgot-password.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import buildMessageTemplate from "../../../lib/emails/buildMessageTemplate";
|
||||
|
||||
export const forgotPasswordSubjectTemplate = "Forgot your password? - Calendso";
|
||||
|
||||
export const forgotPasswordMessageTemplate = `Hey there,
|
||||
|
||||
Use the link below to reset your password.
|
||||
{{link}}
|
||||
|
||||
p.s. It expires in 6 hours.
|
||||
|
||||
- Calendso`;
|
||||
|
||||
export const buildForgotPasswordMessage = (vars) => {
|
||||
return buildMessageTemplate({
|
||||
subjectTemplate: forgotPasswordSubjectTemplate,
|
||||
messageTemplate: forgotPasswordMessageTemplate,
|
||||
vars,
|
||||
});
|
||||
};
|
|
@ -2637,6 +2637,11 @@ lodash.clonedeep@^4.5.0:
|
|||
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
|
||||
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
|
||||
|
||||
lodash.debounce@^4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
|
||||
integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
|
||||
|
||||
lodash.includes@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
|
||||
|
|
Loading…
Reference in a new issue