password-section.tsx 7.8 KB

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