|
|
@@ -0,0 +1,441 @@
|
|
|
+import React, {
|
|
|
+ useState, useEffect, useCallback,
|
|
|
+} from 'react';
|
|
|
+
|
|
|
+import { useTranslation } from 'next-i18next';
|
|
|
+import { useRouter } from 'next/router';
|
|
|
+import ReactCardFlip from 'react-card-flip';
|
|
|
+
|
|
|
+import { apiv3Post } from '~/client/util/apiv3-client';
|
|
|
+
|
|
|
+type LoginFormProps = {
|
|
|
+ username?: string,
|
|
|
+ name?: string,
|
|
|
+ email?: string,
|
|
|
+ isRegistrationEnabled: boolean,
|
|
|
+ isEmailAuthenticationEnabled: boolean,
|
|
|
+ registrationMode?: string,
|
|
|
+ registrationWhiteList: string[],
|
|
|
+ isPasswordResetEnabled: boolean,
|
|
|
+ isLocalStrategySetup: boolean,
|
|
|
+ isLdapStrategySetup: boolean,
|
|
|
+ objOfIsExternalAuthEnableds?: any,
|
|
|
+ isMailerSetup?: boolean
|
|
|
+}
|
|
|
+export const LoginForm = (props: LoginFormProps): JSX.Element => {
|
|
|
+ const { t } = useTranslation();
|
|
|
+ const router = useRouter();
|
|
|
+
|
|
|
+ const {
|
|
|
+ isLocalStrategySetup, isLdapStrategySetup, isPasswordResetEnabled, isRegistrationEnabled,
|
|
|
+ isEmailAuthenticationEnabled, registrationMode, registrationWhiteList, isMailerSetup, objOfIsExternalAuthEnableds,
|
|
|
+ } = props;
|
|
|
+ const isLocalOrLdapStrategiesEnabled = isLocalStrategySetup || isLdapStrategySetup;
|
|
|
+ const isSomeExternalAuthEnabled = Object.values(objOfIsExternalAuthEnableds).some(elem => elem);
|
|
|
+
|
|
|
+ // states
|
|
|
+ const [isRegistering, setIsRegistering] = useState(false);
|
|
|
+ // For Login
|
|
|
+ const [usernameForLogin, setUsernameForLogin] = useState('');
|
|
|
+ const [passwordForLogin, setPasswordForLogin] = useState('');
|
|
|
+ const [loginErrors, setLoginErrors] = useState<Error[]>([]);
|
|
|
+ // For Register
|
|
|
+ const [usernameForRegister, setUsernameForRegister] = useState('');
|
|
|
+ const [nameForRegister, setNameForRegister] = useState('');
|
|
|
+ const [emailForRegister, setEmailForRegister] = useState('');
|
|
|
+ const [passwordForRegister, setPasswordForRegister] = useState('');
|
|
|
+ const [registerErrors, setRegisterErrors] = useState<Error[]>([]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ const { hash } = window.location;
|
|
|
+ if (hash === '#register') {
|
|
|
+ setIsRegistering(true);
|
|
|
+ }
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ // functions
|
|
|
+ const handleLoginWithExternalAuth = useCallback((e) => {
|
|
|
+ const auth = e.currentTarget.id;
|
|
|
+
|
|
|
+ window.location.href = `/passport/${auth}`;
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const handleLoginWithLocalSubmit = useCallback(async(e) => {
|
|
|
+ e.preventDefault();
|
|
|
+
|
|
|
+ const loginForm = {
|
|
|
+ username: usernameForLogin,
|
|
|
+ password: passwordForLogin,
|
|
|
+ };
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await apiv3Post('/login', { loginForm });
|
|
|
+ const { redirectTo } = res.data;
|
|
|
+ router.push(redirectTo);
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ setLoginErrors(err);
|
|
|
+ }
|
|
|
+ return;
|
|
|
+
|
|
|
+ }, [passwordForLogin, router, usernameForLogin]);
|
|
|
+
|
|
|
+ const renderLocalOrLdapLoginForm = useCallback(() => {
|
|
|
+ const { isLdapStrategySetup } = props;
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ {
|
|
|
+ loginErrors != null && loginErrors.length > 0 && (
|
|
|
+ <p className="alert alert-danger">
|
|
|
+ {loginErrors.map((err, index) => {
|
|
|
+ return (
|
|
|
+ <span key={index}>
|
|
|
+ {t(err.message)}<br/>
|
|
|
+ </span>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </p>
|
|
|
+ )
|
|
|
+ }
|
|
|
+ <form role="form" onSubmit={handleLoginWithLocalSubmit} id="login-form">
|
|
|
+ <div className="input-group">
|
|
|
+ <div className="input-group-prepend">
|
|
|
+ <span className="input-group-text">
|
|
|
+ <i className="icon-user"></i>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <input type="text" className="form-control rounded-0" data-testid="tiUsernameForLogin" placeholder="Username or E-mail"
|
|
|
+ onChange={(e) => { setUsernameForLogin(e.target.value) }} name="usernameForLogin" />
|
|
|
+ {isLdapStrategySetup && (
|
|
|
+ <div className="input-group-append">
|
|
|
+ <small className="input-group-text text-success">
|
|
|
+ <i className="icon-fw icon-check"></i> LDAP
|
|
|
+ </small>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="input-group">
|
|
|
+ <div className="input-group-prepend">
|
|
|
+ <span className="input-group-text">
|
|
|
+ <i className="icon-lock"></i>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <input type="password" className="form-control rounded-0" data-testid="tiPasswordForLogin" placeholder="Password"
|
|
|
+ onChange={(e) => { setPasswordForLogin(e.target.value) }} name="passwordForLogin" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="input-group my-4">
|
|
|
+ <button type="submit" id="login" className="btn btn-fill rounded-0 login mx-auto" data-testid="btnSubmitForLogin">
|
|
|
+ <div className="eff"></div>
|
|
|
+ <span className="btn-label">
|
|
|
+ <i className="icon-login"></i>
|
|
|
+ </span>
|
|
|
+ <span className="btn-label-text">{t('Sign in')}</span>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </>
|
|
|
+ );
|
|
|
+ }, [handleLoginWithLocalSubmit, loginErrors, props, t]);
|
|
|
+
|
|
|
+ const renderExternalAuthInput = useCallback((auth) => {
|
|
|
+ const authIconNames = {
|
|
|
+ google: 'google',
|
|
|
+ github: 'github',
|
|
|
+ facebook: 'facebook',
|
|
|
+ twitter: 'twitter',
|
|
|
+ oidc: 'openid',
|
|
|
+ saml: 'key',
|
|
|
+ basic: 'lock',
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div key={auth} className="col-6 my-2">
|
|
|
+ <button type="button" className="btn btn-fill rounded-0" id={auth} onClick={handleLoginWithExternalAuth}>
|
|
|
+ <div className="eff"></div>
|
|
|
+ <span className="btn-label">
|
|
|
+ <i className={`fa fa-${authIconNames[auth]}`}></i>
|
|
|
+ </span>
|
|
|
+ <span className="btn-label-text">{t('Sign in')}</span>
|
|
|
+ </button>
|
|
|
+ <div className="small text-right">by {auth} Account</div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }, [handleLoginWithExternalAuth, t]);
|
|
|
+
|
|
|
+ const renderExternalAuthLoginForm = useCallback(() => {
|
|
|
+ const { isLocalStrategySetup, isLdapStrategySetup, objOfIsExternalAuthEnableds } = props;
|
|
|
+ const isExternalAuthCollapsible = isLocalStrategySetup || isLdapStrategySetup;
|
|
|
+ const collapsibleClass = isExternalAuthCollapsible ? 'collapse collapse-external-auth' : '';
|
|
|
+
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ <div className="grw-external-auth-form border-top border-bottom">
|
|
|
+ <div id="external-auth" className={`external-auth ${collapsibleClass}`}>
|
|
|
+ <div className="row mt-2">
|
|
|
+ {Object.keys(objOfIsExternalAuthEnableds).map((auth) => {
|
|
|
+ if (!objOfIsExternalAuthEnableds[auth]) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ return renderExternalAuthInput(auth);
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div className="text-center">
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ className="btn btn-secondary btn-external-auth-tab btn-sm rounded-0 mb-3"
|
|
|
+ data-toggle={isExternalAuthCollapsible ? 'collapse' : ''}
|
|
|
+ data-target="#external-auth"
|
|
|
+ aria-expanded="false"
|
|
|
+ aria-controls="external-auth"
|
|
|
+ >
|
|
|
+ External Auth
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </>
|
|
|
+ );
|
|
|
+ }, [props, renderExternalAuthInput]);
|
|
|
+
|
|
|
+ const handleRegisterFormSubmit = useCallback(async(e, requestPath) => {
|
|
|
+ e.preventDefault();
|
|
|
+
|
|
|
+ const registerForm = {
|
|
|
+ username: usernameForRegister,
|
|
|
+ name: nameForRegister,
|
|
|
+ email: emailForRegister,
|
|
|
+ password: passwordForRegister,
|
|
|
+ };
|
|
|
+ try {
|
|
|
+ const res = await apiv3Post(requestPath, { registerForm });
|
|
|
+ const { redirectTo } = res.data;
|
|
|
+ router.push(redirectTo);
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ // Execute if error exists
|
|
|
+ if (err != null || err.length > 0) {
|
|
|
+ setRegisterErrors(err);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }, [emailForRegister, nameForRegister, passwordForRegister, router, usernameForRegister]);
|
|
|
+
|
|
|
+ const resetLoginErrors = useCallback(() => {
|
|
|
+ if (loginErrors.length === 0) return;
|
|
|
+ setLoginErrors([]);
|
|
|
+ }, [loginErrors.length]);
|
|
|
+
|
|
|
+ const resetRegisterErrors = useCallback(() => {
|
|
|
+ if (registerErrors.length === 0) return;
|
|
|
+ setRegisterErrors([]);
|
|
|
+ }, [registerErrors.length]);
|
|
|
+
|
|
|
+ const switchForm = useCallback(() => {
|
|
|
+ setIsRegistering(!isRegistering);
|
|
|
+ resetLoginErrors();
|
|
|
+ resetRegisterErrors();
|
|
|
+ }, [isRegistering, resetLoginErrors, resetRegisterErrors]);
|
|
|
+
|
|
|
+ const renderRegisterForm = useCallback(() => {
|
|
|
+ let registerAction = '/register';
|
|
|
+
|
|
|
+ let submitText = t('Sign up');
|
|
|
+ if (isEmailAuthenticationEnabled) {
|
|
|
+ registerAction = '/user-activation/register';
|
|
|
+ submitText = t('page_register.send_email');
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <React.Fragment>
|
|
|
+ {registrationMode === 'Restricted' && (
|
|
|
+ <p className="alert alert-warning">
|
|
|
+ {t('page_register.notice.restricted')}
|
|
|
+ <br />
|
|
|
+ {t('page_register.notice.restricted_defail')}
|
|
|
+ </p>
|
|
|
+ )}
|
|
|
+ { (!isMailerSetup && isEmailAuthenticationEnabled) && (
|
|
|
+ <p className="alert alert-danger">
|
|
|
+ <span>{t('security_settings.Local.please_enable_mailer')}</span>
|
|
|
+ </p>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {
|
|
|
+ registerErrors != null && registerErrors.length > 0 && (
|
|
|
+ <p className="alert alert-danger">
|
|
|
+ {registerErrors.map((err, index) => {
|
|
|
+ return (
|
|
|
+ <span key={index}>
|
|
|
+ {t(err.message)}<br/>
|
|
|
+ </span>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </p>
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ <form role="form" onSubmit={e => handleRegisterFormSubmit(e, registerAction) } id="register-form">
|
|
|
+
|
|
|
+ {!isEmailAuthenticationEnabled && (
|
|
|
+ <div>
|
|
|
+ <div className="input-group" id="input-group-username">
|
|
|
+ <div className="input-group-prepend">
|
|
|
+ <span className="input-group-text">
|
|
|
+ <i className="icon-user"></i>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ {/* username */}
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ className="form-control rounded-0"
|
|
|
+ onChange={(e) => { setUsernameForRegister(e.target.value) }}
|
|
|
+ placeholder={t('User ID')}
|
|
|
+ name="username"
|
|
|
+ defaultValue={props.username}
|
|
|
+ required
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <p className="form-text text-danger">
|
|
|
+ <span id="help-block-username"></span>
|
|
|
+ </p>
|
|
|
+ <div className="input-group">
|
|
|
+ <div className="input-group-prepend">
|
|
|
+ <span className="input-group-text">
|
|
|
+ <i className="icon-tag"></i>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ {/* name */}
|
|
|
+ <input type="text"
|
|
|
+ className="form-control rounded-0"
|
|
|
+ onChange={(e) => { setNameForRegister(e.target.value) }}
|
|
|
+ placeholder={t('Name')}
|
|
|
+ name="name"
|
|
|
+ defaultValue={props.name}
|
|
|
+ required />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ <div className="input-group">
|
|
|
+ <div className="input-group-prepend">
|
|
|
+ <span className="input-group-text">
|
|
|
+ <i className="icon-envelope"></i>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ {/* email */}
|
|
|
+ <input type="email"
|
|
|
+ className="form-control rounded-0"
|
|
|
+ onChange={(e) => { setEmailForRegister(e.target.value) }}
|
|
|
+ placeholder={t('Email')}
|
|
|
+ name="email"
|
|
|
+ defaultValue={props.email}
|
|
|
+ required
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {registrationWhiteList.length > 0 && (
|
|
|
+ <>
|
|
|
+ <p className="form-text">{t('page_register.form_help.email')}</p>
|
|
|
+ <ul>
|
|
|
+ {registrationWhiteList.map((elem) => {
|
|
|
+ return (
|
|
|
+ <li key={elem}>
|
|
|
+ <code>{elem}</code>
|
|
|
+ </li>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </ul>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {!isEmailAuthenticationEnabled && (
|
|
|
+ <div>
|
|
|
+ <div className="input-group">
|
|
|
+ <div className="input-group-prepend">
|
|
|
+ <span className="input-group-text">
|
|
|
+ <i className="icon-lock"></i>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ {/* Password */}
|
|
|
+ <input type="password"
|
|
|
+ className="form-control rounded-0"
|
|
|
+ onChange={(e) => { setPasswordForRegister(e.target.value) }}
|
|
|
+ placeholder={t('Password')}
|
|
|
+ name="password"
|
|
|
+ required />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* Sign up button (submit) */}
|
|
|
+ <div className="input-group justify-content-center my-4">
|
|
|
+ <button
|
|
|
+ className="btn btn-fill rounded-0"
|
|
|
+ id="register"
|
|
|
+ disabled={(!isMailerSetup && isEmailAuthenticationEnabled)}
|
|
|
+ >
|
|
|
+ <div className="eff"></div>
|
|
|
+ <span className="btn-label">
|
|
|
+ <i className="icon-user-follow"></i>
|
|
|
+ </span>
|
|
|
+ <span className="btn-label-text">{submitText}</span>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+
|
|
|
+ <div className="border-bottom"></div>
|
|
|
+
|
|
|
+ <div className="row">
|
|
|
+ <div className="text-right col-12 mt-2 py-2">
|
|
|
+ <a href="#login" id="login" className="link-switch" onClick={switchForm}>
|
|
|
+ <i className="icon-fw icon-login"></i>
|
|
|
+ {t('Sign in is here')}
|
|
|
+ </a>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </React.Fragment>
|
|
|
+ );
|
|
|
+ }, [handleRegisterFormSubmit, isEmailAuthenticationEnabled, isMailerSetup,
|
|
|
+ props.email, props.name, props.username,
|
|
|
+ registerErrors, registrationMode, registrationWhiteList, switchForm, t]);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="noLogin-dialog mx-auto" id="noLogin-dialog">
|
|
|
+ <div className="row mx-0">
|
|
|
+ <div className="col-12">
|
|
|
+ <ReactCardFlip isFlipped={isRegistering} flipDirection="horizontal" cardZIndex="3">
|
|
|
+ <div className="front">
|
|
|
+ {isLocalOrLdapStrategiesEnabled && renderLocalOrLdapLoginForm()}
|
|
|
+ {isSomeExternalAuthEnabled && renderExternalAuthLoginForm()}
|
|
|
+ {isLocalOrLdapStrategiesEnabled && isPasswordResetEnabled && (
|
|
|
+ <div className="text-right mb-2">
|
|
|
+ <a href="/forgot-password" className="d-block link-switch">
|
|
|
+ <i className="icon-key"></i> {t('forgot_password.forgot_password')}
|
|
|
+ </a>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {/* Sign up link */}
|
|
|
+ {isRegistrationEnabled && (
|
|
|
+ <div className="text-right mb-2">
|
|
|
+ <a href="#register" id="register" className="link-switch" onClick={switchForm}>
|
|
|
+ <i className="ti ti-check-box"></i> {t('Sign up is here')}
|
|
|
+ </a>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ <div className="back">
|
|
|
+ {/* Register form for /login#register */}
|
|
|
+ {isRegistrationEnabled && renderRegisterForm()}
|
|
|
+ </div>
|
|
|
+ </ReactCardFlip>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <a href="https://growi.org" className="link-growi-org pl-3">
|
|
|
+ <span className="growi">GROWI</span>.<span className="org">ORG</span>
|
|
|
+ </a>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+
|
|
|
+};
|