password reset implementation;
custom signin form
This commit is contained in:
@ -34,7 +34,8 @@ export default async function handler(req, res) {
|
|||||||
// Retrieve and validate the JWT token
|
// Retrieve and validate the JWT token
|
||||||
|
|
||||||
//response is a special action that does not require a 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) {
|
switch (emailaction) {
|
||||||
case "coverMeAccept":
|
case "coverMeAccept":
|
||||||
//validate shiftId and assignmentId
|
//validate shiftId and assignmentId
|
||||||
@ -201,6 +202,83 @@ export default async function handler(req, res) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
break;
|
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
|
// //send email response to the user
|
||||||
// const emailResponse = await common.sendEmail(user.email, "Email Action Processed",
|
// 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) {
|
switch (action) {
|
||||||
case "sendCoverMeRequestByEmail":
|
case "sendCoverMeRequestByEmail":
|
||||||
// Send CoverMe request to the users
|
// Send CoverMe request to the users
|
||||||
|
135
pages/auth/reset-password.tsx
Normal file
135
pages/auth/reset-password.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -15,7 +15,7 @@ export default function SignIn({ csrfToken }) {
|
|||||||
|
|
||||||
// Perform client-side validation if needed
|
// Perform client-side validation if needed
|
||||||
if (!email || !password) {
|
if (!email || !password) {
|
||||||
setError('All fields are required');
|
setError('Всички полета са задължителни');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,52 +45,55 @@ export default function SignIn({ csrfToken }) {
|
|||||||
<Layout>
|
<Layout>
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<div className="signin">
|
<div className="signin">
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<div className="min-h-screen flex flex-col items-center justify-center">
|
||||||
<div className="provider">
|
{/* SSO Providers */}
|
||||||
{/* Button to sign in with Google */}
|
<div className="space-y-4 w-full px-4">
|
||||||
<button onClick={() => signIn('google', { callbackUrl: '/dash' })}>
|
<button onClick={() => signIn('google', { callbackUrl: '/' })}
|
||||||
<img loading="lazy" height="24" width="24" id="provider-logo" src="https://authjs.dev/img/providers/google.svg" alt="Google Logo" />
|
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">
|
||||||
Sign in with Google
|
<img loading="lazy" height="24" width="24" alt="Google logo"
|
||||||
|
src="https://authjs.dev/img/providers/google.svg" className="mr-2" />
|
||||||
|
Влез чрез Google
|
||||||
</button>
|
</button>
|
||||||
|
{/* Add more buttons for other SSO providers here in similar style */}
|
||||||
</div>
|
</div>
|
||||||
<div className="provider">
|
|
||||||
<form onSubmit={handleSubmit} className="w-full max-w-xs">
|
{/* Email and Password Form */}
|
||||||
|
<form onSubmit={handleSubmit} className="mt-8 w-full max-w-xs px-4">
|
||||||
<input name="csrfToken" type="hidden" defaultValue={csrfToken} />
|
<input name="csrfToken" type="hidden" defaultValue={csrfToken} />
|
||||||
<div>
|
<div className="mb-4">
|
||||||
<label htmlFor="email">Email</label>
|
<label htmlFor="email" className="block text-sm font-medium text-gray-700">имейл</label>
|
||||||
<input
|
<input
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
className="border p-2 w-full"
|
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>
|
||||||
<div>
|
<div className="mb-6">
|
||||||
<label htmlFor="password">Password</label>
|
<label htmlFor="password" className="block text-sm font-medium text-gray-700">парола</label>
|
||||||
<input
|
<input
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
className="border p-2 w-full"
|
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>
|
||||||
{error && <div className="text-red-500">{error}</div>}
|
{error && <div className="text-red-500 text-sm">{error}</div>}
|
||||||
<button type="submit" className="bg-blue-500 text-white p-2 mt-4">
|
<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">
|
||||||
Sign in
|
Влез
|
||||||
</button>
|
</button>
|
||||||
<button
|
{/* <button
|
||||||
type="button"
|
type="button"
|
||||||
className="text-blue-500 p-2 mt-4"
|
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')}>
|
onClick={() => router.push('/auth/reset-password')}>
|
||||||
Forgot password?
|
Забравена парола?
|
||||||
</button>
|
</button> */}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Eventlog`
|
||||||
|
MODIFY `type` ENUM(
|
||||||
|
'AssignmentReplacementRequested', 'AssignmentReplacementAccepted', 'SentEmail', 'PasswordResetRequested', 'PasswordResetEmailConfirmed', 'PasswordResetCompleted'
|
||||||
|
) NOT NULL;
|
@ -263,6 +263,9 @@ enum EventLogType {
|
|||||||
AssignmentReplacementRequested
|
AssignmentReplacementRequested
|
||||||
AssignmentReplacementAccepted
|
AssignmentReplacementAccepted
|
||||||
SentEmail
|
SentEmail
|
||||||
|
PasswordResetRequested
|
||||||
|
PasswordResetEmailConfirmed
|
||||||
|
PasswordResetCompleted
|
||||||
}
|
}
|
||||||
|
|
||||||
model EventLog {
|
model EventLog {
|
||||||
|
22
src/templates/emails/resetPass.hbs
Normal file
22
src/templates/emails/resetPass.hbs
Normal 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>
|
Reference in New Issue
Block a user