| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595 |
- 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';
- import type { IExternalAccountLoginError } from '~/interfaces/errors/external-account-login-error';
- import { LoginErrorCode } from '~/interfaces/errors/login-error';
- import type { IErrorV3 } from '~/interfaces/errors/v3-error';
- import { RegistrationMode } from '~/interfaces/registration-mode';
- import { toArrayIfNot } from '~/utils/array-utils';
- import { CompleteUserRegistration } from './CompleteUserRegistration';
- import styles from './LoginForm.module.scss';
- type LoginFormProps = {
- username?: string,
- name?: string,
- email?: string,
- isEmailAuthenticationEnabled: boolean,
- registrationMode: RegistrationMode,
- registrationWhitelist: string[],
- isPasswordResetEnabled: boolean,
- isLocalStrategySetup: boolean,
- isLdapStrategySetup: boolean,
- isLdapSetupFailed: boolean,
- objOfIsExternalAuthEnableds?: any,
- isMailerSetup?: boolean,
- externalAccountLoginError?: IExternalAccountLoginError,
- }
- export const LoginForm = (props: LoginFormProps): JSX.Element => {
- const { t } = useTranslation();
- const router = useRouter();
- const {
- isLocalStrategySetup, isLdapStrategySetup, isLdapSetupFailed, isPasswordResetEnabled,
- isEmailAuthenticationEnabled, registrationMode, registrationWhitelist, isMailerSetup, objOfIsExternalAuthEnableds,
- } = props;
- const isLocalOrLdapStrategiesEnabled = isLocalStrategySetup || isLdapStrategySetup;
- const isSomeExternalAuthEnabled = Object.values(objOfIsExternalAuthEnableds).some(elem => elem);
- // states
- const [isRegistering, setIsRegistering] = useState(false);
- const [isLoading, setIsLoading] = useState(false);
- // For Login
- const [usernameForLogin, setUsernameForLogin] = useState('');
- const [passwordForLogin, setPasswordForLogin] = useState('');
- const [loginErrors, setLoginErrors] = useState<IErrorV3[]>([]);
- // For Register
- const [usernameForRegister, setUsernameForRegister] = useState('');
- const [nameForRegister, setNameForRegister] = useState('');
- const [emailForRegister, setEmailForRegister] = useState('');
- const [passwordForRegister, setPasswordForRegister] = useState('');
- const [registerErrors, setRegisterErrors] = useState<IErrorV3[]>([]);
- // For UserActivation
- const [emailForRegistrationOrder, setEmailForRegistrationOrder] = useState('');
- const [isSuccessToRagistration, setIsSuccessToRagistration] = useState(false);
- const isRegistrationEnabled = isLocalStrategySetup && registrationMode !== RegistrationMode.CLOSED;
- useEffect(() => {
- const { hash } = window.location;
- if (hash === '#register') {
- setIsRegistering(true);
- }
- }, []);
- const tWithOpt = useCallback((key: string, opt?: any): string => {
- if (typeof opt === 'object') {
- return t(key, opt as object);
- }
- return t(key);
- }, [t]);
- const handleLoginWithExternalAuth = useCallback((e) => {
- const auth = e.currentTarget.id;
- window.location.href = `/passport/${auth}`;
- }, []);
- const resetLoginErrors = useCallback(() => {
- if (loginErrors.length === 0) return;
- setLoginErrors([]);
- }, [loginErrors.length]);
- const handleLoginWithLocalSubmit = useCallback(async(e) => {
- e.preventDefault();
- resetLoginErrors();
- setIsLoading(true);
- const loginForm = {
- username: usernameForLogin,
- password: passwordForLogin,
- };
- try {
- const res = await apiv3Post('/login', { loginForm });
- const { redirectTo } = res.data;
- if (redirectTo != null) {
- return router.push(redirectTo);
- }
- return router.push('/');
- }
- catch (err) {
- const errs = toArrayIfNot(err);
- setLoginErrors(errs);
- setIsLoading(false);
- }
- return;
- }, [passwordForLogin, resetLoginErrors, router, usernameForLogin]);
- // separate errors based on error code
- const separateErrorsBasedOnErrorCode = useCallback((errors: IErrorV3[]) => {
- const loginErrorListForDangerouslySetInnerHTML: IErrorV3[] = [];
- const loginErrorList: IErrorV3[] = [];
- errors.forEach((err) => {
- if (err.code === LoginErrorCode.PROVIDER_DUPLICATED_USERNAME_EXCEPTION) {
- loginErrorListForDangerouslySetInnerHTML.push(err);
- }
- else {
- loginErrorList.push(err);
- }
- });
- return [loginErrorListForDangerouslySetInnerHTML, loginErrorList];
- }, []);
- // wrap error elements which use dangerouslySetInnerHtml
- const generateDangerouslySetErrors = useCallback((errors: IErrorV3[]): JSX.Element => {
- if (errors == null || errors.length === 0) return <></>;
- return (
- <div className="alert alert-danger">
- {errors.map((err) => {
- // eslint-disable-next-line react/no-danger
- return <small dangerouslySetInnerHTML={{ __html: tWithOpt(err.message, err.args) }}></small>;
- })}
- </div>
- );
- }, [tWithOpt]);
- // wrap error elements which do not use dangerouslySetInnerHtml
- const generateSafelySetErrors = useCallback((errors: (IErrorV3 | IExternalAccountLoginError)[]): JSX.Element => {
- if (errors == null || errors.length === 0) return <></>;
- return (
- <ul className="alert alert-danger">
- {errors.map((err, index) => (
- <li className={index > 0 ? 'mt-1' : ''}>
- {tWithOpt(err.message, err.args)}
- </li>
- ))}
- </ul>
- );
- }, [tWithOpt]);
- const renderLocalOrLdapLoginForm = useCallback(() => {
- const { isLdapStrategySetup } = props;
- // separate login errors into two arrays based on error code
- const [loginErrorListForDangerouslySetInnerHTML, loginErrorList] = separateErrorsBasedOnErrorCode(loginErrors);
- // Generate login error elements using dangerouslySetInnerHTML
- const loginErrorElementWithDangerouslySetInnerHTML = generateDangerouslySetErrors(loginErrorListForDangerouslySetInnerHTML);
- // Generate login error elements using <ul>, <li>
- const loginErrorElement = props.externalAccountLoginError != null
- ? generateSafelySetErrors([...loginErrorList, props.externalAccountLoginError])
- : generateSafelySetErrors(loginErrorList);
- return (
- <>
- {/* !! - DO NOT DELETE HIDDEN ELEMENT - !! -- 7.12 ryoji-s */}
- {/* Import font-awesome to prevent MongoStore.js "Unable to find the session to touch" error */}
- <div className="visually-hidden">
- {/* Unsettled 11.17 meiri-k */}
- <i className="fa fa-spinner fa-pulse" />
- </div>
- {/* !! - END OF HIDDEN ELEMENT - !! */}
- {isLdapSetupFailed && (
- <div className="alert alert-warning small">
- <strong><span className="material-symbols-outlined">info</span>{t('login.enabled_ldap_has_configuration_problem')}</strong><br />
- <span dangerouslySetInnerHTML={{ __html: t('login.set_env_var_for_logs') }}></span>
- </div>
- )}
- {loginErrorElementWithDangerouslySetInnerHTML}
- {loginErrorElement}
- <form role="form" onSubmit={handleLoginWithLocalSubmit} id="login-form">
- <div className="input-group">
- <span className="input-group-text">
- <span className="material-symbols-outlined">person</span>
- </span>
- <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 && (
- <small className="input-group-text text-success">
- <span className="material-symbols-outlined">select_check_box</span>LDAP
- </small>
- )}
- </div>
- <div className="input-group">
- <span className="input-group-text">
- <span className="material-symbols-outlined">lock</span>
- </span>
- <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"
- disabled={isLoading}
- >
- <div className="eff"></div>
- <span className="btn-label">
- {/* spinner.Tentative decision meiri-k 11.17 */}
- <span className="material-symbols-outlined">{isLoading ? 'hoge' : 'login'}</span>
- </span>
- <span className="btn-label-text">{t('Sign in')}</span>
- </button>
- </div>
- </form>
- </>
- );
- }, [
- props,
- separateErrorsBasedOnErrorCode,
- loginErrors,
- generateDangerouslySetErrors,
- generateSafelySetErrors,
- isLdapSetupFailed,
- t,
- handleLoginWithLocalSubmit,
- isLoading,
- ]);
- const renderExternalAuthInput = useCallback((auth) => {
- const authIconNames = {
- google: 'google',
- github: 'github',
- facebook: 'facebook',
- oidc: 'openid',
- saml: 'key',
- };
- 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-end">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-bs-toggle={isExternalAuthCollapsible ? 'collapse' : ''}
- data-bs-target="#external-auth"
- aria-expanded="false"
- aria-controls="external-auth"
- >
- External Auth
- </button>
- </div>
- </>
- );
- }, [props, renderExternalAuthInput]);
- const resetRegisterErrors = useCallback(() => {
- if (registerErrors.length === 0) return;
- setRegisterErrors([]);
- }, [registerErrors.length]);
- const handleRegisterFormSubmit = useCallback(async(e, requestPath) => {
- e.preventDefault();
- setEmailForRegistrationOrder('');
- setIsSuccessToRagistration(false);
- setIsLoading(true);
- const registerForm = {
- username: usernameForRegister,
- name: nameForRegister,
- email: emailForRegister,
- password: passwordForRegister,
- };
- try {
- const res = await apiv3Post(requestPath, { registerForm });
- setIsSuccessToRagistration(true);
- resetRegisterErrors();
- const { redirectTo } = res.data;
- if (redirectTo != null) {
- router.push(redirectTo);
- }
- if (isEmailAuthenticationEnabled) {
- setEmailForRegistrationOrder(emailForRegister);
- return;
- }
- }
- catch (err) {
- // Execute if error exists
- if (err != null || err.length > 0) {
- setRegisterErrors(err);
- }
- setIsLoading(false);
- }
- return;
- }, [usernameForRegister, nameForRegister, emailForRegister, passwordForRegister, resetRegisterErrors, router, isEmailAuthenticationEnabled]);
- 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 === 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('commons:alert.please_enable_mailer')}</span>
- </p>
- )}
- {
- registerErrors != null && registerErrors.length > 0 && (
- <p className="alert alert-danger">
- {registerErrors.map(err => (
- <span>
- {t(err.message)}<br />
- </span>
- ))}
- </p>
- )
- }
- {
- (isEmailAuthenticationEnabled && isSuccessToRagistration) && (
- <p className="alert alert-success">
- <span>{t('message.successfully_send_email_auth', { email: emailForRegistrationOrder })}</span>
- </p>
- )
- }
- <form role="form" onSubmit={e => handleRegisterFormSubmit(e, registerAction)} id="register-form">
- {!isEmailAuthenticationEnabled && (
- <div>
- <div className="input-group" id="input-group-username">
- <span className="input-group-text">
- <span className="material-symbols-outlined">person</span>
- </span>
- {/* 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">
- <span className="input-group-text">
- <span className="material-symbols-outlined">sell</span>
- </span>
- {/* 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">
- <span className="input-group-text">
- <span className="material-symbols-outlined">mail</span>
- </span>
- {/* email */}
- <input
- type="email"
- disabled={!isMailerSetup && isEmailAuthenticationEnabled}
- 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">
- <span className="input-group-text">
- <span className="material-symbols-outlined">lock</span>
- </span>
- {/* 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
- type="submit"
- className="btn btn-fill rounded-0"
- id="register"
- disabled={(!isMailerSetup && isEmailAuthenticationEnabled) || isLoading}
- >
- <div className="eff"></div>
- <span className="btn-label">
- {/* spinner.Tentative decision meiri-k 11.17 */}
- <span className="material-symbols-outlined">{isLoading ? 'hoge' : 'login'}</span>
- </span>
- <span className="btn-label-text">{submitText}</span>
- </button>
- </div>
- </form>
- <div className="border-bottom"></div>
- <div className="row">
- <div className="text-end col-12 mt-2 py-2">
- <a
- href="#login"
- id="login"
- className="link-switch"
- style={{ pointerEvents: isLoading ? 'none' : 'auto' }}
- onClick={switchForm}
- >
- <span className="material-symbols-outlined">login</span>{t('Sign in is here')}
- </a>
- </div>
- </div>
- </React.Fragment>
- );
- }, [
- t, isEmailAuthenticationEnabled, registrationMode, isMailerSetup, registerErrors, isSuccessToRagistration,
- emailForRegistrationOrder, props.username, props.name, props.email, registrationWhitelist, switchForm, handleRegisterFormSubmit, isLoading,
- ]);
- if (registrationMode === RegistrationMode.RESTRICTED && isSuccessToRagistration && !isEmailAuthenticationEnabled) {
- return <CompleteUserRegistration />;
- }
- return (
- <div className={`login-form ${styles['login-form']}`}>
- <div className="nologin-dialog mx-auto" id="nologin-dialog" data-testid="login-form">
- <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-end mb-2">
- <a href="/forgot-password" className="d-block link-switch">
- <span className="material-symbols-outlined">vpn_key</span>{t('forgot_password.forgot_password')}
- </a>
- </div>
- )}
- {/* Sign up link */}
- {isRegistrationEnabled && (
- <div className="text-end mb-2">
- <a
- href="#register"
- id="register"
- className="link-switch"
- style={{ pointerEvents: isLoading ? 'none' : 'auto' }}
- onClick={switchForm}
- >
- <span className="material-symbols-outlined">check_box</span> {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 ps-3">
- <span className="growi">GROWI</span>.<span className="org">org</span>
- </a>
- </div>
- </div>
- );
- };
|