added recovery ui

This commit is contained in:
SrGooglo 2025-02-25 23:07:37 +00:00
parent 5b6d36744c
commit be0c61a028
7 changed files with 880 additions and 729 deletions

View File

@ -15,11 +15,13 @@ const stepsOnError = {
const stepsValidations = { const stepsValidations = {
username: async (state) => { username: async (state) => {
const check = await AuthModel.usernameValidation(state.username).catch((err) => { const check = await AuthModel.usernameValidation(state.username).catch(
(err) => {
return { return {
exists: false exists: false,
} }
}) },
)
return check.exists return check.exists
}, },
@ -32,7 +34,7 @@ const phasesToSteps = {
class Login extends React.Component { class Login extends React.Component {
static pageStatement = { static pageStatement = {
bottomBarAllowed: false bottomBarAllowed: false,
} }
state = { state = {
@ -66,7 +68,7 @@ class Login extends React.Component {
if (error.response.data) { if (error.response.data) {
if (error.response.data.violation) { if (error.response.data.violation) {
return this.setState({ return this.setState({
forbidden: error.response.data.violation forbidden: error.response.data.violation,
}) })
} }
@ -101,7 +103,7 @@ class Login extends React.Component {
if (typeof this.props.close === "function") { if (typeof this.props.close === "function") {
await this.props.close({ await this.props.close({
unlock: true unlock: true,
}) })
} }
@ -122,7 +124,7 @@ class Login extends React.Component {
try { try {
await AuthModel.activateAccount( await AuthModel.activateAccount(
this.state.activation.user_id, this.state.activation.user_id,
this.state.activation.code this.state.activation.code,
) )
this.handleFinish() this.handleFinish()
@ -130,8 +132,8 @@ class Login extends React.Component {
this.setState({ this.setState({
activation: { activation: {
...this.state.activation, ...this.state.activation,
error: error error: error,
} },
}) })
console.error(error) console.error(error)
@ -145,8 +147,9 @@ class Login extends React.Component {
return null return null
} }
const rensendObj = await AuthModel.resendActivationCode(activationObj.user_id) const rensendObj = await AuthModel.resendActivationCode(
.catch((error) => { activationObj.user_id,
).catch((error) => {
app.message.info(`Please try again later...`) app.message.info(`Please try again later...`)
return null return null
}) })
@ -170,7 +173,7 @@ class Login extends React.Component {
this.props.close() this.props.close()
} }
app.location.push("/apr") app.location.push("/auth?key=recover")
} }
toggleLoading = (to) => { toggleLoading = (to) => {
@ -179,19 +182,19 @@ class Login extends React.Component {
} }
this.setState({ this.setState({
loading: to loading: to,
}) })
} }
clearError = () => { clearError = () => {
this.setState({ this.setState({
error: null error: null,
}) })
} }
onError = (error) => { onError = (error) => {
this.setState({ this.setState({
error: error error: error,
}) })
} }
@ -205,15 +208,15 @@ class Login extends React.Component {
this.formRef.current.setFields([ this.formRef.current.setFields([
{ {
name: input, name: input,
errors: [] errors: [],
} },
]) ])
this.setState({ this.setState({
loginInputs: { loginInputs: {
...this.state.loginInputs, ...this.state.loginInputs,
[input]: value [input]: value,
} },
}) })
} }
@ -231,7 +234,7 @@ class Login extends React.Component {
this.formRef.current.setFields([ this.formRef.current.setFields([
{ {
name: phase, name: phase,
errors: [stepsOnError[phase]] errors: [stepsOnError[phase]],
}, },
]) ])
@ -246,7 +249,7 @@ class Login extends React.Component {
} }
this.setState({ this.setState({
phase: to phase: to,
}) })
} }
@ -279,10 +282,14 @@ class Login extends React.Component {
render() { render() {
if (this.state.forbidden) { if (this.state.forbidden) {
return <div className="login_wrapper"> return (
<div className="login_wrapper">
<div className="content"> <div className="content">
<h1>Access denied</h1> <h1>Access denied</h1>
<h3>Your account has been disabled due a violation to our terms of service</h3> <h3>
Your account has been disabled due a violation to
our terms of service
</h3>
<p>Here is a detailed description of the violation</p> <p>Here is a detailed description of the violation</p>
@ -290,33 +297,41 @@ class Login extends React.Component {
{this.state.forbidden.reason} {this.state.forbidden.reason}
</div> </div>
<p>If you think this is an error, or you want to apeel this decision please contact our support</p> <p>
If you think this is an error, or you want to apeel
this decision please contact our support
</p>
</div> </div>
</div> </div>
)
} }
if (this.state.activation) { if (this.state.activation) {
return <div className="login_wrapper"> return (
<div className="login_wrapper">
<div className="content"> <div className="content">
<h1>Activate your Account</h1> <h1>Activate your Account</h1>
<p>We have sent you an email with a code that you need to enter below in order to activate your account.</p> <p>
We have sent you an email with a code that you need
to enter below in order to activate your account.
</p>
<antd.Input.OTP <antd.Input.OTP
length={6} length={6}
onChange={(code) => this.setState({ onChange={(code) =>
this.setState({
activation: { activation: {
...this.state.activation, ...this.state.activation,
code: code, code: code,
},
})
} }
})}
/> />
<div className="resend"> <div className="resend">
{ {this.state.activation.resended && (
this.state.activation.resended && <antd.Alert <antd.Alert message={`Mail resended`} />
message={`Mail resended`} )}
/>
}
<a <a
href="#" href="#"
onClick={this.onClickResendActivationCode} onClick={this.onClickResendActivationCode}
@ -325,30 +340,29 @@ class Login extends React.Component {
</a> </a>
</div> </div>
{this.state.activation.error && (
<div className="field-error">
{ {
this.state.activation.error && <div className="field-error"> this.state.activation.error.response.data
{this.state.activation.error.response.data.error} .error
</div>
} }
</div>
)}
<antd.Button <antd.Button onClick={this.onClickActivateAccount}>
onClick={this.onClickActivateAccount}
>
Activate Activate
</antd.Button> </antd.Button>
</div> </div>
</div> </div>
)
} }
return <div className="login_wrapper"> return (
<div className="login_wrapper">
<div className="content"> <div className="content">
<div className="header"> <div className="header">
<h1> <h1>Sign in</h1>
Sign in <h3>To continue to {config.app.siteName}</h3>
</h1>
<h3>
To continue to {config.app.siteName}
</h3>
</div> </div>
<antd.Form <antd.Form
@ -360,14 +374,18 @@ class Login extends React.Component {
onFinish={this.handleFinish} onFinish={this.handleFinish}
ref={this.formRef} ref={this.formRef}
> >
<antd.Form.Item <antd.Form.Item name="username" className="field">
name="username" <span>
className="field" <Icons.FiMail /> Username or Email
> </span>
<span><Icons.FiMail /> Username or Email</span>
<antd.Input <antd.Input
placeholder="myusername / myemail@example.com" placeholder="myusername / myemail@example.com"
onChange={(e) => this.onUpdateInput("username", e.target.value)} onChange={(e) =>
this.onUpdateInput(
"username",
e.target.value,
)
}
onPressEnter={this.nextStep} onPressEnter={this.nextStep}
disabled={this.state.phase !== 0} disabled={this.state.phase !== 0}
autoFocus autoFocus
@ -376,60 +394,71 @@ class Login extends React.Component {
<antd.Form.Item <antd.Form.Item
name="password" name="password"
className={classnames( className={classnames("field", {
"field",
{
["hidden"]: this.state.phase !== 1, ["hidden"]: this.state.phase !== 1,
} })}
)}
> >
<span><Icons.FiLock /> Password</span> <span>
<Icons.FiLock /> Password
</span>
<antd.Input.Password <antd.Input.Password
//placeholder="********" //placeholder="********"
onChange={(e) => this.onUpdateInput("password", e.target.value)} onChange={(e) =>
this.onUpdateInput(
"password",
e.target.value,
)
}
onPressEnter={this.nextStep} onPressEnter={this.nextStep}
/> />
</antd.Form.Item> </antd.Form.Item>
<antd.Form.Item <antd.Form.Item
name="mfa_code" name="mfa_code"
className={classnames( className={classnames("field", {
"field",
{
["hidden"]: !this.state.mfa_required, ["hidden"]: !this.state.mfa_required,
} })}
)}
> >
<span><Icons.FiLock /> Verification Code</span> <span>
<Icons.FiLock /> Verification Code
</span>
{ {this.state.mfa_required && (
this.state.mfa_required && <> <>
<p>We send a verification code to [{this.state.mfa_required.sended_to}]</p> <p>
We send a verification code to [
{this.state.mfa_required.sended_to}]
</p>
<p> <p>
Didn't receive the code? <a onClick={this.handleFinish}>Resend</a> Didn't receive the code?{" "}
<a onClick={this.handleFinish}>
Resend
</a>
</p> </p>
</> </>
} )}
<antd.Input.OTP <antd.Input.OTP
length={4} length={4}
formatter={(str) => str.toUpperCase()} formatter={(str) => str.toUpperCase()}
onChange={(code) => this.onUpdateInput("mfa_code", code)} onChange={(code) =>
this.onUpdateInput("mfa_code", code)
}
onPressEnter={this.nextStep} onPressEnter={this.nextStep}
/> />
</antd.Form.Item> </antd.Form.Item>
</antd.Form> </antd.Form>
<div className="component-row"> <div className="component-row">
{ {this.state.phase > 0 && (
this.state.phase > 0 && <antd.Button <antd.Button
onClick={this.prevStep} onClick={this.prevStep}
disabled={this.state.loading} disabled={this.state.loading}
> >
Back Back
</antd.Button> </antd.Button>
} )}
<antd.Button <antd.Button
onClick={this.nextStep} onClick={this.nextStep}
disabled={!this.canNext() || this.state.loading} disabled={!this.canNext() || this.state.loading}
@ -439,17 +468,16 @@ class Login extends React.Component {
</antd.Button> </antd.Button>
</div> </div>
{ {this.state.error && (
this.state.error && <div className="field-error"> <div className="field-error">{this.state.error}</div>
{this.state.error} )}
</div>
}
<div className="field" onClick={this.onClickForgotPassword}> <div className="field" onClick={this.onClickForgotPassword}>
<a>Forgot your password?</a> <a>Forgot your password?</a>
</div> </div>
</div> </div>
</div> </div>
)
} }
} }

View File

@ -0,0 +1,149 @@
import React from "react"
import { Input } from "antd"
import FormWithSteps from "@components/FormWithSteps"
import AuthModel from "@models/auth"
const Steps = [
{
id: "email_input",
render: ({ updateState, values }) => {
function onChangeInput(e) {
updateState("account", e.target.value)
}
return (
<>
<p>
First enter your account or email address to find your
associated account.
</p>
<Input
placeholder="@username or email"
value={values.account}
onChange={onChangeInput}
autoFocus
/>
</>
)
},
validate: (values) => {
return values.account && values.account.length > 3
},
onNext: async ({ values, updateState, setError }) => {
try {
const recoverSession = await AuthModel.recoverPassword(
values.account,
)
updateState("recoverSession", recoverSession)
} catch (error) {
console.error(error.response.data)
setError(error.response.data.error)
return {
cancel: true,
}
}
},
},
{
id: "new_password",
render: ({ updateState, values }) => {
return (
<>
<p>Enter a new password for your account.</p>
<Input.Password
placeholder="New Password"
value={values.new_password}
onChange={(e) =>
updateState("new_password", e.target.value)
}
autoFocus
/>
</>
)
},
validate: (values) => {
if (!values.new_password) {
return false
}
return values.new_password.length >= 8
},
},
{
id: "otp_input",
render: ({ updateState, values }) => {
return (
<>
<p>
We've sent you a code to your email [
{values.recoverSession.email}]
</p>
<p>Expires in {values.recoverSession.expires_in} minutes</p>
<Input.OTP
length={values.recoverSession.code_length}
onChange={(value) => updateState("otp", value)}
value={values.otp}
autoFocus
/>
</>
)
},
validate: (values) => {
if (!values.otp) {
return false
}
return values.otp.length === values.recoverSession.code_length
},
},
]
const OnFinish = async ({ values, setError }) => {
try {
const result = await AuthModel.changePassword({
newPassword: values.new_password,
code: values.otp,
verificationToken: values.recoverSession.verificationToken,
})
app.message.info("Password changed successfully")
app.navigation.goAuth()
} catch (error) {
console.error(error)
setError(error.message)
return {
cancel: true,
}
}
}
const Header = () => {
return (
<div className="steped-form-header">
<h1>Account Recovery</h1>
</div>
)
}
const RecoveryPage = (props) => {
return (
<FormWithSteps
header={Header}
steps={Steps}
onCancel={() => {
props.setActiveKey("selector")
}}
onFinish={OnFinish}
cancelable
/>
)
}
export default RecoveryPage

View File

@ -13,12 +13,7 @@ import PasswordStep from "./steps/password"
import EmailStep from "./steps/email" import EmailStep from "./steps/email"
import TOSStep from "./steps/tos" import TOSStep from "./steps/tos"
const steps = [ const steps = [UsernameStep, PasswordStep, EmailStep, TOSStep]
UsernameStep,
PasswordStep,
EmailStep,
TOSStep,
]
const RegisterForm = (props) => { const RegisterForm = (props) => {
const [finishing, setFinishing] = React.useState(false) const [finishing, setFinishing] = React.useState(false)
@ -73,10 +68,11 @@ const RegisterForm = (props) => {
}) })
} }
const updateStepValue = (value) => setStepsValues((prev) => { const updateStepValue = (value) =>
setStepsValues((prev) => {
return { return {
...prev, ...prev,
[currentStepData.key]: value [currentStepData.key]: value,
} }
}) })
@ -100,115 +96,104 @@ const RegisterForm = (props) => {
return true return true
} }
return <div return (
className={classnames( <div
"register_form", className={classnames("register_form", {
{ ["welcome_step"]: step === 0 && !finishing,
["welcome_step"]: step === 0 && !finishing })}
}
)}
> >
<div className="register_form_header-text"> <div className="register_form_header-text">
{ {!finishSuccess && !finishing && step === 0 && (
!finishSuccess && !finishing && step === 0 && <> <>
<h1>👋 Hi! Nice to meet you</h1> <h1>👋 Hi! Nice to meet you</h1>
<p>Tell us some basic information to get started creating your account.</p> <p>
Tell us some basic information to get started
creating your account.
</p>
</> </>
} )}
{ {!finishSuccess && !finishing && step > 0 && (
!finishSuccess && !finishing && step > 0 && <> <>
<h1> <h1>
{ {currentStepData?.icon &&
currentStepData?.icon && createIconRender(currentStepData.icon) createIconRender(currentStepData.icon)}
}
{currentStepData?.title} {currentStepData?.title}
</h1> </h1>
<p> <p>
{ {typeof currentStepData?.description === "function"
typeof currentStepData?.description === "function" ? ? currentStepData?.description()
currentStepData?.description() : currentStepData.description : currentStepData.description}
}
</p> </p>
</> </>
} )}
</div> </div>
{ {!finishSuccess &&
!finishSuccess && !finishing && step > 0 && React.createElement(currentStepData.content, { !finishing &&
step > 0 &&
React.createElement(currentStepData.content, {
onPressEnter: nextStep, onPressEnter: nextStep,
currentValue: stepsValues[currentStepData.key], currentValue: stepsValues[currentStepData.key],
updateValue: updateStepValue, updateValue: updateStepValue,
}) })}
}
{ {finishing && (
finishing && <div className="register_form_creating"> <div className="register_form_creating">
<Icons.LoadingOutlined /> <Icons.LoadingOutlined />
<h1> <h1>Creating your account</h1>
Creating your account
</h1>
</div> </div>
} )}
{ {finishSuccess && (
finishSuccess && <div className="register_form_success"> <div className="register_form_success">
<Icons.CheckCircleOutlined /> <Icons.CheckCircleOutlined />
<h1> <h1>Welcome abord!</h1>
Welcome abord!
</h1>
<p> <p>
One last step, we need you to login with your new account. One last step, we need you to login with your new
account.
</p> </p>
<antd.Button <antd.Button
type="primary" type="primary"
onClick={() => props.changeStage(0)} onClick={() => props.setActiveKey("selector")}
> >
Go to login Go to login
</antd.Button> </antd.Button>
</div> </div>
} )}
{ {finishError && (
finishError && <antd.Alert <antd.Alert type="error" message={finishError.message} />
type="error" )}
message={finishError.message}
/>
}
{ {!finishSuccess && !finishing && (
!finishSuccess && !finishing && <div className="register_form_actions"> <div className="register_form_actions">
{ {step === 0 && (
step === 0 &&
<antd.Button <antd.Button
onClick={() => props.changeStage(0)} onClick={() => props.setActiveKey("selector")}
> >
Cancel Cancel
</antd.Button> </antd.Button>
} )}
{ {step > 0 && (
step > 0 && <antd.Button onClick={() => prevStep()}>
<antd.Button
onClick={() => prevStep()}
>
Back Back
</antd.Button> </antd.Button>
} )}
<antd.Button <antd.Button
type="primary" type="primary"
onClick={() => nextStep()} onClick={() => nextStep()}
disabled={!canNextStep()} disabled={!canNextStep()}
> >
{ {step === steps.length ? "Finish" : "Next"}
step === steps.length ? "Finish" : "Next"
}
</antd.Button> </antd.Button>
</div> </div>
} )}
</div> </div>
)
} }
export default RegisterForm export default RegisterForm

View File

@ -4,31 +4,32 @@ import config from "@config"
import { Icons } from "@components/Icons" import { Icons } from "@components/Icons"
const MainSelector = (props) => { const MainSelector = (props) => {
const { return (
onClickLogin, <>
onClickRegister,
} = props
return <>
<div className="content_header"> <div className="content_header">
<img src={config.logo.alt} className="logo" /> <img src={config.logo.alt} className="logo" />
</div> </div>
<div className="actions"> <div className="actions">
{ {app.userData && (
app.userData && <antd.Button <antd.Button
type="default" type="default"
size="large" size="large"
onClick={() => { onClick={() => {
app.navigation.goMain() app.navigation.goMain()
}} }}
> >
<antd.Avatar size={23} shape="square" src={app.userData.avatar} /> Continue as {app.userData.username} <antd.Avatar
size={23}
shape="square"
src={app.userData.avatar}
/>{" "}
Continue as {app.userData.username}
</antd.Button> </antd.Button>
} )}
<antd.Button <antd.Button
onClick={onClickLogin} onClick={() => app.controls.openLoginForm()}
icon={<Icons.FiLogIn />} icon={<Icons.FiLogIn />}
type="primary" type="primary"
> >
@ -36,7 +37,7 @@ const MainSelector = (props) => {
</antd.Button> </antd.Button>
<antd.Button <antd.Button
onClick={onClickLogin} onClick={() => app.controls.openLoginForm()}
icon={<Icons.FiLogIn />} icon={<Icons.FiLogIn />}
type="primary" type="primary"
disabled disabled
@ -47,19 +48,19 @@ const MainSelector = (props) => {
<h4>Or create a new account</h4> <h4>Or create a new account</h4>
<antd.Button <antd.Button
onClick={onClickRegister} onClick={() => props.setActiveKey("register")}
icon={<Icons.FiUserPlus />} icon={<Icons.FiUserPlus />}
type="primary" type="primary"
> >
Create a Comty Account Create a Comty Account
</antd.Button> </antd.Button>
<p style={{ display: "inline" }}> <a onClick={() => props.setActiveKey("recovery")}>
<Icons.FiInfo /> I need help to recover my account
Registering a new account accepts the <a onClick={() => app.location.push("/terms")}>Terms and Conditions</a> and <a onClick={() => app.location.push("/privacy")}>Privacy policy</a> for the services provided by {config.author} </a>
</p>
</div> </div>
</> </>
)
} }
export default MainSelector export default MainSelector

View File

@ -1,14 +1,17 @@
import React from "react" import React from "react"
import useRandomFeaturedWallpaperUrl from "@hooks/useRandomFeaturedWallpaperUrl" import useRandomFeaturedWallpaperUrl from "@hooks/useRandomFeaturedWallpaperUrl"
import useUrlQueryActiveKey from "@hooks/useUrlQueryActiveKey"
import RegisterForm from "./forms/register" import RegisterForm from "./forms/register"
import MainSelector from "./forms/selector" import MainSelector from "./forms/selector"
import RecoveryForm from "./forms/recovery"
import "./index.less" import "./index.less"
const GradientSVG = () => { const GradientSVG = () => {
return <svg height="100%" width="100%"> return (
<svg height="100%" width="100%">
<defs> <defs>
<linearGradient id="0" x1="0" y1="0.5" x2="1" y2="0.5"> <linearGradient id="0" x1="0" y1="0.5" x2="1" y2="0.5">
<stop offset="0%" stop-color="rgba(225, 0, 209, 0.1)" /> <stop offset="0%" stop-color="rgba(225, 0, 209, 0.1)" />
@ -16,7 +19,10 @@ const GradientSVG = () => {
<stop offset="50%" stop-color="rgba(240, 0, 154, 0.05)" /> <stop offset="50%" stop-color="rgba(240, 0, 154, 0.05)" />
<stop offset="100%" stop-color="rgba(255, 0, 0, 0)" /> <stop offset="100%" stop-color="rgba(255, 0, 0, 0)" />
</linearGradient> </linearGradient>
<radialGradient id="1" gradientTransform="translate(-0.81 -0.5) scale(2, 1.2)"> <radialGradient
id="1"
gradientTransform="translate(-0.81 -0.5) scale(2, 1.2)"
>
<stop offset="0%" stop-color="rgba(255, 96, 100, 0.2)" /> <stop offset="0%" stop-color="rgba(255, 96, 100, 0.2)" />
<stop offset="20%" stop-color="rgba(255, 96, 100, 0.16)" /> <stop offset="20%" stop-color="rgba(255, 96, 100, 0.16)" />
<stop offset="40%" stop-color="rgba(255, 96, 100, 0.12)" /> <stop offset="40%" stop-color="rgba(255, 96, 100, 0.12)" />
@ -27,30 +33,23 @@ const GradientSVG = () => {
<rect fill="url(#0)" height="100%" width="100%" /> <rect fill="url(#0)" height="100%" width="100%" />
<rect fill="url(#1)" height="100%" width="100%" /> <rect fill="url(#1)" height="100%" width="100%" />
</svg> </svg>
)
} }
const stagesToComponents = { const keyToComponents = {
0: MainSelector, selector: MainSelector,
2: RegisterForm register: RegisterForm,
recovery: RecoveryForm,
} }
const AuthPage = (props) => { const AuthPage = (props) => {
const [stage, setStage] = React.useState(0) const [activeKey, setActiveKey] = useUrlQueryActiveKey({
defaultKey: "selector",
})
const randomWallpaperURL = useRandomFeaturedWallpaperUrl() const randomWallpaperURL = useRandomFeaturedWallpaperUrl()
function changeStage(nextStage) { return (
setStage(nextStage) <div className="login-page">
}
const onClickLogin = () => {
app.controls.openLoginForm()
}
const onClickRegister = () => {
changeStage(2)
}
return <div className="login-page">
<div className="background"> <div className="background">
<GradientSVG /> <GradientSVG />
</div> </div>
@ -59,22 +58,25 @@ const AuthPage = (props) => {
<div <div
className="wrapper_background" className="wrapper_background"
style={{ style={{
backgroundImage: randomWallpaperURL ? `url(${randomWallpaperURL})` : null, backgroundImage: randomWallpaperURL
animation: randomWallpaperURL ? "opacityIn 1s" : null ? `url(${randomWallpaperURL})`
: null,
animation: randomWallpaperURL ? "opacityIn 1s" : null,
}} }}
/> />
<div className="content"> <div className="content">
{React.createElement(
keyToComponents[activeKey] ??
keyToComponents["selector"],
{ {
React.createElement(stagesToComponents[stage] ?? stagesToComponents[0], { setActiveKey: setActiveKey,
onClickLogin, },
onClickRegister, )}
changeStage,
})
}
</div> </div>
</div> </div>
</div> </div>
)
} }
export default AuthPage export default AuthPage

View File

@ -1,11 +0,0 @@
import React from "react"
import "./index.less"
const AccountPasswordRecovery = () => {
return <div className="password_recover">
<h1>Account Password Recovery</h1>
</div>
}
export default AccountPasswordRecovery

View File

@ -1,3 +0,0 @@
.password_recover {
padding: 20px;
}