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
|
||||
|
||||
//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
|
||||
|
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
|
||||
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>
|
||||
|
@ -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
|
||||
AssignmentReplacementAccepted
|
||||
SentEmail
|
||||
PasswordResetRequested
|
||||
PasswordResetEmailConfirmed
|
||||
PasswordResetCompleted
|
||||
}
|
||||
|
||||
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