cinny/src/app/templates/auth/Auth.jsx
2021-07-28 18:45:52 +05:30

335 lines
11 KiB
JavaScript

import React, { useState, useRef } from 'react';
import PropTypes from 'prop-types';
import './Auth.scss';
import ReCAPTCHA from 'react-google-recaptcha';
import { Link } from 'react-router-dom';
import * as auth from '../../../client/action/auth';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
import Input from '../../atoms/input/Input';
import Spinner from '../../atoms/spinner/Spinner';
import CinnySvg from '../../../../public/res/svg/cinny.svg';
const USERNAME_REGEX = /^[a-z0-9_-]+$/;
const BAD_USERNAME_ERROR = 'Username must contain only lowercase letters, numbers, dashes and underscores.';
const PASSWORD_REGEX = /.+/;
const PASSWORD_STRENGHT_REGEX = /^(?=.*\d)(?=.*[A-Z])(?=.*[a-z])(?=.*[^\w\d\s:])([^\s]){8,16}$/;
const BAD_PASSWORD_ERROR = 'Password must contain 1 number, 1 uppercase letters, 1 lowercase letters, 1 non-alpha numeric number, 8-16 characters with no space.';
const CONFIRM_PASSWORD_ERROR = 'Password don\'t match.';
const EMAIL_REGEX = /([a-z0-9]+[_a-z0-9.-][a-z0-9]+)@([a-z0-9-]+(?:.[a-z0-9-]+).[a-z]{2,4})/;
const BAD_EMAIL_ERROR = 'Invalid email address';
function isValidInput(value, regex) {
return regex.test(value);
}
function renderErrorMessage(error) {
const $error = document.getElementById('auth_error');
$error.textContent = error;
$error.style.display = 'block';
}
function showBadInputError($input, error) {
renderErrorMessage(error);
$input.focus();
const myInput = $input;
myInput.style.border = '1px solid var(--bg-danger)';
myInput.style.boxShadow = 'none';
document.getElementById('auth_submit-btn').disabled = true;
}
function validateOnChange(e, regex, error) {
if (!isValidInput(e.target.value, regex) && e.target.value) {
showBadInputError(e.target, error);
return;
}
document.getElementById('auth_error').style.display = 'none';
e.target.style.removeProperty('border');
e.target.style.removeProperty('box-shadow');
document.getElementById('auth_submit-btn').disabled = false;
}
function Auth({ type }) {
const [process, changeProcess] = useState(null);
const usernameRef = useRef(null);
const homeserverRef = useRef(null);
const passwordRef = useRef(null);
const confirmPasswordRef = useRef(null);
const emailRef = useRef(null);
function register(recaptchaValue, terms, verified) {
auth.register(
usernameRef.current.value,
homeserverRef.current.value,
passwordRef.current.value,
emailRef.current.value,
recaptchaValue,
terms,
verified,
).then((res) => {
document.getElementById('auth_submit-btn').disabled = false;
if (res.type === 'recaptcha') {
changeProcess({ type: res.type, sitekey: res.public_key });
return;
}
if (res.type === 'terms') {
changeProcess({ type: res.type, en: res.en });
}
if (res.type === 'email') {
changeProcess({ type: res.type });
}
if (res.type === 'done') {
window.location.replace('/');
}
}).catch((error) => {
changeProcess(null);
renderErrorMessage(error);
document.getElementById('auth_submit-btn').disabled = false;
});
if (terms) {
changeProcess({ type: 'loading', message: 'Sending email verification link...' });
} else changeProcess({ type: 'loading', message: 'Registration in progress...' });
}
function handleLogin(e) {
e.preventDefault();
document.getElementById('auth_submit-btn').disabled = true;
document.getElementById('auth_error').style.display = 'none';
if (!isValidInput(usernameRef.current.value, USERNAME_REGEX)) {
showBadInputError(usernameRef.current, BAD_USERNAME_ERROR);
return;
}
auth.login(usernameRef.current.value, homeserverRef.current.value, passwordRef.current.value)
.then(() => {
document.getElementById('auth_submit-btn').disabled = false;
window.location.replace('/');
})
.catch((error) => {
changeProcess(null);
renderErrorMessage(error);
document.getElementById('auth_submit-btn').disabled = false;
});
changeProcess({ type: 'loading', message: 'Login in progress...' });
}
function handleRegister(e) {
e.preventDefault();
document.getElementById('auth_submit-btn').disabled = true;
document.getElementById('auth_error').style.display = 'none';
if (!isValidInput(usernameRef.current.value, USERNAME_REGEX)) {
showBadInputError(usernameRef.current, BAD_USERNAME_ERROR);
return;
}
if (!isValidInput(passwordRef.current.value, PASSWORD_STRENGHT_REGEX)) {
showBadInputError(passwordRef.current, BAD_PASSWORD_ERROR);
return;
}
if (passwordRef.current.value !== confirmPasswordRef.current.value) {
showBadInputError(confirmPasswordRef.current, CONFIRM_PASSWORD_ERROR);
return;
}
if (!isValidInput(emailRef.current.value, EMAIL_REGEX)) {
showBadInputError(emailRef.current, BAD_EMAIL_ERROR);
return;
}
register();
}
const handleAuth = (type === 'login') ? handleLogin : handleRegister;
return (
<>
{process?.type === 'loading' && <LoadingScreen message={process.message} />}
{process?.type === 'recaptcha' && <Recaptcha message="Please check the box below to proceed." sitekey={process.sitekey} onChange={(v) => { if (typeof v === 'string') register(v); }} />}
{process?.type === 'terms' && <Terms url={process.en.url} onSubmit={register} />}
{process?.type === 'email' && (
<ProcessWrapper>
<div style={{ margin: 'var(--sp-normal)', maxWidth: '450px' }}>
<Text variant="h2">Verify email</Text>
<div style={{ margin: 'var(--sp-normal) 0' }}>
<Text variant="b1">
Please check your email
{' '}
<b>{`(${emailRef.current.value})`}</b>
{' '}
and validate before continuing further.
</Text>
</div>
<Button variant="primary" onClick={() => register(undefined, undefined, true)}>Continue</Button>
</div>
</ProcessWrapper>
)}
<StaticWrapper>
<div className="auth-form__wrapper flex-v--center">
<form onSubmit={handleAuth} className="auth-form">
<Text variant="h2">{ type === 'login' ? 'Login' : 'Register' }</Text>
<div className="username__wrapper">
<Input
forwardRef={usernameRef}
onChange={(e) => validateOnChange(e, USERNAME_REGEX, BAD_USERNAME_ERROR)}
id="auth_username"
label="Username"
required
/>
<Input
forwardRef={homeserverRef}
id="auth_homeserver"
placeholder="Homeserver"
value="matrix.org"
required
/>
</div>
<Input
forwardRef={passwordRef}
onChange={(e) => validateOnChange(e, ((type === 'login') ? PASSWORD_REGEX : PASSWORD_STRENGHT_REGEX), BAD_PASSWORD_ERROR)}
id="auth_password"
type="password"
label="Password"
required
/>
{type === 'register' && (
<>
<Input
forwardRef={confirmPasswordRef}
onChange={(e) => validateOnChange(e, new RegExp(`^(${passwordRef.current.value})$`), CONFIRM_PASSWORD_ERROR)}
id="auth_confirmPassword"
type="password"
label="Confirm password"
required
/>
<Input
forwardRef={emailRef}
onChange={(e) => validateOnChange(e, EMAIL_REGEX, BAD_EMAIL_ERROR)}
id="auth_email"
type="email"
label="Email"
required
/>
</>
)}
<div className="submit-btn__wrapper flex--end">
<Text id="auth_error" className="error-message" variant="b3">Error</Text>
<Button
id="auth_submit-btn"
variant="primary"
type="submit"
>
{type === 'login' ? 'Login' : 'Register' }
</Button>
</div>
</form>
</div>
<div className="flex--center">
<Text variant="b2">
{`${(type === 'login' ? 'Don\'t have' : 'Already have')} an account?`}
<Link to={type === 'login' ? '/register' : '/login'}>
{ type === 'login' ? ' Register' : ' Login' }
</Link>
</Text>
</div>
</StaticWrapper>
</>
);
}
Auth.propTypes = {
type: PropTypes.string.isRequired,
};
function StaticWrapper({ children }) {
return (
<div className="auth__wrapper flex--center">
<div className="auth-card">
<div className="auth-card__interactive flex-v">
<div className="app-ident flex">
<img className="app-ident__logo noselect" src={CinnySvg} alt="Cinny logo" />
<div className="app-ident__text flex-v--center">
<Text variant="h2">Cinny</Text>
<Text variant="b2">Yet another matrix client.</Text>
</div>
</div>
{ children }
</div>
</div>
</div>
);
}
StaticWrapper.propTypes = {
children: PropTypes.node.isRequired,
};
function LoadingScreen({ message }) {
return (
<ProcessWrapper>
<Spinner />
<div style={{ marginTop: 'var(--sp-normal)' }}>
<Text variant="b1">{message}</Text>
</div>
</ProcessWrapper>
);
}
LoadingScreen.propTypes = {
message: PropTypes.string.isRequired,
};
function Recaptcha({ message, sitekey, onChange }) {
return (
<ProcessWrapper>
<div style={{ marginBottom: 'var(--sp-normal)' }}>
<Text variant="s1">{message}</Text>
</div>
<ReCAPTCHA sitekey={sitekey} onChange={onChange} />
</ProcessWrapper>
);
}
Recaptcha.propTypes = {
message: PropTypes.string.isRequired,
sitekey: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
};
function Terms({ url, onSubmit }) {
return (
<ProcessWrapper>
<form onSubmit={() => onSubmit(undefined, true)}>
<div style={{ margin: 'var(--sp-normal)', maxWidth: '450px' }}>
<Text variant="h2">Agree with terms</Text>
<div style={{ marginBottom: 'var(--sp-normal)' }} />
<Text variant="b1">In order to complete registration, you need to agree with terms and conditions.</Text>
<div style={{ display: 'flex', alignItems: 'center', margin: 'var(--sp-normal) 0' }}>
<input id="termsCheckbox" type="checkbox" required />
<Text variant="b1">
{'I accept '}
<a style={{ cursor: 'pointer' }} href={url} rel="noreferrer" target="_blank">Terms and Conditions</a>
</Text>
</div>
<Button id="termsBtn" type="submit" variant="primary">Submit</Button>
</div>
</form>
</ProcessWrapper>
);
}
Terms.propTypes = {
url: PropTypes.string.isRequired,
onSubmit: PropTypes.func.isRequired,
};
function ProcessWrapper({ children }) {
return (
<div className="process-wrapper">
{children}
</div>
);
}
ProcessWrapper.propTypes = {
children: PropTypes.node.isRequired,
};
export default Auth;