password-section.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  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 = (
  66. event: React.ChangeEvent<HTMLInputElement>
  67. ) => {
  68. setCurrentPassword(event.target.value)
  69. }
  70. const handleNewPassword1Change = (
  71. event: React.ChangeEvent<HTMLInputElement>
  72. ) => {
  73. setNewPassword1(event.target.value)
  74. setIsNewPasswordValid(event.target.validity.valid)
  75. }
  76. const handleNewPassword2Change = (
  77. event: React.ChangeEvent<HTMLInputElement>
  78. ) => {
  79. setNewPassword2(event.target.value)
  80. }
  81. useEffect(() => {
  82. setIsFormValid(
  83. !!currentPassword && isNewPasswordValid && newPassword1 === newPassword2
  84. )
  85. }, [currentPassword, newPassword1, newPassword2, isNewPasswordValid])
  86. const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
  87. event.preventDefault()
  88. if (!isFormValid) {
  89. return
  90. }
  91. runAsync(
  92. postJSON('/user/password/update', {
  93. body: {
  94. currentPassword,
  95. newPassword1,
  96. newPassword2,
  97. },
  98. })
  99. ).catch(() => {})
  100. }
  101. return (
  102. <form id="password-change-form" onSubmit={handleSubmit}>
  103. <PasswordFormGroup
  104. id="current-password-input"
  105. label={t('current_password')}
  106. value={currentPassword}
  107. handleChange={handleCurrentPasswordChange}
  108. />
  109. <PasswordFormGroup
  110. id="new-password-1-input"
  111. label={t('new_password')}
  112. value={newPassword1}
  113. handleChange={handleNewPassword1Change}
  114. minLength={passwordStrengthOptions?.length?.min || 6}
  115. />
  116. <PasswordFormGroup
  117. id="new-password-2-input"
  118. label={t('confirm_new_password')}
  119. value={newPassword2}
  120. handleChange={handleNewPassword2Change}
  121. validationMessage={
  122. newPassword1 !== newPassword2 ? t('doesnt_match') : ''
  123. }
  124. />
  125. {isSuccess && data?.message?.text ? (
  126. <FormGroup>
  127. <Alert bsStyle="success">{data.message.text}</Alert>
  128. </FormGroup>
  129. ) : null}
  130. {isError ? (
  131. <FormGroup>
  132. <Alert bsStyle="danger">{getUserFacingMessage(error)}</Alert>
  133. </FormGroup>
  134. ) : null}
  135. <Button
  136. form="password-change-form"
  137. type="submit"
  138. bsStyle="primary"
  139. disabled={isLoading || !isFormValid}
  140. >
  141. {isLoading ? <>{t('saving')}…</> : t('change')}
  142. </Button>
  143. </form>
  144. )
  145. }
  146. type PasswordFormGroupProps = {
  147. id: string
  148. label: string
  149. value: string
  150. handleChange: (event: React.ChangeEvent<HTMLInputElement>) => void
  151. minLength?: number
  152. validationMessage?: string
  153. }
  154. function PasswordFormGroup({
  155. id,
  156. label,
  157. value,
  158. handleChange,
  159. minLength,
  160. validationMessage: parentValidationMessage,
  161. }: PasswordFormGroupProps) {
  162. const [validationMessage, setValidationMessage] = useState('')
  163. const [hadInteraction, setHadInteraction] = useState(false)
  164. const handleInvalid = (
  165. event: React.InvalidEvent<HTMLInputElement & FormControl>
  166. ) => {
  167. event.preventDefault()
  168. }
  169. const handleChangeAndValidity = (
  170. event: React.ChangeEvent<HTMLInputElement & FormControl>
  171. ) => {
  172. handleChange(event)
  173. setHadInteraction(true)
  174. setValidationMessage(event.target.validationMessage)
  175. }
  176. return (
  177. <FormGroup>
  178. <ControlLabel htmlFor={id}>{label}</ControlLabel>
  179. <FormControl
  180. id={id}
  181. type="password"
  182. placeholder="*********"
  183. autoComplete="new-password"
  184. value={value}
  185. data-ol-dirty={!!validationMessage}
  186. onChange={handleChangeAndValidity}
  187. onInvalid={handleInvalid}
  188. required={hadInteraction}
  189. minLength={minLength}
  190. />
  191. {hadInteraction && (parentValidationMessage || validationMessage) ? (
  192. <span className="small text-danger">
  193. {parentValidationMessage || validationMessage}
  194. </span>
  195. ) : null}
  196. </FormGroup>
  197. )
  198. }
  199. export default PasswordSection