password-section.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  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. autoComplete="current-password"
  109. />
  110. <PasswordFormGroup
  111. id="new-password-1-input"
  112. label={t('new_password')}
  113. value={newPassword1}
  114. handleChange={handleNewPassword1Change}
  115. minLength={passwordStrengthOptions?.length?.min || 6}
  116. autoComplete="new-password"
  117. />
  118. <PasswordFormGroup
  119. id="new-password-2-input"
  120. label={t('confirm_new_password')}
  121. value={newPassword2}
  122. handleChange={handleNewPassword2Change}
  123. validationMessage={
  124. newPassword1 !== newPassword2 ? t('doesnt_match') : ''
  125. }
  126. autoComplete="new-password"
  127. />
  128. {isSuccess && data?.message?.text ? (
  129. <FormGroup>
  130. <Alert bsStyle="success">{data.message.text}</Alert>
  131. </FormGroup>
  132. ) : null}
  133. {isError ? (
  134. <FormGroup>
  135. <Alert bsStyle="danger">{getUserFacingMessage(error)}</Alert>
  136. </FormGroup>
  137. ) : null}
  138. <Button
  139. form="password-change-form"
  140. type="submit"
  141. bsStyle="primary"
  142. disabled={isLoading || !isFormValid}
  143. >
  144. {isLoading ? <>{t('saving')}…</> : t('change')}
  145. </Button>
  146. </form>
  147. )
  148. }
  149. type PasswordFormGroupProps = {
  150. id: string
  151. label: string
  152. value: string
  153. handleChange: (event: React.ChangeEvent<HTMLInputElement>) => void
  154. minLength?: number
  155. validationMessage?: string
  156. autoComplete?: string
  157. }
  158. function PasswordFormGroup({
  159. id,
  160. label,
  161. value,
  162. handleChange,
  163. minLength,
  164. validationMessage: parentValidationMessage,
  165. autoComplete,
  166. }: PasswordFormGroupProps) {
  167. const [validationMessage, setValidationMessage] = useState('')
  168. const [hadInteraction, setHadInteraction] = useState(false)
  169. const handleInvalid = (
  170. event: React.InvalidEvent<HTMLInputElement & FormControl>
  171. ) => {
  172. event.preventDefault()
  173. }
  174. const handleChangeAndValidity = (
  175. event: React.ChangeEvent<HTMLInputElement & FormControl>
  176. ) => {
  177. handleChange(event)
  178. setHadInteraction(true)
  179. setValidationMessage(event.target.validationMessage)
  180. }
  181. return (
  182. <FormGroup>
  183. <ControlLabel htmlFor={id}>{label}</ControlLabel>
  184. <FormControl
  185. id={id}
  186. type="password"
  187. placeholder="*********"
  188. autoComplete={autoComplete}
  189. value={value}
  190. data-ol-dirty={!!validationMessage}
  191. onChange={handleChangeAndValidity}
  192. onInvalid={handleInvalid}
  193. required={hadInteraction}
  194. minLength={minLength}
  195. />
  196. {hadInteraction && (parentValidationMessage || validationMessage) ? (
  197. <span className="small text-danger">
  198. {parentValidationMessage || validationMessage}
  199. </span>
  200. ) : null}
  201. </FormGroup>
  202. )
  203. }
  204. export default PasswordSection