Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

mail-and-password-auth.tsx 5.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. import Link from 'next/link'
  2. import { useCallback, useState } from 'react'
  3. import { useTranslation } from 'react-i18next'
  4. import { useRouter, useSearchParams } from 'next/navigation'
  5. import { useContext } from 'use-context-selector'
  6. import Button from '@/app/components/base/button'
  7. import Toast from '@/app/components/base/toast'
  8. import { emailRegex } from '@/config'
  9. import { webAppLogin } from '@/service/common'
  10. import Input from '@/app/components/base/input'
  11. import I18NContext from '@/context/i18n'
  12. import { noop } from 'lodash-es'
  13. import { setAccessToken } from '@/app/components/share/utils'
  14. import { fetchAccessToken } from '@/service/share'
  15. type MailAndPasswordAuthProps = {
  16. isEmailSetup: boolean
  17. }
  18. const passwordRegex = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
  19. export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAuthProps) {
  20. const { t } = useTranslation()
  21. const { locale } = useContext(I18NContext)
  22. const router = useRouter()
  23. const searchParams = useSearchParams()
  24. const [showPassword, setShowPassword] = useState(false)
  25. const emailFromLink = decodeURIComponent(searchParams.get('email') || '')
  26. const [email, setEmail] = useState(emailFromLink)
  27. const [password, setPassword] = useState('')
  28. const [isLoading, setIsLoading] = useState(false)
  29. const redirectUrl = searchParams.get('redirect_url')
  30. const getAppCodeFromRedirectUrl = useCallback(() => {
  31. const appCode = redirectUrl?.split('/').pop()
  32. if (!appCode)
  33. return null
  34. return appCode
  35. }, [redirectUrl])
  36. const handleEmailPasswordLogin = async () => {
  37. const appCode = getAppCodeFromRedirectUrl()
  38. if (!email) {
  39. Toast.notify({ type: 'error', message: t('login.error.emailEmpty') })
  40. return
  41. }
  42. if (!emailRegex.test(email)) {
  43. Toast.notify({
  44. type: 'error',
  45. message: t('login.error.emailInValid'),
  46. })
  47. return
  48. }
  49. if (!password?.trim()) {
  50. Toast.notify({ type: 'error', message: t('login.error.passwordEmpty') })
  51. return
  52. }
  53. if (!passwordRegex.test(password)) {
  54. Toast.notify({
  55. type: 'error',
  56. message: t('login.error.passwordInvalid'),
  57. })
  58. return
  59. }
  60. if (!redirectUrl || !appCode) {
  61. Toast.notify({
  62. type: 'error',
  63. message: t('login.error.redirectUrlMissing'),
  64. })
  65. return
  66. }
  67. try {
  68. setIsLoading(true)
  69. const loginData: Record<string, any> = {
  70. email,
  71. password,
  72. language: locale,
  73. remember_me: true,
  74. }
  75. const res = await webAppLogin({
  76. url: '/login',
  77. body: loginData,
  78. })
  79. if (res.result === 'success') {
  80. localStorage.setItem('webapp_access_token', res.data.access_token)
  81. const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: res.data.access_token })
  82. await setAccessToken(appCode, tokenResp.access_token)
  83. router.replace(redirectUrl)
  84. }
  85. else {
  86. Toast.notify({
  87. type: 'error',
  88. message: res.data,
  89. })
  90. }
  91. }
  92. finally {
  93. setIsLoading(false)
  94. }
  95. }
  96. return <form onSubmit={noop}>
  97. <div className='mb-3'>
  98. <label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">
  99. {t('login.email')}
  100. </label>
  101. <div className="mt-1">
  102. <Input
  103. value={email}
  104. onChange={e => setEmail(e.target.value)}
  105. id="email"
  106. type="email"
  107. autoComplete="email"
  108. placeholder={t('login.emailPlaceholder') || ''}
  109. tabIndex={1}
  110. />
  111. </div>
  112. </div>
  113. <div className='mb-3'>
  114. <label htmlFor="password" className="my-2 flex items-center justify-between">
  115. <span className='system-md-semibold text-text-secondary'>{t('login.password')}</span>
  116. <Link
  117. href={`/webapp-reset-password?${searchParams.toString()}`}
  118. className={`system-xs-regular ${isEmailSetup ? 'text-components-button-secondary-accent-text' : 'pointer-events-none text-components-button-secondary-accent-text-disabled'}`}
  119. tabIndex={isEmailSetup ? 0 : -1}
  120. aria-disabled={!isEmailSetup}
  121. >
  122. {t('login.forget')}
  123. </Link>
  124. </label>
  125. <div className="relative mt-1">
  126. <Input
  127. id="password"
  128. value={password}
  129. onChange={e => setPassword(e.target.value)}
  130. onKeyDown={(e) => {
  131. if (e.key === 'Enter')
  132. handleEmailPasswordLogin()
  133. }}
  134. type={showPassword ? 'text' : 'password'}
  135. autoComplete="current-password"
  136. placeholder={t('login.passwordPlaceholder') || ''}
  137. tabIndex={2}
  138. />
  139. <div className="absolute inset-y-0 right-0 flex items-center">
  140. <Button
  141. type="button"
  142. variant='ghost'
  143. onClick={() => setShowPassword(!showPassword)}
  144. >
  145. {showPassword ? '👀' : '😝'}
  146. </Button>
  147. </div>
  148. </div>
  149. </div>
  150. <div className='mb-2'>
  151. <Button
  152. tabIndex={2}
  153. variant='primary'
  154. onClick={handleEmailPasswordLogin}
  155. disabled={isLoading || !email || !password}
  156. className="w-full"
  157. >{t('login.signBtn')}</Button>
  158. </div>
  159. </form>
  160. }