password-section.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import { useEffect, useState } from 'react'
  2. import {
  3. Alert,
  4. Button,
  5. ControlLabel,
  6. FormControl,
  7. FormGroup,
  8. } from 'react-bootstrap'
  9. import { useTranslation } from 'react-i18next'
  10. import { postJSON } from '../../../infrastructure/fetch-json'
  11. import getMeta from '../../../utils/meta'
  12. import { ExposedSettings } from '../../../../../types/exposed-settings'
  13. import { PasswordStrengthOptions } from '../../../../../types/password-strength-options'
  14. import useAsync from '../../../shared/hooks/use-async'
  15. function PasswordSection() {
  16. const { t } = useTranslation()
  17. return (
  18. <>
  19. <h3>{t('change_password')}</h3>
  20. <PasswordInnerSection />
  21. </>
  22. )
  23. }
  24. function PasswordInnerSection() {
  25. const { t } = useTranslation()
  26. const { isOverleaf } = getMeta('ol-ExposedSettings') as ExposedSettings
  27. const isExternalAuthenticationSystemUsed = getMeta(
  28. 'ol-isExternalAuthenticationSystemUsed'
  29. ) as boolean
  30. const hasPassword = getMeta('ol-hasPassword') as boolean
  31. if (isExternalAuthenticationSystemUsed && !isOverleaf) {
  32. return <p>{t('password_managed_externally')}</p>
  33. }
  34. if (!hasPassword) {
  35. return (
  36. <p>
  37. <a href="/user/password/reset" target="_blank">
  38. {t('no_existing_password')}
  39. </a>
  40. </p>
  41. )
  42. }
  43. return <PasswordForm />
  44. }
  45. function PasswordForm() {
  46. const { t } = useTranslation()
  47. const passwordStrengthOptions = getMeta(
  48. 'ol-passwordStrengthOptions'
  49. ) as PasswordStrengthOptions
  50. const [currentPassword, setCurrentPassword] = useState('')
  51. const [newPassword1, setNewPassword1] = useState('')
  52. const [newPassword2, setNewPassword2] = useState('')
  53. const { isLoading, error, isSuccess, data, runAsync } = useAsync()
  54. const [isNewPasswordValid, setIsNewPasswordValid] = useState(false)
  55. const [isFormValid, setIsFormValid] = useState(false)
  56. const handleCurrentPasswordChange = event => {
  57. setCurrentPassword(event.target.value)
  58. }
  59. const handleNewPassword1Change = event => {
  60. setNewPassword1(event.target.value)
  61. setIsNewPasswordValid(event.target.validity.valid)
  62. }
  63. const handleNewPassword2Change = event => {
  64. setNewPassword2(event.target.value)
  65. }
  66. useEffect(() => {
  67. setIsFormValid(
  68. !!currentPassword && isNewPasswordValid && newPassword1 === newPassword2
  69. )
  70. }, [currentPassword, newPassword1, newPassword2, isNewPasswordValid])
  71. const handleSubmit = event => {
  72. event.preventDefault()
  73. if (!isFormValid) {
  74. return
  75. }
  76. runAsync(
  77. postJSON('/user/password/update', {
  78. body: {
  79. currentPassword,
  80. newPassword1,
  81. newPassword2,
  82. },
  83. })
  84. ).catch(() => {})
  85. }
  86. return (
  87. <form id="password-change-form" onSubmit={handleSubmit}>
  88. <PasswordFormGroup
  89. id="current-password-input"
  90. label={t('current_password')}
  91. value={currentPassword}
  92. handleChange={handleCurrentPasswordChange}
  93. />
  94. <PasswordFormGroup
  95. id="new-password-1-input"
  96. label={t('new_password')}
  97. value={newPassword1}
  98. handleChange={handleNewPassword1Change}
  99. minLength={passwordStrengthOptions?.length?.min || 6}
  100. />
  101. <PasswordFormGroup
  102. id="new-password-2-input"
  103. label={t('confirm_new_password')}
  104. value={newPassword2}
  105. handleChange={handleNewPassword2Change}
  106. validationMessage={
  107. newPassword1 !== newPassword2 ? t('doesnt_match') : ''
  108. }
  109. />
  110. {isSuccess && data?.message?.text ? (
  111. <FormGroup>
  112. <Alert bsStyle="success">{data.message.text}</Alert>
  113. </FormGroup>
  114. ) : null}
  115. {error ? (
  116. <FormGroup>
  117. <Alert bsStyle="danger">{error.getUserFacingMessage()}</Alert>
  118. </FormGroup>
  119. ) : null}
  120. <Button
  121. form="password-change-form"
  122. type="submit"
  123. bsStyle="primary"
  124. disabled={isLoading || !isFormValid}
  125. >
  126. {isLoading ? <>{t('saving')}…</> : t('change')}
  127. </Button>
  128. </form>
  129. )
  130. }
  131. type PasswordFormGroupProps = {
  132. id: string
  133. label: string
  134. value: string
  135. handleChange: (event: any) => void
  136. minLength?: number
  137. validationMessage?: string
  138. }
  139. function PasswordFormGroup({
  140. id,
  141. label,
  142. value,
  143. handleChange,
  144. minLength,
  145. validationMessage: parentValidationMessage,
  146. }: PasswordFormGroupProps) {
  147. const [validationMessage, setValidationMessage] = useState('')
  148. const [hadInteraction, setHadInteraction] = useState(false)
  149. const handleInvalid = event => {
  150. event.preventDefault()
  151. }
  152. const handleChangeAndValidity = event => {
  153. handleChange(event)
  154. setHadInteraction(true)
  155. setValidationMessage(event.target.validationMessage)
  156. }
  157. return (
  158. <FormGroup>
  159. <ControlLabel htmlFor={id}>{label}</ControlLabel>
  160. <FormControl
  161. id={id}
  162. type="password"
  163. placeholder="*********"
  164. value={value}
  165. data-ol-dirty={!!validationMessage}
  166. onChange={handleChangeAndValidity}
  167. onInvalid={handleInvalid}
  168. required={hadInteraction}
  169. minLength={minLength}
  170. />
  171. {hadInteraction && (parentValidationMessage || validationMessage) ? (
  172. <span className="small text-danger">
  173. {parentValidationMessage || validationMessage}
  174. </span>
  175. ) : null}
  176. </FormGroup>
  177. )
  178. }
  179. export default PasswordSection