From d161e0f4b9d5d07f8425dd9219491653dfd64967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96zt=C3=BCrk?= Date: Tue, 2 Jun 2026 23:04:25 +0300 Subject: [PATCH] =?UTF-8?q?Tenantl=C4=B1=20uygulama=20i=C3=A7in=20ForgotPa?= =?UTF-8?q?ssword,=20ResetPassword?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Identity/PlatformAccountAppService.cs | 10 +- .../Identity/TurnstileCaptchaManager.cs | 22 +- .../PlatformHttpApiHostModule.cs | 4 +- .../components/layouts/AuthLayout/Simple.tsx | 4 +- ui/src/components/template/Logo.tsx | 2 +- ui/src/components/ui/Alert/Alert.tsx | 278 ++++++++---------- ui/src/routes/dynamicRouter.tsx | 57 +++- ui/src/services/account.service.ts | 5 +- ui/src/utils/hooks/useAccount.ts | 22 +- ui/src/views/Views.tsx | 6 +- ui/src/views/auth/ExtendLogin.tsx | 26 +- ui/src/views/auth/ForgotPassword.tsx | 117 ++++---- ui/src/views/auth/Login.tsx | 5 + ui/src/views/auth/Register.tsx | 242 ++++++++------- ui/src/views/auth/ResetPassword.tsx | 8 +- ui/src/views/auth/SendConfirmationCode.tsx | 144 +++++---- ui/src/views/public/Logo.tsx | 2 +- 17 files changed, 538 insertions(+), 416 deletions(-) diff --git a/api/src/Sozsoft.Platform.Application/Identity/PlatformAccountAppService.cs b/api/src/Sozsoft.Platform.Application/Identity/PlatformAccountAppService.cs index 24b1cb1..e869b21 100644 --- a/api/src/Sozsoft.Platform.Application/Identity/PlatformAccountAppService.cs +++ b/api/src/Sozsoft.Platform.Application/Identity/PlatformAccountAppService.cs @@ -138,11 +138,15 @@ User Detail: {string.Format(userDetailUrl, user.Id)}"; } [Captcha] - public async Task SendAccountConfirmationCodeAsync(SendAccountConfirmationCodeInputDto input) + public async Task 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 VerifyAccountConfirmationCodeAsync(VerifyAccountConfirmationCodeInputDto input) diff --git a/api/src/Sozsoft.Platform.Domain/Identity/TurnstileCaptchaManager.cs b/api/src/Sozsoft.Platform.Domain/Identity/TurnstileCaptchaManager.cs index 22fe71e..0ae7902 100644 --- a/api/src/Sozsoft.Platform.Domain/Identity/TurnstileCaptchaManager.cs +++ b/api/src/Sozsoft.Platform.Domain/Identity/TurnstileCaptchaManager.cs @@ -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 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(); - if (!(bool)result.success) + var result = await response.GetJsonAsync(); + 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; } + } +} diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs index 390a8c5..3459fef 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs @@ -187,8 +187,8 @@ public class PlatformHttpApiHostModule : AbpModule options.RedirectAllowedUrls.AddRange(configuration["App:RedirectAllowedUrls"]?.Split(',') ?? Array.Empty()); 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"]; diff --git a/ui/src/components/layouts/AuthLayout/Simple.tsx b/ui/src/components/layouts/AuthLayout/Simple.tsx index f93f0fe..2535dea 100644 --- a/ui/src/components/layouts/AuthLayout/Simple.tsx +++ b/ui/src/components/layouts/AuthLayout/Simple.tsx @@ -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 (
- +
{!hasSubdomain() && ( { return (
) => 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) => 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: , - }, - 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: , - }, - 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: , - }, - 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: , - }, + 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: , + }, + 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: , + }, + 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: , + }, + 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: , + }, } -const TYPE_ARRAY: TypeAttributes.Status[] = [ - 'success', - 'danger', - 'info', - 'warning', -] +const TYPE_ARRAY: TypeAttributes.Status[] = ['success', 'danger', 'info', 'warning'] const Alert = forwardRef((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) => { - setDisplay('hiding') - onClose?.(e) - clear() - if (!triggerByToast) { - setTimeout(() => { - setDisplay('hide') - }, 400) - } - } - - const renderClose = () => { - return ( -
handleClose(e)} - > - {customClose || } -
- ) - } - - 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) => { + setDisplay('hiding') + onClose?.(e) + clear() + if (!triggerByToast) { + setTimeout(() => { + setDisplay('hide') + }, 400) } + } + const renderClose = () => { return ( - -
- {showIcon && ( - - )} -
- {title ? ( -
- {title} -
- ) : null} - {children} -
-
- {closable ? renderClose() : null} -
+
handleClose(e)}> + {customClose || } +
) + } + + 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 ( + +
+ {showIcon && } +
+ {title ?
{title}
: null} + {children} +
+
+ {closable ? renderClose() : null} +
+ ) }) Alert.displayName = 'Alert' diff --git a/ui/src/routes/dynamicRouter.tsx b/ui/src/routes/dynamicRouter.tsx index 93e06ba..2a02b0c 100644 --- a/ui/src/routes/dynamicRouter.tsx +++ b/ui/src/routes/dynamicRouter.tsx @@ -15,11 +15,42 @@ 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 ( + + ) +} + +const LegacyPasswordResetRedirect = () => { + const location = useLocation() + + return +} + +const LegacyEmailConfirmationRedirect = () => { + const location = useLocation() + + return +} + export const DynamicRouter: React.FC = () => { const { routes, loading, error } = useDynamicRoutes() const { registeredComponents, renderComponent, isComponentRegistered } = useComponents() const location = useLocation() - + const dynamicRoutes = React.useMemo(() => mapDynamicRoutes(routes), [routes]) // /setup path'inde loading bekleme — setup route her zaman erişilebilir olmalı @@ -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 ( { hasSubdomain() ? r.routeType === 'authenticated' : r.routeType !== 'protected', ) .map((route) => { - const Component = route.getComponent(registeredComponents, renderComponent, isComponentRegistered) + const Component = route.getComponent( + registeredComponents, + renderComponent, + isComponentRegistered, + ) return ( { })} {/* root redirect */} - - } - /> + } /> + } /> + } /> {/* public access denied (statik) */} { - apiService.fetchData({ +export const sendAccountConfirmationCode = (data: any) => + apiService.fetchData({ 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({ diff --git a/ui/src/utils/hooks/useAccount.ts b/ui/src/utils/hooks/useAccount.ts index 6617bdf..78c7acc 100644 --- a/ui/src/utils/hooks/useAccount.ts +++ b/ui/src/utils/hooks/useAccount.ts @@ -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') diff --git a/ui/src/views/Views.tsx b/ui/src/views/Views.tsx index f69e63b..197a944 100644 --- a/ui/src/views/Views.tsx +++ b/ui/src/views/Views.tsx @@ -46,9 +46,11 @@ const Views = (props: ViewsProps) => { }> {!!warning?.length && ( - + {warning.map((w, i) => ( -
{w}
+
+ {w} +
))}
)} diff --git a/ui/src/views/auth/ExtendLogin.tsx b/ui/src/views/auth/ExtendLogin.tsx index 550ab78..a478cd8 100644 --- a/ui/src/views/auth/ExtendLogin.tsx +++ b/ui/src/views/auth/ExtendLogin.tsx @@ -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(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 = () => { >

{translate('::Abp.Account.ExtendLogin.Title')}

-

{translate('::Abp.Account.ExtendLogin.Description')}

+ + {translate('::Abp.Account.ExtendLogin.Description')} +
{translate('::Abp.Account.Backto')} {translate('::Abp.Account.SignIn')} -
{' '} +
) : ( @@ -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 = () => {
setFieldValue('captchaResponse', '')} onExpire={() => setFieldValue('captchaResponse', '')} onSuccess={(token: string) => setFieldValue('captchaResponse', token)} diff --git a/ui/src/views/auth/ForgotPassword.tsx b/ui/src/views/auth/ForgotPassword.tsx index 814f0fc..6c868da 100644 --- a/ui/src/views/auth/ForgotPassword.tsx +++ b/ui/src/views/auth/ForgotPassword.tsx @@ -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(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 ? ( <>

{translate('::Abp.Account.ForgotPassword.Checkyouremail')}

-

{translate('::Abp.Account.ForgotPassword.Checkyouremail.Message')}

) : ( <> @@ -88,51 +90,64 @@ const ForgotPassword = () => { {message} )} - - { - if (!disableSubmit) { - onSendMail(values, setSubmitting) - } else { - setSubmitting(false) - } - }} - > - {({ touched, errors, isSubmitting, setFieldValue }) => ( -
- -
- - + + {translate('::Abp.Account.ForgotPassword.Checkyouremail.Message')} + +
+ {translate('::Abp.Account.Backto')} + {translate('::Abp.Account.SignIn')} +
+ + ) : ( + <> + + { + if (!disableSubmit) { + onSendMail(values, setSubmitting, setFieldValue) + } else { + setSubmitting(false) + } + }} + > + {({ touched, errors, isSubmitting, setFieldValue }) => ( + + + + + + setFieldValue('captchaResponse', '')} + onExpire={() => setFieldValue('captchaResponse', '')} + onSuccess={(token: string) => setFieldValue('captchaResponse', token)} /> -
-
- setFieldValue('captchaResponse', '')} - onExpire={() => setFieldValue('captchaResponse', '')} - onSuccess={(token: string) => setFieldValue('captchaResponse', token)} - /> - -
- {translate('::Abp.Account.Backto')} - {translate('::Abp.Account.SignIn')} -
-
-
- )} -
+ +
+ {translate('::Abp.Account.Backto')} + {translate('::Abp.Account.SignIn')} +
+ + + )} + + + )} ) diff --git a/ui/src/views/auth/Login.tsx b/ui/src/views/auth/Login.tsx index 1e6e391..f1a7ac0 100644 --- a/ui/src/views/auth/Login.tsx +++ b/ui/src/views/auth/Login.tsx @@ -153,6 +153,11 @@ const Login = () => { findUiVersion() } + if (showCaptcha) { + setFieldValue('captchaResponse', '') + captchaRef.current?.reset() + } + setSubmitting(false) } diff --git a/ui/src/views/auth/Register.tsx b/ui/src/views/auth/Register.tsx index e59d7d7..bdb4ecd 100644 --- a/ui/src/views/auth/Register.tsx +++ b/ui/src/views/auth/Register.tsx @@ -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(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} )} - - { - if (!disableSubmit) { - onSignUp(values, setSubmitting) - } else { - setSubmitting(false) - } - }} - > - {({ touched, errors, isSubmitting, setFieldValue }) => ( -
- - - - - - - - - - - - - - - - - setFieldValue('captchaResponse', '')} - onExpire={() => setFieldValue('captchaResponse', '')} - onSuccess={(token) => setFieldValue('captchaResponse', token)} - /> - -
- {translate('::Abp.Account.Register.AlreadyHaveAnAccount')} - {translate('::Abp.Account.SignIn')} -
-
-
- )} -
+ {message ? ( +
+ {translate('::Abp.Account.Register.AlreadyHaveAnAccount')} + {translate('::Abp.Account.SignIn')} +
+ ) : ( + <> + + { + if (!disableSubmit) { + onSignUp(values, setSubmitting, setFieldValue) + } else { + setSubmitting(false) + } + }} + > + {({ touched, errors, isSubmitting, setFieldValue }) => ( +
+ + + + + + + + + + + + + + + + + setFieldValue('captchaResponse', '')} + onExpire={() => setFieldValue('captchaResponse', '')} + onSuccess={(token) => setFieldValue('captchaResponse', token)} + /> + +
+ {translate('::Abp.Account.Register.AlreadyHaveAnAccount')} + {translate('::Abp.Account.SignIn')} +
+
+
+ )} +
+ + )}
) diff --git a/ui/src/views/auth/ResetPassword.tsx b/ui/src/views/auth/ResetPassword.tsx index 3a8c11f..cca94ea 100644 --- a/ui/src/views/auth/ResetPassword.tsx +++ b/ui/src/views/auth/ResetPassword.tsx @@ -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) } } diff --git a/ui/src/views/auth/SendConfirmationCode.tsx b/ui/src/views/auth/SendConfirmationCode.tsx index 73ca6e8..3a87928 100644 --- a/ui/src/views/auth/SendConfirmationCode.tsx +++ b/ui/src/views/auth/SendConfirmationCode.tsx @@ -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(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} > -
-

{translate('::Abp.Account.SendConfirmationCode')}

-

{translate('::Abp.Account.SendConfirmationCode.Message')}

-
+ {!message && ( +
+

{translate('::Abp.Account.SendConfirmationCode')}

+

{translate('::Abp.Account.SendConfirmationCode.Message')}

+
+ )}
{message && ( @@ -57,51 +80,64 @@ const SendConfirmationCode = () => { {error} )} - - { - onSubmit(values, setSubmitting) - }} - > - {({ touched, errors, isSubmitting, setFieldValue }) => ( -
- - - - - setFieldValue('captchaResponse', '')} - onExpire={() => setFieldValue('captchaResponse', '')} - onSuccess={(token: string) => setFieldValue('captchaResponse', token)} - /> - -
- {translate('::Abp.Account.Backto')} - - {translate('::Abp.Account.SignIn')} - -
-
-
- )} -
+ {message ? ( +
+ {translate('::Abp.Account.Backto')} + + {translate('::Abp.Account.SignIn')} + +
+ ) : ( + <> + + { + onSubmit(values, setSubmitting, setFieldValue) + }} + > + {({ touched, errors, isSubmitting, setFieldValue }) => ( +
+ + + + + setFieldValue('captchaResponse', '')} + onExpire={() => setFieldValue('captchaResponse', '')} + onSuccess={(token: string) => setFieldValue('captchaResponse', token)} + /> + +
+ {translate('::Abp.Account.Backto')} + + {translate('::Abp.Account.SignIn')} + +
+
+
+ )} +
+ + )}
) diff --git a/ui/src/views/public/Logo.tsx b/ui/src/views/public/Logo.tsx index 23d6636..928f62d 100644 --- a/ui/src/views/public/Logo.tsx +++ b/ui/src/views/public/Logo.tsx @@ -19,7 +19,7 @@ const Logo = (props: LogoProps) => { return (