LoginForm.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. import React, {
  2. useState, useEffect, useCallback,
  3. } from 'react';
  4. import { useTranslation } from 'next-i18next';
  5. import { useRouter } from 'next/router';
  6. import ReactCardFlip from 'react-card-flip';
  7. import { apiv3Post } from '~/client/util/apiv3-client';
  8. import { LoginErrorCode } from '~/interfaces/errors/login-form-error';
  9. import { IErrorV3 } from '~/interfaces/errors/v3-error';
  10. type LoginFormProps = {
  11. username?: string,
  12. name?: string,
  13. email?: string,
  14. isRegistrationEnabled: boolean,
  15. isEmailAuthenticationEnabled: boolean,
  16. registrationMode?: string,
  17. registrationWhiteList: string[],
  18. isPasswordResetEnabled: boolean,
  19. isLocalStrategySetup: boolean,
  20. isLdapStrategySetup: boolean,
  21. objOfIsExternalAuthEnableds?: any,
  22. isMailerSetup?: boolean
  23. }
  24. export const LoginForm = (props: LoginFormProps): JSX.Element => {
  25. const { t } = useTranslation();
  26. const router = useRouter();
  27. const {
  28. isLocalStrategySetup, isLdapStrategySetup, isPasswordResetEnabled, isRegistrationEnabled,
  29. isEmailAuthenticationEnabled, registrationMode, registrationWhiteList, isMailerSetup, objOfIsExternalAuthEnableds,
  30. } = props;
  31. const isLocalOrLdapStrategiesEnabled = isLocalStrategySetup || isLdapStrategySetup;
  32. const isSomeExternalAuthEnabled = Object.values(objOfIsExternalAuthEnableds).some(elem => elem);
  33. // states
  34. const [isRegistering, setIsRegistering] = useState(false);
  35. // For Login
  36. const [usernameForLogin, setUsernameForLogin] = useState('');
  37. const [passwordForLogin, setPasswordForLogin] = useState('');
  38. const [loginErrors, setLoginErrors] = useState<IErrorV3[]>([]);
  39. // For Register
  40. const [usernameForRegister, setUsernameForRegister] = useState('');
  41. const [nameForRegister, setNameForRegister] = useState('');
  42. const [emailForRegister, setEmailForRegister] = useState('');
  43. const [passwordForRegister, setPasswordForRegister] = useState('');
  44. const [registerErrors, setRegisterErrors] = useState<IErrorV3[]>([]);
  45. useEffect(() => {
  46. const { hash } = window.location;
  47. if (hash === '#register') {
  48. setIsRegistering(true);
  49. }
  50. }, []);
  51. // functions
  52. const handleLoginWithExternalAuth = useCallback((e) => {
  53. const auth = e.currentTarget.id;
  54. window.location.href = `/passport/${auth}`;
  55. }, []);
  56. const resetLoginErrors = useCallback(() => {
  57. if (loginErrors.length === 0) return;
  58. setLoginErrors([]);
  59. }, [loginErrors.length]);
  60. const handleLoginWithLocalSubmit = useCallback(async(e) => {
  61. e.preventDefault();
  62. resetLoginErrors();
  63. const loginForm = {
  64. username: usernameForLogin,
  65. password: passwordForLogin,
  66. };
  67. try {
  68. const res = await apiv3Post('/login', { loginForm });
  69. const { redirectTo } = res.data;
  70. router.push(redirectTo);
  71. }
  72. catch (err) {
  73. setLoginErrors(err);
  74. }
  75. return;
  76. }, [passwordForLogin, resetLoginErrors, router, usernameForLogin]);
  77. const renderLoginErrors = useCallback((errors, isWithDangerouslySetinnerHtml = false) => {
  78. if (errors.length === 0) return;
  79. return isWithDangerouslySetinnerHtml ? (
  80. <div className="alert alert-danger">
  81. {errors.map((err, index) => {
  82. return (
  83. <small dangerouslySetInnerHTML={{ __html: t(err.message, err.args) }} key={index}></small>
  84. );
  85. })}
  86. </div>
  87. ) : (
  88. <ul className="alert alert-danger">
  89. {errors.map((err, index) => {
  90. return (
  91. <li key={index}>
  92. {t(err.message, err.args)}<br/>
  93. </li>
  94. );
  95. })}
  96. </ul>
  97. );
  98. }, [t]);
  99. const renderLocalOrLdapLoginForm = useCallback(() => {
  100. const { isLdapStrategySetup } = props;
  101. const errorsWithDangerouslySetInnerHTML: IErrorV3[] = [];
  102. const errorsWithoutDanderouslySetInnerHTML: IErrorV3[] = [];
  103. loginErrors.forEach((e) => {
  104. if (e.code === LoginErrorCode.PROVIDER_DUPLICATED_USERNAME_EXCEPTION) {
  105. errorsWithDangerouslySetInnerHTML.push(e);
  106. }
  107. else {
  108. errorsWithoutDanderouslySetInnerHTML.push(e);
  109. }
  110. });
  111. return (
  112. <>
  113. {renderLoginErrors(errorsWithDangerouslySetInnerHTML, true)}
  114. {renderLoginErrors(errorsWithoutDanderouslySetInnerHTML, false)}
  115. <form role="form" onSubmit={handleLoginWithLocalSubmit} id="login-form">
  116. <div className="input-group">
  117. <div className="input-group-prepend">
  118. <span className="input-group-text">
  119. <i className="icon-user"></i>
  120. </span>
  121. </div>
  122. <input type="text" className="form-control rounded-0" data-testid="tiUsernameForLogin" placeholder="Username or E-mail"
  123. onChange={(e) => { setUsernameForLogin(e.target.value) }} name="usernameForLogin" />
  124. {isLdapStrategySetup && (
  125. <div className="input-group-append">
  126. <small className="input-group-text text-success">
  127. <i className="icon-fw icon-check"></i> LDAP
  128. </small>
  129. </div>
  130. )}
  131. </div>
  132. <div className="input-group">
  133. <div className="input-group-prepend">
  134. <span className="input-group-text">
  135. <i className="icon-lock"></i>
  136. </span>
  137. </div>
  138. <input type="password" className="form-control rounded-0" data-testid="tiPasswordForLogin" placeholder="Password"
  139. onChange={(e) => { setPasswordForLogin(e.target.value) }} name="passwordForLogin" />
  140. </div>
  141. <div className="input-group my-4">
  142. <button type="submit" id="login" className="btn btn-fill rounded-0 login mx-auto" data-testid="btnSubmitForLogin">
  143. <div className="eff"></div>
  144. <span className="btn-label">
  145. <i className="icon-login"></i>
  146. </span>
  147. <span className="btn-label-text">{t('Sign in')}</span>
  148. </button>
  149. </div>
  150. </form>
  151. </>
  152. );
  153. }, [handleLoginWithLocalSubmit, loginErrors, props, t]);
  154. const renderExternalAuthInput = useCallback((auth) => {
  155. const authIconNames = {
  156. google: 'google',
  157. github: 'github',
  158. facebook: 'facebook',
  159. twitter: 'twitter',
  160. oidc: 'openid',
  161. saml: 'key',
  162. basic: 'lock',
  163. };
  164. return (
  165. <div key={auth} className="col-6 my-2">
  166. <button type="button" className="btn btn-fill rounded-0" id={auth} onClick={handleLoginWithExternalAuth}>
  167. <div className="eff"></div>
  168. <span className="btn-label">
  169. <i className={`fa fa-${authIconNames[auth]}`}></i>
  170. </span>
  171. <span className="btn-label-text">{t('Sign in')}</span>
  172. </button>
  173. <div className="small text-right">by {auth} Account</div>
  174. </div>
  175. );
  176. }, [handleLoginWithExternalAuth, t]);
  177. const renderExternalAuthLoginForm = useCallback(() => {
  178. const { isLocalStrategySetup, isLdapStrategySetup, objOfIsExternalAuthEnableds } = props;
  179. const isExternalAuthCollapsible = isLocalStrategySetup || isLdapStrategySetup;
  180. const collapsibleClass = isExternalAuthCollapsible ? 'collapse collapse-external-auth' : '';
  181. return (
  182. <>
  183. <div className="grw-external-auth-form border-top border-bottom">
  184. <div id="external-auth" className={`external-auth ${collapsibleClass}`}>
  185. <div className="row mt-2">
  186. {Object.keys(objOfIsExternalAuthEnableds).map((auth) => {
  187. if (!objOfIsExternalAuthEnableds[auth]) {
  188. return;
  189. }
  190. return renderExternalAuthInput(auth);
  191. })}
  192. </div>
  193. </div>
  194. </div>
  195. <div className="text-center">
  196. <button
  197. type="button"
  198. className="btn btn-secondary btn-external-auth-tab btn-sm rounded-0 mb-3"
  199. data-toggle={isExternalAuthCollapsible ? 'collapse' : ''}
  200. data-target="#external-auth"
  201. aria-expanded="false"
  202. aria-controls="external-auth"
  203. >
  204. External Auth
  205. </button>
  206. </div>
  207. </>
  208. );
  209. }, [props, renderExternalAuthInput]);
  210. const handleRegisterFormSubmit = useCallback(async(e, requestPath) => {
  211. e.preventDefault();
  212. const registerForm = {
  213. username: usernameForRegister,
  214. name: nameForRegister,
  215. email: emailForRegister,
  216. password: passwordForRegister,
  217. };
  218. try {
  219. const res = await apiv3Post(requestPath, { registerForm });
  220. const { redirectTo } = res.data;
  221. router.push(redirectTo);
  222. }
  223. catch (err) {
  224. // Execute if error exists
  225. if (err != null || err.length > 0) {
  226. setRegisterErrors(err);
  227. }
  228. }
  229. return;
  230. }, [emailForRegister, nameForRegister, passwordForRegister, router, usernameForRegister]);
  231. const resetRegisterErrors = useCallback(() => {
  232. if (registerErrors.length === 0) return;
  233. setRegisterErrors([]);
  234. }, [registerErrors.length]);
  235. const switchForm = useCallback(() => {
  236. setIsRegistering(!isRegistering);
  237. resetLoginErrors();
  238. resetRegisterErrors();
  239. }, [isRegistering, resetLoginErrors, resetRegisterErrors]);
  240. const renderRegisterForm = useCallback(() => {
  241. let registerAction = '/register';
  242. let submitText = t('Sign up');
  243. if (isEmailAuthenticationEnabled) {
  244. registerAction = '/user-activation/register';
  245. submitText = t('page_register.send_email');
  246. }
  247. return (
  248. <React.Fragment>
  249. {registrationMode === 'Restricted' && (
  250. <p className="alert alert-warning">
  251. {t('page_register.notice.restricted')}
  252. <br />
  253. {t('page_register.notice.restricted_defail')}
  254. </p>
  255. )}
  256. { (!isMailerSetup && isEmailAuthenticationEnabled) && (
  257. <p className="alert alert-danger">
  258. <span>{t('security_settings.Local.please_enable_mailer')}</span>
  259. </p>
  260. )}
  261. {
  262. registerErrors != null && registerErrors.length > 0 && (
  263. <p className="alert alert-danger">
  264. {registerErrors.map((err, index) => {
  265. return (
  266. <span key={index}>
  267. {t(err.message)}<br/>
  268. </span>
  269. );
  270. })}
  271. </p>
  272. )
  273. }
  274. <form role="form" onSubmit={e => handleRegisterFormSubmit(e, registerAction) } id="register-form">
  275. {!isEmailAuthenticationEnabled && (
  276. <div>
  277. <div className="input-group" id="input-group-username">
  278. <div className="input-group-prepend">
  279. <span className="input-group-text">
  280. <i className="icon-user"></i>
  281. </span>
  282. </div>
  283. {/* username */}
  284. <input
  285. type="text"
  286. className="form-control rounded-0"
  287. onChange={(e) => { setUsernameForRegister(e.target.value) }}
  288. placeholder={t('User ID')}
  289. name="username"
  290. defaultValue={props.username}
  291. required
  292. />
  293. </div>
  294. <p className="form-text text-danger">
  295. <span id="help-block-username"></span>
  296. </p>
  297. <div className="input-group">
  298. <div className="input-group-prepend">
  299. <span className="input-group-text">
  300. <i className="icon-tag"></i>
  301. </span>
  302. </div>
  303. {/* name */}
  304. <input type="text"
  305. className="form-control rounded-0"
  306. onChange={(e) => { setNameForRegister(e.target.value) }}
  307. placeholder={t('Name')}
  308. name="name"
  309. defaultValue={props.name}
  310. required />
  311. </div>
  312. </div>
  313. )}
  314. <div className="input-group">
  315. <div className="input-group-prepend">
  316. <span className="input-group-text">
  317. <i className="icon-envelope"></i>
  318. </span>
  319. </div>
  320. {/* email */}
  321. <input type="email"
  322. className="form-control rounded-0"
  323. onChange={(e) => { setEmailForRegister(e.target.value) }}
  324. placeholder={t('Email')}
  325. name="email"
  326. defaultValue={props.email}
  327. required
  328. />
  329. </div>
  330. {registrationWhiteList.length > 0 && (
  331. <>
  332. <p className="form-text">{t('page_register.form_help.email')}</p>
  333. <ul>
  334. {registrationWhiteList.map((elem) => {
  335. return (
  336. <li key={elem}>
  337. <code>{elem}</code>
  338. </li>
  339. );
  340. })}
  341. </ul>
  342. </>
  343. )}
  344. {!isEmailAuthenticationEnabled && (
  345. <div>
  346. <div className="input-group">
  347. <div className="input-group-prepend">
  348. <span className="input-group-text">
  349. <i className="icon-lock"></i>
  350. </span>
  351. </div>
  352. {/* Password */}
  353. <input type="password"
  354. className="form-control rounded-0"
  355. onChange={(e) => { setPasswordForRegister(e.target.value) }}
  356. placeholder={t('Password')}
  357. name="password"
  358. required />
  359. </div>
  360. </div>
  361. )}
  362. {/* Sign up button (submit) */}
  363. <div className="input-group justify-content-center my-4">
  364. <button
  365. className="btn btn-fill rounded-0"
  366. id="register"
  367. disabled={(!isMailerSetup && isEmailAuthenticationEnabled)}
  368. >
  369. <div className="eff"></div>
  370. <span className="btn-label">
  371. <i className="icon-user-follow"></i>
  372. </span>
  373. <span className="btn-label-text">{submitText}</span>
  374. </button>
  375. </div>
  376. </form>
  377. <div className="border-bottom"></div>
  378. <div className="row">
  379. <div className="text-right col-12 mt-2 py-2">
  380. <a href="#login" id="login" className="link-switch" onClick={switchForm}>
  381. <i className="icon-fw icon-login"></i>
  382. {t('Sign in is here')}
  383. </a>
  384. </div>
  385. </div>
  386. </React.Fragment>
  387. );
  388. }, [handleRegisterFormSubmit, isEmailAuthenticationEnabled, isMailerSetup,
  389. props.email, props.name, props.username,
  390. registerErrors, registrationMode, registrationWhiteList, switchForm, t]);
  391. return (
  392. <div className="noLogin-dialog mx-auto" id="noLogin-dialog">
  393. <div className="row mx-0">
  394. <div className="col-12">
  395. <ReactCardFlip isFlipped={isRegistering} flipDirection="horizontal" cardZIndex="3">
  396. <div className="front">
  397. {isLocalOrLdapStrategiesEnabled && renderLocalOrLdapLoginForm()}
  398. {isSomeExternalAuthEnabled && renderExternalAuthLoginForm()}
  399. {isLocalOrLdapStrategiesEnabled && isPasswordResetEnabled && (
  400. <div className="text-right mb-2">
  401. <a href="/forgot-password" className="d-block link-switch">
  402. <i className="icon-key"></i> {t('forgot_password.forgot_password')}
  403. </a>
  404. </div>
  405. )}
  406. {/* Sign up link */}
  407. {isRegistrationEnabled && (
  408. <div className="text-right mb-2">
  409. <a href="#register" id="register" className="link-switch" onClick={switchForm}>
  410. <i className="ti ti-check-box"></i> {t('Sign up is here')}
  411. </a>
  412. </div>
  413. )}
  414. </div>
  415. <div className="back">
  416. {/* Register form for /login#register */}
  417. {isRegistrationEnabled && renderRegisterForm()}
  418. </div>
  419. </ReactCardFlip>
  420. </div>
  421. </div>
  422. <a href="https://growi.org" className="link-growi-org pl-3">
  423. <span className="growi">GROWI</span>.<span className="org">ORG</span>
  424. </a>
  425. </div>
  426. );
  427. };