| import Link from 'next/link' | |
| import { useState } from 'react' | |
| import { useTranslation } from 'react-i18next' | |
| import { useRouter, useSearchParams } from 'next/navigation' | |
| import { useContext } from 'use-context-selector' | |
| import Button from '@/app/components/base/button' | |
| import Toast from '@/app/components/base/toast' | |
| import { emailRegex } from '@/config' | |
| import { login } from '@/service/common' | |
| import Input from '@/app/components/base/input' | |
| import I18NContext from '@/context/i18n' | |
| type MailAndPasswordAuthProps = { | |
| isInvite: boolean | |
| allowRegistration: boolean | |
| } | |
| const passwordRegex = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/ | |
| export default function MailAndPasswordAuth({ isInvite, allowRegistration }: MailAndPasswordAuthProps) { | |
| const { t } = useTranslation() | |
| const { locale } = useContext(I18NContext) | |
| const router = useRouter() | |
| const searchParams = useSearchParams() | |
| const [showPassword, setShowPassword] = useState(false) | |
| const emailFromLink = decodeURIComponent(searchParams.get('email') || '') | |
| const [email, setEmail] = useState(emailFromLink) | |
| const [password, setPassword] = useState('') | |
| const [isLoading, setIsLoading] = useState(false) | |
| const handleEmailPasswordLogin = async () => { | |
| if (!email) { | |
| Toast.notify({ type: 'error', message: t('login.error.emailEmpty') }) | |
| return | |
| } | |
| if (!emailRegex.test(email)) { | |
| Toast.notify({ | |
| type: 'error', | |
| message: t('login.error.emailInValid'), | |
| }) | |
| return | |
| } | |
| if (!password?.trim()) { | |
| Toast.notify({ type: 'error', message: t('login.error.passwordEmpty') }) | |
| return | |
| } | |
| if (!passwordRegex.test(password)) { | |
| Toast.notify({ | |
| type: 'error', | |
| message: t('login.error.passwordInvalid'), | |
| }) | |
| return | |
| } | |
| try { | |
| setIsLoading(true) | |
| const loginData: Record<string, any> = { | |
| email, | |
| password, | |
| language: locale, | |
| remember_me: true, | |
| } | |
| if (isInvite) | |
| loginData.invite_token = decodeURIComponent(searchParams.get('invite_token') as string) | |
| const res = await login({ | |
| url: '/login', | |
| body: loginData, | |
| }) | |
| if (res.result === 'success') { | |
| if (isInvite) { | |
| router.replace(`/signin/invite-settings?${searchParams.toString()}`) | |
| } | |
| else { | |
| localStorage.setItem('console_token', res.data.access_token) | |
| localStorage.setItem('refresh_token', res.data.refresh_token) | |
| router.replace('/apps') | |
| } | |
| } | |
| else if (res.code === 'account_not_found') { | |
| if (allowRegistration) { | |
| const params = new URLSearchParams() | |
| params.append('email', encodeURIComponent(email)) | |
| params.append('token', encodeURIComponent(res.data)) | |
| router.replace(`/reset-password/check-code?${params.toString()}`) | |
| } | |
| else { | |
| Toast.notify({ | |
| type: 'error', | |
| message: t('login.error.registrationNotAllowed'), | |
| }) | |
| } | |
| } | |
| else { | |
| Toast.notify({ | |
| type: 'error', | |
| message: res.data, | |
| }) | |
| } | |
| } | |
| finally { | |
| setIsLoading(false) | |
| } | |
| } | |
| return <form onSubmit={() => { }}> | |
| <div className='mb-3'> | |
| <label htmlFor="email" className="my-2 system-md-semibold text-text-secondary"> | |
| {t('login.email')} | |
| </label> | |
| <div className="mt-1"> | |
| <Input | |
| value={email} | |
| onChange={e => setEmail(e.target.value)} | |
| disabled={isInvite} | |
| id="email" | |
| type="email" | |
| autoComplete="email" | |
| placeholder={t('login.emailPlaceholder') || ''} | |
| tabIndex={1} | |
| /> | |
| </div> | |
| </div> | |
| <div className='mb-3'> | |
| <label htmlFor="password" className="my-2 flex items-center justify-between"> | |
| <span className='system-md-semibold text-text-secondary'>{t('login.password')}</span> | |
| <Link href={`/reset-password?${searchParams.toString()}`} className='system-xs-regular text-components-button-secondary-accent-text'> | |
| {t('login.forget')} | |
| </Link> | |
| </label> | |
| <div className="relative mt-1"> | |
| <Input | |
| id="password" | |
| value={password} | |
| onChange={e => setPassword(e.target.value)} | |
| onKeyDown={(e) => { | |
| if (e.key === 'Enter') | |
| handleEmailPasswordLogin() | |
| }} | |
| type={showPassword ? 'text' : 'password'} | |
| autoComplete="current-password" | |
| placeholder={t('login.passwordPlaceholder') || ''} | |
| tabIndex={2} | |
| /> | |
| <div className="absolute inset-y-0 right-0 flex items-center"> | |
| <Button | |
| type="button" | |
| variant='ghost' | |
| onClick={() => setShowPassword(!showPassword)} | |
| > | |
| {showPassword ? 'π' : 'π'} | |
| </Button> | |
| </div> | |
| </div> | |
| </div> | |
| <div className='mb-2'> | |
| <Button | |
| tabIndex={2} | |
| variant='primary' | |
| onClick={handleEmailPasswordLogin} | |
| disabled={isLoading || !email || !password} | |
| className="w-full" | |
| >{t('login.signBtn')}</Button> | |
| </div> | |
| </form> | |
| } | |