mirror of
https://github.com/ragestudio/comty.git
synced 2025-07-08 08:44: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-rnd": "^10.4.14",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"react-turnstile": "^1.1.4",
|
||||
"react-useanimations": "^2.10.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"rxjs": "^7.5.5",
|
||||
|
@ -12,8 +12,9 @@ import UsernameStep from "./steps/username"
|
||||
import PasswordStep from "./steps/password"
|
||||
import EmailStep from "./steps/email"
|
||||
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 [finishing, setFinishing] = React.useState(false)
|
||||
@ -35,6 +36,7 @@ const RegisterForm = (props) => {
|
||||
password: stepsValues.password,
|
||||
email: stepsValues.email,
|
||||
tos: stepsValues.tos,
|
||||
captcha: stepsValues.captcha,
|
||||
}).catch((err) => {
|
||||
setFinishSuccess(false)
|
||||
setFinishing(false)
|
||||
@ -107,8 +109,8 @@ const RegisterForm = (props) => {
|
||||
<>
|
||||
<h1>👋 Hi! Nice to meet you</h1>
|
||||
<p>
|
||||
Tell us some basic information to get started
|
||||
creating your account.
|
||||
Tell us some basic information to get started creating your
|
||||
account.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
@ -116,8 +118,7 @@ const RegisterForm = (props) => {
|
||||
{!finishSuccess && !finishing && step > 0 && (
|
||||
<>
|
||||
<h1>
|
||||
{currentStepData?.icon &&
|
||||
createIconRender(currentStepData.icon)}
|
||||
{currentStepData?.icon && createIconRender(currentStepData.icon)}
|
||||
|
||||
{currentStepData?.title}
|
||||
</h1>
|
||||
@ -150,10 +151,7 @@ const RegisterForm = (props) => {
|
||||
<div className="register_form_success">
|
||||
<Icons.CheckCircleOutlined />
|
||||
<h1>Welcome abord!</h1>
|
||||
<p>
|
||||
One last step, we need you to login with your new
|
||||
account.
|
||||
</p>
|
||||
<p>One last step, we need you to login with your new account.</p>
|
||||
|
||||
<antd.Button
|
||||
type="primary"
|
||||
@ -165,22 +163,21 @@ const RegisterForm = (props) => {
|
||||
)}
|
||||
|
||||
{finishError && (
|
||||
<antd.Alert type="error" message={finishError.message} />
|
||||
<antd.Alert
|
||||
type="error"
|
||||
message={finishError.message}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!finishSuccess && !finishing && (
|
||||
<div className="register_form_actions">
|
||||
{step === 0 && (
|
||||
<antd.Button
|
||||
onClick={() => props.setActiveKey("selector")}
|
||||
>
|
||||
<antd.Button onClick={() => props.setActiveKey("selector")}>
|
||||
Cancel
|
||||
</antd.Button>
|
||||
)}
|
||||
{step > 0 && (
|
||||
<antd.Button onClick={() => prevStep()}>
|
||||
Back
|
||||
</antd.Button>
|
||||
<antd.Button onClick={() => prevStep()}>Back</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"
|
||||
|
||||
const EmailStepComponent = (props) => {
|
||||
const [email, setEmail] = React.useState(props.currentValue ?? "")
|
||||
const [email, setEmail] = React.useState(props.currentValue ?? "")
|
||||
|
||||
const [loading, setLoading] = React.useState(false)
|
||||
const [validFormat, setValidFormat] = React.useState(null)
|
||||
const [emailAvailable, setEmailAvailable] = React.useState(null)
|
||||
const [loading, setLoading] = React.useState(false)
|
||||
const [validFormat, setValidFormat] = React.useState(null)
|
||||
const [emailAvailable, setEmailAvailable] = React.useState(null)
|
||||
|
||||
const isValid = () => {
|
||||
return email.length > 0 && validFormat && emailAvailable
|
||||
}
|
||||
const isValid = () => {
|
||||
return email.length > 0 && validFormat && emailAvailable
|
||||
}
|
||||
|
||||
const checkIfIsEmail = (email) => {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
|
||||
}
|
||||
const checkIfIsEmail = (email) => {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
|
||||
}
|
||||
|
||||
const submit = () => {
|
||||
if (!isValid()) return
|
||||
const submit = () => {
|
||||
if (!isValid()) {
|
||||
return false
|
||||
}
|
||||
|
||||
props.onPressEnter()
|
||||
}
|
||||
props.onPressEnter()
|
||||
}
|
||||
|
||||
const handleUpdate = (e) => {
|
||||
setEmail(e.target.value)
|
||||
}
|
||||
const handleUpdate = (e) => {
|
||||
setEmail(e.target.value)
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (email.length === 0) {
|
||||
setEmailAvailable(null)
|
||||
setValidFormat(null)
|
||||
React.useEffect(() => {
|
||||
if (email.length === 0) {
|
||||
setEmailAvailable(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
|
||||
const timer = setTimeout(async () => {
|
||||
if (!validFormat) return
|
||||
// check if email is available
|
||||
const timer = setTimeout(async () => {
|
||||
if (!isEmailValid) {
|
||||
return false
|
||||
}
|
||||
|
||||
const request = await AuthModel.availability({ email }).catch((error) => {
|
||||
antd.message.error(`Cannot check email availability: ${error.message}`)
|
||||
const request = await AuthModel.availability({ email }).catch((error) => {
|
||||
antd.message.error(`Cannot check email availability: ${error.message}`)
|
||||
|
||||
return false
|
||||
})
|
||||
return false
|
||||
})
|
||||
|
||||
if (request) {
|
||||
setEmailAvailable(!request.exist)
|
||||
if (request) {
|
||||
setEmailAvailable(!request.exist)
|
||||
|
||||
if (request.exist) {
|
||||
antd.message.error("Email is already in use")
|
||||
props.updateValue(null)
|
||||
} else {
|
||||
props.updateValue(email)
|
||||
}
|
||||
}
|
||||
if (request.exist) {
|
||||
antd.message.error("Email is already in use")
|
||||
props.updateValue(null)
|
||||
} else {
|
||||
props.updateValue(email)
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(false)
|
||||
}, 1000)
|
||||
setLoading(false)
|
||||
}, 1000)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [email])
|
||||
return () => clearTimeout(timer)
|
||||
}, [email])
|
||||
|
||||
return <div className="register_form_step_content">
|
||||
<antd.Input
|
||||
defaultValue={props.currentValue}
|
||||
placeholder="Email"
|
||||
onPressEnter={submit}
|
||||
onChange={handleUpdate}
|
||||
status={email.length == 0 ? "default" : loading ? "default" : (isValid() ? "success" : "error")}
|
||||
/>
|
||||
</div>
|
||||
return (
|
||||
<div className="register_form_step_content">
|
||||
<antd.Input
|
||||
defaultValue={props.currentValue}
|
||||
placeholder="Email"
|
||||
onPressEnter={submit}
|
||||
onChange={handleUpdate}
|
||||
status={
|
||||
email.length == 0
|
||||
? "default"
|
||||
: loading
|
||||
? "default"
|
||||
: isValid()
|
||||
? "success"
|
||||
: "error"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
key: "email",
|
||||
title: "Step 3",
|
||||
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.",
|
||||
required: true,
|
||||
content: EmailStepComponent,
|
||||
}
|
||||
key: "email",
|
||||
title: "Step 3",
|
||||
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.",
|
||||
required: true,
|
||||
content: EmailStepComponent,
|
||||
}
|
||||
|
@ -1,80 +1,81 @@
|
||||
import React from "react"
|
||||
import * as antd from "antd"
|
||||
|
||||
import MarkdownReader from "@components/MarkdownReader"
|
||||
import config from "@config"
|
||||
|
||||
const FrameStyle = {
|
||||
"width": "60vw",
|
||||
"max-width": "60vw",
|
||||
"height": "90vh",
|
||||
"max-height": "90vh",
|
||||
"overflow": "overlay",
|
||||
"justify-content": "flex-start",
|
||||
width: "60vw",
|
||||
"max-width": "60vw",
|
||||
height: "90vh",
|
||||
"max-height": "90vh",
|
||||
overflow: "overlay",
|
||||
"justify-content": "flex-start",
|
||||
}
|
||||
|
||||
const LegalDocumentsDecorators = {
|
||||
"terms": "Terms of Service",
|
||||
"privacy": "Privacy Policy",
|
||||
terms: "Terms of Service",
|
||||
privacy: "Privacy Policy",
|
||||
}
|
||||
|
||||
function composeConfirmationCheckboxLabel(documents) {
|
||||
let labels = [
|
||||
"I have read and accept"
|
||||
]
|
||||
let labels = ["I have read and accept"]
|
||||
|
||||
documents.forEach(([key, value], index) => {
|
||||
const isLast = index === documents.length - 1
|
||||
documents.forEach(([key, value], index) => {
|
||||
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 legalDocuments = Object.entries(config.legal)
|
||||
const legalDocuments = Object.entries(config.legal)
|
||||
|
||||
return <div className="register_form_step_content">
|
||||
{
|
||||
Object.entries(config.legal).map(([key, value]) => {
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div className="register_form_step_content">
|
||||
{Object.entries(config.legal).map(([key, value]) => {
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <antd.Button
|
||||
key={key}
|
||||
onClick={() => {
|
||||
app.layout.modal.open(key, MarkdownReader, {
|
||||
includeCloseButton: true,
|
||||
frameContentStyle: FrameStyle,
|
||||
props: {
|
||||
url: value
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
Read {LegalDocumentsDecorators[key] ?? `document (${key})`}
|
||||
</antd.Button>
|
||||
})
|
||||
}
|
||||
return (
|
||||
<antd.Button
|
||||
key={key}
|
||||
onClick={() => {
|
||||
app.layout.modal.open(key, MarkdownReader, {
|
||||
includeCloseButton: true,
|
||||
frameContentStyle: FrameStyle,
|
||||
props: {
|
||||
url: value,
|
||||
},
|
||||
})
|
||||
}}
|
||||
>
|
||||
Read {LegalDocumentsDecorators[key] ?? `document (${key})`}
|
||||
</antd.Button>
|
||||
)
|
||||
})}
|
||||
|
||||
<antd.Checkbox
|
||||
defaultChecked={props.currentValue}
|
||||
onChange={(event) => {
|
||||
props.updateValue(event.target.checked)
|
||||
}}
|
||||
>
|
||||
{composeConfirmationCheckboxLabel(legalDocuments)}
|
||||
</antd.Checkbox>
|
||||
</div>
|
||||
<antd.Checkbox
|
||||
defaultChecked={props.currentValue}
|
||||
onChange={(event) => {
|
||||
props.updateValue(event.target.checked)
|
||||
}}
|
||||
>
|
||||
{composeConfirmationCheckboxLabel(legalDocuments)}
|
||||
</antd.Checkbox>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
key: "tos",
|
||||
title: "Step 3",
|
||||
icon: "FileDone",
|
||||
description: "Take your time to read these legal documents.",
|
||||
required: true,
|
||||
content: TermsOfServiceStepComponent,
|
||||
key: "tos",
|
||||
title: "Step 3",
|
||||
icon: "FileDone",
|
||||
description: "Take your time to read these legal documents.",
|
||||
required: true,
|
||||
content: TermsOfServiceStepComponent,
|
||||
}
|
||||
|
@ -6,9 +6,14 @@ import { Icons } from "@components/Icons"
|
||||
const MainSelector = (props) => {
|
||||
return (
|
||||
<>
|
||||
<div className="content_header">
|
||||
<img src={config.logo.alt} className="logo" />
|
||||
</div>
|
||||
{!app.isMobile && (
|
||||
<div className="content_header">
|
||||
<img
|
||||
src={config.logo.alt}
|
||||
className="logo"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="actions">
|
||||
{app.userData && (
|
||||
|
@ -1,32 +1,55 @@
|
||||
import React from "react"
|
||||
import config from "@config"
|
||||
|
||||
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"
|
||||
|
||||
export default (props) => {
|
||||
const AuthPage = (props) => {
|
||||
const randomWallpaperURL = useRandomFeaturedWallpaperUrl()
|
||||
|
||||
React.useEffect(() => {
|
||||
if (app.userData) {
|
||||
app.navigation.goMain()
|
||||
} else {
|
||||
app.auth.login()
|
||||
}
|
||||
}, [])
|
||||
const [activeKey, setActiveKey] = useUrlQueryActiveKey({
|
||||
defaultKey: "selector",
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="loginPage">
|
||||
<div className="login-page">
|
||||
<div
|
||||
style={{
|
||||
backgroundImage: `url(${randomWallpaperURL})`,
|
||||
}}
|
||||
className="wallpaper"
|
||||
>
|
||||
{/* <p>
|
||||
{wallpaperData?.author ? wallpaperData.author : null}
|
||||
</p> */}
|
||||
/>
|
||||
|
||||
<div className="login-page-card">
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
export default AuthPage
|
||||
|
@ -1,22 +1,89 @@
|
||||
.loginPage {
|
||||
position: relative;
|
||||
.login-page {
|
||||
position: relative;
|
||||
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.wallpaper {
|
||||
position: absolute;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
.wallpaper {
|
||||
position: fixed;
|
||||
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
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(() => {
|
||||
setVisible(false)
|
||||
setCurrentLineIndex(0)
|
||||
// set scroll top to 0
|
||||
textRef.current.scrollTop = 0
|
||||
}, [playerState.track_manifest])
|
||||
|
||||
React.useEffect(() => {
|
||||
|
@ -35,7 +35,6 @@ export default class API extends Server {
|
||||
|
||||
onExit() {
|
||||
this.queuesManager.cleanUp()
|
||||
console.log("Jijija")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,60 +1,84 @@
|
||||
import bcrypt from "bcrypt"
|
||||
import { User } from "@db_models"
|
||||
import requiredFields from "@shared-utils/requiredFields"
|
||||
|
||||
import Account from "@classes/account"
|
||||
|
||||
import requiredFields from "@shared-utils/requiredFields"
|
||||
import verifyTurnstileToken from "@utils/verifyTurnstileToken"
|
||||
|
||||
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) {
|
||||
throw new OperationError(400, "You must accept the terms of service in order to create an account.")
|
||||
}
|
||||
if (ToBoolean(accept_tos) !== true) {
|
||||
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 existentUser = await User
|
||||
.findOne({ username: username })
|
||||
const turnstileResponse = await verifyTurnstileToken(captcha)
|
||||
|
||||
if (existentUser) {
|
||||
throw new OperationError(400, "User already exists")
|
||||
}
|
||||
if (turnstileResponse.success !== true) {
|
||||
throw new OperationError(400, "Invalid captcha token")
|
||||
}
|
||||
|
||||
// check if the email is already in use
|
||||
const existentEmail = await User
|
||||
.findOne({ email: email })
|
||||
.select("+email")
|
||||
await Account.usernameMeetPolicy(username)
|
||||
|
||||
if (existentEmail) {
|
||||
throw new OperationError(400, "Email already in use")
|
||||
}
|
||||
// check if username is already taken
|
||||
const existentUser = await User.findOne({ username: username })
|
||||
|
||||
await Account.passwordMeetPolicy(password)
|
||||
if (existentUser) {
|
||||
throw new OperationError(400, "User already exists")
|
||||
}
|
||||
|
||||
// hash the password
|
||||
const hash = bcrypt.hashSync(password, parseInt(process.env.BCRYPT_ROUNDS ?? 3))
|
||||
// check if the email is already in use
|
||||
const existentEmail = await User.findOne({ email: email }).select("+email")
|
||||
|
||||
let user = new User({
|
||||
username: username,
|
||||
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,
|
||||
})
|
||||
if (existentEmail) {
|
||||
throw new OperationError(400, "Email already in use")
|
||||
}
|
||||
|
||||
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 {
|
||||
activation_required: true,
|
||||
user: user,
|
||||
}
|
||||
}
|
||||
let user = new User({
|
||||
username: username,
|
||||
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