You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

mail-and-password-auth.tsx 5.3KB

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