password reset implementation;

custom signin form
This commit is contained in:
Dobromir Popov
2024-04-30 02:49:40 +03:00
parent d777913910
commit aa766f4e1e
6 changed files with 290 additions and 43 deletions

View File

@ -34,7 +34,8 @@ export default async function handler(req, res) {
// Retrieve and validate the JWT token
//response is a special action that does not require a token
if (action == "email_response") {
//PUBLIC
if (action == "email_response" || action == "account") {
switch (emailaction) {
case "coverMeAccept":
//validate shiftId and assignmentId
@ -201,6 +202,83 @@ export default async function handler(req, res) {
});
break;
case "resetPassword":
// Send password reset form to the user
//parse the request body
let email = req.body.email || req.query.email;
let actualUser = await prisma.publisher.findUnique({
where: {
email: email
}
});
if (!actualUser) {
return res.status(200).json({ message: "Няма потребител с този имейл" });
}
else {
let requestGuid = req.query.guid;
if (!requestGuid) {
console.log("User: " + email + " requested a password reset");
let requestGuid = uuidv4();
//save the request in the database as EventLog
let eventLog = await prisma.eventLog.create({
data: {
date: new Date(),
publisher: { connect: { id: actualUser.id } },
type: EventLogType.PasswordResetRequested,
content: JSON.stringify({ guid: requestGuid })
}
});
logger.info("User: " + email + " requested a password reset. EventLogId: " + eventLog.id + "");
let model = {
email: email,
firstName: actualUser.firstName,
lastName: actualUser.lastName,
resetUrl: process.env.NEXTAUTH_URL + "/api/email?action=email_response&emailaction=resetPassword&guid=" + requestGuid + "&email=" + email,
sentDate: common.getDateFormated(new Date())
};
emailHelper.SendEmailHandlebars(to, "resetPassword", model);
res.status(200).json({ message: "Password reset request sent" });
}
else {
//1. validate the guid
let eventLog = await prisma.eventLog.findFirst({
where: {//can we query "{ guid: requestGuid }"?
type: EventLogType.PasswordResetRequested,
publisherId: actualUser.id,
date: {
gt: new Date(new Date().getTime() - 24 * 60 * 60 * 1000) //24 hours
}
}
});
if (!eventLog) {
return res.status(400).json({ message: "Invalid or expired password reset request" });
}
else {
let eventLog = await prisma.eventLog.update({
where: {
id: parseInt(requestGuid)
},
data: {
type: EventLogType.PasswordResetEmailConfirmed
}
});
//2. redirect to the password reset page
const messagePageUrl = `/auth/reset-password?email=${email}&resetToken=${requestGuid}`;
res.redirect(messagePageUrl);
}
//2.login the user
//3. redirect to the password reset page
}
}
break;
}
// //send email response to the user
// const emailResponse = await common.sendEmail(user.email, "Email Action Processed",
@ -220,6 +298,7 @@ export default async function handler(req, res) {
}
});
//PRIVATE ACTIONS
switch (action) {
case "sendCoverMeRequestByEmail":
// Send CoverMe request to the users

View File

@ -0,0 +1,135 @@
import { use, useEffect, useState } from 'react';
import Layout from '../../components/layout';
import axiosInstance from "../../src/axiosSecure";
import common from '../../src/helpers/common';
import { EventLogType } from '@prisma/client';
import { useRouter } from "next/router";
export default function ResetPassword(req, res) {
const [email, setEmail] = useState('');
const [message, setMessage] = useState('');
const [resetToken, setResetToken] = useState(req.query?.resetToken || '');
const [isConfirmed, setIsConfirmed] = useState(false);
const router = useRouter();
useEffect(async () => {
if (resetToken) {
const prisma = common.getPrismaClient();
let eventLog = await prisma.eventLog.findUnique({
where: {
content: resetToken,
type: EventLogType.PasswordResetEmailConfirmed,
date: {
gt: new Date(new Date().getTime() - 24 * 60 * 60 * 1000) //24 hours
}
}
});
if (eventLog) {
setIsConfirmed(true);
}
}
}, [resetToken]);
const handleResetRequest = async (event) => {
event.preventDefault();
// Call your email API endpoint here
try {
const response = await axiosInstance.post('/api/email?action=account&emailaction=resetPassword', { email },
{ headers: { 'Content-Type': 'application/json' } });
if (response.data.message) {
setMessage(response.data.message);
} else {
if (response.ok) {
setMessage('Провери твоя имейл за инструкции как да промениш паролата си.');
} else {
if (response.error) {
setMessage(response.error);
}
}
}
} catch (error) {
setMessage(error.message);
}
};
const setNewPassword = async (event) => {
event.preventDefault();
try {
const prisma = common.getPrismaClient();
const user = await prisma.user.findUnique({
where: {
email
}
});
if (!user) {
throw new Error('Няма потребител с този имейл.');
}
const passHash = await crypto.hash(event.target.newPassword.value, 10);
await prisma.user.update({
where: {
email
},
data: {
passwordHashLocalAccount: passHash
}
});
setMessage('Паролата беше успешно променена.');
router.push('/auth/signin');
} catch (error) {
setMessage(error.message);
}
}
return (
<Layout>
<div className="min-h-screen flex items-center justify-center">
<div className="w-full max-w-md p-8 space-y-6 bg-white shadow-lg rounded-lg">
<h1 className="text-xl font-bold text-center">Променете паролата си</h1>
<form onSubmit={handleResetRequest} className="space-y-4">
{!isConfirmed &&
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">имейл</label>
<input
id="email"
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
/>
</div>}
{isConfirmed &&
<div>
<label htmlFor="newPassword" className="block text-sm font-medium text-gray-700">имейл</label>
<input
id="newPassword"
type="password"
required
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
/>
</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">
Изпрати линк за промяна на паролата
</button>
<button type="button" className="mt-4 w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-blue-600 hover:text-blue-700 focus:outline-none"
onClick={() => window.location.href = '/auth/signin'}
>
страница за вход
</button>
</div>
{message && <div className="text-center text-sm text-gray-500">{message}</div>}
</form>
</div>
</div>
</Layout>
);
}

View File

@ -15,7 +15,7 @@ export default function SignIn({ csrfToken }) {
// Perform client-side validation if needed
if (!email || !password) {
setError('All fields are required');
setError('Всички полета са задължителни');
return;
}
@ -45,49 +45,52 @@ export default function SignIn({ csrfToken }) {
<Layout>
<div className="page">
<div className="signin">
<div className="min-h-screen flex items-center justify-center">
<div className="provider">
{/* Button to sign in with Google */}
<button onClick={() => signIn('google', { callbackUrl: '/dash' })}>
<img loading="lazy" height="24" width="24" id="provider-logo" src="https://authjs.dev/img/providers/google.svg" alt="Google Logo" />
Sign in with Google
<div className="min-h-screen flex flex-col items-center justify-center">
{/* SSO Providers */}
<div className="space-y-4 w-full px-4">
<button onClick={() => signIn('google', { callbackUrl: '/' })}
className="flex items-center justify-center w-full py-2 px-4 border border-gray-300 rounded shadow-sm text-sm text-gray-700 bg-white hover:bg-gray-50">
<img loading="lazy" height="24" width="24" alt="Google logo"
src="https://authjs.dev/img/providers/google.svg" className="mr-2" />
Влез чрез Google
</button>
{/* Add more buttons for other SSO providers here in similar style */}
</div>
<div className="provider">
<form onSubmit={handleSubmit} className="w-full max-w-xs">
<input name="csrfToken" type="hidden" defaultValue={csrfToken} />
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="border p-2 w-full"
/>
</div>
<div>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="border p-2 w-full"
/>
</div>
{error && <div className="text-red-500">{error}</div>}
<button type="submit" className="bg-blue-500 text-white p-2 mt-4">
Sign in
</button>
<button
type="button"
className="text-blue-500 p-2 mt-4"
onClick={() => router.push('/auth/reset-password')}>
Forgot password?
</button>
</form>
</div>
{/* Email and Password Form */}
<form onSubmit={handleSubmit} className="mt-8 w-full max-w-xs px-4">
<input name="csrfToken" type="hidden" defaultValue={csrfToken} />
<div className="mb-4">
<label htmlFor="email" className="block text-sm font-medium text-gray-700">имейл</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
/>
</div>
<div className="mb-6">
<label htmlFor="password" className="block text-sm font-medium text-gray-700">парола</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
/>
</div>
{error && <div className="text-red-500 text-sm">{error}</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">
Влез
</button>
{/* <button
type="button"
className="mt-4 w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-blue-600 hover:text-blue-700 focus:outline-none"
onClick={() => router.push('/auth/reset-password')}>
Забравена парола?
</button> */}
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE `Eventlog`
MODIFY `type` ENUM(
'AssignmentReplacementRequested', 'AssignmentReplacementAccepted', 'SentEmail', 'PasswordResetRequested', 'PasswordResetEmailConfirmed', 'PasswordResetCompleted'
) NOT NULL;

View File

@ -263,6 +263,9 @@ enum EventLogType {
AssignmentReplacementRequested
AssignmentReplacementAccepted
SentEmail
PasswordResetRequested
PasswordResetEmailConfirmed
PasswordResetCompleted
}
model EventLog {

View File

@ -0,0 +1,22 @@
{{!-- Subject: ССОМ: Нужен е заместник--}}
{{!-- Text: Plain text version of your email. If not provided, HTML tags will be stripped from the HTML version for the
text version. --}}
<section>
<h3>Промяна на парола</h3>
<p>Здравей, {{firstName}} {{lastName}}</p>
<p>
Получихме заявка за промяна на паролата на твоя акаунт. Ако това не си ти, моля игнорирай този имейл.
</p>
<p style="text-align: center;">
<a href="{{resetUrl}}"
target="_blank"
style="background-color: #4CAF50; color: white; padding: 10px 20px; text-decoration: none; display: inline-block; border-radius: 5px;">Смени
паролата си</a>
</p>
{{!-- <p>Thank you very much for considering my request.</p>
<p>Best regards,<br>{{name}}</p> --}}
</section>
<footer style="margin-top: 20px; text-align: center;">
<p>Изпратено на: {{sentDate}}</p>
</footer>