openstackid/resources/js/login/login.js

587 lines
23 KiB
JavaScript

import styles from './login.module.scss'
import "./third_party_identity_providers.scss";
import React from 'react';
import ReCAPTCHA from "react-google-recaptcha";
import ReactDOM from 'react-dom';
import Avatar from '@material-ui/core/Avatar';
import Button from '@material-ui/core/Button';
import CssBaseline from '@material-ui/core/CssBaseline';
import TextField from '@material-ui/core/TextField';
import Link from '@material-ui/core/Link';
import Typography from '@material-ui/core/Typography';
import Paper from '@material-ui/core/Paper';
import Container from '@material-ui/core/Container';
import Chip from '@material-ui/core/Chip';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Checkbox from '@material-ui/core/Checkbox';
import {verifyAccount, emitOTP} from './actions';
import {MuiThemeProvider, createMuiTheme} from '@material-ui/core/styles';
import DividerWithText from '../components/divider_with_text';
import Visibility from '@material-ui/icons/Visibility';
import VisibilityOff from '@material-ui/icons/VisibilityOff';
import InputAdornment from '@material-ui/core/InputAdornment';
import IconButton from '@material-ui/core/IconButton';
import {emailValidator} from '../validator';
import Grid from '@material-ui/core/Grid';
import Swal from 'sweetalert2'
const EmailInputForm = ({onValidateEmail, onHandleUserNameChange, disableInput, emailError}) => {
return (
<Paper elevation={0} component="form"
target="_self"
className={styles.paper_root}
onSubmit={onValidateEmail}>
<TextField
id="email"
name="email"
autoComplete="email"
variant="outlined"
margin="normal"
required
fullWidth
disabled={disableInput}
label="Email Address"
autoFocus
onChange={onHandleUserNameChange}
error={emailError != ""}
helperText={emailError}
/>
{emailError == "" &&
<Button variant="contained"
color="primary"
title="Continue"
className={styles.apply_button}
disabled={disableInput}
onClick={onValidateEmail}>
&gt;
</Button>
}
</Paper>
);
}
const PasswordInputForm = ({
formAction,
onAuthenticate,
disableInput,
showPassword,
passwordValue,
passwordError,
onUserPasswordChange,
handleClickShowPassword,
handleMouseDownPassword,
userNameValue,
csrfToken,
shouldShowCaptcha,
captchaPublicKey,
onChangeRecaptcha
}) => {
return(
<form method="post" action={formAction} onSubmit={onAuthenticate} target="_self">
<TextField
id="password"
name="password"
disabled={disableInput}
type={showPassword ? 'text' : 'password'}
value={passwordValue}
variant="outlined"
margin="normal"
required
fullWidth
label="Enter Your Password"
autoComplete="current-password"
error={passwordError != ""}
helperText={passwordError}
onChange={onUserPasswordChange}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
edge="end"
>
{showPassword ? <Visibility/> : <VisibilityOff/>}
</IconButton>
</InputAdornment>
)
}}
/>
<FormControlLabel
disabled={disableInput}
control={<Checkbox value="remember" name="remember" id="remember" color="primary"/>}
label="Remember me"
/>
<input type="hidden" value={userNameValue} id="username" name="username"/>
<input type="hidden" value={csrfToken} id="_token" name="_token"/>
<input type="hidden" value="password" id="flow" name="flow"/>
{shouldShowCaptcha() &&
<ReCAPTCHA
className={styles.recaptcha}
sitekey={captchaPublicKey}
onChange={onChangeRecaptcha}
/>
}
<Button variant="contained"
disabled={disableInput}
className={styles.continue_btn}
color="primary"
type="submit"
onClick={onAuthenticate}>
Continue
</Button>
</form>
);
}
const OTPInputForm = ({
formAction,
onAuthenticate,
disableInput,
showPassword,
passwordValue,
passwordError,
onUserPasswordChange,
handleClickShowPassword,
handleMouseDownPassword,
userNameValue,
csrfToken,
shouldShowCaptcha,
captchaPublicKey,
onChangeRecaptcha
}) => {
return(
<form method="post" action={formAction} onSubmit={onAuthenticate}>
<TextField
id="password"
name="password"
disabled={disableInput}
type={showPassword ? 'text' : 'password'}
value={passwordValue}
variant="outlined"
margin="normal"
required
fullWidth
label="Enter Your verification code"
autoComplete="new-password"
error={passwordError != ""}
helperText={passwordError}
onChange={onUserPasswordChange}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle code visibility"
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
edge="end"
>
{showPassword ? <Visibility/> : <VisibilityOff/>}
</IconButton>
</InputAdornment>
)
}}
/>
<p>A Verification Code was just sent to your Email.</p>
<FormControlLabel
disabled={disableInput}
control={<Checkbox value="remember" name="remember" id="remember" color="primary"/>}
label="Remember me"
/>
<input type="hidden" value={userNameValue} id="username" name="username"/>
<input type="hidden" value={csrfToken} id="_token" name="_token"/>
<input type="hidden" value="otp" id="flow" name="flow"/>
<input type="hidden" value="email" id="connection" name="connection"/>
{shouldShowCaptcha() &&
<ReCAPTCHA
className={styles.recaptcha}
sitekey={captchaPublicKey}
onChange={onChangeRecaptcha}
/>
}
<Button variant="contained"
disabled={disableInput}
className={styles.continue_btn}
color="primary"
type="submit"
onClick={onAuthenticate}>
Verify
</Button>
</form>
);
}
const HelpLinks = ({forgotPasswordAction, verifyEmailAction, helpAction, appName, emitOtpAction}) => {
return (
<>
<hr className={styles.separator}/>
<Link href="#" onClick={emitOtpAction} variant="body2" target="_self" >
Get A Login Code emailed to you
</Link>
<Link href={forgotPasswordAction} target="_self" variant="body2">
Forgot password?
</Link>
<Link href={verifyEmailAction} target="_self" variant="body2">
Verify {appName}
</Link>
<Link href={helpAction} variant="body2" target="_self">
Having trouble?
</Link>
</>
);
}
const OTPHelpLinks = ({emitOtpAction}) => {
return (
<>
<hr className={styles.separator}/>
<p className={styles.otp_p}>Didn't receive it ?</p>
<p className={styles.otp_p}>Check your spam folder or <Link href="#" onClick={emitOtpAction} variant="body2" target="_self">resend email.</Link>
</p>
</>
);
}
const EmailErrorActions = ({createAccountAction, onValidateEmail, disableInput}) => {
return(
<Grid container style={{alignItems: 'center', marginTop: "20%"}}>
<Grid item xs>
<Link href={createAccountAction} variant="body2" target="_self">
Create Account
</Link>
</Grid>
<Grid item>
<Button variant="contained"
onClick={onValidateEmail}
disabled={disableInput}
color="primary">
Continue
</Button>
</Grid>
</Grid>
);
}
const ThirdPartyIdentityProviders = ({thirdPartyProviders, formAction, disableInput}) => {
return(
<>
<DividerWithText>Or</DividerWithText>
{
thirdPartyProviders.map((provider) => {
return (
<Button
disabled={disableInput}
key={provider.name}
variant="contained"
className={styles.third_party_idp_button+` ${provider.name}`}
color="primary"
title={`Sign In with ${provider.label}`}
href={`${formAction}/${provider.name}`}>
{provider.label}
</Button>
);
})
}
</>
);
}
class LoginPage extends React.Component {
constructor(props) {
super(props);
this.state = {
user_name: props.userName,
user_password: '',
user_pic: props.hasOwnProperty('user_pic') ? props.user_pic : null,
user_fullname: props.hasOwnProperty('user_fullname') ? props.user_fullname : null,
user_verified: props.hasOwnProperty('user_verified') ? props.user_verified : false,
errors: {
email: "",
password: props.authError != "" ? props.authError : "",
},
captcha_value: '',
showPassword: false,
disableInput: false,
authFlow: props.flow,
}
this.onHandleUserNameChange = this.onHandleUserNameChange.bind(this);
this.onValidateEmail = this.onValidateEmail.bind(this);
this.handleDelete = this.handleDelete.bind(this);
this.onAuthenticate = this.onAuthenticate.bind(this);
this.onChangeRecaptcha = this.onChangeRecaptcha.bind(this);
this.onUserPasswordChange = this.onUserPasswordChange.bind(this);
this.shouldShowCaptcha = this.shouldShowCaptcha.bind(this);
this.handleClickShowPassword = this.handleClickShowPassword.bind(this);
this.handleMouseDownPassword = this.handleMouseDownPassword.bind(this);
this.handleEmitOtpAction = this.handleEmitOtpAction.bind(this);
}
handleEmitOtpAction(ev){
ev.preventDefault();
let user_fullname = this.state.user_fullname ? this.state.user_fullname : this.state.user_name;
emitOTP(this.state.user_name, this.props.token).then((payload) => {
let {response} = payload;
this.setState({...this.state,
authFlow:"otp",
errors: {
email: "",
password:"",
},
user_verified: true,
user_fullname:user_fullname,
});
}, (error) => {
let {response, status, message} = error;
Swal('Oops...', 'Something went wrong!', 'error')
});
return false;
}
shouldShowCaptcha() {
return (
this.props.hasOwnProperty('maxLoginAttempts2ShowCaptcha') &&
this.props.hasOwnProperty('loginAttempts') &&
this.props.loginAttempts >= this.props.maxLoginAttempts2ShowCaptcha
)
}
onAuthenticate(ev) {
if (this.state.user_password == '') {
let error = 'Password is empty';
if(this.state.authFlow == 'OTP'){
error = 'Verification Code is empty';
}
this.setState({...this.state, errors: {...this.state.errors, password: error}});
ev.preventDefault();
return false;
}
if (this.state.captcha_value == '' && this.shouldShowCaptcha()) {
this.setState({...this.state, errors: {...this.state.errors, password: 'you must check CAPTCHA'}});
ev.preventDefault();
return false;
}
return true;
}
onChangeRecaptcha(value) {
this.setState({...this.state, captcha_value: value});
}
onHandleUserNameChange(ev) {
let {value, id} = ev.target;
this.setState({...this.state, user_name: value});
}
onUserPasswordChange(ev) {
let {errors} = this.state;
let {value, id} = ev.target;
if(value == "") // clean error
errors[id] = '';
this.setState({...this.state, user_password: value, errors: {...errors}});
}
onValidateEmail(ev) {
ev.preventDefault();
if (this.state.user_name == '') {
return false;
}
if (!emailValidator(this.state.user_name)) {
return false;
}
this.setState({...this.state, disableInput: true});
verifyAccount(this.state.user_name).then((payload) => {
let {response} = payload;
this.setState({
...this.state,
user_pic: response.pic,
user_fullname: response.full_name,
user_verified: true,
errors: {
email: '',
password: ''
},
disableInput: false
})
}, (error) => {
let {response, status, message} = error;
let newErrors = {};
newErrors['password'] = '';
newErrors['email'] = "We could not find an Account with that email Address";
if(status == 429){
newErrors['email'] = "Too many requests. Try it later.";
}
this.setState({
...this.state,
user_pic: null,
user_fullname: null,
user_verified: false,
errors: newErrors,
disableInput: false
});
});
return true;
}
handleDelete() {
this.setState({...this.state, user_name: null, user_pic: null, user_fullname: null, user_verified: false, authFlow:"password", errors: {
email: "",
password:"",
},});
}
handleClickShowPassword(ev) {
this.setState({...this.state, showPassword: !this.state.showPassword})
}
handleMouseDownPassword(ev) {
ev.preventDefault();
}
render() {
return (
<Container component="main" maxWidth="xs" className={styles.main_container}>
<CssBaseline/>
<div className={styles.inner_container}>
<Typography component="h1">
<a href={window.location.href}><img className={styles.app_logo} alt="appLogo" src={this.props.appLogo}/></a>
</Typography>
<Typography component="h1" variant="h5">
Sign in {this.state.user_fullname && <Chip
avatar={<Avatar alt={this.state.user_fullname} src={this.state.user_pic}/>}
variant="outlined"
className={styles.valid_user_name_chip}
label={this.state.user_fullname}
onDelete={this.handleDelete}/>}
</Typography>
{!this.state.user_verified &&
<>
<EmailInputForm
onValidateEmail={this.onValidateEmail}
onHandleUserNameChange={this.onHandleUserNameChange}
disableInput={this.state.disableInput}
emailError={this.state.errors.email}/>
{ this.state.errors.email == '' &&
this.props.thirdPartyProviders.length > 0 &&
<ThirdPartyIdentityProviders
thirdPartyProviders={this.props.thirdPartyProviders}
formAction={this.props.formAction}
disableInput={this.state.disableInput}
/>
}
{
// we already had an interaction and got an user error...
this.state.errors.email != '' &&
<>
<EmailErrorActions
onValidateEmail={this.onValidateEmail}
disableInput={this.state.disableInput}
createAccountAction={(this.state.user_name) ? `${this.props.createAccountAction}?email=${encodeURIComponent(this.state.user_name)}`: this.props.createAccountAction}
/>
<HelpLinks
appName={this.props.appName}
forgotPasswordAction={this.props.forgotPasswordAction}
verifyEmailAction={this.props.verifyEmailAction}
helpAction={this.props.helpAction}
emitOtpAction={this.handleEmitOtpAction}
/>
</>
}
</>
}
{this.state.user_verified && this.state.authFlow == 'password' &&
// proceed to ask for password ( 2nd step )
<>
<PasswordInputForm
formAction={this.props.formAction}
onAuthenticate={this.onAuthenticate}
disableInput={this.state.disableInput}
showPassword={this.state.showPassword}
passwordValue={this.state.user_password}
passwordError={this.state.errors.password}
onUserPasswordChange={this.onUserPasswordChange}
handleClickShowPassword={this.handleClickShowPassword}
handleMouseDownPassword={this.handleMouseDownPassword}
userNameValue={this.state.user_name}
csrfToken={this.props.token}
shouldShowCaptcha={this.shouldShowCaptcha}
captchaPublicKey={this.props.captchaPublicKey}
onChangeRecaptcha={this.onChangeRecaptcha}
/>
<HelpLinks
appName={this.props.appName}
forgotPasswordAction={this.props.forgotPasswordAction}
verifyEmailAction={this.props.verifyEmailAction}
helpAction={this.props.helpAction}
emitOtpAction={this.handleEmitOtpAction}
/>
</>
}
{this.state.user_verified && this.state.authFlow == 'otp' &&
// proceed to ask for password ( 2nd step )
<>
<OTPInputForm
formAction={this.props.formAction}
onAuthenticate={this.onAuthenticate}
disableInput={this.state.disableInput}
showPassword={this.state.showPassword}
passwordValue={this.state.user_password}
passwordError={this.state.errors.password}
onUserPasswordChange={this.onUserPasswordChange}
handleClickShowPassword={this.handleClickShowPassword}
handleMouseDownPassword={this.handleMouseDownPassword}
userNameValue={this.state.user_name}
csrfToken={this.props.token}
shouldShowCaptcha={this.shouldShowCaptcha}
captchaPublicKey={this.props.captchaPublicKey}
onChangeRecaptcha={this.onChangeRecaptcha}
/>
<OTPHelpLinks emitOtpAction={this.handleEmitOtpAction}/>
</>
}
</div>
</Container>
);
}
}
// Or Create your Own theme:
const theme = createMuiTheme({
palette: {
primary: {
main: '#3fa2f7'
},
},
overrides: {
MuiButton: {
containedPrimary: {
color: 'white'
}
}
}
});
ReactDOM.render(
<MuiThemeProvider theme={theme}>
<LoginPage {...config}/>
</MuiThemeProvider>,
document.querySelector('#root')
);