Files
tasknote/client/src/components/LoginForm/index.tsx
T
rmcamposandGitHub 84f6a00572 feat: prevent user form login before conforming his email (#56)
* feat: prevent user form login before conforming his email

* chore: update files and improve things

* feat: fix errors issue and improve logs
2026-05-17 17:53:59 -03:00

315 lines
11 KiB
TypeScript

import React, {
useCallback,
useContext,
useEffect,
useState
} from 'react';
import {
Alert,
Button,
Card,
Col,
Container,
Form,
Row
} from 'react-bootstrap';
import { Link, useNavigate, useSearchParams } from 'react-router';
import { useTranslation } from 'react-i18next';
import AuthContext from '../../context/AuthContext';
import { translateServerResponse } from '../../utils/TranslatorUtils';
import { handleDefaultLang } from '../../lang-service/LangHandler';
import FormInput from '../FormInput';
import api from '../../api-service/api';
import ApiConfig from '../../api-service/apiConfig';
/**
* @returns {React.ReactNode} The form component for Login and Register pages.
*/
function LoginForm({ prefix }: { prefix: string }): React.ReactNode {
const { signIn, register } = useContext(AuthContext);
const { i18n, t } = useTranslation();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [validated, setValidated] = useState<boolean>(false);
const [formInvalid, setFormInvalid] = useState<boolean>(false);
const [errorMessage, setErrorMessage] = useState<string>('');
const [successMessage, setSuccessMessage] = useState<string>('');
const [showResend, setShowResend] = useState<boolean>(false);
const [email, setEmail] = useState<string>('');
const [password, setPassword] = useState<string>('');
const [passwordAgain, setPasswordAgain] = useState<string>('');
const [secondsLeft, setSecondsLeft] = useState<number>(0);
const [isResendEnabled, setIsResendEnabled] = useState(true);
const [langToSave, setLangToSave] = useState<string>('');
/**
* Navigates to the specified page.
*
* @param {string} page - The page to navigate to.
*/
const goTo = useCallback((page: string) => {
navigate(page);
}, []);
/**
* Handles the form submit button click event.
*
* When clicked, checks if the form is valid. If it's not, raise an error
* message in an Alert. Otherwise, it calls the signIn API and navigates to the home page.
*/
const handleSubmit = async (event: React.SubmitEvent<HTMLFormElement>): Promise<void> => {
event.preventDefault();
event.stopPropagation();
setValidated(true);
const form = event.currentTarget;
if (!form.checkValidity()) {
setFormInvalid(true);
if (prefix === 'complete_reset') {
setErrorMessage(translateServerResponse('Please fill in the new password', i18n.language));
}
else if (prefix !== 'reset') {
setErrorMessage(translateServerResponse('Please fill in your username and password!' + prefix, i18n.language));
}
else {
setErrorMessage(translateServerResponse('Please fill in your email', i18n.language));
}
return;
}
if (password !== passwordAgain && prefix === 'register') {
setFormInvalid(true);
setErrorMessage(translateServerResponse('Please fill in your username and password!', i18n.language));
}
setFormInvalid(false);
setShowResend(false);
try {
if (prefix === 'login') {
await signIn(email, password);
goTo('/home');
}
else if (prefix === 'register') {
await register({ email, password, passwordAgain, lang: langToSave });
// Do not clear the email, because user might request to resend
setPassword('');
setPasswordAgain('');
setSuccessMessage(translateServerResponse('Please confirm your email before proceeding', i18n.language));
setShowResend(true);
setSecondsLeft(30);
setIsResendEnabled(false);
}
else if (prefix === 'reset') {
await api.postJSON(ApiConfig.resetPwdUrl, { email });
setSuccessMessage(translateServerResponse('If the email address you entered is associated with an account, you will receive a password reset link shortly.', i18n.language));
}
else if (prefix === 'complete_reset') {
const token = searchParams.get('token');
await api.postJSON(ApiConfig.completeResetPwdUrl, { token, password, passwordAgain });
goTo('/home');
}
}
catch (e) {
setFormInvalid(true);
let msg = '';
if (typeof e === 'string') {
console.log('string');
msg = e;
}
else if (e instanceof Error) {
msg = e.message;
}
setErrorMessage(translateServerResponse(msg, i18n.language));
if (msg === 'Email not confirmed!') {
setShowResend(true);
}
}
};
const handleResend = async () => {
try {
await api.postJSON(ApiConfig.resendConfirmUrl, { email });
setSuccessMessage(translateServerResponse('Confirmation email re-sent! Please check the spam folder.', i18n.language));
setFormInvalid(false);
setShowResend(true);
setSecondsLeft(30);
setIsResendEnabled(false);
}
catch (e) {
setFormInvalid(true);
if (typeof e === 'string') {
setErrorMessage(translateServerResponse(e, i18n.language));
}
else if (e instanceof Error) {
setErrorMessage(translateServerResponse(e.message, i18n.language));
}
}
};
useEffect(() => {
const lang = handleDefaultLang();
setLangToSave(lang);
}, [formInvalid]);
useEffect(() => {
if (secondsLeft > 0) {
const timer = setTimeout(() => setSecondsLeft(prev => prev - 1), 1000);
return () => clearTimeout(timer);
}
else {
setIsResendEnabled(true);
}
}, [secondsLeft]);
return (
<Container as="main" fluid className="login-page">
<Row className="justify-content-center w-100">
<Col xs={12} md={6} lg={4}>
<Card>
<Card.Body className="p-4">
<h2 className="text-center">{t(`${prefix}_title`)}</h2>
{formInvalid
? (
<>
<Alert variant="danger">{errorMessage}</Alert>
{showResend && (
<div className="text-center mt-2">
<Button
variant="outline-secondary"
size="sm"
onClick={handleResend}
disabled={!isResendEnabled}
>
{isResendEnabled
? `${t('register_resent_email')}`
: `${t('register_resent_email_secs')} ${secondsLeft}`}
</Button>
</div>
)}
</>
)
: null}
{successMessage.length > 1 && (
<>
<Alert variant="success">
{ successMessage }
</Alert>
{showResend && (
<div className="text-center mb-3">
<Button
variant="outline-secondary"
onClick={handleResend}
disabled={!isResendEnabled}
>
{isResendEnabled
? `${t('register_resent_email')}`
: `${t('register_resent_email_secs')} ${secondsLeft}`}
</Button>
</div>
)}
</>
)}
<Form noValidate validated={validated} onSubmit={handleSubmit}>
{prefix !== 'complete_reset' && (
<FormInput
labelText={t(`${prefix}_email_label`)}
iconName="At"
required={true}
name="email"
placeholder={t(`${prefix}_email_placeholder`)}
dataTestId={`${prefix}_email_input`}
value={email}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value);
}}
/>
)}
{prefix !== 'reset' && (
<FormInput
labelText={t(`${prefix}_password_label`)}
iconName="Lock"
required={true}
type="password"
name="password"
placeholder={t(`${prefix}_password_placeholder`)}
value={password}
pwdShowText={t('password_show_txt')}
pwdHideText={t('password_hide_txt')}
pwdHelperTxt={t('password_helper')}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value);
}}
dataTestId="account-password-login"
/>
)}
{(prefix === 'register' || prefix === 'complete_reset') && (
<FormInput
labelText={t('register_password_repeat_label')}
iconName="Lock"
required={true}
type="password"
name="passwordAgain"
placeholder={t('login_password_placeholder')}
value={passwordAgain}
pwdShowText={t('password_show_txt')}
pwdHideText={t('password_hide_txt')}
pwdHelperTxt={t('password_helper')}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setPasswordAgain(e.target.value);
}}
dataTestId={`account-repeat-password-${prefix}`}
/>
)}
<Button
variant="primary"
type="submit"
className="w-100"
>
{t(`${prefix}_btn_submit`)}
</Button>
</Form>
<div className="text-center mt-3">
{t(`${prefix}_account`)}
<Link
to={prefix === 'login' ? '/register' : '/login'}
className="text-decoration-none ms-2"
>
{t(`${prefix}_go_other`)}
</Link>
</div>
<div className="text-center mt-3">
<Link to="/" className="text-decoration-none">
{t(`${prefix}_back_home`)}
</Link>
</div>
<div className="text-center mt-3">
{prefix === 'login' && (
<Link
to="/reset-password"
className="text-decoration-none ms-2"
>
{t('login_forgot_password')}
</Link>
)}
</div>
</Card.Body>
</Card>
</Col>
</Row>
</Container>
);
}
export default LoginForm;