mirror of
https://github.com/ragestudio/comty.git
synced 2025-07-08 16:54:15 +00:00
Improve mobile auth & added Turnstile captcha to registration flow
This change adds a Cloudflare Turnstile captcha verification step to the user registration process, helping prevent automated account creation.
This commit is contained in:
parent
44c2500491
commit
abd65cf6ff
@ -68,6 +68,7 @@
|
|||||||
"react-player": "^2.16.0",
|
"react-player": "^2.16.0",
|
||||||
"react-rnd": "^10.4.14",
|
"react-rnd": "^10.4.14",
|
||||||
"react-transition-group": "^4.4.5",
|
"react-transition-group": "^4.4.5",
|
||||||
|
"react-turnstile": "^1.1.4",
|
||||||
"react-useanimations": "^2.10.0",
|
"react-useanimations": "^2.10.0",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"rxjs": "^7.5.5",
|
"rxjs": "^7.5.5",
|
||||||
|
@ -12,8 +12,9 @@ import UsernameStep from "./steps/username"
|
|||||||
import PasswordStep from "./steps/password"
|
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"
|
||||||
|
import CaptchaStep from "./steps/captcha"
|
||||||
|
|
||||||
const steps = [UsernameStep, PasswordStep, EmailStep, TOSStep]
|
const steps = [UsernameStep, PasswordStep, EmailStep, TOSStep, CaptchaStep]
|
||||||
|
|
||||||
const RegisterForm = (props) => {
|
const RegisterForm = (props) => {
|
||||||
const [finishing, setFinishing] = React.useState(false)
|
const [finishing, setFinishing] = React.useState(false)
|
||||||
@ -35,6 +36,7 @@ const RegisterForm = (props) => {
|
|||||||
password: stepsValues.password,
|
password: stepsValues.password,
|
||||||
email: stepsValues.email,
|
email: stepsValues.email,
|
||||||
tos: stepsValues.tos,
|
tos: stepsValues.tos,
|
||||||
|
captcha: stepsValues.captcha,
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
setFinishSuccess(false)
|
setFinishSuccess(false)
|
||||||
setFinishing(false)
|
setFinishing(false)
|
||||||
@ -107,8 +109,8 @@ const RegisterForm = (props) => {
|
|||||||
<>
|
<>
|
||||||
<h1>👋 Hi! Nice to meet you</h1>
|
<h1>👋 Hi! Nice to meet you</h1>
|
||||||
<p>
|
<p>
|
||||||
Tell us some basic information to get started
|
Tell us some basic information to get started creating your
|
||||||
creating your account.
|
account.
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -116,8 +118,7 @@ const RegisterForm = (props) => {
|
|||||||
{!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>
|
||||||
@ -150,10 +151,7 @@ const RegisterForm = (props) => {
|
|||||||
<div className="register_form_success">
|
<div className="register_form_success">
|
||||||
<Icons.CheckCircleOutlined />
|
<Icons.CheckCircleOutlined />
|
||||||
<h1>Welcome abord!</h1>
|
<h1>Welcome abord!</h1>
|
||||||
<p>
|
<p>One last step, we need you to login with your new account.</p>
|
||||||
One last step, we need you to login with your new
|
|
||||||
account.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<antd.Button
|
<antd.Button
|
||||||
type="primary"
|
type="primary"
|
||||||
@ -165,22 +163,21 @@ const RegisterForm = (props) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{finishError && (
|
{finishError && (
|
||||||
<antd.Alert type="error" message={finishError.message} />
|
<antd.Alert
|
||||||
|
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.setActiveKey("selector")}>
|
||||||
onClick={() => props.setActiveKey("selector")}
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</antd.Button>
|
</antd.Button>
|
||||||
)}
|
)}
|
||||||
{step > 0 && (
|
{step > 0 && (
|
||||||
<antd.Button onClick={() => prevStep()}>
|
<antd.Button onClick={() => prevStep()}>Back</antd.Button>
|
||||||
Back
|
|
||||||
</antd.Button>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<antd.Button
|
<antd.Button
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
import Turnstile from "react-turnstile"
|
||||||
|
|
||||||
|
const CaptchaStepComponent = (props) => {
|
||||||
|
return (
|
||||||
|
<Turnstile
|
||||||
|
sitekey={import.meta.env.VITE_TURNSTILE_SITEKEY}
|
||||||
|
onVerify={(token) => {
|
||||||
|
props.updateValue(token)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
key: "captcha",
|
||||||
|
title: "Step 4",
|
||||||
|
icon: "FiLock",
|
||||||
|
description:
|
||||||
|
"We need you to prove that you are a human. Please enter the captcha below.",
|
||||||
|
required: true,
|
||||||
|
content: CaptchaStepComponent,
|
||||||
|
}
|
@ -4,87 +4,103 @@ import * as antd from "antd"
|
|||||||
import AuthModel from "@models/auth"
|
import AuthModel from "@models/auth"
|
||||||
|
|
||||||
const EmailStepComponent = (props) => {
|
const EmailStepComponent = (props) => {
|
||||||
const [email, setEmail] = React.useState(props.currentValue ?? "")
|
const [email, setEmail] = React.useState(props.currentValue ?? "")
|
||||||
|
|
||||||
const [loading, setLoading] = React.useState(false)
|
const [loading, setLoading] = React.useState(false)
|
||||||
const [validFormat, setValidFormat] = React.useState(null)
|
const [validFormat, setValidFormat] = React.useState(null)
|
||||||
const [emailAvailable, setEmailAvailable] = React.useState(null)
|
const [emailAvailable, setEmailAvailable] = React.useState(null)
|
||||||
|
|
||||||
const isValid = () => {
|
const isValid = () => {
|
||||||
return email.length > 0 && validFormat && emailAvailable
|
return email.length > 0 && validFormat && emailAvailable
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkIfIsEmail = (email) => {
|
const checkIfIsEmail = (email) => {
|
||||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
|
||||||
}
|
}
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
if (!isValid()) return
|
if (!isValid()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
props.onPressEnter()
|
props.onPressEnter()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUpdate = (e) => {
|
const handleUpdate = (e) => {
|
||||||
setEmail(e.target.value)
|
setEmail(e.target.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (email.length === 0) {
|
if (email.length === 0) {
|
||||||
setEmailAvailable(null)
|
setEmailAvailable(null)
|
||||||
setValidFormat(null)
|
setValidFormat(null)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
props.updateValue(null)
|
props.updateValue(null)
|
||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
setValidFormat(checkIfIsEmail(email))
|
const isEmailValid = checkIfIsEmail(email)
|
||||||
|
setValidFormat(isEmailValid)
|
||||||
|
|
||||||
// check if email is available
|
// check if email is available
|
||||||
const timer = setTimeout(async () => {
|
const timer = setTimeout(async () => {
|
||||||
if (!validFormat) return
|
if (!isEmailValid) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const request = await AuthModel.availability({ email }).catch((error) => {
|
const request = await AuthModel.availability({ email }).catch((error) => {
|
||||||
antd.message.error(`Cannot check email availability: ${error.message}`)
|
antd.message.error(`Cannot check email availability: ${error.message}`)
|
||||||
|
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
if (request) {
|
if (request) {
|
||||||
setEmailAvailable(!request.exist)
|
setEmailAvailable(!request.exist)
|
||||||
|
|
||||||
if (request.exist) {
|
if (request.exist) {
|
||||||
antd.message.error("Email is already in use")
|
antd.message.error("Email is already in use")
|
||||||
props.updateValue(null)
|
props.updateValue(null)
|
||||||
} else {
|
} else {
|
||||||
props.updateValue(email)
|
props.updateValue(email)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
return () => clearTimeout(timer)
|
return () => clearTimeout(timer)
|
||||||
}, [email])
|
}, [email])
|
||||||
|
|
||||||
return <div className="register_form_step_content">
|
return (
|
||||||
<antd.Input
|
<div className="register_form_step_content">
|
||||||
defaultValue={props.currentValue}
|
<antd.Input
|
||||||
placeholder="Email"
|
defaultValue={props.currentValue}
|
||||||
onPressEnter={submit}
|
placeholder="Email"
|
||||||
onChange={handleUpdate}
|
onPressEnter={submit}
|
||||||
status={email.length == 0 ? "default" : loading ? "default" : (isValid() ? "success" : "error")}
|
onChange={handleUpdate}
|
||||||
/>
|
status={
|
||||||
</div>
|
email.length == 0
|
||||||
|
? "default"
|
||||||
|
: loading
|
||||||
|
? "default"
|
||||||
|
: isValid()
|
||||||
|
? "success"
|
||||||
|
: "error"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
key: "email",
|
key: "email",
|
||||||
title: "Step 3",
|
title: "Step 3",
|
||||||
icon: "FiMail",
|
icon: "FiMail",
|
||||||
description: "Enter a email for the account, it can be used to access to your account. \n Will not be shared with anyone else and not be used for marketing purposes.",
|
description:
|
||||||
required: true,
|
"Enter a email for the account, it can be used to access to your account. \n Will not be shared with anyone else and not be used for marketing purposes.",
|
||||||
content: EmailStepComponent,
|
required: true,
|
||||||
|
content: EmailStepComponent,
|
||||||
}
|
}
|
@ -1,80 +1,81 @@
|
|||||||
import React from "react"
|
|
||||||
import * as antd from "antd"
|
import * as antd from "antd"
|
||||||
|
|
||||||
import MarkdownReader from "@components/MarkdownReader"
|
import MarkdownReader from "@components/MarkdownReader"
|
||||||
import config from "@config"
|
import config from "@config"
|
||||||
|
|
||||||
const FrameStyle = {
|
const FrameStyle = {
|
||||||
"width": "60vw",
|
width: "60vw",
|
||||||
"max-width": "60vw",
|
"max-width": "60vw",
|
||||||
"height": "90vh",
|
height: "90vh",
|
||||||
"max-height": "90vh",
|
"max-height": "90vh",
|
||||||
"overflow": "overlay",
|
overflow: "overlay",
|
||||||
"justify-content": "flex-start",
|
"justify-content": "flex-start",
|
||||||
}
|
}
|
||||||
|
|
||||||
const LegalDocumentsDecorators = {
|
const LegalDocumentsDecorators = {
|
||||||
"terms": "Terms of Service",
|
terms: "Terms of Service",
|
||||||
"privacy": "Privacy Policy",
|
privacy: "Privacy Policy",
|
||||||
}
|
}
|
||||||
|
|
||||||
function composeConfirmationCheckboxLabel(documents) {
|
function composeConfirmationCheckboxLabel(documents) {
|
||||||
let labels = [
|
let labels = ["I have read and accept"]
|
||||||
"I have read and accept"
|
|
||||||
]
|
|
||||||
|
|
||||||
documents.forEach(([key, value], index) => {
|
documents.forEach(([key, value], index) => {
|
||||||
const isLast = index === documents.length - 1
|
const isLast = index === documents.length - 1
|
||||||
|
|
||||||
labels.push(`the ${LegalDocumentsDecorators[key] ?? `document (${key})`} ${!isLast ? "and" : ""}`)
|
labels.push(
|
||||||
})
|
`the ${LegalDocumentsDecorators[key] ?? `document (${key})`} ${!isLast ? "and" : ""}`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
return labels.join(" ")
|
return labels.join(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
const TermsOfServiceStepComponent = (props) => {
|
const TermsOfServiceStepComponent = (props) => {
|
||||||
const legalDocuments = Object.entries(config.legal)
|
const legalDocuments = Object.entries(config.legal)
|
||||||
|
|
||||||
return <div className="register_form_step_content">
|
return (
|
||||||
{
|
<div className="register_form_step_content">
|
||||||
Object.entries(config.legal).map(([key, value]) => {
|
{Object.entries(config.legal).map(([key, value]) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return <antd.Button
|
return (
|
||||||
key={key}
|
<antd.Button
|
||||||
onClick={() => {
|
key={key}
|
||||||
app.layout.modal.open(key, MarkdownReader, {
|
onClick={() => {
|
||||||
includeCloseButton: true,
|
app.layout.modal.open(key, MarkdownReader, {
|
||||||
frameContentStyle: FrameStyle,
|
includeCloseButton: true,
|
||||||
props: {
|
frameContentStyle: FrameStyle,
|
||||||
url: value
|
props: {
|
||||||
}
|
url: value,
|
||||||
})
|
},
|
||||||
}}
|
})
|
||||||
>
|
}}
|
||||||
Read {LegalDocumentsDecorators[key] ?? `document (${key})`}
|
>
|
||||||
</antd.Button>
|
Read {LegalDocumentsDecorators[key] ?? `document (${key})`}
|
||||||
})
|
</antd.Button>
|
||||||
}
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
<antd.Checkbox
|
<antd.Checkbox
|
||||||
defaultChecked={props.currentValue}
|
defaultChecked={props.currentValue}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
props.updateValue(event.target.checked)
|
props.updateValue(event.target.checked)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{composeConfirmationCheckboxLabel(legalDocuments)}
|
{composeConfirmationCheckboxLabel(legalDocuments)}
|
||||||
</antd.Checkbox>
|
</antd.Checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
key: "tos",
|
key: "tos",
|
||||||
title: "Step 3",
|
title: "Step 3",
|
||||||
icon: "FileDone",
|
icon: "FileDone",
|
||||||
description: "Take your time to read these legal documents.",
|
description: "Take your time to read these legal documents.",
|
||||||
required: true,
|
required: true,
|
||||||
content: TermsOfServiceStepComponent,
|
content: TermsOfServiceStepComponent,
|
||||||
}
|
}
|
||||||
|
@ -6,9 +6,14 @@ import { Icons } from "@components/Icons"
|
|||||||
const MainSelector = (props) => {
|
const MainSelector = (props) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="content_header">
|
{!app.isMobile && (
|
||||||
<img src={config.logo.alt} className="logo" />
|
<div className="content_header">
|
||||||
</div>
|
<img
|
||||||
|
src={config.logo.alt}
|
||||||
|
className="logo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
{app.userData && (
|
{app.userData && (
|
||||||
|
@ -1,32 +1,55 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
|
import config from "@config"
|
||||||
|
|
||||||
import useRandomFeaturedWallpaperUrl from "@hooks/useRandomFeaturedWallpaperUrl"
|
import useRandomFeaturedWallpaperUrl from "@hooks/useRandomFeaturedWallpaperUrl"
|
||||||
|
import useUrlQueryActiveKey from "@hooks/useUrlQueryActiveKey"
|
||||||
|
|
||||||
|
import RegisterForm from "./forms/register"
|
||||||
|
import MainSelector from "./forms/selector"
|
||||||
|
import RecoveryForm from "./forms/recovery"
|
||||||
|
|
||||||
|
const keyToComponents = {
|
||||||
|
selector: MainSelector,
|
||||||
|
register: RegisterForm,
|
||||||
|
recovery: RecoveryForm,
|
||||||
|
}
|
||||||
|
|
||||||
import "./index.mobile.less"
|
import "./index.mobile.less"
|
||||||
|
|
||||||
export default (props) => {
|
const AuthPage = (props) => {
|
||||||
const randomWallpaperURL = useRandomFeaturedWallpaperUrl()
|
const randomWallpaperURL = useRandomFeaturedWallpaperUrl()
|
||||||
|
const [activeKey, setActiveKey] = useUrlQueryActiveKey({
|
||||||
React.useEffect(() => {
|
defaultKey: "selector",
|
||||||
if (app.userData) {
|
})
|
||||||
app.navigation.goMain()
|
|
||||||
} else {
|
|
||||||
app.auth.login()
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="loginPage">
|
<div className="login-page">
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url(${randomWallpaperURL})`,
|
backgroundImage: `url(${randomWallpaperURL})`,
|
||||||
}}
|
}}
|
||||||
className="wallpaper"
|
className="wallpaper"
|
||||||
>
|
/>
|
||||||
{/* <p>
|
|
||||||
{wallpaperData?.author ? wallpaperData.author : null}
|
<div className="login-page-card">
|
||||||
</p> */}
|
<div className="login-page-card__header">
|
||||||
|
<img
|
||||||
|
className="login-page-card__header__logo"
|
||||||
|
src={config.logo.alt}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="login-page-card__content">
|
||||||
|
{React.createElement(
|
||||||
|
keyToComponents[activeKey] ?? keyToComponents["selector"],
|
||||||
|
{
|
||||||
|
setActiveKey: setActiveKey,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default AuthPage
|
||||||
|
@ -1,22 +1,89 @@
|
|||||||
.loginPage {
|
.login-page {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
width: 100%;
|
display: flex;
|
||||||
height: 100vh;
|
flex-direction: column;
|
||||||
height: 100dvh;
|
|
||||||
|
|
||||||
.wallpaper {
|
align-items: center;
|
||||||
position: absolute;
|
justify-content: center;
|
||||||
|
|
||||||
top: 0;
|
width: 100%;
|
||||||
left: 0;
|
height: 100vh;
|
||||||
|
height: 100dvh;
|
||||||
|
|
||||||
width: 100%;
|
.wallpaper {
|
||||||
height: 100vh;
|
position: fixed;
|
||||||
height: 100dvh;
|
|
||||||
|
|
||||||
background-position: center;
|
top: 0;
|
||||||
background-size: cover;
|
left: 0;
|
||||||
background-repeat: no-repeat;
|
|
||||||
}
|
z-index: -1;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
height: 100dvh;
|
||||||
|
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page-card {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
width: 95%;
|
||||||
|
padding: 15px;
|
||||||
|
|
||||||
|
background-color: var(--background-color-primary);
|
||||||
|
|
||||||
|
border-radius: 24px;
|
||||||
|
|
||||||
|
gap: 30px;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
&__logo {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
height: fit-content;
|
||||||
|
padding: 7px 15px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register_form {
|
||||||
|
position: unset;
|
||||||
|
|
||||||
|
.register_form_actions {
|
||||||
|
top: 0;
|
||||||
|
bottom: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -104,6 +104,8 @@ const LyricsText = React.forwardRef((props, textRef) => {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setVisible(false)
|
setVisible(false)
|
||||||
setCurrentLineIndex(0)
|
setCurrentLineIndex(0)
|
||||||
|
// set scroll top to 0
|
||||||
|
textRef.current.scrollTop = 0
|
||||||
}, [playerState.track_manifest])
|
}, [playerState.track_manifest])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@ -35,7 +35,6 @@ export default class API extends Server {
|
|||||||
|
|
||||||
onExit() {
|
onExit() {
|
||||||
this.queuesManager.cleanUp()
|
this.queuesManager.cleanUp()
|
||||||
console.log("Jijija")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,60 +1,84 @@
|
|||||||
import bcrypt from "bcrypt"
|
import bcrypt from "bcrypt"
|
||||||
import { User } from "@db_models"
|
import { User } from "@db_models"
|
||||||
import requiredFields from "@shared-utils/requiredFields"
|
|
||||||
|
|
||||||
import Account from "@classes/account"
|
import Account from "@classes/account"
|
||||||
|
|
||||||
|
import requiredFields from "@shared-utils/requiredFields"
|
||||||
|
import verifyTurnstileToken from "@utils/verifyTurnstileToken"
|
||||||
|
|
||||||
export default async (payload) => {
|
export default async (payload) => {
|
||||||
requiredFields(["username", "password", "email"], payload)
|
requiredFields(["username", "password", "email"], payload)
|
||||||
|
|
||||||
let { username, password, email, public_name, roles, avatar, accept_tos } = payload
|
let {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
email,
|
||||||
|
public_name,
|
||||||
|
roles,
|
||||||
|
avatar,
|
||||||
|
accept_tos,
|
||||||
|
captcha,
|
||||||
|
} = payload
|
||||||
|
|
||||||
if (ToBoolean(accept_tos) !== true) {
|
if (ToBoolean(accept_tos) !== true) {
|
||||||
throw new OperationError(400, "You must accept the terms of service in order to create an account.")
|
throw new OperationError(
|
||||||
}
|
400,
|
||||||
|
"You must accept the terms of service in order to create an account.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
await Account.usernameMeetPolicy(username)
|
if (!captcha) {
|
||||||
|
throw new OperationError(400, "Captcha token is required")
|
||||||
|
}
|
||||||
|
|
||||||
// check if username is already taken
|
const turnstileResponse = await verifyTurnstileToken(captcha)
|
||||||
const existentUser = await User
|
|
||||||
.findOne({ username: username })
|
|
||||||
|
|
||||||
if (existentUser) {
|
if (turnstileResponse.success !== true) {
|
||||||
throw new OperationError(400, "User already exists")
|
throw new OperationError(400, "Invalid captcha token")
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the email is already in use
|
await Account.usernameMeetPolicy(username)
|
||||||
const existentEmail = await User
|
|
||||||
.findOne({ email: email })
|
|
||||||
.select("+email")
|
|
||||||
|
|
||||||
if (existentEmail) {
|
// check if username is already taken
|
||||||
throw new OperationError(400, "Email already in use")
|
const existentUser = await User.findOne({ username: username })
|
||||||
}
|
|
||||||
|
|
||||||
await Account.passwordMeetPolicy(password)
|
if (existentUser) {
|
||||||
|
throw new OperationError(400, "User already exists")
|
||||||
|
}
|
||||||
|
|
||||||
// hash the password
|
// check if the email is already in use
|
||||||
const hash = bcrypt.hashSync(password, parseInt(process.env.BCRYPT_ROUNDS ?? 3))
|
const existentEmail = await User.findOne({ email: email }).select("+email")
|
||||||
|
|
||||||
let user = new User({
|
if (existentEmail) {
|
||||||
username: username,
|
throw new OperationError(400, "Email already in use")
|
||||||
password: hash,
|
}
|
||||||
email: email,
|
|
||||||
public_name: public_name,
|
|
||||||
avatar: avatar ?? `https://api.dicebear.com/7.x/thumbs/svg?seed=${username}`,
|
|
||||||
roles: roles,
|
|
||||||
created_at: new Date().getTime(),
|
|
||||||
accept_tos: accept_tos,
|
|
||||||
activated: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
await user.save()
|
await Account.passwordMeetPolicy(password)
|
||||||
|
|
||||||
await Account.sendActivationCode(user._id.toString())
|
// hash the password
|
||||||
|
const hash = bcrypt.hashSync(
|
||||||
|
password,
|
||||||
|
parseInt(process.env.BCRYPT_ROUNDS ?? 3),
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
let user = new User({
|
||||||
activation_required: true,
|
username: username,
|
||||||
user: user,
|
password: hash,
|
||||||
}
|
email: email,
|
||||||
|
public_name: public_name,
|
||||||
|
avatar:
|
||||||
|
avatar ?? `https://api.dicebear.com/7.x/thumbs/svg?seed=${username}`,
|
||||||
|
roles: roles,
|
||||||
|
created_at: new Date().getTime(),
|
||||||
|
accept_tos: accept_tos,
|
||||||
|
activated: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
await user.save()
|
||||||
|
|
||||||
|
await Account.sendActivationCode(user._id.toString())
|
||||||
|
|
||||||
|
return {
|
||||||
|
activation_required: true,
|
||||||
|
user: user,
|
||||||
|
}
|
||||||
}
|
}
|
26
packages/server/services/auth/utils/verifyTurnstileToken.js
Normal file
26
packages/server/services/auth/utils/verifyTurnstileToken.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import axios from "axios"
|
||||||
|
|
||||||
|
export default async (token) => {
|
||||||
|
const secret = process.env.TURNSTILE_SECRET
|
||||||
|
|
||||||
|
if (!secret) {
|
||||||
|
throw new Error("Turnstile secret is not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = await axios({
|
||||||
|
url: "https://challenges.cloudflare.com/turnstile/v0/siteverify",
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
secret: secret,
|
||||||
|
response: token,
|
||||||
|
},
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error(err.response.data)
|
||||||
|
throw new Error("Turnstile verification failed")
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user