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

View File

@ -40,9 +40,7 @@ export const usePlayerStateContext = (updater) => {
app.cores.player.eventBus().on("player.state.update", handleStateChange) app.cores.player.eventBus().on("player.state.update", handleStateChange)
return () => { return () => {
app.cores.player app.cores.player.eventBus().off("player.state.update", handleStateChange)
.eventBus()
.off("player.state.update", handleStateChange)
} }
}, []) }, [])
@ -56,9 +54,7 @@ export class WithPlayerContext extends React.Component {
events = { events = {
"player.state.update": async (state) => { "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") const transitionLayer = document.getElementById("transitionLayer")
if (!transitionLayer) { if (!transitionLayer) {
console.warn( console.warn("transitionLayer not found, no animation will be played")
"transitionLayer not found, no animation will be played",
)
return false return false
} }
@ -42,9 +40,7 @@ export default class Layout extends React.PureComponent {
const transitionLayer = document.getElementById("transitionLayer") const transitionLayer = document.getElementById("transitionLayer")
if (!transitionLayer) { if (!transitionLayer) {
console.warn( console.warn("transitionLayer not found, no animation will be played")
"transitionLayer not found, no animation will be played",
)
return false return false
} }
@ -114,10 +110,7 @@ export default class Layout extends React.PureComponent {
) )
}, },
toggleMobileStyle: (to) => { toggleMobileStyle: (to) => {
return this.layoutInterface.toggleRootContainerClassname( return this.layoutInterface.toggleRootContainerClassname("mobile", to)
"mobile",
to,
)
}, },
toggleReducedAnimations: (to) => { toggleReducedAnimations: (to) => {
return this.layoutInterface.toggleRootContainerClassname( return this.layoutInterface.toggleRootContainerClassname(
@ -150,17 +143,14 @@ export default class Layout extends React.PureComponent {
) )
}, },
toggleRootContainerClassname: (classname, to) => { toggleRootContainerClassname: (classname, to) => {
const root = document.documentElement const root = document.documentElement
if (!root) { if (!root) {
console.error("root not found") console.error("root not found")
return false return false
} }
to = to = typeof to === "boolean" ? to : !root.classList.contains(classname)
typeof to === "boolean"
? to
: !root.classList.contains(classname)
if (root.classList.contains(classname) === to) { if (root.classList.contains(classname) === to) {
// ignore // ignore
@ -208,10 +198,9 @@ export default class Layout extends React.PureComponent {
if (this.state.renderError) { if (this.state.renderError) {
if (this.props.staticRenders?.RenderError) { if (this.props.staticRenders?.RenderError) {
return React.createElement( return React.createElement(this.props.staticRenders?.RenderError, {
this.props.staticRenders?.RenderError, error: this.state.renderError,
{ error: this.state.renderError }, })
)
} }
return JSON.stringify(this.state.renderError) return JSON.stringify(this.state.renderError)

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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
}