import React, { useState, useEffect, useCallback, type JSX, } from 'react'; import { LoadingSpinner } from '@growi/ui/dist/components'; import { useTranslation } from 'next-i18next'; import { useRouter } from 'next/router'; import ReactCardFlip from 'react-card-flip'; import { apiv3Post } from '~/client/util/apiv3-client'; import { useTWithOpt } from '~/client/util/t-with-opt'; 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 type { IExternalAuthProviderType } from '~/interfaces/external-auth-provider'; import { RegistrationMode } from '~/interfaces/registration-mode'; import { toArrayIfNot } from '~/utils/array-utils'; import { CompleteUserRegistration } from '../CompleteUserRegistration'; import { ExternalAuthButton } from './ExternalAuthButton'; import styles from './LoginForm.module.scss'; const moduleClass = styles['login-form']; type LoginFormProps = { username?: string, name?: string, email?: string, isEmailAuthenticationEnabled: boolean, registrationMode: RegistrationMode, registrationWhitelist: string[], isPasswordResetEnabled: boolean, isLocalStrategySetup: boolean, isLdapStrategySetup: boolean, isLdapSetupFailed: boolean, enabledExternalAuthType?: IExternalAuthProviderType[], isMailerSetup?: boolean, externalAccountLoginError?: IExternalAccountLoginError, minPasswordLength: number, } export const LoginForm = (props: LoginFormProps): JSX.Element => { const { t } = useTranslation(); const router = useRouter(); const { isLocalStrategySetup, isLdapStrategySetup, isLdapSetupFailed, isPasswordResetEnabled, isEmailAuthenticationEnabled, registrationMode, registrationWhitelist, isMailerSetup, enabledExternalAuthType, minPasswordLength, } = props; const isLocalOrLdapStrategiesEnabled = isLocalStrategySetup || isLdapStrategySetup; const isSomeExternalAuthEnabled = enabledExternalAuthType != null && enabledExternalAuthType.length > 0; // 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([]); // For Register const [usernameForRegister, setUsernameForRegister] = useState(''); const [nameForRegister, setNameForRegister] = useState(''); const [emailForRegister, setEmailForRegister] = useState(''); const [passwordForRegister, setPasswordForRegister] = useState(''); const [registerErrors, setRegisterErrors] = useState([]); // For UserActivation const [emailForRegistrationOrder, setEmailForRegistrationOrder] = useState(''); const [isSuccessToRagistration, setIsSuccessToRagistration] = useState(false); const isRegistrationEnabled = isLocalStrategySetup && registrationMode !== RegistrationMode.CLOSED; const tWithOpt = useTWithOpt(); useEffect(() => { const { hash } = window.location; if (hash === '#register') { setIsRegistering(true); } }, []); 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 (
{errors.map((err) => { // eslint-disable-next-line react/no-danger return ; })}
); }, [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 ( ); }, [tWithOpt]); const renderLocalOrLdapLoginForm = useCallback(() => { const { isLdapStrategySetup } = props; return ( <> {/* !! - DO NOT DELETE HIDDEN ELEMENT - !! -- 7.12 ryoji-s */} {/* https://github.com/growilabs/growi/pull/7873 */}
{/* !! - END OF HIDDEN ELEMENT - !! */} {isLdapSetupFailed && (
info{t('login.enabled_ldap_has_configuration_problem')}
{/* eslint-disable-next-line react/no-danger */}
)}
{ setUsernameForLogin(e.target.value) }} name="usernameForLogin" /> {isLdapStrategySetup && ( network_node LDAP )}
{ setPasswordForLogin(e.target.value) }} name="passwordForLogin" />
); }, [ props, isLdapSetupFailed, t, handleLoginWithLocalSubmit, isLoading, ]); const renderExternalAuthLoginForm = useCallback(() => { const { enabledExternalAuthType } = props; if (enabledExternalAuthType == null) { return <>; } return ( <>
{enabledExternalAuthType.map(authType => )}
); }, [props]); 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 ( {registrationMode === RegistrationMode.RESTRICTED && (

{t('page_register.notice.restricted')}
{t('page_register.notice.restricted_defail')}

)} {(!isMailerSetup && isEmailAuthenticationEnabled) && (

{t('commons:alert.please_enable_mailer')}

)} { registerErrors != null && registerErrors.length > 0 && (

{registerErrors.map(err => ( {tWithOpt(err.message, err.args)}
))}

) } { (isEmailAuthenticationEnabled && isSuccessToRagistration) && (

{t('message.successfully_send_email_auth', { email: emailForRegistrationOrder })}

) }
handleRegisterFormSubmit(e, registerAction)} id="register-form"> {!isEmailAuthenticationEnabled && (
person {/* username */} { setUsernameForRegister(e.target.value) }} placeholder={t('User ID')} name="username" defaultValue={props.username} required />

sell {/* name */} { setNameForRegister(e.target.value) }} placeholder={t('Name')} name="name" defaultValue={props.name} required />
)}
mail {/* email */} { setEmailForRegister(e.target.value) }} placeholder={t('Email')} name="email" defaultValue={props.email} required />
{registrationWhitelist.length > 0 && ( <>

{t('page_register.form_help.email')}

    {registrationWhitelist.map((elem) => { return (
  • {elem}
  • ); })}
)} {!isEmailAuthenticationEnabled && (
lock {/* Password */} { setPasswordForRegister(e.target.value) }} placeholder={t('Password')} name="password" required minLength={minPasswordLength} />
)} {/* Sign up button (submit) */}
login {t('Sign in is here')}
); }, [ t, isEmailAuthenticationEnabled, registrationMode, isMailerSetup, registerErrors, isSuccessToRagistration, emailForRegistrationOrder, props.username, props.name, props.email, registrationWhitelist, minPasswordLength, isLoading, switchForm, tWithOpt, handleRegisterFormSubmit, ]); if (registrationMode === RegistrationMode.RESTRICTED && isSuccessToRagistration && !isEmailAuthenticationEnabled) { return ; } return (
{/* Error display section - always shown regardless of login method configuration */} {(() => { // 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 - prioritize loginErrorList, fallback to externalAccountLoginError const loginErrorElement = (loginErrorList ?? []).length > 0 ? generateSafelySetErrors(loginErrorList) : generateSafelySetErrors(props.externalAccountLoginError != null ? [props.externalAccountLoginError] : []); return ( <> {loginErrorElementWithDangerouslySetInnerHTML} {loginErrorElement} ); })()} {isLocalOrLdapStrategiesEnabled && renderLocalOrLdapLoginForm()} {isLocalOrLdapStrategiesEnabled && isSomeExternalAuthEnabled && (

{t('or')}

)} {isSomeExternalAuthEnabled && renderExternalAuthLoginForm()} {isLocalOrLdapStrategiesEnabled && isPasswordResetEnabled && ( )} {/* Sign up link */} {isRegistrationEnabled && ( )}
{/* Register form for /login#register */} {isRegistrationEnabled && renderRegisterForm()}
GROWI.org
); };