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' ;
2021-10-19 19:38:09 +05:30
import { useLocation } from 'react-router-dom' ;
2021-07-28 18:45:52 +05:30
import * as auth from '../../../client/action/auth' ;
2021-10-10 16:36:44 -05:00
import cons from '../../../client/state/cons' ;
2021-07-28 18:45:52 +05:30
import Text from '../../atoms/text/Text' ;
import Button from '../../atoms/button/Button' ;
2021-09-14 09:01:31 +05:30
import IconButton from '../../atoms/button/IconButton' ;
2021-07-28 18:45:52 +05:30
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
2021-09-14 09:01:31 +05:30
import EyeIC from '../../../../public/res/ic/outlined/eye.svg' ;
2021-07-28 18:45:52 +05:30
import CinnySvg from '../../../../public/res/svg/cinny.svg' ;
2021-10-10 16:36:44 -05:00
import SSOButtons from '../../molecules/sso-buttons/SSOButtons' ;
2021-07-28 18:45:52 +05:30
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 ) {
2021-10-06 12:35:51 +05:30
if ( typeof regex === 'string' ) return regex === value ;
2021-07-28 18:45:52 +05:30
return regex . test ( value ) ;
}
function renderErrorMessage ( error ) {
const $error = document . getElementById ( 'auth_error' ) ;
$error . textContent = error ;
$error . style . display = 'block' ;
}
2021-10-06 12:35:51 +05:30
function showBadInputError ( $input , error , stopAutoFocus ) {
2021-07-28 18:45:52 +05:30
renderErrorMessage ( error ) ;
2021-10-06 12:35:51 +05:30
if ( ! stopAutoFocus ) $input . focus ( ) ;
2021-07-28 18:45:52 +05:30
const myInput = $input ;
myInput . style . border = '1px solid var(--bg-danger)' ;
myInput . style . boxShadow = 'none' ;
document . getElementById ( 'auth_submit-btn' ) . disabled = true ;
}
2021-10-06 12:35:51 +05:30
function validateOnChange ( targetInput , regex , error , stopAutoFocus ) {
if ( ! isValidInput ( targetInput . value , regex ) && targetInput . value ) {
showBadInputError ( targetInput , error , stopAutoFocus ) ;
return false ;
2021-07-28 18:45:52 +05:30
}
document . getElementById ( 'auth_error' ) . style . display = 'none' ;
2021-10-06 12:35:51 +05:30
targetInput . style . removeProperty ( 'border' ) ;
targetInput . style . removeProperty ( 'box-shadow' ) ;
2021-07-28 18:45:52 +05:30
document . getElementById ( 'auth_submit-btn' ) . disabled = false ;
2021-10-06 12:35:51 +05:30
return true ;
2021-07-28 18:45:52 +05:30
}
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-10-19 19:38:09 +05:30
function Auth ( ) {
const [ type , setType ] = useState ( 'login' ) ;
2021-07-28 18:45:52 +05:30
const [ process , changeProcess ] = useState ( null ) ;
2021-10-10 16:36:44 -05:00
const [ homeserver , changeHomeserver ] = useState ( 'matrix.org' ) ;
2021-07-28 18:45:52 +05:30
const usernameRef = useRef ( null ) ;
const homeserverRef = useRef ( null ) ;
const passwordRef = useRef ( null ) ;
const confirmPasswordRef = useRef ( null ) ;
const emailRef = useRef ( null ) ;
2021-10-10 16:36:44 -05:00
const { search } = useLocation ( ) ;
const searchParams = new URLSearchParams ( search ) ;
if ( searchParams . has ( 'loginToken' ) ) {
const loginToken = searchParams . get ( 'loginToken' ) ;
if ( loginToken !== undefined ) {
if ( localStorage . getItem ( cons . secretKey . BASE _URL ) !== undefined ) {
const baseUrl = localStorage . getItem ( cons . secretKey . BASE _URL ) ;
auth . loginWithToken ( baseUrl , loginToken )
. then ( ( ) => {
2021-10-19 20:23:15 +05:30
const { href } = window . location ;
window . location . replace ( href . slice ( 0 , href . indexOf ( '?' ) ) ) ;
2021-10-10 16:36:44 -05:00
} )
. catch ( ( error ) => {
changeProcess ( null ) ;
if ( ! error . contains ( 'CORS request rejected' ) ) {
renderErrorMessage ( error ) ;
}
} ) ;
}
}
}
2021-07-28 18:45:52 +05:30
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'
2021-10-06 12:35:51 +05:30
? validateOnChange ( e . target , LOCALPART _LOGIN _REGEX , BAD _LOCALPART _ERROR )
: validateOnChange ( e . target , LOCALPART _SIGNUP _REGEX , BAD _LOCALPART _ERROR ) ) }
2021-07-28 18:45:52 +05:30
id = "auth_username"
label = "Username"
required
/ >
< Input
forwardRef = { homeserverRef }
2021-10-10 16:36:44 -05:00
onChange = { ( e ) => changeHomeserver ( e . target . value ) }
2021-07-28 18:45:52 +05:30
id = "auth_homeserver"
placeholder = "Homeserver"
value = "matrix.org"
required
/ >
< / div >
2021-09-14 09:01:31 +05:30
< div className = "password__wrapper" >
< Input
forwardRef = { passwordRef }
2021-10-06 12:35:51 +05:30
onChange = { ( e ) => {
const isValidPass = validateOnChange ( e . target , ( ( type === 'login' ) ? PASSWORD _REGEX : PASSWORD _STRENGHT _REGEX ) , BAD _PASSWORD _ERROR ) ;
if ( type === 'register' && isValidPass ) {
validateOnChange (
confirmPasswordRef . current , passwordRef . current . value ,
CONFIRM _PASSWORD _ERROR , true ,
) ;
}
} }
2021-09-14 09:01:31 +05:30
id = "auth_password"
type = "password"
label = "Password"
required
/ >
< IconButton
onClick = { ( ) => {
if ( passwordRef . current . type === 'password' ) {
passwordRef . current . type = 'text' ;
} else passwordRef . current . type = 'password' ;
} }
size = "extra-small"
src = { EyeIC }
/ >
< / div >
2021-07-28 18:45:52 +05:30
{ type === 'register' && (
< >
2021-09-14 09:01:31 +05:30
< div className = "password__wrapper" >
< Input
forwardRef = { confirmPasswordRef }
2021-10-06 12:35:51 +05:30
onChange = { ( e ) => {
validateOnChange ( e . target , passwordRef . current . value , CONFIRM _PASSWORD _ERROR ) ;
} }
2021-09-14 09:01:31 +05:30
id = "auth_confirmPassword"
type = "password"
label = "Confirm password"
required
/ >
< IconButton
onClick = { ( ) => {
if ( confirmPasswordRef . current . type === 'password' ) {
confirmPasswordRef . current . type = 'text' ;
} else confirmPasswordRef . current . type = 'password' ;
} }
size = "extra-small"
src = { EyeIC }
/ >
< / div >
2021-07-28 18:45:52 +05:30
< Input
forwardRef = { emailRef }
2021-10-06 12:35:51 +05:30
onChange = { ( e ) => validateOnChange ( e . target , EMAIL _REGEX , BAD _EMAIL _ERROR ) }
2021-07-28 18:45:52 +05:30
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 >
2021-10-10 16:36:44 -05:00
{ type === 'login' && (
< SSOButtons homeserver = { homeserver } / >
) }
2021-07-28 18:45:52 +05:30
< / form >
< / div >
2021-10-19 20:23:15 +05:30
< div style = { { flexDirection : 'column' } } className = "flex--center" >
2021-07-28 18:45:52 +05:30
< Text variant = "b2" >
{ ` ${ ( type === 'login' ? 'Don\'t have' : 'Already have' ) } an account? ` }
2021-10-19 19:38:09 +05:30
< button
type = "button"
style = { { color : 'var(--tc-link)' , cursor : 'pointer' , margin : '0 var(--sp-ultra-tight)' } }
onClick = { ( ) => {
if ( type === 'login' ) setType ( 'register' ) ;
else setType ( 'login' ) ;
} }
>
2021-07-28 18:45:52 +05:30
{ type === 'login' ? ' Register' : ' Login' }
2021-10-19 19:38:09 +05:30
< / button >
2021-07-28 18:45:52 +05:30
< / Text >
2021-10-19 20:23:15 +05:30
< span style = { { marginTop : 'var(--sp-extra-tight)' } } >
2021-10-25 17:53:25 +05:30
< Text variant = "b3" > v1 .4 .0 < / Text >
2021-10-19 20:23:15 +05:30
< / span >
2021-07-28 18:45:52 +05:30
< / div >
< / StaticWrapper >
< / >
) ;
}
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 ;