Tenantlı uygulama için ForgotPassword, ResetPassword

This commit is contained in:
Sedat Öztürk 2026-06-02 23:04:25 +03:00
parent f9c5910813
commit d161e0f4b9
17 changed files with 538 additions and 416 deletions

View file

@ -138,11 +138,15 @@ User Detail: {string.Format(userDetailUrl, user.Id)}";
} }
[Captcha] [Captcha]
public async Task SendAccountConfirmationCodeAsync(SendAccountConfirmationCodeInputDto input) public async Task<bool> SendAccountConfirmationCodeAsync(SendAccountConfirmationCodeInputDto input)
{ {
var user = await UserManager.FindByEmailAsync(input.EmailAddress); var user = await UserManager.FindByEmailAsync(input.EmailAddress);
if (user != null) if (user == null)
await SendConfirmationCodeAsync(user); {
return false;
}
return await SendConfirmationCodeAsync(user);
} }
public async Task<bool> VerifyAccountConfirmationCodeAsync(VerifyAccountConfirmationCodeInputDto input) public async Task<bool> VerifyAccountConfirmationCodeAsync(VerifyAccountConfirmationCodeInputDto input)

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Text.Json.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
using Flurl.Http; using Flurl.Http;
using Sozsoft.Platform.Localization; using Sozsoft.Platform.Localization;
@ -23,6 +24,11 @@ public class TurnstileCaptchaManager : PlatformDomainService, ICaptchaManager
public async Task<bool> VerifyCaptchaAsync(string CaptchaResponse, bool throwOnFailure = false) 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 endPoint = await SettingProvider.GetOrNullAsync(PlatformConsts.AbpAccount.Captcha.EndPoint);
var privateKey = await SettingProvider.GetOrNullAsync(PlatformConsts.AbpAccount.Captcha.SecretKey); var privateKey = await SettingProvider.GetOrNullAsync(PlatformConsts.AbpAccount.Captcha.SecretKey);
if (endPoint.IsNullOrWhiteSpace() || privateKey.IsNullOrWhiteSpace()) if (endPoint.IsNullOrWhiteSpace() || privateKey.IsNullOrWhiteSpace())
@ -46,18 +52,24 @@ public class TurnstileCaptchaManager : PlatformDomainService, ICaptchaManager
if (response != null && response.StatusCode == 200) if (response != null && response.StatusCode == 200)
{ {
var result = await response.GetJsonAsync<dynamic>(); var result = await response.GetJsonAsync<TurnstileCaptchaVerifyResponse>();
if (!(bool)result.success) if (result?.Success != true)
{ {
if (throwOnFailure) if (throwOnFailure)
{ {
throw new UserFriendlyException(Localizer[PlatformConsts.AbpIdentity.User.CaptchaWrongCode]); throw new UserFriendlyException(Localizer[PlatformConsts.AbpIdentity.User.CaptchaWrongCode]);
} }
} }
return (bool)result.success;
return result?.Success == true;
} }
return false; return false;
} }
}
private class TurnstileCaptchaVerifyResponse
{
[JsonPropertyName("success")]
public bool Success { get; set; }
}
}

View file

@ -187,8 +187,8 @@ public class PlatformHttpApiHostModule : AbpModule
options.RedirectAllowedUrls.AddRange(configuration["App:RedirectAllowedUrls"]?.Split(',') ?? Array.Empty<string>()); options.RedirectAllowedUrls.AddRange(configuration["App:RedirectAllowedUrls"]?.Split(',') ?? Array.Empty<string>());
options.Applications[PlatformConsts.React].RootUrl = configuration["App:ClientUrl"]; 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.EmailConfirmation] = "confirm";
options.Applications[PlatformConsts.React].Urls[PlatformConsts.Urls.PasswordReset] = "account/reset-password"; options.Applications[PlatformConsts.React].Urls[PlatformConsts.Urls.PasswordReset] = "reset-password";
options.Applications[PlatformConsts.React].Urls[PlatformConsts.Urls.UserDetail] = "account/{0}"; options.Applications[PlatformConsts.React].Urls[PlatformConsts.Urls.UserDetail] = "account/{0}";
//options.Applications["MVC"].RootUrl = configuration["App:SelfUrl"]; //options.Applications["MVC"].RootUrl = configuration["App:SelfUrl"];

View file

@ -4,7 +4,7 @@ import Card from '@/components/ui/Card'
import Logo from '@/components/template/Logo' import Logo from '@/components/template/Logo'
import type { ReactNode, ReactElement } from 'react' import type { ReactNode, ReactElement } from 'react'
import type { CommonProps } from '@/proxy/common' 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 { Avatar, Select } from '@/components/ui'
import { useStoreActions, useStoreState } from '@/store' import { useStoreActions, useStoreState } from '@/store'
import appConfig from '@/proxy/configs/app.config' import appConfig from '@/proxy/configs/app.config'
@ -90,7 +90,7 @@ const Simple = ({ children, content, ...rest }: SimpleProps) => {
return ( return (
<div className="h-full"> <div className="h-full">
<Container className="flex flex-col flex-auto items-center justify-center min-w-0 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"> <div className="flex justify-between items-center mb-2">
{!hasSubdomain() && ( {!hasSubdomain() && (
<a <a

View file

@ -28,7 +28,7 @@ const Logo = (props: LogoProps) => {
return ( return (
<div <div
className={classNames('logo', 'my-1', className)} className={classNames('logo', 'my-2', className)}
style={{ style={{
...style, ...style,
...{ width: logoWidth }, ...{ width: logoWidth },

View file

@ -1,7 +1,7 @@
import { useState, forwardRef } from 'react' import { useState, forwardRef } from 'react'
import classNames from 'classnames' import classNames from 'classnames'
import useTimeout from '../hooks/useTimeout' 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 { motion } from 'framer-motion'
import CloseButton from '../CloseButton' import CloseButton from '../CloseButton'
import StatusIcon from '../StatusIcon' import StatusIcon from '../StatusIcon'
@ -9,173 +9,153 @@ import type { TypeAttributes, CommonProps } from '../@types/common'
import type { ReactNode, MouseEvent } from 'react' import type { ReactNode, MouseEvent } from 'react'
export interface AlertProps extends CommonProps { export interface AlertProps extends CommonProps {
closable?: boolean closable?: boolean
customClose?: ReactNode | string customClose?: ReactNode | string
customIcon?: ReactNode | string customIcon?: ReactNode | string
duration?: number duration?: number
title?: ReactNode | string title?: ReactNode | string
onClose?: (e?: MouseEvent<HTMLDivElement>) => void onClose?: (e?: MouseEvent<HTMLDivElement>) => void
rounded?: boolean rounded?: boolean
showIcon?: boolean showIcon?: boolean
triggerByToast?: boolean triggerByToast?: boolean
type?: TypeAttributes.Status type?: TypeAttributes.Status
} }
const DEFAULT_TYPE = 'warning' const DEFAULT_TYPE = 'warning'
const TYPE_MAP = { const TYPE_MAP = {
success: { success: {
backgroundColor: 'bg-emerald-50 dark:bg-emerald-500', backgroundColor: 'bg-emerald-50 dark:bg-emerald-500',
titleColor: 'text-emerald-700 dark:text-emerald-50', titleColor: 'text-emerald-700 dark:text-emerald-50',
textColor: 'text-emerald-500 dark:text-emerald-50', textColor: 'text-emerald-500 dark:text-emerald-50',
iconColor: 'text-emerald-400 dark:text-emerald-50', iconColor: 'text-emerald-400 dark:text-emerald-50',
icon: <FaCheckCircle />, icon: <FaCheckCircle />,
}, },
info: { info: {
backgroundColor: 'bg-blue-50 dark:bg-blue-500', backgroundColor: 'bg-blue-50 dark:bg-blue-500',
titleColor: 'text-blue-700 dark:text-blue-100', titleColor: 'text-blue-700 dark:text-blue-100',
textColor: 'text-blue-500 dark:text-blue-100', textColor: 'text-blue-500 dark:text-blue-100',
iconColor: 'text-blue-400 dark:text-blue-100', iconColor: 'text-blue-400 dark:text-blue-100',
icon: <FaInfoCircle />, icon: <FaInfoCircle />,
}, },
warning: { warning: {
backgroundColor: 'bg-yellow-50 dark:bg-yellow-500', backgroundColor: 'bg-yellow-50 dark:bg-yellow-500',
titleColor: 'text-yellow-700 dark:text-yellow-50', titleColor: 'text-yellow-700 dark:text-yellow-50',
textColor: 'text-yellow-500 dark:text-yellow-50', textColor: 'text-yellow-500 dark:text-yellow-50',
iconColor: 'text-yellow-400 dark:text-yellow-50', iconColor: 'text-yellow-400 dark:text-yellow-50',
icon: <FaExclamation />, icon: <FaExclamation />,
}, },
danger: { danger: {
backgroundColor: 'bg-red-50 dark:bg-red-500', backgroundColor: 'bg-red-50 dark:bg-red-500',
titleColor: 'text-red-700 dark:text-red-100', titleColor: 'text-red-700 dark:text-red-100',
textColor: 'text-red-500 dark:text-red-100', textColor: 'text-red-500 dark:text-red-100',
iconColor: 'text-red-400 dark:text-red-100', iconColor: 'text-red-400 dark:text-red-100',
icon: <FaTimesCircle />, icon: <FaTimesCircle />,
}, },
} }
const TYPE_ARRAY: TypeAttributes.Status[] = [ const TYPE_ARRAY: TypeAttributes.Status[] = ['success', 'danger', 'info', 'warning']
'success',
'danger',
'info',
'warning',
]
const Alert = forwardRef<HTMLDivElement, AlertProps>((props, ref) => { const Alert = forwardRef<HTMLDivElement, AlertProps>((props, ref) => {
const { const {
children, children,
className, className,
closable = false, closable = false,
customClose, customClose,
customIcon, customIcon,
duration = 3000, duration = 3000,
title = null, title = null,
onClose, onClose,
rounded = true, rounded = true,
showIcon = false, showIcon = false,
triggerByToast = false, triggerByToast = false,
...rest ...rest
} = props } = props
const getType = () => { const getType = () => {
const { type = DEFAULT_TYPE } = props const { type = DEFAULT_TYPE } = props
if (TYPE_ARRAY.includes(type)) { if (TYPE_ARRAY.includes(type)) {
return type return type
}
return DEFAULT_TYPE
} }
return DEFAULT_TYPE
}
const type = getType() const type = getType()
const typeMap = TYPE_MAP[type] const typeMap = TYPE_MAP[type]
const [display, setDisplay] = useState('show') const [display, setDisplay] = useState('show')
const { clear } = useTimeout( const { clear } = useTimeout(onClose as () => void, duration, (duration as number) > 0)
onClose as () => void,
duration,
(duration as number) > 0
)
const handleClose = (e: MouseEvent<HTMLDivElement>) => { const handleClose = (e: MouseEvent<HTMLDivElement>) => {
setDisplay('hiding') setDisplay('hiding')
onClose?.(e) onClose?.(e)
clear() clear()
if (!triggerByToast) { if (!triggerByToast) {
setTimeout(() => { setTimeout(() => {
setDisplay('hide') setDisplay('hide')
}, 400) }, 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 renderClose = () => {
return ( return (
<motion.div <div className="cursor-pointer" role="presentation" onClick={(e) => handleClose(e)}>
ref={ref} {customClose || <CloseButton defaultStyle={false} />}
className={alertClass} </div>
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>
) )
}
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' Alert.displayName = 'Alert'

View file

@ -15,11 +15,42 @@ const AccessDenied = React.lazy(() => import('@/views/AccessDenied'))
const NotFound = React.lazy(() => import('@/views/NotFound')) const NotFound = React.lazy(() => import('@/views/NotFound'))
const DatabaseSetup = React.lazy(() => import('@/views/setup/DatabaseSetup')) 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 = () => { export const DynamicRouter: React.FC = () => {
const { routes, loading, error } = useDynamicRoutes() const { routes, loading, error } = useDynamicRoutes()
const { registeredComponents, renderComponent, isComponentRegistered } = useComponents() const { registeredComponents, renderComponent, isComponentRegistered } = useComponents()
const location = useLocation() const location = useLocation()
const dynamicRoutes = React.useMemo(() => mapDynamicRoutes(routes), [routes]) const dynamicRoutes = React.useMemo(() => mapDynamicRoutes(routes), [routes])
// /setup path'inde loading bekleme — setup route her zaman erişilebilir olmalı // /setup path'inde loading bekleme — setup route her zaman erişilebilir olmalı
@ -33,7 +64,11 @@ export const DynamicRouter: React.FC = () => {
{dynamicRoutes {dynamicRoutes
.filter((r) => r.routeType === 'protected') .filter((r) => r.routeType === 'protected')
.map((route) => { .map((route) => {
const Component = route.getComponent(registeredComponents, renderComponent, isComponentRegistered) const Component = route.getComponent(
registeredComponents,
renderComponent,
isComponentRegistered,
)
return ( return (
<Route <Route
key={route.key} key={route.key}
@ -85,7 +120,11 @@ export const DynamicRouter: React.FC = () => {
hasSubdomain() ? r.routeType === 'authenticated' : r.routeType !== 'protected', hasSubdomain() ? r.routeType === 'authenticated' : r.routeType !== 'protected',
) )
.map((route) => { .map((route) => {
const Component = route.getComponent(registeredComponents, renderComponent, isComponentRegistered) const Component = route.getComponent(
registeredComponents,
renderComponent,
isComponentRegistered,
)
return ( return (
<Route <Route
key={route.key} key={route.key}
@ -100,15 +139,9 @@ export const DynamicRouter: React.FC = () => {
})} })}
{/* root redirect */} {/* root redirect */}
<Route <Route path="/" element={<RootRedirect />} />
path="/" <Route path="/account/confirm/:userId/:token" element={<LegacyEmailConfirmationRedirect />} />
element={ <Route path="/account/reset-password" element={<LegacyPasswordResetRedirect />} />
<Navigate
to={hasSubdomain() ? ROUTES_ENUM.authenticated.login : ROUTES_ENUM.public.home}
replace
/>
}
/>
{/* public access denied (statik) */} {/* public access denied (statik) */}
<Route <Route

View file

@ -52,8 +52,8 @@ export const resetPassword = (userId: string, resetToken: string, password: stri
}, },
}) })
export const sendAccountConfirmationCode = (data: any) => { export const sendAccountConfirmationCode = (data: any) =>
apiService.fetchData({ apiService.fetchData<boolean>({
method: 'POST', method: 'POST',
url: 'api/app/platform-account/send-account-confirmation-code', url: 'api/app/platform-account/send-account-confirmation-code',
data: { data: {
@ -61,7 +61,6 @@ export const sendAccountConfirmationCode = (data: any) => {
captchaResponse: data.captchaResponse, captchaResponse: data.captchaResponse,
}, },
}) })
}
export const verifyAccountConfirmationCode = (userId: string, token: string) => export const verifyAccountConfirmationCode = (userId: string, token: string) =>
apiService.fetchData({ apiService.fetchData({

View file

@ -1,5 +1,6 @@
import { useState } from 'react' import { useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { import {
sendAccountConfirmationCode, sendAccountConfirmationCode,
verifyAccountConfirmationCode, verifyAccountConfirmationCode,
@ -12,15 +13,28 @@ function useAccount() {
const sendConfirmationCode = async (values: any) => { const sendConfirmationCode = async (values: any) => {
try { 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('') setError('')
setMessage('Verification code has been sent to your e-mail address.') setMessage('Verification code has been sent to your e-mail address.')
return {
status: 'success',
}
} catch (error: any) { } catch (error: any) {
const err =
error?.response?.data?.error?.message ||
error?.response?.data?.message ||
error?.message ||
error.toString()
setMessage('') setMessage('')
setError(error?.response?.data?.message || error.toString()) setError(err)
return { return {
status: 'failed', status: 'failed',
message: error?.response?.data?.message || error.toString(), message: err,
} }
} }
} }
@ -38,7 +52,7 @@ function useAccount() {
setError('') setError('')
setMessage('Your account is confirmed') setMessage('Your account is confirmed')
setTimeout(() => { setTimeout(() => {
navigate('/account/login') navigate(ROUTES_ENUM.authenticated.login)
}, 3000) }, 3000)
} else { } else {
throw new Error('Invalid token') throw new Error('Invalid token')

View file

@ -46,9 +46,11 @@ const Views = (props: ViewsProps) => {
<ErrorBoundary fallbackRender={fallbackRender}> <ErrorBoundary fallbackRender={fallbackRender}>
<Suspense fallback={<Loading loading={true} />}> <Suspense fallback={<Loading loading={true} />}>
{!!warning?.length && ( {!!warning?.length && (
<Alert showIcon className="mb-4" type="warning"> <Alert showIcon className="mb-4 text-sm text-left" type="warning">
{warning.map((w, i) => ( {warning.map((w, i) => (
<div key={i}>{w}</div> <div key={i} className="whitespace-normal break-words">
{w}
</div>
))} ))}
</Alert> </Alert>
)} )}

View file

@ -11,8 +11,9 @@ import { sendExtendLoginRequest } from '@/services/account.service'
import { store } from '@/store' import { store } from '@/store'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import useTimeOutMessage from '@/utils/hooks/useTimeOutMessage' import useTimeOutMessage from '@/utils/hooks/useTimeOutMessage'
import { TurnstileInstance } from '@marsidev/react-turnstile'
import { Field, Form, Formik } from 'formik' import { Field, Form, Formik } from 'formik'
import { useState } from 'react' import { useRef, useState } from 'react'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import * as Yup from 'yup' import * as Yup from 'yup'
@ -35,21 +36,23 @@ const ExtendLogin = () => {
const [message, setMessage] = useTimeOutMessage(10000) const [message, setMessage] = useTimeOutMessage(10000)
const { translate } = useLocalization() const { translate } = useLocalization()
const captchaRef = useRef<TurnstileInstance>(null)
const onSendMail = async ( const onSendMail = async (
values: ExtendLoginFormSchema, values: ExtendLoginFormSchema,
setSubmitting: (isSubmitting: boolean) => void, setSubmitting: (isSubmitting: boolean) => void,
setFieldValue: (field: string, value: string) => void,
) => { ) => {
const { email, captchaResponse } = values const { email, captchaResponse } = values
setSubmitting(true) setSubmitting(true)
try { try {
const resp = await sendExtendLoginRequest({ email, captchaResponse }) await sendExtendLoginRequest({ email, captchaResponse })
if (resp.data) { setEmailSent(true)
setSubmitting(false)
setEmailSent(true)
}
} catch (error: any) { } catch (error: any) {
setMessage(error?.response?.data || error.toString()) setMessage(error?.response?.data || error.toString())
} finally {
setFieldValue('captchaResponse', '')
captchaRef.current?.reset()
setSubmitting(false) setSubmitting(false)
} }
} }
@ -63,11 +66,13 @@ const ExtendLogin = () => {
></Helmet> ></Helmet>
<div> <div>
<h3 className="mb-1">{translate('::Abp.Account.ExtendLogin.Title')}</h3> <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"> <div className="mt-4 text-center">
<span>{translate('::Abp.Account.Backto')} </span> <span>{translate('::Abp.Account.Backto')} </span>
<ActionLink to={signInUrl}>{translate('::Abp.Account.SignIn')}</ActionLink> <ActionLink to={signInUrl}>{translate('::Abp.Account.SignIn')}</ActionLink>
</div>{' '} </div>
</div> </div>
</> </>
) : ( ) : (
@ -92,9 +97,9 @@ const ExtendLogin = () => {
captchaResponse: '', captchaResponse: '',
}} }}
validationSchema={validationSchema} validationSchema={validationSchema}
onSubmit={(values, { setSubmitting }) => { onSubmit={(values, { setSubmitting, setFieldValue }) => {
if (!disableSubmit) { if (!disableSubmit) {
onSendMail(values, setSubmitting) onSendMail(values, setSubmitting, setFieldValue)
} else { } else {
setSubmitting(false) setSubmitting(false)
} }
@ -115,6 +120,7 @@ const ExtendLogin = () => {
</FormItem> </FormItem>
</div> </div>
<Captcha <Captcha
ref={captchaRef}
onError={() => setFieldValue('captchaResponse', '')} onError={() => setFieldValue('captchaResponse', '')}
onExpire={() => setFieldValue('captchaResponse', '')} onExpire={() => setFieldValue('captchaResponse', '')}
onSuccess={(token: string) => setFieldValue('captchaResponse', token)} onSuccess={(token: string) => setFieldValue('captchaResponse', token)}

View file

@ -11,10 +11,11 @@ import { sendPasswordResetCode } from '@/services/account.service'
import { store } from '@/store' import { store } from '@/store'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import useTimeOutMessage from '@/utils/hooks/useTimeOutMessage' import useTimeOutMessage from '@/utils/hooks/useTimeOutMessage'
import { TurnstileInstance } from '@marsidev/react-turnstile'
import type { AxiosError } from 'axios' import type { AxiosError } from 'axios'
import { Field, Form, Formik } from 'formik' import { Field, Form, Formik } from 'formik'
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
import { useState } from 'react' import { useRef, useState } from 'react'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import * as Yup from 'yup' import * as Yup from 'yup'
@ -37,23 +38,25 @@ const ForgotPassword = () => {
const [message, setMessage] = useTimeOutMessage() const [message, setMessage] = useTimeOutMessage()
const { translate } = useLocalization() const { translate } = useLocalization()
const captchaRef = useRef<TurnstileInstance>(null)
const onSendMail = async ( const onSendMail = async (
values: ForgotPasswordFormSchema, values: ForgotPasswordFormSchema,
setSubmitting: (isSubmitting: boolean) => void, setSubmitting: (isSubmitting: boolean) => void,
setFieldValue: (field: string, value: string) => void,
) => { ) => {
setSubmitting(true) setSubmitting(true)
try { try {
const resp = await sendPasswordResetCode(values) await sendPasswordResetCode(values)
if (resp.data) { setEmailSent(true)
setSubmitting(false)
setEmailSent(true)
}
} catch (errors) { } catch (errors) {
setMessage( setMessage(
(errors as AxiosError<{ message: string }>)?.response?.data?.message || (errors as AxiosError<{ message: string }>)?.response?.data?.message ||
(errors as Error).toString(), (errors as Error).toString(),
) )
} finally {
setFieldValue('captchaResponse', '')
captchaRef.current?.reset()
setSubmitting(false) setSubmitting(false)
} }
} }
@ -74,7 +77,6 @@ const ForgotPassword = () => {
{emailSent ? ( {emailSent ? (
<> <>
<h3 className="mb-1">{translate('::Abp.Account.ForgotPassword.Checkyouremail')}</h3> <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} {message}
</Alert> </Alert>
)} )}
<TenantSelector /> {emailSent ? (
<Formik <>
initialValues={{ <Alert showIcon className="mb-4" type="success">
email: userName, {translate('::Abp.Account.ForgotPassword.Checkyouremail.Message')}
captchaResponse: '', </Alert>
}} <div className="mt-4 text-center">
validationSchema={validationSchema} <span>{translate('::Abp.Account.Backto')} </span>
onSubmit={(values, { setSubmitting }) => { <ActionLink to={signInUrl}>{translate('::Abp.Account.SignIn')}</ActionLink>
if (!disableSubmit) { </div>
onSendMail(values, setSubmitting) </>
} else { ) : (
setSubmitting(false) <>
} <TenantSelector />
}} <Formik
> initialValues={{
{({ touched, errors, isSubmitting, setFieldValue }) => ( email: userName,
<Form> captchaResponse: '',
<FormContainer> }}
<div className={emailSent ? 'hidden' : ''}> validationSchema={validationSchema}
<FormItem invalid={errors.email && touched.email} errorMessage={errors.email}> onSubmit={(values, { setSubmitting, setFieldValue }) => {
<Field if (!disableSubmit) {
type="email" onSendMail(values, setSubmitting, setFieldValue)
autoComplete="off" } else {
name="email" setSubmitting(false)
placeholder={translate('::Abp.Account.EmailAddress')} }
component={Input} }}
>
{({ 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> <Button block loading={isSubmitting} variant="solid" type="submit">
</div> Send Email
<Captcha </Button>
onError={() => setFieldValue('captchaResponse', '')} <div className="mt-4 text-center">
onExpire={() => setFieldValue('captchaResponse', '')} <span>{translate('::Abp.Account.Backto')} </span>
onSuccess={(token: string) => setFieldValue('captchaResponse', token)} <ActionLink to={signInUrl}>{translate('::Abp.Account.SignIn')}</ActionLink>
/> </div>
<Button block loading={isSubmitting} variant="solid" type="submit"> </FormContainer>
{emailSent ? 'Resend Email' : 'Send Email'} </Form>
</Button> )}
<div className="mt-4 text-center"> </Formik>
<span>{translate('::Abp.Account.Backto')} </span> </>
<ActionLink to={signInUrl}>{translate('::Abp.Account.SignIn')}</ActionLink> )}
</div>
</FormContainer>
</Form>
)}
</Formik>
</motion.div> </motion.div>
</> </>
) )

View file

@ -153,6 +153,11 @@ const Login = () => {
findUiVersion() findUiVersion()
} }
if (showCaptcha) {
setFieldValue('captchaResponse', '')
captchaRef.current?.reset()
}
setSubmitting(false) setSubmitting(false)
} }

View file

@ -9,8 +9,9 @@ import { ROUTES_ENUM } from '@/routes/route.constant'
import useAuth from '@/utils/hooks/useAuth' import useAuth from '@/utils/hooks/useAuth'
import useTimeOutMessage from '@/utils/hooks/useTimeOutMessage' import useTimeOutMessage from '@/utils/hooks/useTimeOutMessage'
import Captcha from '@/components/shared/Captcha' import Captcha from '@/components/shared/Captcha'
import { TurnstileInstance } from '@marsidev/react-turnstile'
import { Field, Form, Formik } from 'formik' import { Field, Form, Formik } from 'formik'
import { useState } from 'react' import { useRef, useState } from 'react'
import * as Yup from 'yup' import * as Yup from 'yup'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { Helmet } from 'react-helmet' 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'), confirmPassword: Yup.string().oneOf([Yup.ref('password')], 'Your passwords do not match'),
name: Yup.string().required(), name: Yup.string().required(),
surname: Yup.string().required(), surname: Yup.string().required(),
captchaResponse: Yup.string().required(),
}) })
const Register = () => { const Register = () => {
@ -41,24 +43,30 @@ const Register = () => {
const [message, setMessage] = useState('') const [message, setMessage] = useState('')
const [error, setError] = useTimeOutMessage(10000) const [error, setError] = useTimeOutMessage(10000)
const captchaRef = useRef<TurnstileInstance>(null)
const onSignUp = async ( const onSignUp = async (
values: SignUpFormSchema, values: SignUpFormSchema,
setSubmitting: (isSubmitting: boolean) => void, setSubmitting: (isSubmitting: boolean) => void,
setFieldValue: (field: string, value: string) => void,
) => { ) => {
setSubmitting(true) setSubmitting(true)
const result = await signUp(values) try {
const result = await signUp(values)
if (result?.status === 'failed') { if (result?.status === 'failed' || result?.status === 'error') {
setError(result.message) setError(result.message)
setMessage('') setMessage('')
} else { } else {
setMessage(translate('::Abp.Account.Register.ResultMessage')) setMessage(translate('::Abp.Account.Register.ResultMessage'))
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }) window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
setError('') setError('')
}
} finally {
setFieldValue('captchaResponse', '')
captchaRef.current?.reset()
setSubmitting(false)
} }
setSubmitting(false)
} }
return ( return (
@ -84,107 +92,117 @@ const Register = () => {
{error} {error}
</Alert> </Alert>
)} )}
<TenantSelector /> {message ? (
<Formik <div className="mt-4 text-center">
initialValues={{ <span>{translate('::Abp.Account.Register.AlreadyHaveAnAccount')} </span>
email: '', <ActionLink to={signInUrl}>{translate('::Abp.Account.SignIn')}</ActionLink>
password: '', </div>
confirmPassword: '', ) : (
name: '', <>
surname: '', <TenantSelector />
captchaResponse: '', <Formik
}} initialValues={{
validationSchema={validationSchema} email: '',
onSubmit={(values, { setSubmitting }) => { password: '',
if (!disableSubmit) { confirmPassword: '',
onSignUp(values, setSubmitting) name: '',
} else { surname: '',
setSubmitting(false) captchaResponse: '',
} }}
}} validationSchema={validationSchema}
> onSubmit={(values, { setSubmitting, setFieldValue }) => {
{({ touched, errors, isSubmitting, setFieldValue }) => ( if (!disableSubmit) {
<Form> onSignUp(values, setSubmitting, setFieldValue)
<FormContainer> } else {
<FormItem setSubmitting(false)
label={translate('::Abp.Account.EmailAddress')} }
invalid={errors.email && touched.email} }}
errorMessage={errors.email} >
> {({ touched, errors, isSubmitting, setFieldValue }) => (
<Field <Form>
type="email" <FormContainer>
autoComplete="off" <FormItem
name="email" label={translate('::Abp.Account.EmailAddress')}
placeholder={translate('::Abp.Account.EmailAddress')} invalid={errors.email && touched.email}
component={Input} errorMessage={errors.email}
/> >
</FormItem> <Field
<FormItem type="email"
label={translate('::Abp.Identity.Password')} autoComplete="off"
invalid={errors.password && touched.password} name="email"
errorMessage={errors.password} placeholder={translate('::Abp.Account.EmailAddress')}
> component={Input}
<Field />
autoComplete="off" </FormItem>
name="password" <FormItem
placeholder={translate('::Abp.Identity.Password')} label={translate('::Abp.Identity.Password')}
component={PasswordInput} invalid={errors.password && touched.password}
/> errorMessage={errors.password}
</FormItem> >
<FormItem <Field
label={translate('::Abp.Account.ConfirmPassword')} autoComplete="off"
invalid={errors.confirmPassword && touched.confirmPassword} name="password"
errorMessage={errors.confirmPassword} placeholder={translate('::Abp.Identity.Password')}
> component={PasswordInput}
<Field />
autoComplete="off" </FormItem>
name="confirmPassword" <FormItem
placeholder={translate('::Abp.Account.ConfirmPassword')} label={translate('::Abp.Account.ConfirmPassword')}
component={PasswordInput} invalid={errors.confirmPassword && touched.confirmPassword}
/> errorMessage={errors.confirmPassword}
</FormItem> >
<FormItem <Field
label={translate('::Abp.Identity.User.UserInformation.Name')} autoComplete="off"
invalid={errors.name && touched.name} name="confirmPassword"
errorMessage={errors.name} placeholder={translate('::Abp.Account.ConfirmPassword')}
> component={PasswordInput}
<Field />
type="name" </FormItem>
autoComplete="off" <FormItem
name="name" label={translate('::Abp.Identity.User.UserInformation.Name')}
placeholder={translate('::Abp.Identity.User.UserInformation.Name')} invalid={errors.name && touched.name}
component={Input} errorMessage={errors.name}
/> >
</FormItem> <Field
<FormItem type="name"
label={translate('::Abp.Identity.User.UserInformation.Surname')} autoComplete="off"
invalid={errors.surname && touched.surname} name="name"
errorMessage={errors.surname} placeholder={translate('::Abp.Identity.User.UserInformation.Name')}
> component={Input}
<Field />
type="surname" </FormItem>
autoComplete="off" <FormItem
name="surname" label={translate('::Abp.Identity.User.UserInformation.Surname')}
placeholder={translate('::Abp.Identity.User.UserInformation.Surname')} invalid={errors.surname && touched.surname}
component={Input} errorMessage={errors.surname}
/> >
</FormItem> <Field
<Captcha type="surname"
onError={() => setFieldValue('captchaResponse', '')} autoComplete="off"
onExpire={() => setFieldValue('captchaResponse', '')} name="surname"
onSuccess={(token) => setFieldValue('captchaResponse', token)} placeholder={translate('::Abp.Identity.User.UserInformation.Surname')}
/> component={Input}
<Button block loading={isSubmitting} variant="solid" type="submit"> />
{isSubmitting ? 'Creating Account...' : 'Sign Up'} </FormItem>
</Button> <Captcha
<div className="mt-4 text-center"> ref={captchaRef}
<span>{translate('::Abp.Account.Register.AlreadyHaveAnAccount')} </span> onError={() => setFieldValue('captchaResponse', '')}
<ActionLink to={signInUrl}>{translate('::Abp.Account.SignIn')}</ActionLink> onExpire={() => setFieldValue('captchaResponse', '')}
</div> onSuccess={(token) => setFieldValue('captchaResponse', token)}
</FormContainer> />
</Form> <Button block loading={isSubmitting} variant="solid" type="submit">
)} {isSubmitting ? 'Creating Account...' : 'Sign Up'}
</Formik> </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> </div>
</> </>
) )

View file

@ -59,16 +59,14 @@ const ResetPassword = () => {
const { password } = values const { password } = values
setSubmitting(true) setSubmitting(true)
try { try {
const resp = await resetPassword(userId, resetToken, password) await resetPassword(userId, resetToken, password)
if (resp.data) { setResetComplete(true)
setSubmitting(false)
setResetComplete(true)
}
} catch (errors) { } catch (errors) {
setMessage( setMessage(
(errors as AxiosError<{ message: string }>)?.response?.data?.message || (errors as AxiosError<{ message: string }>)?.response?.data?.message ||
(errors as Error).toString(), (errors as Error).toString(),
) )
} finally {
setSubmitting(false) setSubmitting(false)
} }
} }

View file

@ -4,11 +4,13 @@ import { Field, Form, Formik } from 'formik'
import * as Yup from 'yup' import * as Yup from 'yup'
import { ActionLink, TenantSelector } from '@/components/shared' import { ActionLink, TenantSelector } from '@/components/shared'
import { ROUTES_ENUM } from '@/routes/route.constant' import { ROUTES_ENUM } from '@/routes/route.constant'
import { store } from '@/store' import { store, useStoreActions } from '@/store'
import Captcha from '@/components/shared/Captcha' import Captcha from '@/components/shared/Captcha'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import { APP_NAME } from '@/constants/app.constant' import { APP_NAME } from '@/constants/app.constant'
import { TurnstileInstance } from '@marsidev/react-turnstile'
import { useEffect, useRef, useState } from 'react'
type FormSchema = { type FormSchema = {
email: string email: string
@ -25,13 +27,32 @@ const SendConfirmationCode = () => {
const { translate } = useLocalization() const { translate } = useLocalization()
const { message, error, sendConfirmationCode } = useAccount() 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) setSubmitting(true)
await sendConfirmationCode(values) try {
const result = await sendConfirmationCode(values)
setSubmitting(false) if (result?.status !== 'failed') {
setWarning('')
}
} finally {
setFieldValue('captchaResponse', '')
captchaRef.current?.reset()
setCaptchaKey((key) => key + 1)
setSubmitting(false)
}
} }
return ( return (
@ -42,10 +63,12 @@ const SendConfirmationCode = () => {
defaultTitle={APP_NAME} defaultTitle={APP_NAME}
></Helmet> ></Helmet>
<div className="mb-8"> {!message && (
<h3 className="mb-1">{translate('::Abp.Account.SendConfirmationCode')}</h3> <div className="mb-8">
<p>{translate('::Abp.Account.SendConfirmationCode.Message')}</p> <h3 className="mb-1">{translate('::Abp.Account.SendConfirmationCode')}</h3>
</div> <p>{translate('::Abp.Account.SendConfirmationCode.Message')}</p>
</div>
)}
<div> <div>
{message && ( {message && (
<Alert showIcon className="mb-4" type="success"> <Alert showIcon className="mb-4" type="success">
@ -57,51 +80,64 @@ const SendConfirmationCode = () => {
{error} {error}
</Alert> </Alert>
)} )}
<TenantSelector /> {message ? (
<Formik <div className="mt-4 text-center">
initialValues={{ <span>{translate('::Abp.Account.Backto')} </span>
email: userName, <ActionLink to={ROUTES_ENUM.authenticated.login}>
captchaResponse: '', {translate('::Abp.Account.SignIn')}
}} </ActionLink>
validationSchema={validationSchema} </div>
onSubmit={(values, { setSubmitting }) => { ) : (
onSubmit(values, setSubmitting) <>
}} <TenantSelector />
> <Formik
{({ touched, errors, isSubmitting, setFieldValue }) => ( initialValues={{
<Form> email: userName,
<FormContainer> captchaResponse: '',
<FormItem }}
label={translate('::Abp.Account.EmailAddress')} validationSchema={validationSchema}
invalid={errors.email && touched.email} onSubmit={(values, { setSubmitting, setFieldValue }) => {
errorMessage={errors.email} onSubmit(values, setSubmitting, setFieldValue)
> }}
<Field >
type="email" {({ touched, errors, isSubmitting, setFieldValue }) => (
autoComplete="off" <Form>
name="email" <FormContainer>
placeholder={translate('::Abp.Account.EmailAddress')} <FormItem
component={Input} label={translate('::Abp.Account.EmailAddress')}
/> invalid={errors.email && touched.email}
</FormItem> errorMessage={errors.email}
<Captcha >
onError={() => setFieldValue('captchaResponse', '')} <Field
onExpire={() => setFieldValue('captchaResponse', '')} type="email"
onSuccess={(token: string) => setFieldValue('captchaResponse', token)} autoComplete="off"
/> name="email"
<Button block loading={isSubmitting} variant="solid" type="submit"> placeholder={translate('::Abp.Account.EmailAddress')}
{isSubmitting ? 'Sending code...' : 'Send Code'} component={Input}
</Button> />
<div className="mt-4 text-center"> </FormItem>
<span>{translate('::Abp.Account.Backto')} </span> <Captcha
<ActionLink to={ROUTES_ENUM.authenticated.login}> key={captchaKey}
{translate('::Abp.Account.SignIn')} ref={captchaRef}
</ActionLink> onError={() => setFieldValue('captchaResponse', '')}
</div> onExpire={() => setFieldValue('captchaResponse', '')}
</FormContainer> onSuccess={(token: string) => setFieldValue('captchaResponse', token)}
</Form> />
)} <Button block loading={isSubmitting} variant="solid" type="submit">
</Formik> {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> </div>
</> </>
) )

View file

@ -19,7 +19,7 @@ const Logo = (props: LogoProps) => {
return ( return (
<div <div
className={classNames('logo', 'my-1', className)} className={classNames('logo', 'my-2', className)}
style={{ style={{
...style, ...style,
...{ width: logoWidth }, ...{ width: logoWidth },