Merge pull request #169 from ragestudio/dev

This commit is contained in:
srgooglo 2025-07-07 17:35:25 +02:00 committed by GitHub
commit d279264277
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 510 additions and 338 deletions

@ -1 +1 @@
Subproject commit 0dd24fee4a231dbb41d5b14ff92b67d8f14cb5a2
Subproject commit fb4d666576a937eaad4ea69f9e5a13778f06b3f5

View File

@ -1,90 +1,91 @@
{
"name": "@comty/app",
"version": "1.44.0@alpha",
"license": "ComtyLicense",
"main": "electron/main",
"type": "module",
"author": "RageStudio",
"description": "A prototype of a social network.",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"release": "node ./scripts/release.js",
"postinstall": "./scripts/postinstall.sh",
"eslint": "eslint"
},
"dependencies": {
"@ant-design/icons": "^5.4.0",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^7.0.2",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.13.0",
"@emotion/styled": "^11.13.0",
"@ffmpeg/ffmpeg": "^0.12.10",
"@ffmpeg/util": "^0.12.1",
"@mui/material": "^5.11.9",
"@ragestudio/cordova-nfc": "^1.2.0",
"@ragestudio/vessel": "^0.23.1",
"@sentry/browser": "^7.64.0",
"@tauri-apps/api": "^1.5.4",
"@tsmx/human-readable": "^1.0.7",
"antd": "^5.20.6",
"axios": "^1.7.7",
"bear-react-carousel": "^4.0.10-alpha.0",
"classnames": "2.3.1",
"comty.js": "^0.68.0",
"d3": "^7.9.0",
"dashjs": "^5.0.3",
"dompurify": "^3.0.0",
"fast-average-color": "^9.2.0",
"fuse.js": "6.5.3",
"hls.js": "^1.5.17",
"howler": "2.2.3",
"i18next": "21.6.6",
"js-cookie": "3.0.1",
"jsmediatags": "^3.9.7",
"lottie-react": "^2.4.0",
"luxon": "^3.0.4",
"mime": "^3.0.0",
"moment": "2.29.4",
"motion": "^12.4.2",
"music-metadata": "^11.2.1",
"plyr": "^3.7.8",
"prop-types": "^15.8.1",
"qs": "^6.14.0",
"react": "18.3.1",
"react-beautiful-dnd": "^13.1.1",
"react-color": "2.19.3",
"react-countup": "^6.4.1",
"react-dom": "18.3.1",
"react-fast-marquee": "^1.3.5",
"react-i18next": "11.15.3",
"react-icons": "^5.4.0",
"react-lazy-load-image-component": "^1.5.4",
"react-markdown": "^8.0.3",
"react-modal-image": "^2.6.0",
"react-player": "^2.16.0",
"react-rnd": "^10.4.14",
"react-transition-group": "^4.4.5",
"react-useanimations": "^2.10.0",
"remark-gfm": "^3.0.1",
"rxjs": "^7.5.5",
"store": "^2.0.12",
"swapy": "^1.0.5",
"ua-parser-js": "^1.0.36",
"vaul": "^1.1.2",
"vite": "^6.2.6"
},
"devDependencies": {
"@eslint/js": "^9.26.0",
"@octokit/rest": "^21.1.1",
"7zip-min": "1.4.3",
"dotenv": "16.0.3",
"eslint": "^9.26.0",
"eslint-plugin-react": "^7.37.5",
"form-data": "^4.0.0",
"globals": "^16.1.0"
}
"name": "@comty/app",
"version": "1.44.1@alpha",
"license": "ComtyLicense",
"main": "electron/main",
"type": "module",
"author": "RageStudio",
"description": "A prototype of a social network.",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"release": "node ./scripts/release.js",
"postinstall": "./scripts/postinstall.sh",
"eslint": "eslint"
},
"dependencies": {
"@ant-design/icons": "^5.4.0",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^7.0.2",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.13.0",
"@emotion/styled": "^11.13.0",
"@ffmpeg/ffmpeg": "^0.12.10",
"@ffmpeg/util": "^0.12.1",
"@mui/material": "^5.11.9",
"@ragestudio/cordova-nfc": "^1.2.0",
"@ragestudio/vessel": "^0.23.1",
"@sentry/browser": "^7.64.0",
"@tauri-apps/api": "^1.5.4",
"@tsmx/human-readable": "^1.0.7",
"antd": "^5.20.6",
"axios": "^1.7.7",
"bear-react-carousel": "^4.0.10-alpha.0",
"classnames": "2.3.1",
"comty.js": "^0.68.1",
"d3": "^7.9.0",
"dashjs": "^5.0.3",
"dompurify": "^3.0.0",
"fast-average-color": "^9.2.0",
"fuse.js": "6.5.3",
"hls.js": "^1.5.17",
"howler": "2.2.3",
"i18next": "21.6.6",
"js-cookie": "3.0.1",
"jsmediatags": "^3.9.7",
"lottie-react": "^2.4.0",
"luxon": "^3.0.4",
"mime": "^3.0.0",
"moment": "2.29.4",
"motion": "^12.4.2",
"music-metadata": "^11.2.1",
"plyr": "^3.7.8",
"prop-types": "^15.8.1",
"qs": "^6.14.0",
"react": "18.3.1",
"react-beautiful-dnd": "^13.1.1",
"react-color": "2.19.3",
"react-countup": "^6.4.1",
"react-dom": "18.3.1",
"react-fast-marquee": "^1.3.5",
"react-i18next": "11.15.3",
"react-icons": "^5.4.0",
"react-lazy-load-image-component": "^1.5.4",
"react-markdown": "^8.0.3",
"react-modal-image": "^2.6.0",
"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",
"store": "^2.0.12",
"swapy": "^1.0.5",
"ua-parser-js": "^1.0.36",
"vaul": "^1.1.2",
"vite": "^6.2.6"
},
"devDependencies": {
"@eslint/js": "^9.26.0",
"@octokit/rest": "^21.1.1",
"7zip-min": "1.4.3",
"dotenv": "16.0.3",
"eslint": "^9.26.0",
"eslint-plugin-react": "^7.37.5",
"form-data": "^4.0.0",
"globals": "^16.1.0"
}
}

View File

@ -1,3 +1,11 @@
&.mobile {
.player-seek_bar {
.slider-container {
height: 10px;
}
}
}
.player-seek_bar {
z-index: 330;

View File

@ -40,9 +40,7 @@ export const usePlayerStateContext = (updater) => {
app.cores.player.eventBus().on("player.state.update", handleStateChange)
return () => {
app.cores.player
.eventBus()
.off("player.state.update", handleStateChange)
app.cores.player.eventBus().off("player.state.update", handleStateChange)
}
}, [])
@ -56,9 +54,7 @@ export class WithPlayerContext extends React.Component {
events = {
"player.state.update": async (state) => {
if (state !== this.state) {
this.setState(state)
}
this.setState(state)
},
}

View File

@ -23,9 +23,7 @@ export default class Layout extends React.PureComponent {
const transitionLayer = document.getElementById("transitionLayer")
if (!transitionLayer) {
console.warn(
"transitionLayer not found, no animation will be played",
)
console.warn("transitionLayer not found, no animation will be played")
return false
}
@ -42,9 +40,7 @@ export default class Layout extends React.PureComponent {
const transitionLayer = document.getElementById("transitionLayer")
if (!transitionLayer) {
console.warn(
"transitionLayer not found, no animation will be played",
)
console.warn("transitionLayer not found, no animation will be played")
return false
}
@ -114,10 +110,7 @@ export default class Layout extends React.PureComponent {
)
},
toggleMobileStyle: (to) => {
return this.layoutInterface.toggleRootContainerClassname(
"mobile",
to,
)
return this.layoutInterface.toggleRootContainerClassname("mobile", to)
},
toggleReducedAnimations: (to) => {
return this.layoutInterface.toggleRootContainerClassname(
@ -150,17 +143,14 @@ export default class Layout extends React.PureComponent {
)
},
toggleRootContainerClassname: (classname, to) => {
const root = document.documentElement
const root = document.documentElement
if (!root) {
console.error("root not found")
return false
}
to =
typeof to === "boolean"
? to
: !root.classList.contains(classname)
to = typeof to === "boolean" ? to : !root.classList.contains(classname)
if (root.classList.contains(classname) === to) {
// ignore
@ -208,10 +198,9 @@ export default class Layout extends React.PureComponent {
if (this.state.renderError) {
if (this.props.staticRenders?.RenderError) {
return React.createElement(
this.props.staticRenders?.RenderError,
{ error: this.state.renderError },
)
return React.createElement(this.props.staticRenders?.RenderError, {
error: this.state.renderError,
})
}
return JSON.stringify(this.state.renderError)

View File

@ -159,7 +159,10 @@ const AccountButton = React.forwardRef((props, ref) => {
>
<div className="icon">
{user ? (
<antd.Avatar shape="square" src={app.userData.avatar} />
<antd.Avatar
shape="square"
src={app.userData.avatar}
/>
) : (
createIconRender("FiLogin")
)}
@ -168,6 +171,8 @@ const AccountButton = React.forwardRef((props, ref) => {
)
})
AccountButton.displayName = "AccountButton"
export class BottomBar extends React.Component {
static contextType = Context
@ -366,9 +371,7 @@ export class BottomBar extends React.Component {
}
const heightValue = Number(
app.cores.style
.getDefaultVar("bottom-bar-height")
.replace("px", ""),
app.cores.style.getDefaultVar("bottom-bar-height").replace("px", ""),
)
return (
@ -409,10 +412,7 @@ export class BottomBar extends React.Component {
<div
key="creator"
id="creator"
className={classnames(
"item",
"primary",
)}
className={classnames("item", "primary")}
onClick={openCreator}
>
<div className="icon">
@ -441,9 +441,7 @@ export class BottomBar extends React.Component {
})
}}
>
<div className="icon">
{createIconRender("FiHome")}
</div>
<div className="icon">{createIconRender("FiHome")}</div>
</div>
<div
@ -452,9 +450,7 @@ export class BottomBar extends React.Component {
className="item"
onClick={app.controls.openSearcher}
>
<div className="icon">
{createIconRender("FiSearch")}
</div>
<div className="icon">{createIconRender("FiSearch")}</div>
</div>
<AccountButton ref={this.accountBtnRef} />

View File

@ -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

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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 && (

View File

@ -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

View File

@ -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;
}
}
}
}
}

View File

@ -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(() => {

View File

@ -35,7 +35,6 @@ export default class API extends Server {
onExit() {
this.queuesManager.cleanUp()
console.log("Jijija")
}
}

View File

@ -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,
}
}

View 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
}