password-section.tsx 5.4 KB

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