password-section.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. import { useEffect, useState } from 'react'
  2. import {
  3. Alert,
  4. Button,
  5. ControlLabel,
  6. FormControl,
  7. FormGroup,
  8. } from 'react-bootstrap'
  9. import { Trans, useTranslation } from 'react-i18next'
  10. import {
  11. getUserFacingMessage,
  12. getErrorMessageKey,
  13. postJSON,
  14. } from '../../../infrastructure/fetch-json'
  15. import getMeta from '../../../utils/meta'
  16. import { ExposedSettings } from '../../../../../types/exposed-settings'
  17. import { PasswordStrengthOptions } from '../../../../../types/password-strength-options'
  18. import useAsync from '../../../shared/hooks/use-async'
  19. type PasswordUpdateResult = {
  20. message?: {
  21. text: string
  22. }
  23. }
  24. function PasswordSection() {
  25. const { t } = useTranslation()
  26. const hideChangePassword = getMeta('ol-cannot-change-password') as boolean
  27. return (
  28. <>
  29. <h3>{t('change_password')}</h3>
  30. {hideChangePassword ? (
  31. <CanOnlyLogInThroughSSO />
  32. ) : (
  33. <PasswordInnerSection />
  34. )}
  35. </>
  36. )
  37. }
  38. function CanOnlyLogInThroughSSO() {
  39. return (
  40. <p>
  41. <Trans
  42. i18nKey="you_cant_add_or_change_password_due_to_sso"
  43. components={[
  44. // eslint-disable-next-line react/jsx-key, jsx-a11y/anchor-has-content
  45. <a href="/learn/how-to/Logging_in_with_Group_single_sign-on" />,
  46. ]}
  47. />
  48. </p>
  49. )
  50. }
  51. function PasswordInnerSection() {
  52. const { t } = useTranslation()
  53. const { isOverleaf } = getMeta('ol-ExposedSettings') as ExposedSettings
  54. const isExternalAuthenticationSystemUsed = getMeta(
  55. 'ol-isExternalAuthenticationSystemUsed'
  56. ) as boolean
  57. const hasPassword = getMeta('ol-hasPassword') as boolean
  58. if (isExternalAuthenticationSystemUsed && !isOverleaf) {
  59. return <p>{t('password_managed_externally')}</p>
  60. }
  61. if (!hasPassword) {
  62. return (
  63. <p>
  64. <a href="/user/password/reset" target="_blank">
  65. {t('no_existing_password')}
  66. </a>
  67. </p>
  68. )
  69. }
  70. return <PasswordForm />
  71. }
  72. function PasswordForm() {
  73. const { t } = useTranslation()
  74. const passwordStrengthOptions = getMeta(
  75. 'ol-passwordStrengthOptions'
  76. ) as PasswordStrengthOptions
  77. const [currentPassword, setCurrentPassword] = useState('')
  78. const [newPassword1, setNewPassword1] = useState('')
  79. const [newPassword2, setNewPassword2] = useState('')
  80. const { isLoading, isSuccess, isError, data, error, runAsync } =
  81. useAsync<PasswordUpdateResult>()
  82. const [isNewPasswordValid, setIsNewPasswordValid] = useState(false)
  83. const [isFormValid, setIsFormValid] = useState(false)
  84. const handleCurrentPasswordChange = (
  85. event: React.ChangeEvent<HTMLInputElement>
  86. ) => {
  87. setCurrentPassword(event.target.value)
  88. }
  89. const handleNewPassword1Change = (
  90. event: React.ChangeEvent<HTMLInputElement>
  91. ) => {
  92. setNewPassword1(event.target.value)
  93. setIsNewPasswordValid(event.target.validity.valid)
  94. }
  95. const handleNewPassword2Change = (
  96. event: React.ChangeEvent<HTMLInputElement>
  97. ) => {
  98. setNewPassword2(event.target.value)
  99. }
  100. useEffect(() => {
  101. setIsFormValid(
  102. !!currentPassword && isNewPasswordValid && newPassword1 === newPassword2
  103. )
  104. }, [currentPassword, newPassword1, newPassword2, isNewPasswordValid])
  105. const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
  106. event.preventDefault()
  107. if (!isFormValid) {
  108. return
  109. }
  110. runAsync(
  111. postJSON('/user/password/update', {
  112. body: {
  113. currentPassword,
  114. newPassword1,
  115. newPassword2,
  116. },
  117. })
  118. ).catch(() => {})
  119. }
  120. return (
  121. <form id="password-change-form" onSubmit={handleSubmit}>
  122. <PasswordFormGroup
  123. id="current-password-input"
  124. label={t('current_password')}
  125. value={currentPassword}
  126. handleChange={handleCurrentPasswordChange}
  127. autoComplete="current-password"
  128. />
  129. <PasswordFormGroup
  130. id="new-password-1-input"
  131. label={t('new_password')}
  132. value={newPassword1}
  133. handleChange={handleNewPassword1Change}
  134. minLength={passwordStrengthOptions?.length?.min || 8}
  135. autoComplete="new-password"
  136. />
  137. <PasswordFormGroup
  138. id="new-password-2-input"
  139. label={t('confirm_new_password')}
  140. value={newPassword2}
  141. handleChange={handleNewPassword2Change}
  142. validationMessage={
  143. newPassword1 !== newPassword2 ? t('doesnt_match') : ''
  144. }
  145. autoComplete="new-password"
  146. />
  147. {isSuccess && data?.message?.text ? (
  148. <FormGroup>
  149. <Alert bsStyle="success">{data.message.text}</Alert>
  150. </FormGroup>
  151. ) : null}
  152. {isError ? (
  153. <FormGroup>
  154. <Alert bsStyle="danger">
  155. {getErrorMessageKey(error) === 'password-must-be-strong' ? (
  156. <>
  157. <Trans
  158. i18nKey="password_was_detected_on_a_public_list_of_known_compromised_passwords"
  159. components={[
  160. /* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
  161. <a
  162. href="https://haveibeenpwned.com"
  163. target="_blank"
  164. rel="noreferrer noopener"
  165. />,
  166. ]}
  167. />
  168. . {t('use_a_different_password')}.
  169. </>
  170. ) : getErrorMessageKey(error) === 'password-contains-email' ? (
  171. <>
  172. {t('invalid_password_contains_email')}.{' '}
  173. {t('use_a_different_password')}.
  174. </>
  175. ) : getErrorMessageKey(error) === 'password-too-similar' ? (
  176. <>
  177. {t('invalid_password_too_similar')}.{' '}
  178. {t('use_a_different_password')}.
  179. </>
  180. ) : (
  181. getUserFacingMessage(error)
  182. )}
  183. </Alert>
  184. </FormGroup>
  185. ) : null}
  186. <Button
  187. form="password-change-form"
  188. type="submit"
  189. bsStyle="primary"
  190. disabled={isLoading || !isFormValid}
  191. >
  192. {isLoading ? <>{t('saving')}…</> : t('change')}
  193. </Button>
  194. </form>
  195. )
  196. }
  197. type PasswordFormGroupProps = {
  198. id: string
  199. label: string
  200. value: string
  201. handleChange: (event: React.ChangeEvent<HTMLInputElement>) => void
  202. minLength?: number
  203. validationMessage?: string
  204. autoComplete?: string
  205. }
  206. function PasswordFormGroup({
  207. id,
  208. label,
  209. value,
  210. handleChange,
  211. minLength,
  212. validationMessage: parentValidationMessage,
  213. autoComplete,
  214. }: PasswordFormGroupProps) {
  215. const [validationMessage, setValidationMessage] = useState('')
  216. const [hadInteraction, setHadInteraction] = useState(false)
  217. const handleInvalid = (
  218. event: React.InvalidEvent<HTMLInputElement & FormControl>
  219. ) => {
  220. event.preventDefault()
  221. }
  222. const handleChangeAndValidity = (
  223. event: React.ChangeEvent<HTMLInputElement & FormControl>
  224. ) => {
  225. handleChange(event)
  226. setHadInteraction(true)
  227. setValidationMessage(event.target.validationMessage)
  228. }
  229. return (
  230. <FormGroup>
  231. <ControlLabel htmlFor={id}>{label}</ControlLabel>
  232. <FormControl
  233. id={id}
  234. type="password"
  235. placeholder="*********"
  236. autoComplete={autoComplete}
  237. value={value}
  238. data-ol-dirty={!!validationMessage}
  239. onChange={handleChangeAndValidity}
  240. onInvalid={handleInvalid}
  241. required={hadInteraction}
  242. minLength={minLength}
  243. />
  244. {hadInteraction && (parentValidationMessage || validationMessage) ? (
  245. <span className="small text-danger">
  246. {parentValidationMessage || validationMessage}
  247. </span>
  248. ) : null}
  249. </FormGroup>
  250. )
  251. }
  252. export default PasswordSection