2021-07-28 18:45:52 +05:30
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' ;
2021-09-14 08:33:17 +05:30
import ScrollView from '../../atoms/scroll/ScrollView' ;
2021-07-28 18:45:52 +05:30
import CinnySvg from '../../../../public/res/svg/cinny.svg' ;
2021-09-03 17:58:01 +05:30
// This regex validates historical usernames, which don't satisfy today's username requirements.
2021-07-28 22:07:58 -07:00
// See https://matrix.org/docs/spec/appendices#id13 for more info.
2021-08-12 16:18:01 +05:30
const LOCALPART _LOGIN _REGEX = /.*/ ;
2021-07-29 11:21:11 +05:30
const LOCALPART _SIGNUP _REGEX = /^[a-z0-9_\-.=/]+$/ ;
const BAD _LOCALPART _ERROR = 'Username must contain only a-z, 0-9, ., _, =, -, and /.' ;
2021-07-28 22:07:58 -07:00
const USER _ID _TOO _LONG _ERROR = 'Your user ID, including the hostname, can\'t be more than 255 characters long.' ;
2021-07-28 18:45:52 +05:30
const PASSWORD _REGEX = /.+/ ;
2021-07-29 13:58:15 +05:30
const PASSWORD _STRENGHT _REGEX = /^(?=.*\d)(?=.*[A-Z])(?=.*[a-z])(?=.*[^\w\d\s:])([^\s]){8,127}$/ ;
2021-07-31 16:22:54 +02:00
const BAD _PASSWORD _ERROR = 'Password must contain at least 1 number, 1 uppercase letter, 1 lowercase letter, 1 non-alphanumeric character. Passwords can range from 8-127 characters with no whitespaces.' ;
const CONFIRM _PASSWORD _ERROR = 'Passwords don\'t match.' ;
2021-07-28 18:45:52 +05:30
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 ;
}
2021-07-28 22:07:58 -07:00
/**
* Normalizes a username into a standard format.
*
* Removes leading and trailing whitespaces and leading "@" symbols.
* @param {string} rawUsername A raw-input username, which may include invalid characters.
* @returns {string}
*/
function normalizeUsername ( rawUsername ) {
const noLeadingAt = rawUsername . indexOf ( '@' ) === 0 ? rawUsername . substr ( 1 ) : rawUsername ;
return noLeadingAt . trim ( ) ;
}
2021-07-28 18:45:52 +05:30
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' ;
2021-07-28 22:07:58 -07:00
/** @type {string} */
const rawUsername = usernameRef . current . value ;
/** @type {string} */
const normalizedUsername = normalizeUsername ( rawUsername ) ;
auth . login ( normalizedUsername , homeserverRef . current . value , passwordRef . current . value )
2021-07-28 18:45:52 +05:30
. 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' ;
2021-07-28 22:07:58 -07:00
if ( ! isValidInput ( usernameRef . current . value , LOCALPART _SIGNUP _REGEX ) ) {
showBadInputError ( usernameRef . current , BAD _LOCALPART _ERROR ) ;
2021-07-28 18:45:52 +05:30
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 ;
}
2021-07-28 22:07:58 -07:00
if ( ` @ ${ usernameRef . current . value } : ${ homeserverRef . current . value } ` . length > 255 ) {
showBadInputError ( usernameRef . current , USER _ID _TOO _LONG _ERROR ) ;
return ;
}
2021-07-28 18:45:52 +05:30
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 }
2021-07-28 22:07:58 -07:00
onChange = { ( e ) => ( type === 'login'
? validateOnChange ( e , LOCALPART _LOGIN _REGEX , BAD _LOCALPART _ERROR )
: validateOnChange ( e , LOCALPART _SIGNUP _REGEX , BAD _LOCALPART _ERROR ) ) }
2021-07-28 18:45:52 +05:30
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 (
2021-09-14 08:33:17 +05:30
< ScrollView invisible >
< 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 >
2021-07-28 18:45:52 +05:30
< / div >
2021-09-14 08:33:17 +05:30
{ children }
2021-07-28 18:45:52 +05:30
< / div >
< / div >
< / div >
2021-09-14 08:33:17 +05:30
< / ScrollView >
2021-07-28 18:45:52 +05:30
) ;
}
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)' } } / >
2021-07-31 16:22:54 +02:00
< Text variant = "b1" > In order to complete registration , you need to agree to the terms and conditions . < / Text >
2021-07-28 18:45:52 +05:30
< 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 ;