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,6 +1,6 @@
{
"name": "@comty/app",
"version": "1.44.0@alpha",
"version": "1.44.1@alpha",
"license": "ComtyLicense",
"main": "electron/main",
"type": "module",
@ -34,7 +34,7 @@
"axios": "^1.7.7",
"bear-react-carousel": "^4.0.10-alpha.0",
"classnames": "2.3.1",
"comty.js": "^0.68.0",
"comty.js": "^0.68.1",
"d3": "^7.9.0",
"dashjs": "^5.0.3",
"dompurify": "^3.0.0",
@ -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",

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

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(
@ -157,10 +150,7 @@ export default class Layout extends React.PureComponent {
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

@ -19,7 +19,9 @@ const EmailStepComponent = (props) => {
}
const submit = () => {
if (!isValid()) return
if (!isValid()) {
return false
}
props.onPressEnter()
}
@ -40,11 +42,14 @@ const EmailStepComponent = (props) => {
setLoading(true)
setValidFormat(checkIfIsEmail(email))
const isEmailValid = checkIfIsEmail(email)
setValidFormat(isEmailValid)
// check if email is available
const timer = setTimeout(async () => {
if (!validFormat) return
if (!isEmailValid) {
return false
}
const request = await AuthModel.availability({ email }).catch((error) => {
antd.message.error(`Cannot check email availability: ${error.message}`)
@ -69,22 +74,33 @@ const EmailStepComponent = (props) => {
return () => clearTimeout(timer)
}, [email])
return <div className="register_form_step_content">
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")}
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.",
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,32 +1,31 @@
import React from "react"
import * as antd from "antd"
import MarkdownReader from "@components/MarkdownReader"
import config from "@config"
const FrameStyle = {
"width": "60vw",
width: "60vw",
"max-width": "60vw",
"height": "90vh",
height: "90vh",
"max-height": "90vh",
"overflow": "overlay",
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
labels.push(`the ${LegalDocumentsDecorators[key] ?? `document (${key})`} ${!isLast ? "and" : ""}`)
labels.push(
`the ${LegalDocumentsDecorators[key] ?? `document (${key})`} ${!isLast ? "and" : ""}`,
)
})
return labels.join(" ")
@ -35,29 +34,30 @@ function composeConfirmationCheckboxLabel(documents) {
const TermsOfServiceStepComponent = (props) => {
const legalDocuments = Object.entries(config.legal)
return <div className="register_form_step_content">
{
Object.entries(config.legal).map(([key, value]) => {
return (
<div className="register_form_step_content">
{Object.entries(config.legal).map(([key, value]) => {
if (!value) {
return null
}
return <antd.Button
return (
<antd.Button
key={key}
onClick={() => {
app.layout.modal.open(key, MarkdownReader, {
includeCloseButton: true,
frameContentStyle: FrameStyle,
props: {
url: value
}
url: value,
},
})
}}
>
Read {LegalDocumentsDecorators[key] ?? `document (${key})`}
</antd.Button>
})
}
)
})}
<antd.Checkbox
defaultChecked={props.currentValue}
@ -68,6 +68,7 @@ const TermsOfServiceStepComponent = (props) => {
{composeConfirmationCheckboxLabel(legalDocuments)}
</antd.Checkbox>
</div>
)
}
export default {

View File

@ -6,9 +6,14 @@ import { Icons } from "@components/Icons"
const MainSelector = (props) => {
return (
<>
{!app.isMobile && (
<div className="content_header">
<img src={config.logo.alt} className="logo" />
<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,16 +1,24 @@
.loginPage {
.login-page {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100vh;
height: 100dvh;
.wallpaper {
position: absolute;
position: fixed;
top: 0;
left: 0;
z-index: -1;
width: 100%;
height: 100vh;
height: 100dvh;
@ -19,4 +27,63 @@
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,32 +1,52 @@
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)
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.")
throw new OperationError(
400,
"You must accept the terms of service in order to create an account.",
)
}
if (!captcha) {
throw new OperationError(400, "Captcha token is required")
}
const turnstileResponse = await verifyTurnstileToken(captcha)
if (turnstileResponse.success !== true) {
throw new OperationError(400, "Invalid captcha token")
}
await Account.usernameMeetPolicy(username)
// check if username is already taken
const existentUser = await User
.findOne({ username: username })
const existentUser = await User.findOne({ username: username })
if (existentUser) {
throw new OperationError(400, "User already exists")
}
// check if the email is already in use
const existentEmail = await User
.findOne({ email: email })
.select("+email")
const existentEmail = await User.findOne({ email: email }).select("+email")
if (existentEmail) {
throw new OperationError(400, "Email already in use")
@ -35,14 +55,18 @@ export default async (payload) => {
await Account.passwordMeetPolicy(password)
// hash the password
const hash = bcrypt.hashSync(password, parseInt(process.env.BCRYPT_ROUNDS ?? 3))
const hash = bcrypt.hashSync(
password,
parseInt(process.env.BCRYPT_ROUNDS ?? 3),
)
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}`,
avatar:
avatar ?? `https://api.dicebear.com/7.x/thumbs/svg?seed=${username}`,
roles: roles,
created_at: new Date().getTime(),
accept_tos: accept_tos,

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
}