Skip to content

Commit

Permalink
Merge pull request #994 from hpcc-systems/yadhap/unverified-users-login
Browse files Browse the repository at this point in the history
Yadhap/unverified users login
  • Loading branch information
FancMa01 authored Jan 21, 2025
2 parents ca7a8fe + f11d1b4 commit 1da3750
Show file tree
Hide file tree
Showing 7 changed files with 369 additions and 107 deletions.
124 changes: 124 additions & 0 deletions Tombolo/client-reactjs/src/components/login/UnverifiedUser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//Libraries
import React, { useRef, useState, useEffect } from 'react';
import { Button, Typography, Card, message } from 'antd';
import { CheckCircleFilled } from '@ant-design/icons';

// Local imports
const { resendVerificationCode } = require('./utils');

const { Text } = Typography;
const defaultDelay = 90; // 10 seconds

function UnverifiedUser({ setUnverifiedUserLoginAttempt, email = 'yadhap.dahal@lexisnexisrisk.com' }) {
const [sendingEmail, setSendingEmail] = useState(false);
const [sentVerificationEmail, setSentVerificationEmail] = useState(false);
const [canSendVerificationEmail, setCanSendVerificationEmail] = useState(true);
const [emailSendDelay, setEmailSendDelay] = useState(0);
const [timerInterval, setTimerInterval] = useState(null);

const t = useRef(null);

// Set timer function
const startTimer = () => {
setEmailSendDelay(0);
const interval = setInterval(() => {
setEmailSendDelay((prev) => {
const newSeconds = prev + 1;
if (newSeconds === defaultDelay) {
setCanSendVerificationEmail(true);
clearTimer(interval); // Clear the timer when it reaches defaultDelay
}
return newSeconds;
});
}, 1000);
setTimerInterval(interval); // Store the interval ID
};

// Clear timer function
const clearTimer = (interval) => {
clearInterval(interval);
setEmailSendDelay(defaultDelay); // Reset the time
};

const resendVerificationLink = async () => {
// Make a request to the server
try {
setSendingEmail(true);
setCanSendVerificationEmail(false);
setSendingEmail(false);
await resendVerificationCode(email);
setSentVerificationEmail(true);
// Start the delay timer
t.current = startTimer();
} catch (error) {
message.error(error.message);
setCanSendVerificationEmail(true);
}
};

useEffect(() => {
return () => {
clearTimer(timerInterval);
console.log('Cleared timer');
};
}, []);

return (
<Card style={{ margin: '1rem auto', padding: '0.5rem', textAlign: 'center' }}>
{!sentVerificationEmail ? (
<>
<Text type="danger" style={{ fontSize: '1.2rem', fontWeight: 'bold' }}>
Your account is not yet verified.
</Text>
<Text style={{ display: 'block', margin: '1rem 0' }}>
Please check your email for a verification link. If you did not receive an email, click the button below to
resend the verification link.
</Text>
<div
direction="vertical"
size="middle"
style={{ display: 'flex', gap: '0.2rem', justifyContent: 'space-around' }}>
<Button type="primary" onClick={resendVerificationLink} style={{ width: '50%' }}>
Resend Verification Link
</Button>
<Button type="primary" danger style={{ width: '50%' }} onClick={() => setUnverifiedUserLoginAttempt(false)}>
Back to Login
</Button>
</div>
</>
) : (
<>
{sendingEmail ? (
<Text type="success" style={{ color: 'var(--dark)', fontSize: '1rem', fontWeight: 'bold' }}>
Sending Verification E-mail ...
</Text>
) : (
<Text type="success" style={{ fontSize: '1.2rem', fontWeight: 'bold' }}>
<CheckCircleFilled /> Verification E-mail sent
</Text>
)}

<Text style={{ display: 'block', margin: '1rem 0' }}>
{` Verification email has been sent to your email address. Please check your inbox and click the link in the
email to complete the verification process. If you don't see the email, check your spam or junk folder.`}
</Text>
<div direction="vertical" size="middle" style={{ display: 'flex', gap: '1rem' }}>
<Button
type="primary"
disabled={!canSendVerificationEmail}
onClick={resendVerificationLink}
style={{ width: '50%' }}>
Resend Verification Link{' '}
{emailSendDelay !== defaultDelay ? `(Wait ${defaultDelay - emailSendDelay} seconds)` : ''}
</Button>
<Button type="primary" danger onClick={() => setUnverifiedUserLoginAttempt(false)} style={{ width: '50%' }}>
Back to Login
</Button>
</div>
</>
)}
</Card>
);
}

export default UnverifiedUser;
199 changes: 103 additions & 96 deletions Tombolo/client-reactjs/src/components/login/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,31 @@ import msLogo from '../../images/mslogo.png';
import { getDeviceInfo } from './utils';
import { authActions } from '../../redux/actions/Auth';
import { Constants } from '../common/Constants';
import UnverifiedUser from './UnverifiedUser';

const Login = () => {
const [unverifiedUserLoginAttempt, setUnverifiedUserLoginAttempt] = useState(false);
const [loading, setLoading] = useState(false);
const [azureLoginAttempted, setAzureLoginAttempted] = useState(false);
const [email, setEmail] = useState(null);

// When the form is submitted, this function is called
const onFinish = async (values) => {
const { email, password } = values;
setEmail(email);
setLoading(true);

//get browser and os info and put in deviceInfo variable
const deviceInfo = getDeviceInfo();

const test = await authActions.login({ email, password, deviceInfo });

if (test?.type === 'unverified') {
setUnverifiedUserLoginAttempt(true);
setLoading(false);
return;
}

if (test?.type === Constants.LOGIN_SUCCESS) {
//reload page if login is succesful
window.location.href = '/';
Expand Down Expand Up @@ -48,16 +59,16 @@ const Login = () => {
}
});

//if session expired relay message to user what happened

// If the URL contains a code parameter, it means the user has been redirected from Azure AD
useEffect(() => {
const sessionExpired = localStorage.getItem('sessionExpired');
//get url and check for id token
const url = new URL(window.location.href);
const code = url.searchParams.get('code');

if (sessionExpired) {
localStorage.removeItem('sessionExpired');
message.error('Session expired. Please log in again.');
if (code && !loading && !azureLoginAttempted) {
azureLoginFunc(code);
}
});
}, [azureLoginAttempted, loading]);

const azureLogin = () => {
authActions.azureLoginRedirect();
Expand All @@ -83,16 +94,6 @@ const Login = () => {
}
};

useEffect(() => {
//get url and check for id token
const url = new URL(window.location.href);
const code = url.searchParams.get('code');

if (code && !loading && !azureLoginAttempted) {
azureLoginFunc(code);
}
}, [azureLoginAttempted, loading]);

const authMethods = process.env.REACT_APP_AUTH_METHODS;
let azureEnabled = false;
let traditionalEnabled = false;
Expand All @@ -104,85 +105,91 @@ const Login = () => {

return (
<>
<Form onFinish={onFinish} layout="vertical">
{loading && (
<div
style={{
textAlign: 'center',
display: 'flex',
width: '100%',
justifyContent: 'center',
alignItems: 'center',
flexWrap: 'wrap',
position: 'absolute',
zIndex: '2000',
height: '100%',
opacity: '.4',
backgroundColor: 'black',
top: '0',
left: '0',
}}>
<Spin size="large" style={{ margin: '0 auto' }} />
</div>
)}
<Divider>Log In With</Divider>
{azureEnabled && (
<>
<Form.Item>
<Button
size="large"
style={{ background: 'black', color: 'white' }}
className="fullWidth"
onClick={() => azureLogin()}>
<img src={msLogo} style={{ height: '3rem', width: 'auto' }} />
</Button>
</Form.Item>
</>
)}

{traditionalEnabled && azureEnabled && <Divider>Or</Divider>}

{traditionalEnabled && (
<>
<Form.Item
label="Email"
name="email"
rules={[
{
required: true,
whitespace: true,
type: 'email',
message: 'Invalid e-mail address.',
},
{ max: 64, message: 'Maximum of 64 characters allowed' },
]}>
<Input size="large" />
</Form.Item>
<Form.Item
label={
<>
<span>Password&nbsp;</span>
</>
}
name="password"
rules={[
{ required: true, message: 'Please input your password!' },
{ max: 64, message: 'Maximum of 64 characters allowed' },
]}>
<Input.Password size="large" autoComplete="new-password" />
</Form.Item>
<a href="/forgot-password">Forgot password?</a>
<Form.Item>
<Button type="primary" htmlType="submit" disabled={loading && true} className="fullWidth">
Log in
</Button>
</Form.Item>
<p style={{ width: '100%', textAlign: 'center', marginTop: '1rem' }}>
<span>Need an account?</span> <a href="/register">Register</a>
</p>
</>
)}
</Form>
{unverifiedUserLoginAttempt ? (
<UnverifiedUser setUnverifiedUserLoginAttempt={setUnverifiedUserLoginAttempt} email={email} />
) : (
<>
<Form onFinish={onFinish} layout="vertical">
{loading && (
<div
style={{
textAlign: 'center',
display: 'flex',
width: '100%',
justifyContent: 'center',
alignItems: 'center',
flexWrap: 'wrap',
position: 'absolute',
zIndex: '2000',
height: '100%',
opacity: '.4',
backgroundColor: 'black',
top: '0',
left: '0',
}}>
<Spin size="large" style={{ margin: '0 auto' }} />
</div>
)}
<Divider>Log In With</Divider>
{azureEnabled && (
<>
<Form.Item>
<Button
size="large"
style={{ background: 'black', color: 'white' }}
className="fullWidth"
onClick={() => azureLogin()}>
<img src={msLogo} style={{ height: '3rem', width: 'auto' }} />
</Button>
</Form.Item>
</>
)}

{traditionalEnabled && azureEnabled && <Divider>Or</Divider>}

{traditionalEnabled && (
<>
<Form.Item
label="Email"
name="email"
rules={[
{
required: true,
whitespace: true,
type: 'email',
message: 'Invalid e-mail address.',
},
{ max: 64, message: 'Maximum of 64 characters allowed' },
]}>
<Input size="large" />
</Form.Item>
<Form.Item
label={
<>
<span>Password&nbsp;</span>
</>
}
name="password"
rules={[
{ required: true, message: 'Please input your password!' },
{ max: 64, message: 'Maximum of 64 characters allowed' },
]}>
<Input.Password size="large" autoComplete="new-password" />
</Form.Item>
<a href="/forgot-password">Forgot password?</a>
<Form.Item>
<Button type="primary" htmlType="submit" disabled={loading && true} className="fullWidth">
Log in
</Button>
</Form.Item>
<p style={{ width: '100%', textAlign: 'center', marginTop: '1rem' }}>
<span>Need an account?</span> <a href="/register">Register</a>
</p>
</>
)}
</Form>
</>
)}
</>
);
};
Expand Down
21 changes: 21 additions & 0 deletions Tombolo/client-reactjs/src/components/login/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,24 @@ export const verifyEmail = async (token) => {

return responseJson;
};

// Make Post request to /resend-verification-code with id in body
export const resendVerificationCode = async (email) => {
const payload = {
method: 'POST',
headers: authHeader(),
body: JSON.stringify({ email }),
};

const response = await fetch('/api/auth/resendVerificationCode', payload);

// Get the data from the response
const responseJson = await response.json();

// Check if the response is ok
if (!response.ok) {
throw new Error(responseJson.message);
}

return responseJson;
};
Loading

0 comments on commit 1da3750

Please sign in to comment.