Tenantlı uygulama için ForgotPassword, ResetPassword
This commit is contained in:
parent
f9c5910813
commit
d161e0f4b9
17 changed files with 538 additions and 416 deletions
|
|
@ -138,11 +138,15 @@ User Detail: {string.Format(userDetailUrl, user.Id)}";
|
|||
}
|
||||
|
||||
[Captcha]
|
||||
public async Task SendAccountConfirmationCodeAsync(SendAccountConfirmationCodeInputDto input)
|
||||
public async Task<bool> SendAccountConfirmationCodeAsync(SendAccountConfirmationCodeInputDto input)
|
||||
{
|
||||
var user = await UserManager.FindByEmailAsync(input.EmailAddress);
|
||||
if (user != null)
|
||||
await SendConfirmationCodeAsync(user);
|
||||
if (user == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return await SendConfirmationCodeAsync(user);
|
||||
}
|
||||
|
||||
public async Task<bool> VerifyAccountConfirmationCodeAsync(VerifyAccountConfirmationCodeInputDto input)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Flurl.Http;
|
||||
using Sozsoft.Platform.Localization;
|
||||
|
|
@ -23,6 +24,11 @@ public class TurnstileCaptchaManager : PlatformDomainService, ICaptchaManager
|
|||
|
||||
public async Task<bool> VerifyCaptchaAsync(string CaptchaResponse, bool throwOnFailure = false)
|
||||
{
|
||||
if (CaptchaResponse.IsNullOrWhiteSpace())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var endPoint = await SettingProvider.GetOrNullAsync(PlatformConsts.AbpAccount.Captcha.EndPoint);
|
||||
var privateKey = await SettingProvider.GetOrNullAsync(PlatformConsts.AbpAccount.Captcha.SecretKey);
|
||||
if (endPoint.IsNullOrWhiteSpace() || privateKey.IsNullOrWhiteSpace())
|
||||
|
|
@ -46,18 +52,24 @@ public class TurnstileCaptchaManager : PlatformDomainService, ICaptchaManager
|
|||
|
||||
if (response != null && response.StatusCode == 200)
|
||||
{
|
||||
var result = await response.GetJsonAsync<dynamic>();
|
||||
if (!(bool)result.success)
|
||||
var result = await response.GetJsonAsync<TurnstileCaptchaVerifyResponse>();
|
||||
if (result?.Success != true)
|
||||
{
|
||||
if (throwOnFailure)
|
||||
{
|
||||
throw new UserFriendlyException(Localizer[PlatformConsts.AbpIdentity.User.CaptchaWrongCode]);
|
||||
}
|
||||
}
|
||||
return (bool)result.success;
|
||||
|
||||
return result?.Success == true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private class TurnstileCaptchaVerifyResponse
|
||||
{
|
||||
[JsonPropertyName("success")]
|
||||
public bool Success { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -187,8 +187,8 @@ public class PlatformHttpApiHostModule : AbpModule
|
|||
options.RedirectAllowedUrls.AddRange(configuration["App:RedirectAllowedUrls"]?.Split(',') ?? Array.Empty<string>());
|
||||
|
||||
options.Applications[PlatformConsts.React].RootUrl = configuration["App:ClientUrl"];
|
||||
options.Applications[PlatformConsts.React].Urls[PlatformConsts.Urls.EmailConfirmation] = "account/confirm";
|
||||
options.Applications[PlatformConsts.React].Urls[PlatformConsts.Urls.PasswordReset] = "account/reset-password";
|
||||
options.Applications[PlatformConsts.React].Urls[PlatformConsts.Urls.EmailConfirmation] = "confirm";
|
||||
options.Applications[PlatformConsts.React].Urls[PlatformConsts.Urls.PasswordReset] = "reset-password";
|
||||
options.Applications[PlatformConsts.React].Urls[PlatformConsts.Urls.UserDetail] = "account/{0}";
|
||||
|
||||
//options.Applications["MVC"].RootUrl = configuration["App:SelfUrl"];
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import Card from '@/components/ui/Card'
|
|||
import Logo from '@/components/template/Logo'
|
||||
import type { ReactNode, ReactElement } from 'react'
|
||||
import type { CommonProps } from '@/proxy/common'
|
||||
import { FaArrowLeft, FaCheck } from 'react-icons/fa';
|
||||
import { FaArrowLeft, FaCheck } from 'react-icons/fa'
|
||||
import { Avatar, Select } from '@/components/ui'
|
||||
import { useStoreActions, useStoreState } from '@/store'
|
||||
import appConfig from '@/proxy/configs/app.config'
|
||||
|
|
@ -90,7 +90,7 @@ const Simple = ({ children, content, ...rest }: SimpleProps) => {
|
|||
return (
|
||||
<div className="h-full">
|
||||
<Container className="flex flex-col flex-auto items-center justify-center min-w-0 h-full">
|
||||
<Card className="min-w-[320px] md:min-w-[450px]" bodyClass="md:p-5">
|
||||
<Card className="w-full min-w-[320px] max-w-[360px] md:min-w-[450px]" bodyClass="md:p-5">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
{!hasSubdomain() && (
|
||||
<a
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ const Logo = (props: LogoProps) => {
|
|||
|
||||
return (
|
||||
<div
|
||||
className={classNames('logo', 'my-1', className)}
|
||||
className={classNames('logo', 'my-2', className)}
|
||||
style={{
|
||||
...style,
|
||||
...{ width: logoWidth },
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useState, forwardRef } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import useTimeout from '../hooks/useTimeout'
|
||||
import { FaCheckCircle, FaInfoCircle, FaExclamation, FaTimesCircle } from 'react-icons/fa';
|
||||
import { FaCheckCircle, FaInfoCircle, FaExclamation, FaTimesCircle } from 'react-icons/fa'
|
||||
import { motion } from 'framer-motion'
|
||||
import CloseButton from '../CloseButton'
|
||||
import StatusIcon from '../StatusIcon'
|
||||
|
|
@ -9,173 +9,153 @@ import type { TypeAttributes, CommonProps } from '../@types/common'
|
|||
import type { ReactNode, MouseEvent } from 'react'
|
||||
|
||||
export interface AlertProps extends CommonProps {
|
||||
closable?: boolean
|
||||
customClose?: ReactNode | string
|
||||
customIcon?: ReactNode | string
|
||||
duration?: number
|
||||
title?: ReactNode | string
|
||||
onClose?: (e?: MouseEvent<HTMLDivElement>) => void
|
||||
rounded?: boolean
|
||||
showIcon?: boolean
|
||||
triggerByToast?: boolean
|
||||
type?: TypeAttributes.Status
|
||||
closable?: boolean
|
||||
customClose?: ReactNode | string
|
||||
customIcon?: ReactNode | string
|
||||
duration?: number
|
||||
title?: ReactNode | string
|
||||
onClose?: (e?: MouseEvent<HTMLDivElement>) => void
|
||||
rounded?: boolean
|
||||
showIcon?: boolean
|
||||
triggerByToast?: boolean
|
||||
type?: TypeAttributes.Status
|
||||
}
|
||||
|
||||
const DEFAULT_TYPE = 'warning'
|
||||
|
||||
const TYPE_MAP = {
|
||||
success: {
|
||||
backgroundColor: 'bg-emerald-50 dark:bg-emerald-500',
|
||||
titleColor: 'text-emerald-700 dark:text-emerald-50',
|
||||
textColor: 'text-emerald-500 dark:text-emerald-50',
|
||||
iconColor: 'text-emerald-400 dark:text-emerald-50',
|
||||
icon: <FaCheckCircle />,
|
||||
},
|
||||
info: {
|
||||
backgroundColor: 'bg-blue-50 dark:bg-blue-500',
|
||||
titleColor: 'text-blue-700 dark:text-blue-100',
|
||||
textColor: 'text-blue-500 dark:text-blue-100',
|
||||
iconColor: 'text-blue-400 dark:text-blue-100',
|
||||
icon: <FaInfoCircle />,
|
||||
},
|
||||
warning: {
|
||||
backgroundColor: 'bg-yellow-50 dark:bg-yellow-500',
|
||||
titleColor: 'text-yellow-700 dark:text-yellow-50',
|
||||
textColor: 'text-yellow-500 dark:text-yellow-50',
|
||||
iconColor: 'text-yellow-400 dark:text-yellow-50',
|
||||
icon: <FaExclamation />,
|
||||
},
|
||||
danger: {
|
||||
backgroundColor: 'bg-red-50 dark:bg-red-500',
|
||||
titleColor: 'text-red-700 dark:text-red-100',
|
||||
textColor: 'text-red-500 dark:text-red-100',
|
||||
iconColor: 'text-red-400 dark:text-red-100',
|
||||
icon: <FaTimesCircle />,
|
||||
},
|
||||
success: {
|
||||
backgroundColor: 'bg-emerald-50 dark:bg-emerald-500',
|
||||
titleColor: 'text-emerald-700 dark:text-emerald-50',
|
||||
textColor: 'text-emerald-500 dark:text-emerald-50',
|
||||
iconColor: 'text-emerald-400 dark:text-emerald-50',
|
||||
icon: <FaCheckCircle />,
|
||||
},
|
||||
info: {
|
||||
backgroundColor: 'bg-blue-50 dark:bg-blue-500',
|
||||
titleColor: 'text-blue-700 dark:text-blue-100',
|
||||
textColor: 'text-blue-500 dark:text-blue-100',
|
||||
iconColor: 'text-blue-400 dark:text-blue-100',
|
||||
icon: <FaInfoCircle />,
|
||||
},
|
||||
warning: {
|
||||
backgroundColor: 'bg-yellow-50 dark:bg-yellow-500',
|
||||
titleColor: 'text-yellow-700 dark:text-yellow-50',
|
||||
textColor: 'text-yellow-500 dark:text-yellow-50',
|
||||
iconColor: 'text-yellow-400 dark:text-yellow-50',
|
||||
icon: <FaExclamation />,
|
||||
},
|
||||
danger: {
|
||||
backgroundColor: 'bg-red-50 dark:bg-red-500',
|
||||
titleColor: 'text-red-700 dark:text-red-100',
|
||||
textColor: 'text-red-500 dark:text-red-100',
|
||||
iconColor: 'text-red-400 dark:text-red-100',
|
||||
icon: <FaTimesCircle />,
|
||||
},
|
||||
}
|
||||
|
||||
const TYPE_ARRAY: TypeAttributes.Status[] = [
|
||||
'success',
|
||||
'danger',
|
||||
'info',
|
||||
'warning',
|
||||
]
|
||||
const TYPE_ARRAY: TypeAttributes.Status[] = ['success', 'danger', 'info', 'warning']
|
||||
|
||||
const Alert = forwardRef<HTMLDivElement, AlertProps>((props, ref) => {
|
||||
const {
|
||||
children,
|
||||
className,
|
||||
closable = false,
|
||||
customClose,
|
||||
customIcon,
|
||||
duration = 3000,
|
||||
title = null,
|
||||
onClose,
|
||||
rounded = true,
|
||||
showIcon = false,
|
||||
triggerByToast = false,
|
||||
...rest
|
||||
} = props
|
||||
const {
|
||||
children,
|
||||
className,
|
||||
closable = false,
|
||||
customClose,
|
||||
customIcon,
|
||||
duration = 3000,
|
||||
title = null,
|
||||
onClose,
|
||||
rounded = true,
|
||||
showIcon = false,
|
||||
triggerByToast = false,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
const getType = () => {
|
||||
const { type = DEFAULT_TYPE } = props
|
||||
if (TYPE_ARRAY.includes(type)) {
|
||||
return type
|
||||
}
|
||||
return DEFAULT_TYPE
|
||||
const getType = () => {
|
||||
const { type = DEFAULT_TYPE } = props
|
||||
if (TYPE_ARRAY.includes(type)) {
|
||||
return type
|
||||
}
|
||||
return DEFAULT_TYPE
|
||||
}
|
||||
|
||||
const type = getType()
|
||||
const typeMap = TYPE_MAP[type]
|
||||
const type = getType()
|
||||
const typeMap = TYPE_MAP[type]
|
||||
|
||||
const [display, setDisplay] = useState('show')
|
||||
const [display, setDisplay] = useState('show')
|
||||
|
||||
const { clear } = useTimeout(
|
||||
onClose as () => void,
|
||||
duration,
|
||||
(duration as number) > 0
|
||||
)
|
||||
const { clear } = useTimeout(onClose as () => void, duration, (duration as number) > 0)
|
||||
|
||||
const handleClose = (e: MouseEvent<HTMLDivElement>) => {
|
||||
setDisplay('hiding')
|
||||
onClose?.(e)
|
||||
clear()
|
||||
if (!triggerByToast) {
|
||||
setTimeout(() => {
|
||||
setDisplay('hide')
|
||||
}, 400)
|
||||
}
|
||||
}
|
||||
|
||||
const renderClose = () => {
|
||||
return (
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
role="presentation"
|
||||
onClick={(e) => handleClose(e)}
|
||||
>
|
||||
{customClose || <CloseButton defaultStyle={false} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const alertDefaultClass = 'p-2 relative flex'
|
||||
|
||||
const alertClass = classNames(
|
||||
'alert',
|
||||
alertDefaultClass,
|
||||
typeMap.backgroundColor,
|
||||
typeMap.textColor,
|
||||
!title ? 'font-semibold' : '',
|
||||
closable ? 'justify-between' : '',
|
||||
closable && !title ? 'items-center' : '',
|
||||
rounded && 'rounded-lg',
|
||||
className
|
||||
)
|
||||
|
||||
if (display === 'hide') {
|
||||
return null
|
||||
const handleClose = (e: MouseEvent<HTMLDivElement>) => {
|
||||
setDisplay('hiding')
|
||||
onClose?.(e)
|
||||
clear()
|
||||
if (!triggerByToast) {
|
||||
setTimeout(() => {
|
||||
setDisplay('hide')
|
||||
}, 400)
|
||||
}
|
||||
}
|
||||
|
||||
const renderClose = () => {
|
||||
return (
|
||||
<motion.div
|
||||
ref={ref}
|
||||
className={alertClass}
|
||||
initial={{ opacity: 1 }}
|
||||
animate={display === 'hiding' ? 'exit' : 'animate'}
|
||||
transition={{ duration: 0.25, type: 'tween' }}
|
||||
variants={{
|
||||
animate: {
|
||||
opacity: 1,
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
},
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
<div className={`flex ${title ? '' : 'items-center'}`}>
|
||||
{showIcon && (
|
||||
<StatusIcon
|
||||
iconColor={typeMap.iconColor}
|
||||
custom={customIcon}
|
||||
type={type}
|
||||
/>
|
||||
)}
|
||||
<div className={showIcon ? 'ltr:ml-2 rtl:mr-2' : ''}>
|
||||
{title ? (
|
||||
<div
|
||||
className={`font-semibold mb-1 ${typeMap.titleColor}`}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
) : null}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
{closable ? renderClose() : null}
|
||||
</motion.div>
|
||||
<div className="cursor-pointer" role="presentation" onClick={(e) => handleClose(e)}>
|
||||
{customClose || <CloseButton defaultStyle={false} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const alertDefaultClass = 'p-2 relative flex'
|
||||
|
||||
const alertClass = classNames(
|
||||
'alert',
|
||||
alertDefaultClass,
|
||||
typeMap.backgroundColor,
|
||||
typeMap.textColor,
|
||||
!title ? 'font-semibold' : '',
|
||||
closable ? 'justify-between' : '',
|
||||
closable && !title ? 'items-center' : '',
|
||||
rounded && 'rounded-lg',
|
||||
className,
|
||||
)
|
||||
|
||||
if (display === 'hide') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={ref}
|
||||
className={alertClass}
|
||||
initial={{ opacity: 1 }}
|
||||
animate={display === 'hiding' ? 'exit' : 'animate'}
|
||||
transition={{ duration: 0.25, type: 'tween' }}
|
||||
variants={{
|
||||
animate: {
|
||||
opacity: 1,
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
},
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
<div className={`flex min-w-0 ${title ? '' : 'items-center'}`}>
|
||||
{showIcon && <StatusIcon iconColor={typeMap.iconColor} custom={customIcon} type={type} />}
|
||||
<div
|
||||
className={classNames(
|
||||
'min-w-0 flex-1 whitespace-normal break-words leading-6',
|
||||
showIcon ? 'ltr:ml-2 rtl:mr-2' : '',
|
||||
)}
|
||||
>
|
||||
{title ? <div className={`font-semibold mb-1 ${typeMap.titleColor}`}>{title}</div> : null}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
{closable ? renderClose() : null}
|
||||
</motion.div>
|
||||
)
|
||||
})
|
||||
|
||||
Alert.displayName = 'Alert'
|
||||
|
|
|
|||
|
|
@ -15,6 +15,37 @@ const AccessDenied = React.lazy(() => import('@/views/AccessDenied'))
|
|||
const NotFound = React.lazy(() => import('@/views/NotFound'))
|
||||
const DatabaseSetup = React.lazy(() => import('@/views/setup/DatabaseSetup'))
|
||||
|
||||
const RootRedirect = () => {
|
||||
const location = useLocation()
|
||||
const searchParams = new URLSearchParams(location.search)
|
||||
const isPasswordResetLink = searchParams.has('userId') && searchParams.has('resetToken')
|
||||
|
||||
return (
|
||||
<Navigate
|
||||
to={
|
||||
isPasswordResetLink
|
||||
? `${ROUTES_ENUM.authenticated.resetPassword}${location.search}`
|
||||
: hasSubdomain()
|
||||
? ROUTES_ENUM.authenticated.login
|
||||
: ROUTES_ENUM.public.home
|
||||
}
|
||||
replace
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const LegacyPasswordResetRedirect = () => {
|
||||
const location = useLocation()
|
||||
|
||||
return <Navigate to={`${ROUTES_ENUM.authenticated.resetPassword}${location.search}`} replace />
|
||||
}
|
||||
|
||||
const LegacyEmailConfirmationRedirect = () => {
|
||||
const location = useLocation()
|
||||
|
||||
return <Navigate to={location.pathname.replace(/^\/account/, '')} replace />
|
||||
}
|
||||
|
||||
export const DynamicRouter: React.FC = () => {
|
||||
const { routes, loading, error } = useDynamicRoutes()
|
||||
const { registeredComponents, renderComponent, isComponentRegistered } = useComponents()
|
||||
|
|
@ -33,7 +64,11 @@ export const DynamicRouter: React.FC = () => {
|
|||
{dynamicRoutes
|
||||
.filter((r) => r.routeType === 'protected')
|
||||
.map((route) => {
|
||||
const Component = route.getComponent(registeredComponents, renderComponent, isComponentRegistered)
|
||||
const Component = route.getComponent(
|
||||
registeredComponents,
|
||||
renderComponent,
|
||||
isComponentRegistered,
|
||||
)
|
||||
return (
|
||||
<Route
|
||||
key={route.key}
|
||||
|
|
@ -85,7 +120,11 @@ export const DynamicRouter: React.FC = () => {
|
|||
hasSubdomain() ? r.routeType === 'authenticated' : r.routeType !== 'protected',
|
||||
)
|
||||
.map((route) => {
|
||||
const Component = route.getComponent(registeredComponents, renderComponent, isComponentRegistered)
|
||||
const Component = route.getComponent(
|
||||
registeredComponents,
|
||||
renderComponent,
|
||||
isComponentRegistered,
|
||||
)
|
||||
return (
|
||||
<Route
|
||||
key={route.key}
|
||||
|
|
@ -100,15 +139,9 @@ export const DynamicRouter: React.FC = () => {
|
|||
})}
|
||||
|
||||
{/* root redirect */}
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<Navigate
|
||||
to={hasSubdomain() ? ROUTES_ENUM.authenticated.login : ROUTES_ENUM.public.home}
|
||||
replace
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route path="/" element={<RootRedirect />} />
|
||||
<Route path="/account/confirm/:userId/:token" element={<LegacyEmailConfirmationRedirect />} />
|
||||
<Route path="/account/reset-password" element={<LegacyPasswordResetRedirect />} />
|
||||
|
||||
{/* public access denied (statik) */}
|
||||
<Route
|
||||
|
|
|
|||
|
|
@ -52,8 +52,8 @@ export const resetPassword = (userId: string, resetToken: string, password: stri
|
|||
},
|
||||
})
|
||||
|
||||
export const sendAccountConfirmationCode = (data: any) => {
|
||||
apiService.fetchData({
|
||||
export const sendAccountConfirmationCode = (data: any) =>
|
||||
apiService.fetchData<boolean>({
|
||||
method: 'POST',
|
||||
url: 'api/app/platform-account/send-account-confirmation-code',
|
||||
data: {
|
||||
|
|
@ -61,7 +61,6 @@ export const sendAccountConfirmationCode = (data: any) => {
|
|||
captchaResponse: data.captchaResponse,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const verifyAccountConfirmationCode = (userId: string, token: string) =>
|
||||
apiService.fetchData({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||
import {
|
||||
sendAccountConfirmationCode,
|
||||
verifyAccountConfirmationCode,
|
||||
|
|
@ -12,15 +13,28 @@ function useAccount() {
|
|||
|
||||
const sendConfirmationCode = async (values: any) => {
|
||||
try {
|
||||
await sendAccountConfirmationCode(values)
|
||||
const result = await sendAccountConfirmationCode(values)
|
||||
if (result.data !== true) {
|
||||
throw new Error('This email is already confirmed or no account was found.')
|
||||
}
|
||||
|
||||
setError('')
|
||||
setMessage('Verification code has been sent to your e-mail address.')
|
||||
return {
|
||||
status: 'success',
|
||||
}
|
||||
} catch (error: any) {
|
||||
const err =
|
||||
error?.response?.data?.error?.message ||
|
||||
error?.response?.data?.message ||
|
||||
error?.message ||
|
||||
error.toString()
|
||||
|
||||
setMessage('')
|
||||
setError(error?.response?.data?.message || error.toString())
|
||||
setError(err)
|
||||
return {
|
||||
status: 'failed',
|
||||
message: error?.response?.data?.message || error.toString(),
|
||||
message: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -38,7 +52,7 @@ function useAccount() {
|
|||
setError('')
|
||||
setMessage('Your account is confirmed')
|
||||
setTimeout(() => {
|
||||
navigate('/account/login')
|
||||
navigate(ROUTES_ENUM.authenticated.login)
|
||||
}, 3000)
|
||||
} else {
|
||||
throw new Error('Invalid token')
|
||||
|
|
|
|||
|
|
@ -46,9 +46,11 @@ const Views = (props: ViewsProps) => {
|
|||
<ErrorBoundary fallbackRender={fallbackRender}>
|
||||
<Suspense fallback={<Loading loading={true} />}>
|
||||
{!!warning?.length && (
|
||||
<Alert showIcon className="mb-4" type="warning">
|
||||
<Alert showIcon className="mb-4 text-sm text-left" type="warning">
|
||||
{warning.map((w, i) => (
|
||||
<div key={i}>{w}</div>
|
||||
<div key={i} className="whitespace-normal break-words">
|
||||
{w}
|
||||
</div>
|
||||
))}
|
||||
</Alert>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,9 @@ import { sendExtendLoginRequest } from '@/services/account.service'
|
|||
import { store } from '@/store'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import useTimeOutMessage from '@/utils/hooks/useTimeOutMessage'
|
||||
import { TurnstileInstance } from '@marsidev/react-turnstile'
|
||||
import { Field, Form, Formik } from 'formik'
|
||||
import { useState } from 'react'
|
||||
import { useRef, useState } from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
|
|
@ -35,21 +36,23 @@ const ExtendLogin = () => {
|
|||
|
||||
const [message, setMessage] = useTimeOutMessage(10000)
|
||||
const { translate } = useLocalization()
|
||||
const captchaRef = useRef<TurnstileInstance>(null)
|
||||
|
||||
const onSendMail = async (
|
||||
values: ExtendLoginFormSchema,
|
||||
setSubmitting: (isSubmitting: boolean) => void,
|
||||
setFieldValue: (field: string, value: string) => void,
|
||||
) => {
|
||||
const { email, captchaResponse } = values
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const resp = await sendExtendLoginRequest({ email, captchaResponse })
|
||||
if (resp.data) {
|
||||
setSubmitting(false)
|
||||
setEmailSent(true)
|
||||
}
|
||||
await sendExtendLoginRequest({ email, captchaResponse })
|
||||
setEmailSent(true)
|
||||
} catch (error: any) {
|
||||
setMessage(error?.response?.data || error.toString())
|
||||
} finally {
|
||||
setFieldValue('captchaResponse', '')
|
||||
captchaRef.current?.reset()
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
|
@ -63,11 +66,13 @@ const ExtendLogin = () => {
|
|||
></Helmet>
|
||||
<div>
|
||||
<h3 className="mb-1">{translate('::Abp.Account.ExtendLogin.Title')}</h3>
|
||||
<p>{translate('::Abp.Account.ExtendLogin.Description')}</p>
|
||||
<Alert showIcon className="mt-4 mb-4" type="success">
|
||||
{translate('::Abp.Account.ExtendLogin.Description')}
|
||||
</Alert>
|
||||
<div className="mt-4 text-center">
|
||||
<span>{translate('::Abp.Account.Backto')} </span>
|
||||
<ActionLink to={signInUrl}>{translate('::Abp.Account.SignIn')}</ActionLink>
|
||||
</div>{' '}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
|
|
@ -92,9 +97,9 @@ const ExtendLogin = () => {
|
|||
captchaResponse: '',
|
||||
}}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={(values, { setSubmitting }) => {
|
||||
onSubmit={(values, { setSubmitting, setFieldValue }) => {
|
||||
if (!disableSubmit) {
|
||||
onSendMail(values, setSubmitting)
|
||||
onSendMail(values, setSubmitting, setFieldValue)
|
||||
} else {
|
||||
setSubmitting(false)
|
||||
}
|
||||
|
|
@ -115,6 +120,7 @@ const ExtendLogin = () => {
|
|||
</FormItem>
|
||||
</div>
|
||||
<Captcha
|
||||
ref={captchaRef}
|
||||
onError={() => setFieldValue('captchaResponse', '')}
|
||||
onExpire={() => setFieldValue('captchaResponse', '')}
|
||||
onSuccess={(token: string) => setFieldValue('captchaResponse', token)}
|
||||
|
|
|
|||
|
|
@ -11,10 +11,11 @@ import { sendPasswordResetCode } from '@/services/account.service'
|
|||
import { store } from '@/store'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import useTimeOutMessage from '@/utils/hooks/useTimeOutMessage'
|
||||
import { TurnstileInstance } from '@marsidev/react-turnstile'
|
||||
import type { AxiosError } from 'axios'
|
||||
import { Field, Form, Formik } from 'formik'
|
||||
import { motion } from 'framer-motion'
|
||||
import { useState } from 'react'
|
||||
import { useRef, useState } from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
|
|
@ -37,23 +38,25 @@ const ForgotPassword = () => {
|
|||
|
||||
const [message, setMessage] = useTimeOutMessage()
|
||||
const { translate } = useLocalization()
|
||||
const captchaRef = useRef<TurnstileInstance>(null)
|
||||
|
||||
const onSendMail = async (
|
||||
values: ForgotPasswordFormSchema,
|
||||
setSubmitting: (isSubmitting: boolean) => void,
|
||||
setFieldValue: (field: string, value: string) => void,
|
||||
) => {
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const resp = await sendPasswordResetCode(values)
|
||||
if (resp.data) {
|
||||
setSubmitting(false)
|
||||
setEmailSent(true)
|
||||
}
|
||||
await sendPasswordResetCode(values)
|
||||
setEmailSent(true)
|
||||
} catch (errors) {
|
||||
setMessage(
|
||||
(errors as AxiosError<{ message: string }>)?.response?.data?.message ||
|
||||
(errors as Error).toString(),
|
||||
)
|
||||
} finally {
|
||||
setFieldValue('captchaResponse', '')
|
||||
captchaRef.current?.reset()
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
|
@ -74,7 +77,6 @@ const ForgotPassword = () => {
|
|||
{emailSent ? (
|
||||
<>
|
||||
<h3 className="mb-1">{translate('::Abp.Account.ForgotPassword.Checkyouremail')}</h3>
|
||||
<p>{translate('::Abp.Account.ForgotPassword.Checkyouremail.Message')}</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -88,51 +90,64 @@ const ForgotPassword = () => {
|
|||
{message}
|
||||
</Alert>
|
||||
)}
|
||||
<TenantSelector />
|
||||
<Formik
|
||||
initialValues={{
|
||||
email: userName,
|
||||
captchaResponse: '',
|
||||
}}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={(values, { setSubmitting }) => {
|
||||
if (!disableSubmit) {
|
||||
onSendMail(values, setSubmitting)
|
||||
} else {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ touched, errors, isSubmitting, setFieldValue }) => (
|
||||
<Form>
|
||||
<FormContainer>
|
||||
<div className={emailSent ? 'hidden' : ''}>
|
||||
<FormItem invalid={errors.email && touched.email} errorMessage={errors.email}>
|
||||
<Field
|
||||
type="email"
|
||||
autoComplete="off"
|
||||
name="email"
|
||||
placeholder={translate('::Abp.Account.EmailAddress')}
|
||||
component={Input}
|
||||
{emailSent ? (
|
||||
<>
|
||||
<Alert showIcon className="mb-4" type="success">
|
||||
{translate('::Abp.Account.ForgotPassword.Checkyouremail.Message')}
|
||||
</Alert>
|
||||
<div className="mt-4 text-center">
|
||||
<span>{translate('::Abp.Account.Backto')} </span>
|
||||
<ActionLink to={signInUrl}>{translate('::Abp.Account.SignIn')}</ActionLink>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TenantSelector />
|
||||
<Formik
|
||||
initialValues={{
|
||||
email: userName,
|
||||
captchaResponse: '',
|
||||
}}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={(values, { setSubmitting, setFieldValue }) => {
|
||||
if (!disableSubmit) {
|
||||
onSendMail(values, setSubmitting, setFieldValue)
|
||||
} else {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ touched, errors, isSubmitting, setFieldValue }) => (
|
||||
<Form>
|
||||
<FormContainer>
|
||||
<FormItem invalid={errors.email && touched.email} errorMessage={errors.email}>
|
||||
<Field
|
||||
type="email"
|
||||
autoComplete="off"
|
||||
name="email"
|
||||
placeholder={translate('::Abp.Account.EmailAddress')}
|
||||
component={Input}
|
||||
/>
|
||||
</FormItem>
|
||||
<Captcha
|
||||
ref={captchaRef}
|
||||
onError={() => setFieldValue('captchaResponse', '')}
|
||||
onExpire={() => setFieldValue('captchaResponse', '')}
|
||||
onSuccess={(token: string) => setFieldValue('captchaResponse', token)}
|
||||
/>
|
||||
</FormItem>
|
||||
</div>
|
||||
<Captcha
|
||||
onError={() => setFieldValue('captchaResponse', '')}
|
||||
onExpire={() => setFieldValue('captchaResponse', '')}
|
||||
onSuccess={(token: string) => setFieldValue('captchaResponse', token)}
|
||||
/>
|
||||
<Button block loading={isSubmitting} variant="solid" type="submit">
|
||||
{emailSent ? 'Resend Email' : 'Send Email'}
|
||||
</Button>
|
||||
<div className="mt-4 text-center">
|
||||
<span>{translate('::Abp.Account.Backto')} </span>
|
||||
<ActionLink to={signInUrl}>{translate('::Abp.Account.SignIn')}</ActionLink>
|
||||
</div>
|
||||
</FormContainer>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
<Button block loading={isSubmitting} variant="solid" type="submit">
|
||||
Send Email
|
||||
</Button>
|
||||
<div className="mt-4 text-center">
|
||||
<span>{translate('::Abp.Account.Backto')} </span>
|
||||
<ActionLink to={signInUrl}>{translate('::Abp.Account.SignIn')}</ActionLink>
|
||||
</div>
|
||||
</FormContainer>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -153,6 +153,11 @@ const Login = () => {
|
|||
findUiVersion()
|
||||
}
|
||||
|
||||
if (showCaptcha) {
|
||||
setFieldValue('captchaResponse', '')
|
||||
captchaRef.current?.reset()
|
||||
}
|
||||
|
||||
setSubmitting(false)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ import { ROUTES_ENUM } from '@/routes/route.constant'
|
|||
import useAuth from '@/utils/hooks/useAuth'
|
||||
import useTimeOutMessage from '@/utils/hooks/useTimeOutMessage'
|
||||
import Captcha from '@/components/shared/Captcha'
|
||||
import { TurnstileInstance } from '@marsidev/react-turnstile'
|
||||
import { Field, Form, Formik } from 'formik'
|
||||
import { useState } from 'react'
|
||||
import { useRef, useState } from 'react'
|
||||
import * as Yup from 'yup'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { Helmet } from 'react-helmet'
|
||||
|
|
@ -30,6 +31,7 @@ const validationSchema = Yup.object().shape({
|
|||
confirmPassword: Yup.string().oneOf([Yup.ref('password')], 'Your passwords do not match'),
|
||||
name: Yup.string().required(),
|
||||
surname: Yup.string().required(),
|
||||
captchaResponse: Yup.string().required(),
|
||||
})
|
||||
|
||||
const Register = () => {
|
||||
|
|
@ -41,24 +43,30 @@ const Register = () => {
|
|||
|
||||
const [message, setMessage] = useState('')
|
||||
const [error, setError] = useTimeOutMessage(10000)
|
||||
const captchaRef = useRef<TurnstileInstance>(null)
|
||||
|
||||
const onSignUp = async (
|
||||
values: SignUpFormSchema,
|
||||
setSubmitting: (isSubmitting: boolean) => void,
|
||||
setFieldValue: (field: string, value: string) => void,
|
||||
) => {
|
||||
setSubmitting(true)
|
||||
const result = await signUp(values)
|
||||
try {
|
||||
const result = await signUp(values)
|
||||
|
||||
if (result?.status === 'failed') {
|
||||
setError(result.message)
|
||||
setMessage('')
|
||||
} else {
|
||||
setMessage(translate('::Abp.Account.Register.ResultMessage'))
|
||||
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
|
||||
setError('')
|
||||
if (result?.status === 'failed' || result?.status === 'error') {
|
||||
setError(result.message)
|
||||
setMessage('')
|
||||
} else {
|
||||
setMessage(translate('::Abp.Account.Register.ResultMessage'))
|
||||
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
|
||||
setError('')
|
||||
}
|
||||
} finally {
|
||||
setFieldValue('captchaResponse', '')
|
||||
captchaRef.current?.reset()
|
||||
setSubmitting(false)
|
||||
}
|
||||
|
||||
setSubmitting(false)
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -84,107 +92,117 @@ const Register = () => {
|
|||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
<TenantSelector />
|
||||
<Formik
|
||||
initialValues={{
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
name: '',
|
||||
surname: '',
|
||||
captchaResponse: '',
|
||||
}}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={(values, { setSubmitting }) => {
|
||||
if (!disableSubmit) {
|
||||
onSignUp(values, setSubmitting)
|
||||
} else {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ touched, errors, isSubmitting, setFieldValue }) => (
|
||||
<Form>
|
||||
<FormContainer>
|
||||
<FormItem
|
||||
label={translate('::Abp.Account.EmailAddress')}
|
||||
invalid={errors.email && touched.email}
|
||||
errorMessage={errors.email}
|
||||
>
|
||||
<Field
|
||||
type="email"
|
||||
autoComplete="off"
|
||||
name="email"
|
||||
placeholder={translate('::Abp.Account.EmailAddress')}
|
||||
component={Input}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label={translate('::Abp.Identity.Password')}
|
||||
invalid={errors.password && touched.password}
|
||||
errorMessage={errors.password}
|
||||
>
|
||||
<Field
|
||||
autoComplete="off"
|
||||
name="password"
|
||||
placeholder={translate('::Abp.Identity.Password')}
|
||||
component={PasswordInput}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label={translate('::Abp.Account.ConfirmPassword')}
|
||||
invalid={errors.confirmPassword && touched.confirmPassword}
|
||||
errorMessage={errors.confirmPassword}
|
||||
>
|
||||
<Field
|
||||
autoComplete="off"
|
||||
name="confirmPassword"
|
||||
placeholder={translate('::Abp.Account.ConfirmPassword')}
|
||||
component={PasswordInput}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label={translate('::Abp.Identity.User.UserInformation.Name')}
|
||||
invalid={errors.name && touched.name}
|
||||
errorMessage={errors.name}
|
||||
>
|
||||
<Field
|
||||
type="name"
|
||||
autoComplete="off"
|
||||
name="name"
|
||||
placeholder={translate('::Abp.Identity.User.UserInformation.Name')}
|
||||
component={Input}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label={translate('::Abp.Identity.User.UserInformation.Surname')}
|
||||
invalid={errors.surname && touched.surname}
|
||||
errorMessage={errors.surname}
|
||||
>
|
||||
<Field
|
||||
type="surname"
|
||||
autoComplete="off"
|
||||
name="surname"
|
||||
placeholder={translate('::Abp.Identity.User.UserInformation.Surname')}
|
||||
component={Input}
|
||||
/>
|
||||
</FormItem>
|
||||
<Captcha
|
||||
onError={() => setFieldValue('captchaResponse', '')}
|
||||
onExpire={() => setFieldValue('captchaResponse', '')}
|
||||
onSuccess={(token) => setFieldValue('captchaResponse', token)}
|
||||
/>
|
||||
<Button block loading={isSubmitting} variant="solid" type="submit">
|
||||
{isSubmitting ? 'Creating Account...' : 'Sign Up'}
|
||||
</Button>
|
||||
<div className="mt-4 text-center">
|
||||
<span>{translate('::Abp.Account.Register.AlreadyHaveAnAccount')} </span>
|
||||
<ActionLink to={signInUrl}>{translate('::Abp.Account.SignIn')}</ActionLink>
|
||||
</div>
|
||||
</FormContainer>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
{message ? (
|
||||
<div className="mt-4 text-center">
|
||||
<span>{translate('::Abp.Account.Register.AlreadyHaveAnAccount')} </span>
|
||||
<ActionLink to={signInUrl}>{translate('::Abp.Account.SignIn')}</ActionLink>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<TenantSelector />
|
||||
<Formik
|
||||
initialValues={{
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
name: '',
|
||||
surname: '',
|
||||
captchaResponse: '',
|
||||
}}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={(values, { setSubmitting, setFieldValue }) => {
|
||||
if (!disableSubmit) {
|
||||
onSignUp(values, setSubmitting, setFieldValue)
|
||||
} else {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ touched, errors, isSubmitting, setFieldValue }) => (
|
||||
<Form>
|
||||
<FormContainer>
|
||||
<FormItem
|
||||
label={translate('::Abp.Account.EmailAddress')}
|
||||
invalid={errors.email && touched.email}
|
||||
errorMessage={errors.email}
|
||||
>
|
||||
<Field
|
||||
type="email"
|
||||
autoComplete="off"
|
||||
name="email"
|
||||
placeholder={translate('::Abp.Account.EmailAddress')}
|
||||
component={Input}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label={translate('::Abp.Identity.Password')}
|
||||
invalid={errors.password && touched.password}
|
||||
errorMessage={errors.password}
|
||||
>
|
||||
<Field
|
||||
autoComplete="off"
|
||||
name="password"
|
||||
placeholder={translate('::Abp.Identity.Password')}
|
||||
component={PasswordInput}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label={translate('::Abp.Account.ConfirmPassword')}
|
||||
invalid={errors.confirmPassword && touched.confirmPassword}
|
||||
errorMessage={errors.confirmPassword}
|
||||
>
|
||||
<Field
|
||||
autoComplete="off"
|
||||
name="confirmPassword"
|
||||
placeholder={translate('::Abp.Account.ConfirmPassword')}
|
||||
component={PasswordInput}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label={translate('::Abp.Identity.User.UserInformation.Name')}
|
||||
invalid={errors.name && touched.name}
|
||||
errorMessage={errors.name}
|
||||
>
|
||||
<Field
|
||||
type="name"
|
||||
autoComplete="off"
|
||||
name="name"
|
||||
placeholder={translate('::Abp.Identity.User.UserInformation.Name')}
|
||||
component={Input}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label={translate('::Abp.Identity.User.UserInformation.Surname')}
|
||||
invalid={errors.surname && touched.surname}
|
||||
errorMessage={errors.surname}
|
||||
>
|
||||
<Field
|
||||
type="surname"
|
||||
autoComplete="off"
|
||||
name="surname"
|
||||
placeholder={translate('::Abp.Identity.User.UserInformation.Surname')}
|
||||
component={Input}
|
||||
/>
|
||||
</FormItem>
|
||||
<Captcha
|
||||
ref={captchaRef}
|
||||
onError={() => setFieldValue('captchaResponse', '')}
|
||||
onExpire={() => setFieldValue('captchaResponse', '')}
|
||||
onSuccess={(token) => setFieldValue('captchaResponse', token)}
|
||||
/>
|
||||
<Button block loading={isSubmitting} variant="solid" type="submit">
|
||||
{isSubmitting ? 'Creating Account...' : 'Sign Up'}
|
||||
</Button>
|
||||
<div className="mt-4 text-center">
|
||||
<span>{translate('::Abp.Account.Register.AlreadyHaveAnAccount')} </span>
|
||||
<ActionLink to={signInUrl}>{translate('::Abp.Account.SignIn')}</ActionLink>
|
||||
</div>
|
||||
</FormContainer>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -59,16 +59,14 @@ const ResetPassword = () => {
|
|||
const { password } = values
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const resp = await resetPassword(userId, resetToken, password)
|
||||
if (resp.data) {
|
||||
setSubmitting(false)
|
||||
setResetComplete(true)
|
||||
}
|
||||
await resetPassword(userId, resetToken, password)
|
||||
setResetComplete(true)
|
||||
} catch (errors) {
|
||||
setMessage(
|
||||
(errors as AxiosError<{ message: string }>)?.response?.data?.message ||
|
||||
(errors as Error).toString(),
|
||||
)
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@ import { Field, Form, Formik } from 'formik'
|
|||
import * as Yup from 'yup'
|
||||
import { ActionLink, TenantSelector } from '@/components/shared'
|
||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||
import { store } from '@/store'
|
||||
import { store, useStoreActions } from '@/store'
|
||||
import Captcha from '@/components/shared/Captcha'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { APP_NAME } from '@/constants/app.constant'
|
||||
import { TurnstileInstance } from '@marsidev/react-turnstile'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
type FormSchema = {
|
||||
email: string
|
||||
|
|
@ -25,13 +27,32 @@ const SendConfirmationCode = () => {
|
|||
const { translate } = useLocalization()
|
||||
|
||||
const { message, error, sendConfirmationCode } = useAccount()
|
||||
const { setWarning } = useStoreActions((actions) => actions.base.messages)
|
||||
const captchaRef = useRef<TurnstileInstance>(null)
|
||||
const [captchaKey, setCaptchaKey] = useState(0)
|
||||
|
||||
const onSubmit = async (values: FormSchema, setSubmitting: (isSubmitting: boolean) => void) => {
|
||||
useEffect(() => {
|
||||
setWarning('')
|
||||
}, [setWarning])
|
||||
|
||||
const onSubmit = async (
|
||||
values: FormSchema,
|
||||
setSubmitting: (isSubmitting: boolean) => void,
|
||||
setFieldValue: (field: string, value: string) => void,
|
||||
) => {
|
||||
setSubmitting(true)
|
||||
|
||||
await sendConfirmationCode(values)
|
||||
|
||||
setSubmitting(false)
|
||||
try {
|
||||
const result = await sendConfirmationCode(values)
|
||||
if (result?.status !== 'failed') {
|
||||
setWarning('')
|
||||
}
|
||||
} finally {
|
||||
setFieldValue('captchaResponse', '')
|
||||
captchaRef.current?.reset()
|
||||
setCaptchaKey((key) => key + 1)
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -42,10 +63,12 @@ const SendConfirmationCode = () => {
|
|||
defaultTitle={APP_NAME}
|
||||
></Helmet>
|
||||
|
||||
<div className="mb-8">
|
||||
<h3 className="mb-1">{translate('::Abp.Account.SendConfirmationCode')}</h3>
|
||||
<p>{translate('::Abp.Account.SendConfirmationCode.Message')}</p>
|
||||
</div>
|
||||
{!message && (
|
||||
<div className="mb-8">
|
||||
<h3 className="mb-1">{translate('::Abp.Account.SendConfirmationCode')}</h3>
|
||||
<p>{translate('::Abp.Account.SendConfirmationCode.Message')}</p>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
{message && (
|
||||
<Alert showIcon className="mb-4" type="success">
|
||||
|
|
@ -57,51 +80,64 @@ const SendConfirmationCode = () => {
|
|||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
<TenantSelector />
|
||||
<Formik
|
||||
initialValues={{
|
||||
email: userName,
|
||||
captchaResponse: '',
|
||||
}}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={(values, { setSubmitting }) => {
|
||||
onSubmit(values, setSubmitting)
|
||||
}}
|
||||
>
|
||||
{({ touched, errors, isSubmitting, setFieldValue }) => (
|
||||
<Form>
|
||||
<FormContainer>
|
||||
<FormItem
|
||||
label={translate('::Abp.Account.EmailAddress')}
|
||||
invalid={errors.email && touched.email}
|
||||
errorMessage={errors.email}
|
||||
>
|
||||
<Field
|
||||
type="email"
|
||||
autoComplete="off"
|
||||
name="email"
|
||||
placeholder={translate('::Abp.Account.EmailAddress')}
|
||||
component={Input}
|
||||
/>
|
||||
</FormItem>
|
||||
<Captcha
|
||||
onError={() => setFieldValue('captchaResponse', '')}
|
||||
onExpire={() => setFieldValue('captchaResponse', '')}
|
||||
onSuccess={(token: string) => setFieldValue('captchaResponse', token)}
|
||||
/>
|
||||
<Button block loading={isSubmitting} variant="solid" type="submit">
|
||||
{isSubmitting ? 'Sending code...' : 'Send Code'}
|
||||
</Button>
|
||||
<div className="mt-4 text-center">
|
||||
<span>{translate('::Abp.Account.Backto')} </span>
|
||||
<ActionLink to={ROUTES_ENUM.authenticated.login}>
|
||||
{translate('::Abp.Account.SignIn')}
|
||||
</ActionLink>
|
||||
</div>
|
||||
</FormContainer>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
{message ? (
|
||||
<div className="mt-4 text-center">
|
||||
<span>{translate('::Abp.Account.Backto')} </span>
|
||||
<ActionLink to={ROUTES_ENUM.authenticated.login}>
|
||||
{translate('::Abp.Account.SignIn')}
|
||||
</ActionLink>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<TenantSelector />
|
||||
<Formik
|
||||
initialValues={{
|
||||
email: userName,
|
||||
captchaResponse: '',
|
||||
}}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={(values, { setSubmitting, setFieldValue }) => {
|
||||
onSubmit(values, setSubmitting, setFieldValue)
|
||||
}}
|
||||
>
|
||||
{({ touched, errors, isSubmitting, setFieldValue }) => (
|
||||
<Form>
|
||||
<FormContainer>
|
||||
<FormItem
|
||||
label={translate('::Abp.Account.EmailAddress')}
|
||||
invalid={errors.email && touched.email}
|
||||
errorMessage={errors.email}
|
||||
>
|
||||
<Field
|
||||
type="email"
|
||||
autoComplete="off"
|
||||
name="email"
|
||||
placeholder={translate('::Abp.Account.EmailAddress')}
|
||||
component={Input}
|
||||
/>
|
||||
</FormItem>
|
||||
<Captcha
|
||||
key={captchaKey}
|
||||
ref={captchaRef}
|
||||
onError={() => setFieldValue('captchaResponse', '')}
|
||||
onExpire={() => setFieldValue('captchaResponse', '')}
|
||||
onSuccess={(token: string) => setFieldValue('captchaResponse', token)}
|
||||
/>
|
||||
<Button block loading={isSubmitting} variant="solid" type="submit">
|
||||
{isSubmitting ? 'Sending code...' : 'Send Code'}
|
||||
</Button>
|
||||
<div className="mt-4 text-center">
|
||||
<span>{translate('::Abp.Account.Backto')} </span>
|
||||
<ActionLink to={ROUTES_ENUM.authenticated.login}>
|
||||
{translate('::Abp.Account.SignIn')}
|
||||
</ActionLink>
|
||||
</div>
|
||||
</FormContainer>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const Logo = (props: LogoProps) => {
|
|||
|
||||
return (
|
||||
<div
|
||||
className={classNames('logo', 'my-1', className)}
|
||||
className={classNames('logo', 'my-2', className)}
|
||||
style={{
|
||||
...style,
|
||||
...{ width: logoWidth },
|
||||
|
|
|
|||
Loading…
Reference in a new issue