merge from local

This commit is contained in:
SrGooglo 2024-03-11 20:28:19 +00:00
parent bbb94d195e
commit 65d75ef939
143 changed files with 636 additions and 4404 deletions

1
comty.js Submodule

@ -0,0 +1 @@
Subproject commit 8acb3f008477bbb782eca1c7f747b494a293e57b

@ -1 +1 @@
Subproject commit c011f2353f8db14a2ed287015d108c2620098a84
Subproject commit 6d553830ab4661ffab952253d77ccb0bfc1363d8

View File

@ -18,7 +18,7 @@ const aliases = {
layouts: path.join(__dirname, "src/layouts"),
hooks: path.join(__dirname, "src/hooks"),
classes: path.join(__dirname, "src/classes"),
"comty.js": path.join(__dirname, "../", "comty.js", "src"),
"comty.js": path.join(__dirname, "../../", "comty.js", "src"),
models: path.join(__dirname, "../comty.js/src/models"),
}

View File

@ -77,6 +77,11 @@ export default [
useLayout: "default",
public: true
},
{
path: "/apr/*",
useLayout: "minimal",
public: true
},
// THIS MUST BE THE LAST ROUTE
{
path: "/",

View File

@ -143,9 +143,7 @@ class ComtyApp extends React.Component {
document.getElementById("root").classList.add("electron")
}
console.log(import.meta.env)
if (import.meta.env.VITE_SENTRY_DSN) {
if (import.meta.env.VITE_SENTRY_DSN && import.meta.env.PROD) {
console.log(`Initializing Sentry...`)
Sentry.init({

View File

@ -47,7 +47,7 @@ export default (props) => {
console.log(`Loading Followers for [${props.user_id}]...`)
const followers = await FollowsModel.getFollowers(props.user_id).catch((err) => {
const followers = await FollowsModel.getFollowers(props.user_id, true).catch((err) => {
console.error(err)
app.message.error("Failed to fetch followers")

View File

@ -40,30 +40,45 @@ export default class Login extends React.Component {
loginInputs: {},
error: null,
phase: 0,
mfa_required: null
}
formRef = React.createRef()
handleFinish = async () => {
this.setState({
mfa_required: false,
})
const payload = {
username: this.state.loginInputs.username,
password: this.state.loginInputs.password,
mfa_code: this.state.loginInputs.mfa_code,
}
this.clearError()
this.toggleLoading(true)
await AuthModel.login(payload, () => this.onDone()).catch((error) => {
await AuthModel.login(payload, this.onDone).catch((error) => {
console.error(error, error.response)
this.toggleLoading(false)
this.onError(error.response.data.message)
this.onError(error.response.data.error)
return false
})
}
onDone = async () => {
onDone = async ({ mfa_required } = {}) => {
if (mfa_required) {
this.setState({
loading: false,
mfa_required: mfa_required,
})
return false
}
if (typeof this.props.close === "function") {
await this.props.close({
unlock: true
@ -77,6 +92,18 @@ export default class Login extends React.Component {
return true
}
onClickForgotPassword = () => {
if (this.props.locked) {
this.props.unlock()
}
if (typeof this.props.close === "function") {
this.props.close()
}
app.location.push("/apr")
}
onClickRegister = () => {
if (this.props.locked) {
this.props.unlock()
@ -238,6 +265,34 @@ export default class Login extends React.Component {
onPressEnter={this.nextStep}
/>
</antd.Form.Item>
<antd.Form.Item
name="mfa_code"
className={classnames(
"field",
{
["hidden"]: !this.state.mfa_required,
}
)}
>
<span><Icons.Lock /> Verification Code</span>
{
this.state.mfa_required && <>
<p>We send a verification code to [{this.state.mfa_required.sended_to}]</p>
<p>
Didn't receive the code? <a onClick={this.handleFinish}>Resend</a>
</p>
</>
}
<antd.Input
placeholder="4 Digit MFA code"
onChange={(e) => this.onUpdateInput("mfa_code", e.target.value)}
onPressEnter={this.nextStep}
/>
</antd.Form.Item>
</antd.Form>
<div className="component-row">
@ -262,6 +317,10 @@ export default class Login extends React.Component {
{this.state.error}
</div>
<div className="field" onClick={this.onClickForgotPassword}>
<a>Forgot your password?</a>
</div>
<div className="field" onClick={this.onClickRegister}>
<a>You need a account?</a>
</div>

View File

@ -29,48 +29,60 @@ export default class APICore extends Core {
}
listenEvent(key, handler, instance) {
this.instance.wsInstances[instance ?? "default"].on(key, handler)
if (!this.instance.wsInstances[instance ?? "default"]) {
console.error(`[API] Websocket instance ${instance} not found`)
return false
}
return this.instance.wsInstances[instance ?? "default"].on(key, handler)
}
unlistenEvent(key, handler, instance) {
this.instance.wsInstances[instance ?? "default"].off(key, handler)
if (!this.instance.wsInstances[instance ?? "default"]) {
console.error(`[API] Websocket instance ${instance} not found`)
return false
}
return this.instance.wsInstances[instance ?? "default"].off(key, handler)
}
pendingPingsFromInstance = {}
createPingIntervals() {
Object.keys(this.instance.wsInstances).forEach((instance) => {
this.console.debug(`[API] Creating ping interval for ${instance}`)
// Object.keys(this.instance.wsInstances).forEach((instance) => {
// this.console.debug(`[API] Creating ping interval for ${instance}`)
if (this.instance.wsInstances[instance].pingInterval) {
clearInterval(this.instance.wsInstances[instance].pingInterval)
}
// if (this.instance.wsInstances[instance].pingInterval) {
// clearInterval(this.instance.wsInstances[instance].pingInterval)
// }
this.instance.wsInstances[instance].pingInterval = setInterval(() => {
if (this.instance.wsInstances[instance].pendingPingTry && this.instance.wsInstances[instance].pendingPingTry > 3) {
this.console.debug(`[API] Ping timeout for ${instance}`)
// this.instance.wsInstances[instance].pingInterval = setInterval(() => {
// if (this.instance.wsInstances[instance].pendingPingTry && this.instance.wsInstances[instance].pendingPingTry > 3) {
// this.console.debug(`[API] Ping timeout for ${instance}`)
return clearInterval(this.instance.wsInstances[instance].pingInterval)
}
// return clearInterval(this.instance.wsInstances[instance].pingInterval)
// }
const timeStart = Date.now()
// const timeStart = Date.now()
//this.console.debug(`[API] Ping ${instance}`, this.instance.wsInstances[instance].pendingPingTry)
// //this.console.debug(`[API] Ping ${instance}`, this.instance.wsInstances[instance].pendingPingTry)
this.instance.wsInstances[instance].emit("ping", () => {
this.instance.wsInstances[instance].latency = Date.now() - timeStart
// this.instance.wsInstances[instance].emit("ping", () => {
// this.instance.wsInstances[instance].latency = Date.now() - timeStart
this.instance.wsInstances[instance].pendingPingTry = 0
})
// this.instance.wsInstances[instance].pendingPingTry = 0
// })
this.instance.wsInstances[instance].pendingPingTry = this.instance.wsInstances[instance].pendingPingTry ? this.instance.wsInstances[instance].pendingPingTry + 1 : 1
}, 5000)
// this.instance.wsInstances[instance].pendingPingTry = this.instance.wsInstances[instance].pendingPingTry ? this.instance.wsInstances[instance].pendingPingTry + 1 : 1
// }, 5000)
// clear interval on close
this.instance.wsInstances[instance].on("close", () => {
clearInterval(this.instance.wsInstances[instance].pingInterval)
})
})
// // clear interval on close
// this.instance.wsInstances[instance].on("close", () => {
// clearInterval(this.instance.wsInstances[instance].pingInterval)
// })
// })
}
async onInitialize() {
@ -92,8 +104,8 @@ export default class APICore extends Core {
// make a basic request to check if the API is available
await this.instance.instances["default"]({
method: "GET",
url: "/ping",
method: "head",
url: "/",
}).catch((error) => {
this.console.error("[API] Ping error", error)
@ -105,7 +117,7 @@ export default class APICore extends Core {
this.console.debug("[API] Attached to", this.instance)
this.createPingIntervals()
//this.createPingIntervals()
return this.instance
}

View File

@ -56,10 +56,10 @@ export default class Account extends React.Component {
requestedUser: null,
user: null,
followers: [],
isSelf: false,
isFollowed: false,
followersCount: 0,
following: false,
tabActiveKey: "posts",
@ -87,8 +87,7 @@ export default class Account extends React.Component {
let isSelf = false
let user = null
let isFollowed = false
let followers = []
let followersCount = 0
if (requestedUser != null) {
if (token.username === requestedUser) {
@ -113,33 +112,24 @@ export default class Account extends React.Component {
console.log(`Loaded User [${user.username}] >`, user)
if (!isSelf) {
const followedResult = await FollowsModel.imFollowing(user._id).catch(() => false)
if (followedResult) {
isFollowed = followedResult.isFollowed
}
}
const followersResult = await FollowsModel.getFollowers(user._id).catch(() => false)
if (followersResult) {
followers = followersResult
followersCount = followersResult.count
}
}
await this.setState({
isSelf,
user,
requestedUser,
isFollowed,
followers,
user,
following: user.following,
followersCount: followersCount,
})
}
onPostListTopVisibility = (to) => {
console.log("onPostListTopVisibility", to)
if (to) {
this.profileRef.current.classList.remove("topHidden")
} else {
@ -149,7 +139,7 @@ export default class Account extends React.Component {
onClickFollow = async () => {
const result = await FollowsModel.toggleFollow({
username: this.state.requestedUser,
user_id: this.state.user._id,
}).catch((error) => {
console.error(error)
antd.message.error(error.message)
@ -158,8 +148,8 @@ export default class Account extends React.Component {
})
await this.setState({
isFollowed: result.following,
followers: result.followers,
following: result.following,
followersCount: result.count,
})
}
@ -240,9 +230,9 @@ export default class Account extends React.Component {
ref={this.actionsRef}
>
<FollowButton
count={this.state.followers.length}
count={this.state.followersCount}
onClick={this.onClickFollow}
followed={this.state.isFollowed}
followed={this.state.following}
self={this.state.isSelf}
/>
</div>

View File

@ -0,0 +1,11 @@
import React from "react"
import "./index.less"
const AccountPasswordRecovery = () => {
return <div className="password_recover">
<h1>Account Password Recovery</h1>
</div>
}
export default AccountPasswordRecovery

View File

@ -0,0 +1,3 @@
.password_recover {
padding: 20px;
}

View File

@ -46,6 +46,7 @@
flex-direction: row;
width: 55vw;
min-width: 700px;
max-width: 800px;
overflow: hidden;
@ -83,6 +84,8 @@
width: 100%;
min-width: 420px;
padding: 40px;
.content_header {

View File

@ -1,26 +0,0 @@
{
"name": "comty.js",
"version": "0.60.3",
"main": "./dist/index.js",
"author": "RageStudio <support@ragestudio.net>",
"scripts": {
"build": "hermes build"
},
"files": [
"dist"
],
"license": "MIT",
"dependencies": {
"@foxify/events": "^2.1.0",
"axios": "^1.4.0",
"js-cookie": "^3.0.5",
"jsonwebtoken": "^9.0.0",
"jwt-decode": "^3.1.2",
"luxon": "^3.3.0",
"socket.io-client": "^4.6.1"
},
"devDependencies": {
"@ragestudio/hermes": "^0.1.0",
"corenode": "^0.28.26"
}
}

View File

@ -1,58 +0,0 @@
import request from "./request"
export default async () => {
const timings = {}
const promises = [
new Promise(async (resolve) => {
const start = Date.now()
const failTimeout = setTimeout(() => {
timings.http = "failed"
resolve()
}, 10000)
request({
method: "GET",
url: "/ping",
})
.then(() => {
// set http timing in ms
timings.http = Date.now() - start
failTimeout && clearTimeout(failTimeout)
resolve()
})
.catch(() => {
timings.http = "failed"
resolve()
})
}),
new Promise((resolve) => {
const start = Date.now()
const failTimeout = setTimeout(() => {
timings.ws = "failed"
resolve()
}, 10000)
__comty_shared_state.wsInstances["default"].on("pong", () => {
timings.ws = Date.now() - start
failTimeout && clearTimeout(failTimeout)
resolve()
})
__comty_shared_state.wsInstances["default"].emit("ping")
})
]
await Promise.all(promises)
return timings
}

View File

@ -1,51 +0,0 @@
import handleBeforeRequest from "../helpers/handleBeforeRequest"
import handleAfterRequest from "../helpers/handleAfterRequest"
export default async (
request = {
method: "GET",
},
...args
) => {
const instance = request.instance ?? __comty_shared_state.instances.default
if (!instance) {
throw new Error("No instance provided")
}
// handle before request
await handleBeforeRequest(request)
if (typeof request === "string") {
request = {
url: request,
}
}
if (typeof request.headers !== "object") {
request.headers = {}
}
let result = null
const makeRequest = async () => {
const _result = await instance(request, ...args)
.catch((error) => {
return error
})
result = _result
}
await makeRequest()
// handle after request
await handleAfterRequest(result, makeRequest)
// if error, throw it
if (result instanceof Error) {
throw result
}
return result
}

View File

@ -1,34 +0,0 @@
import handleRegenerationEvent from "./handleRegenerationEvent"
export default async (data, callback) => {
// handle 401, 403 responses
if (data instanceof Error) {
if (data.code && (data.code === "ECONNABORTED" || data.code === "ERR_NETWORK")) {
console.error(`Request aborted or network error, ignoring`)
return false
}
if (data.response.status === 401) {
// check if the server issue a refresh token on data
if (data.response.data.refreshToken) {
console.log(`Session expired, but the server issued a refresh token, handling regeneration event`)
// handle regeneration event
await handleRegenerationEvent(data.response.data.refreshToken)
return await callback()
}
// check if route is from "session" namespace
if (data.config.url.includes("/session")) {
return __comty_shared_state.eventBus.emit("session.invalid", "Session expired, but the server did not issue a refresh token")
}
}
if (data.response.status === 403) {
if (data.config.url.includes("/session")) {
return __comty_shared_state.eventBus.emit("session.invalid", "Session not valid or not existent")
}
}
}
}

View File

@ -1,13 +0,0 @@
export default async (request) => {
if (__comty_shared_state.onExpiredExceptionEvent) {
if (__comty_shared_state.excludedExpiredExceptionURL.includes(request.url)) return
await new Promise((resolve) => {
__comty_shared_state.eventBus.once("session.regenerated", () => {
console.log(`Session has been regenerated, retrying request`)
resolve()
})
})
}
}

View File

@ -1,43 +0,0 @@
import SessionModel from "../models/session"
import request from "../handlers/request"
import { reconnectWebsockets } from "../"
export default async (refreshToken) => {
__comty_shared_state.eventBus.emit("session.expiredExceptionEvent", refreshToken)
__comty_shared_state.onExpiredExceptionEvent = true
const expiredToken = await SessionModel.token
// send request to regenerate token
const response = await request({
method: "POST",
url: "/session/regenerate",
data: {
expiredToken: expiredToken,
refreshToken,
}
}).catch((error) => {
console.error(`Failed to regenerate token: ${error.message}`)
return false
})
if (!response) {
return __comty_shared_state.eventBus.emit("session.invalid", "Failed to regenerate token")
}
if (!response.data?.token) {
return __comty_shared_state.eventBus.emit("session.invalid", "Failed to regenerate token, invalid server response.")
}
// set new token
SessionModel.token = response.data.token
__comty_shared_state.onExpiredExceptionEvent = false
// emit event
__comty_shared_state.eventBus.emit("session.regenerated")
// reconnect websockets
reconnectWebsockets()
}

View File

@ -1,25 +0,0 @@
export default class Settings {
static get = (key) => {
if (typeof window === "undefined") {
return null
}
return window?.app?.cores?.settings.get(key)
}
static set = (key, value) => {
if (typeof window === "undefined") {
return null
}
return window?.app?.cores?.settings.set(key, value)
}
static is = (key) => {
if (typeof window === "undefined") {
return null
}
return window?.app?.cores?.settings.is(key)
}
}

View File

@ -1,31 +0,0 @@
import jscookies from "js-cookie"
class InternalStorage {
#storage = {}
get(key) {
// get value from storage
return this.#storage[key]
}
set(key, value) {
// storage securely in memory
return this.#storage[key] = value
}
}
export default class Storage {
static get engine() {
// check if is running in browser, if is import js-cookie
// else use in-memory safe storage
if (typeof window !== "undefined") {
return jscookies
}
if (!globalThis.__comty_shared_state["_internal_storage"]) {
globalThis.__comty_shared_state["_internal_storage"] = new InternalStorage()
}
return globalThis.__comty_shared_state["_internal_storage"]
}
}

View File

@ -1,32 +0,0 @@
import React from "react"
export default (method, ...args) => {
if (typeof method !== "function") {
throw new Error("useRequest: method must be a function")
}
const [loading, setLoading] = React.useState(true)
const [result, setResult] = React.useState(null)
const [error, setError] = React.useState(null)
const makeRequest = (...newArgs) => {
method(...newArgs)
.then((data) => {
setResult(data)
setLoading(false)
})
.catch((err) => {
setError(err)
setLoading(false)
})
}
React.useEffect(() => {
makeRequest(...args)
}, [])
return [loading, result, error, (...newArgs) => {
setLoading(true)
makeRequest(...newArgs)
}]
}

View File

@ -1,201 +0,0 @@
import pkg from "../package.json"
import EventEmitter from "@foxify/events"
import axios from "axios"
import { io } from "socket.io-client"
import remotes from "./remotes"
//import request from "./handlers/request"
import Storage from "./helpers/withStorage"
import SessionModel from "./models/session"
import { createHandlers } from "./models"
globalThis.isServerMode = typeof window === "undefined" && typeof global !== "undefined"
if (globalThis.isServerMode) {
const { Buffer } = require("buffer")
globalThis.b64Decode = (data) => {
return Buffer.from(data, "base64").toString("utf-8")
}
globalThis.b64Encode = (data) => {
return Buffer.from(data, "utf-8").toString("base64")
}
}
export async function createWebsockets() {
const instances = globalThis.__comty_shared_state.wsInstances
for (let [key, instance] of Object.entries(instances)) {
if (instance.connected) {
// disconnect first
instance.disconnect()
}
// remove current listeners
instance.removeAllListeners()
delete globalThis.__comty_shared_state.wsInstances[key]
}
for (let [key, remote] of Object.entries(remotes)) {
if (!remote.hasWebsocket) {
continue
}
let opts = {
transports: ["websocket"],
autoConnect: remote.autoConnect ?? true,
...remote.wsParams ?? {},
}
if (remote.noAuth !== true) {
opts.auth = {
token: SessionModel.token,
}
}
globalThis.__comty_shared_state.wsInstances[key] = io(remote.wsOrigin ?? remote.origin, opts)
}
// regsister events
for (let [key, instance] of Object.entries(instances)) {
instance.on("connect", () => {
console.debug(`[WS-API][${key}] Connected`)
if (remotes[key].useClassicAuth && remotes[key].noAuth !== true) {
// try to auth
instance.emit("auth", {
token: SessionModel.token,
})
}
globalThis.__comty_shared_state.eventBus.emit(`${key}:connected`)
})
instance.on("disconnect", () => {
console.debug(`[WS-API][${key}] Disconnected`)
globalThis.__comty_shared_state.eventBus.emit(`${key}:disconnected`)
})
instance.on("error", (error) => {
console.error(`[WS-API][${key}] Error`, error)
globalThis.__comty_shared_state.eventBus.emit(`${key}:error`, error)
})
instance.onAny((event, ...args) => {
console.debug(`[WS-API][${key}] Event (${event})`, ...args)
globalThis.__comty_shared_state.eventBus.emit(`${key}:${event}`, ...args)
})
}
}
export async function disconnectWebsockets() {
const instances = globalThis.__comty_shared_state.wsInstances
for (let [key, instance] of Object.entries(instances)) {
if (instance.connected) {
instance.disconnect()
}
}
}
export async function reconnectWebsockets({ force = false } = {}) {
const instances = globalThis.__comty_shared_state.wsInstances
for (let [key, instance] of Object.entries(instances)) {
if (instance.connected) {
if (!instance.auth) {
instance.disconnect()
instance.auth = {
token: SessionModel.token,
}
instance.connect()
continue
}
if (!force) {
instance.emit("reauthenticate", {
token: SessionModel.token,
})
continue
}
// disconnect first
instance.disconnect()
}
if (remotes[key].noAuth !== true) {
instance.auth = {
token: SessionModel.token,
}
}
instance.connect()
}
}
export default function createClient({
accessKey = null,
privateKey = null,
enableWs = false,
} = {}) {
const sharedState = globalThis.__comty_shared_state = {
onExpiredExceptionEvent: false,
excludedExpiredExceptionURL: ["/session/regenerate"],
eventBus: new EventEmitter(),
mainOrigin: remotes.default.origin,
instances: Object(),
wsInstances: Object(),
rest: null,
version: pkg.version,
}
if (globalThis.isServerMode) {
sharedState.rest = createHandlers()
}
if (privateKey && accessKey && globalThis.isServerMode) {
Storage.engine.set("token", `${accessKey}:${privateKey}`)
}
// create instances for every remote
for (const [key, remote] of Object.entries(remotes)) {
sharedState.instances[key] = axios.create({
baseURL: remote.origin,
headers: {
"Content-Type": "application/json",
}
})
// create a interceptor to attach the token every request
sharedState.instances[key].interceptors.request.use((config) => {
// check if current request has no Authorization header, if so, attach the token
if (!config.headers["Authorization"]) {
const sessionToken = SessionModel.token
if (sessionToken) {
config.headers["Authorization"] = `${globalThis.isServerMode ? "Server" : "Bearer"} ${sessionToken}`
} else {
console.warn("Making a request with no session token")
}
}
return config
})
}
if (enableWs) {
createWebsockets()
}
return sharedState
}

View File

@ -1,86 +0,0 @@
import request from "../../handlers/request"
import SessionModel from "../session"
const emailRegex = new RegExp(/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/)
export default class AuthModel {
static login = async (payload, callback) => {
const response = await request({
method: "post",
url: "/auth/login",
data: {
username: payload.username, //window.btoa(payload.username),
password: payload.password, //window.btoa(payload.password),
},
})
SessionModel.token = response.data.token
if (typeof callback === "function") {
await callback()
}
__comty_shared_state.eventBus.emit("auth:login_success")
return response.data
}
static logout = async () => {
await SessionModel.destroyCurrentSession()
SessionModel.removeToken()
__comty_shared_state.eventBus.emit("auth:logout_success")
}
static register = async (payload) => {
const { username, password, email } = payload
const response = await request({
method: "post",
url: "/auth/register",
data: {
username,
password,
email,
}
}).catch((error) => {
console.error(error)
return false
})
if (!response) {
throw new Error("Unable to register user")
}
return response
}
static usernameValidation = async (username) => {
let payload = {}
// check if usename arguemnt is an email
if (emailRegex.test(username)) {
payload.email = username
} else {
payload.username = username
}
const response = await request({
method: "get",
url: "/auth/login/validation",
params: payload,
}).catch((error) => {
console.error(error)
return false
})
if (!response) {
throw new Error("Unable to validate user")
}
return response.data
}
}

View File

@ -1,56 +0,0 @@
import request from "../../handlers/request"
import Settings from "../../helpers/withSettings"
export default class FeedModel {
static getMusicFeed = async ({ trim, limit } = {}) => {
const { data } = await request({
method: "GET",
url: `/feed/music`,
params: {
trim: trim ?? 0,
limit: limit ?? Settings.get("feed_max_fetch"),
}
})
return data
}
static getGlobalMusicFeed = async ({ trim, limit } = {}) => {
const { data } = await request({
method: "GET",
url: `/feed/music/global`,
params: {
trim: trim ?? 0,
limit: limit ?? Settings.get("feed_max_fetch"),
}
})
return data
}
static getTimelineFeed = async ({ trim, limit = 10 } = {}) => {
const { data } = await request({
method: "GET",
url: `/feed/timeline`,
params: {
trim: trim ?? 0,
limit: limit ?? Settings.get("feed_max_fetch"),
}
})
return data
}
static getPostsFeed = async ({ trim, limit } = {}) => {
const { data } = await request({
method: "GET",
url: `/feed/posts`,
params: {
trim: trim ?? 0,
limit: limit ?? Settings.get("feed_max_fetch"),
}
})
return data
}
}

View File

@ -1,48 +0,0 @@
import { SessionModel } from "../../models"
import request from "../../handlers/request"
export default class FollowsModel {
static imFollowing = async (user_id) => {
if (!user_id) {
throw new Error("user_id is required")
}
const response = await request({
method: "GET",
url: `/follow/user/${user_id}`,
})
return response.data
}
static getFollowers = async (user_id) => {
if (!user_id) {
// set current user_id
user_id = SessionModel.user_id
}
const response = await request({
method: "GET",
url: `/follow/user/${user_id}/followers`,
})
return response.data
}
static toggleFollow = async ({ user_id, username }) => {
if (!user_id && !username) {
throw new Error("user_id or username is required")
}
const response = await request({
method: "POST",
url: "/follow/user/toggle",
data: {
user_id: user_id,
username: username
},
})
return response.data
}
}

View File

@ -1,41 +0,0 @@
import AuthModel from "./auth"
import FeedModel from "./feed"
import FollowsModel from "./follows"
import LivestreamModel from "./livestream"
import PostModel from "./post"
import SessionModel from "./session"
import SyncModel from "./sync"
import UserModel from "./user"
function getEndpointsFromModel(model) {
return Object.entries(model).reduce((acc, [key, value]) => {
acc[key] = value
return acc
}, {})
}
function createHandlers() {
return {
auth: getEndpointsFromModel(AuthModel),
feed: getEndpointsFromModel(FeedModel),
follows: getEndpointsFromModel(FollowsModel),
livestream: getEndpointsFromModel(LivestreamModel),
post: getEndpointsFromModel(PostModel),
session: getEndpointsFromModel(SessionModel),
sync: getEndpointsFromModel(SyncModel),
user: getEndpointsFromModel(UserModel),
}
}
export {
AuthModel,
FeedModel,
FollowsModel,
LivestreamModel,
PostModel,
SessionModel,
SyncModel,
UserModel,
createHandlers,
}

View File

@ -1,84 +0,0 @@
import request from "../../handlers/request"
export default class Livestream {
static deleteProfile = async (profile_id) => {
const response = await request({
method: "DELETE",
url: `/tv/streaming/profile`,
data: {
profile_id
}
})
return response.data
}
static postProfile = async (payload) => {
const response = await request({
method: "POST",
url: `/tv/streaming/profile`,
data: payload,
})
return response.data
}
static getProfiles = async () => {
const response = await request({
method: "GET",
url: `/tv/streaming/profiles`,
})
return response.data
}
static regenerateStreamingKey = async (profile_id) => {
const response = await request({
method: "POST",
url: `/tv/streaming/regenerate_key`,
data: {
profile_id
}
})
return response.data
}
static getCategories = async (key) => {
const response = await request({
method: "GET",
url: `/tv/streaming/categories`,
params: {
key,
}
})
return response.data
}
static getLivestream = async (payload = {}) => {
if (!payload.username) {
throw new Error("Username is required")
}
let response = await request({
method: "GET",
url: `/tv/streams`,
params: {
username: payload.username,
profile_id: payload.profile_id,
}
})
return response.data
}
static getLivestreams = async () => {
const response = await request({
method: "GET",
url: `/tv/streams`,
})
return response.data
}
}

View File

@ -1,561 +0,0 @@
import request from "../../handlers/request"
import pmap from "p-map"
import SyncModel from "../sync"
export default class MusicModel {
static get api_instance() {
return globalThis.__comty_shared_state.instances["music"]
}
/**
* Retrieves the official featured playlists.
*
* @return {Promise<Object>} The data containing the featured playlists.
*/
static async getFeaturedPlaylists() {
const response = await request({
instance: MusicModel.api_instance,
method: "GET",
url: "/featured/playlists",
})
return response.data
}
/**
* Retrieves track data for a given ID.
*
* @param {string} id - The ID of the track.
* @return {Promise<Object>} The track data.
*/
static async getTrackData(id) {
const response = await request({
instance: MusicModel.api_instance,
method: "GET",
url: `/tracks/${id}/data`,
})
return response.data
}
/**
* Retrieves tracks data for the given track IDs.
*
* @param {Array} ids - An array of track IDs.
* @return {Promise<Object>} A promise that resolves to the tracks data.
*/
static async getTracksData(ids) {
const response = await request({
instance: MusicModel.api_instance,
method: "GET",
url: `/tracks/many`,
params: {
ids,
}
})
return response.data
}
/**
* Retrieves favorite tracks based on specified parameters.
*
* @param {Object} options - The options for retrieving favorite tracks.
* @param {boolean} options.useTidal - Whether to use Tidal for retrieving tracks. Defaults to false.
* @param {number} options.limit - The maximum number of tracks to retrieve.
* @param {number} options.offset - The offset from which to start retrieving tracks.
* @return {Promise<Object>} - An object containing the total length of the tracks and the retrieved tracks.
*/
static async getFavoriteTracks({ useTidal = false, limit, offset }) {
let result = []
let limitPerRequesters = limit
if (useTidal) {
limitPerRequesters = limitPerRequesters / 2
}
const requesters = [
async () => {
let { data } = await request({
instance: MusicModel.api_instance,
method: "GET",
url: `/tracks/liked`,
params: {
limit: limitPerRequesters,
offset,
},
})
return data
},
async () => {
if (!useTidal) {
return {
total_length: 0,
tracks: [],
}
}
const tidalResult = await SyncModel.tidalCore.getMyFavoriteTracks({
limit: limitPerRequesters,
offset,
})
return tidalResult
},
]
result = await pmap(
requesters,
async requester => {
const data = await requester()
return data
},
{
concurrency: 3,
},
)
let total_length = 0
result.forEach(result => {
total_length += result.total_length
})
let tracks = result.reduce((acc, cur) => {
return [...acc, ...cur.tracks]
}, [])
tracks = tracks.sort((a, b) => {
return b.liked_at - a.liked_at
})
return {
total_length,
tracks,
}
}
/**
* Retrieves favorite playlists based on the specified parameters.
*
* @param {Object} options - The options for retrieving favorite playlists.
* @param {number} options.limit - The maximum number of playlists to retrieve. Default is 50.
* @param {number} options.offset - The offset of playlists to retrieve. Default is 0.
* @param {Object} options.services - The services to include for retrieving playlists. Default is an empty object.
* @param {string} options.keywords - The keywords to filter playlists by.
* @return {Promise<Object>} - An object containing the total length of the playlists and the playlist items.
*/
static async getFavoritePlaylists({ limit = 50, offset = 0, services = {}, keywords } = {}) {
let result = []
let limitPerRequesters = limit
const requesters = [
async () => {
const { data } = await request({
instance: MusicModel.api_instance,
method: "GET",
url: `/playlists/self`,
params: {
keywords,
},
})
return data
},
]
if (services["tidal"] === true) {
limitPerRequesters = limitPerRequesters / (requesters.length + 1)
requesters.push(async () => {
const _result = await SyncModel.tidalCore.getMyFavoritePlaylists({
limit: limitPerRequesters,
offset,
})
return _result
})
}
result = await pmap(
requesters,
async requester => {
const data = await requester()
return data
},
{
concurrency: 3,
},
)
// calculate total length
let total_length = 0
result.forEach(result => {
total_length += result.total_length
})
// reduce items
let items = result.reduce((acc, cur) => {
return [...acc, ...cur.items]
}, [])
// sort by created_at
items = items.sort((a, b) => {
return new Date(b.created_at) - new Date(a.created_at)
})
return {
total_length: total_length,
items,
}
}
/**
* Retrieves playlist items based on the provided parameters.
*
* @param {Object} options - The options object.
* @param {string} options.playlist_id - The ID of the playlist.
* @param {string} options.service - The service from which to retrieve the playlist items.
* @param {number} options.limit - The maximum number of items to retrieve.
* @param {number} options.offset - The number of items to skip before retrieving.
* @return {Promise<Object>} Playlist items data.
*/
static async getPlaylistItems({
playlist_id,
service,
limit,
offset,
}) {
if (service === "tidal") {
const result = await SyncModel.tidalCore.getPlaylistItems({
playlist_id,
limit,
offset,
resolve_items: true,
})
return result
}
const { data } = await request({
instance: MusicModel.api_instance,
method: "GET",
url: `/playlists/${playlist_id}/items`,
params: {
limit,
offset,
}
})
return data
}
/**
* Retrieves playlist data based on the provided parameters.
*
* @param {Object} options - The options object.
* @param {string} options.playlist_id - The ID of the playlist.
* @param {string} options.service - The service to use.
* @param {number} options.limit - The maximum number of items to retrieve.
* @param {number} options.offset - The offset for pagination.
* @return {Promise<Object>} Playlist data.
*/
static async getPlaylistData({
playlist_id,
service,
limit,
offset,
}) {
if (service === "tidal") {
const result = await SyncModel.tidalCore.getPlaylistData({
playlist_id,
limit,
offset,
resolve_items: true,
})
return result
}
const { data } = await request({
instance: MusicModel.api_instance,
method: "GET",
url: `/playlists/${playlist_id}/data`,
params: {
limit,
offset,
}
})
return data
}
/**
* Performs a search based on the provided keywords, with optional parameters for limiting the number of results and pagination.
*
* @param {string} keywords - The keywords to search for.
* @param {object} options - An optional object containing additional parameters.
* @param {number} options.limit - The maximum number of results to return. Defaults to 5.
* @param {number} options.offset - The offset to start returning results from. Defaults to 0.
* @param {boolean} options.useTidal - Whether to use Tidal for the search. Defaults to false.
* @return {Promise<Object>} The search results.
*/
static async search(keywords, { limit = 5, offset = 0, useTidal = false }) {
const { data } = await request({
instance: MusicModel.api_instance,
method: "GET",
url: `/search`,
params: {
keywords,
limit,
offset,
useTidal,
},
})
return data
}
/**
* Creates a new playlist.
*
* @param {object} payload - The payload containing the data for the new playlist.
* @return {Promise<Object>} The new playlist data.
*/
static async newPlaylist(payload) {
const { data } = await request({
instance: MusicModel.api_instance,
method: "POST",
url: `/playlists/new`,
data: payload,
})
return data
}
/**
* Updates a playlist item in the specified playlist.
*
* @param {string} playlist_id - The ID of the playlist to update.
* @param {object} item - The updated playlist item to be added.
* @return {Promise<Object>} - The updated playlist item.
*/
static async putPlaylistItem(playlist_id, item) {
const response = await request({
instance: MusicModel.api_instance,
method: "PUT",
url: `/playlists/${playlist_id}/items`,
data: item,
})
return response.data
}
/**
* Delete a playlist item.
*
* @param {string} playlist_id - The ID of the playlist.
* @param {string} item_id - The ID of the item to delete.
* @return {Promise<Object>} The data returned by the server after the item is deleted.
*/
static async deletePlaylistItem(playlist_id, item_id) {
const response = await request({
instance: MusicModel.api_instance,
method: "DELETE",
url: `/playlists/${playlist_id}/items/${item_id}`,
})
return response.data
}
/**
* Deletes a playlist.
*
* @param {number} playlist_id - The ID of the playlist to be deleted.
* @return {Promise<Object>} The response data from the server.
*/
static async deletePlaylist(playlist_id) {
const response = await request({
instance: MusicModel.api_instance,
method: "DELETE",
url: `/playlists/${playlist_id}`,
})
return response.data
}
/**
* Execute a PUT request to update or create a release.
*
* @param {object} payload - The payload data.
* @return {Promise<Object>} The response data from the server.
*/
static async putRelease(payload) {
const response = await request({
instance: MusicModel.api_instance,
method: "PUT",
url: `/releases/release`,
data: payload
})
return response.data
}
/**
* Retrieves the releases associated with the authenticated user.
*
* @param {string} keywords - The keywords to filter the releases by.
* @return {Promise<Object>} A promise that resolves to the data of the releases.
*/
static async getMyReleases(keywords) {
const response = await request({
instance: MusicModel.api_instance,
method: "GET",
url: `/releases/self`,
params: {
keywords,
}
})
return response.data
}
/**
* Retrieves releases based on the provided parameters.
*
* @param {object} options - The options for retrieving releases.
* @param {string} options.user_id - The ID of the user.
* @param {string[]} options.keywords - The keywords to filter releases by.
* @param {number} options.limit - The maximum number of releases to retrieve.
* @param {number} options.offset - The offset for paginated results.
* @return {Promise<Object>} - A promise that resolves to the retrieved releases.
*/
static async getReleases({
user_id,
keywords,
limit = 50,
offset = 0,
}) {
const response = await request({
instance: MusicModel.api_instance,
method: "GET",
url: `/releases/user/${user_id}`,
params: {
keywords,
limit,
offset,
}
})
return response.data
}
/**
* Retrieves release data by ID.
*
* @param {number} id - The ID of the release.
* @return {Promise<Object>} The release data.
*/
static async getReleaseData(id) {
const response = await request({
instance: MusicModel.api_instance,
method: "GET",
url: `/releases/${id}/data`
})
return response.data
}
/**
* Deletes a release by its ID.
*
* @param {string} id - The ID of the release to delete.
* @return {Promise<Object>} - A Promise that resolves to the data returned by the API.
*/
static async deleteRelease(id) {
const response = await request({
instance: MusicModel.api_instance,
method: "DELETE",
url: `/releases/${id}`
})
return response.data
}
/**
* Refreshes the track cache for a given track ID.
*
* @param {string} track_id - The ID of the track to refresh the cache for.
* @throws {Error} If track_id is not provided.
* @return {Promise<Object>} The response data from the API call.
*/
static async refreshTrackCache(track_id) {
if (!track_id) {
throw new Error("Track ID is required")
}
const response = await request({
instance: MusicModel.api_instance,
method: "POST",
url: `/tracks/${track_id}/refresh-cache`,
})
return response.data
}
/**
* Toggles the like status of a track.
*
* @param {Object} manifest - The manifest object containing track information.
* @param {boolean} to - The like status to toggle (true for like, false for unlike).
* @throws {Error} Throws an error if the manifest is missing.
* @return {Object} The response data from the API.
*/
static async toggleTrackLike(manifest, to) {
if (!manifest) {
throw new Error("Manifest is required")
}
console.log(`Toggling track ${manifest._id} like status to ${to}`)
const track_id = manifest._id
switch (manifest.service) {
case "tidal": {
const response = await SyncModel.tidalCore.toggleTrackLike({
track_id,
to,
})
return response
}
default: {
const response = await request({
instance: MusicModel.api_instance,
method: to ? "POST" : "DELETE",
url: `/tracks/${track_id}/like`,
params: {
service: manifest.service
}
})
return response.data
}
}
}
}

View File

@ -1,56 +0,0 @@
import request from "../../handlers/request"
export default class NFCModel {
static async getOwnTags() {
const { data } = await request({
method: "GET",
url: `/nfc/tags`
})
return data
}
static async getTagById(id) {
if (!id) {
throw new Error("ID is required")
}
const { data } = await request({
method: "GET",
url: `/nfc/tags/${id}`
})
return data
}
static async getTagBySerial(serial) {
if (!serial) {
throw new Error("Serial is required")
}
const { data } = await request({
method: "GET",
url: `/nfc/tag/serial/${serial}`
})
return data
}
static async registerTag(serial, payload) {
if (!serial) {
throw new Error("Serial is required")
}
if (!payload) {
throw new Error("Payload is required")
}
const { data } = await request({
method: "POST",
url: `/nfc/tag/${serial}`,
data: payload
})
return data
}
}

View File

@ -1,169 +0,0 @@
import request from "../../handlers/request"
import Settings from "../../helpers/withSettings"
export default class Post {
static get maxPostTextLength() {
return 3200
}
static get maxCommentLength() {
return 1200
}
static getPostingPolicy = async () => {
const { data } = await request({
method: "GET",
url: "/posting_policy",
})
return data
}
static getPost = async ({ post_id }) => {
if (!post_id) {
throw new Error("Post ID is required")
}
const { data } = await request({
method: "GET",
url: `/posts/post/${post_id}`,
})
return data
}
static getPostComments = async ({ post_id }) => {
if (!post_id) {
throw new Error("Post ID is required")
}
const { data } = await request({
method: "GET",
url: `/comments/post/${post_id}`,
})
return data
}
static sendComment = async ({ post_id, comment }) => {
if (!post_id || !comment) {
throw new Error("Post ID and/or comment are required")
}
const { data } = await request({
method: "POST",
url: `/comments/post/${post_id}`,
data: {
message: comment,
},
})
return data
}
static deleteComment = async ({ post_id, comment_id }) => {
if (!post_id || !comment_id) {
throw new Error("Post ID and/or comment ID are required")
}
const { data } = await request({
method: "DELETE",
url: `/comments/post/${post_id}/${comment_id}`,
})
return data
}
static getExplorePosts = async ({ trim, limit }) => {
const { data } = await request({
method: "GET",
url: `/posts/explore`,
params: {
trim: trim ?? 0,
limit: limit ?? Settings.get("feed_max_fetch"),
}
})
return data
}
static getSavedPosts = async ({ trim, limit }) => {
const { data } = await request({
method: "GET",
url: `/posts/saved`,
params: {
trim: trim ?? 0,
limit: limit ?? Settings.get("feed_max_fetch"),
}
})
return data
}
static getUserPosts = async ({ user_id, trim, limit }) => {
if (!user_id) {
// use current user_id
user_id = app.userData?._id
}
const { data } = await request({
method: "GET",
url: `/posts/user/${user_id}`,
params: {
trim: trim ?? 0,
limit: limit ?? Settings.get("feed_max_fetch"),
}
})
return data
}
static toggleLike = async ({ post_id }) => {
if (!post_id) {
throw new Error("Post ID is required")
}
const { data } = await request({
method: "POST",
url: `/posts/${post_id}/toggle_like`,
})
return data
}
static toggleSave = async ({ post_id }) => {
if (!post_id) {
throw new Error("Post ID is required")
}
const { data } = await request({
method: "POST",
url: `/posts/${post_id}/toggle_save`,
})
return data
}
static create = async (payload) => {
const { data } = await request({
method: "POST",
url: `/posts/new`,
data: payload,
})
return data
}
static deletePost = async ({ post_id }) => {
if (!post_id) {
throw new Error("Post ID is required")
}
const { data } = await request({
method: "DELETE",
url: `/posts/${post_id}`,
})
return data
}
}

View File

@ -1,26 +0,0 @@
import request from "../../handlers/request"
export default class Search {
static search = async (keywords, params = {}) => {
const { data } = await request({
method: "GET",
url: `/search`,
params: {
keywords: keywords,
params: params
}
})
return data
}
static async quickSearch(params) {
const response = await request({
method: "GET",
url: "/search/quick",
params: params
})
return response.data
}
}

View File

@ -1,135 +0,0 @@
import jwt_decode from "jwt-decode"
import request from "../../handlers/request"
import Storage from "../../helpers/withStorage"
export default class Session {
static storageTokenKey = "token"
static get token() {
return Storage.engine.get(this.storageTokenKey)
}
static set token(token) {
return Storage.engine.set(this.storageTokenKey, token)
}
static get roles() {
return this.getDecodedToken()?.roles
}
static get user_id() {
return this.getDecodedToken()?.user_id
}
static get session_uuid() {
return this.getDecodedToken()?.session_uuid
}
static getDecodedToken = () => {
const token = this.token
return token && jwt_decode(token)
}
static getAllSessions = async () => {
const response = await request({
method: "get",
url: "/session/all"
})
return response.data
}
static getCurrentSession = async () => {
const response = await request({
method: "get",
url: "/session/current"
})
return response.data
}
static getTokenValidation = async () => {
const session = await Session.token
const response = await request({
method: "get",
url: "/session/validate",
data: {
session: session
}
})
return response.data
}
static removeToken() {
return Storage.engine.remove(Session.storageTokenKey)
}
static destroyCurrentSession = async () => {
const token = await Session.token
const session = await Session.getDecodedToken()
if (!session || !token) {
return false
}
const response = await request({
method: "delete",
url: "/session/current"
}).catch((error) => {
console.error(error)
return false
})
Session.removeToken()
__comty_shared_state.eventBus.emit("session.destroyed")
return response.data
}
static destroyAllSessions = async () => {
const session = await Session.getDecodedToken()
if (!session) {
return false
}
const response = await request({
method: "delete",
url: "/session/all"
})
Session.removeToken()
__comty_shared_state.eventBus.emit("session.destroyed")
return response.data
}
// alias for validateToken method
static validSession = async (token) => {
return await Session.validateToken(token)
}
static validateToken = async (token) => {
const response = await request({
method: "post",
url: "/session/validate",
data: {
token: token
}
})
return response.data
}
static isCurrentTokenValid = async () => {
const health = await Session.getTokenValidation()
return health.valid
}
}

View File

@ -1,59 +0,0 @@
import spotifyService from "./services/spotify"
import tidalService from "./services/tidal"
import request from "../../handlers/request"
const namespacesServices = {
spotify: spotifyService,
tidal: tidalService
}
export default class SyncModel {
static get spotifyCore() {
return namespacesServices.spotify
}
static get tidalCore() {
return namespacesServices.tidal
}
static async linkService(namespace) {
const service = namespacesServices[namespace]
if (!service || typeof service.linkAccount !== "function") {
throw new Error(`Service ${namespace} not found or not accepting linking.`)
}
return await service.linkAccount()
}
static async unlinkService(namespace) {
const service = namespacesServices[namespace]
if (!service || typeof service.unlinkAccount !== "function") {
throw new Error(`Service ${namespace} not found or not accepting unlinking.`)
}
return await service.unlinkAccount()
}
static async hasServiceLinked(namespace) {
const service = namespacesServices[namespace]
if (!service || typeof service.isActive !== "function") {
throw new Error(`Service ${namespace} not found or not accepting linking.`)
}
return await service.isActive()
}
static async getLinkedServices() {
const response = await request({
instance: globalThis.__comty_shared_state.instances["sync"],
method: "GET",
url: "/active_services",
})
return response.data
}
}

View File

@ -1,87 +0,0 @@
export default class SpotifySyncModel {
static get spotify_redirect_uri() {
return window.location.origin + "/callbacks/sync/spotify"
}
static get spotify_authorize_endpoint() {
return "https://accounts.spotify.com/authorize?response_type=code&client_id={{client_id}}&scope={{scope}}&redirect_uri={{redirect_uri}}&response_type=code"
}
static async authorizeAccount() {
const scopes = [
"user-read-private",
"user-modify-playback-state",
"user-read-currently-playing",
"user-read-playback-state",
"streaming",
]
const { client_id } = await SpotifySyncModel.get_client_id()
const parsedUrl = SpotifySyncModel.spotify_authorize_endpoint
.replace("{{client_id}}", client_id)
.replace("{{scope}}", scopes.join(" "))
.replace("{{redirect_uri}}", SpotifySyncModel.spotify_redirect_uri)
// open on a new tab
window.open(parsedUrl, "_blank")
}
static async get_client_id() {
const { data } = await app.cores.api.customRequest({
method: "GET",
url: `/sync/spotify/client_id`,
})
return data
}
static async syncAuthCode(code) {
const { data } = await app.cores.api.customRequest({
method: "POST",
url: `/sync/spotify/auth`,
data: {
redirect_uri: SpotifySyncModel.spotify_redirect_uri,
code,
},
})
return data
}
static async unlinkAccount() {
const { data } = await app.cores.api.customRequest({
method: "POST",
url: `/sync/spotify/unlink`,
})
return data
}
static async isAuthorized() {
const { data } = await app.cores.api.customRequest({
method: "GET",
url: `/sync/spotify/is_authorized`,
})
return data
}
static async getData() {
const { data } = await app.cores.api.customRequest({
method: "GET",
url: `/sync/spotify/data`,
})
return data
}
static async getCurrentPlaying() {
const { data } = await app.cores.api.customRequest({
method: "GET",
url: `/sync/spotify/currently_playing`,
})
return data
}
}

View File

@ -1,172 +0,0 @@
import request from "../../../handlers/request"
export default class TidalService {
static get api_instance() {
return globalThis.__comty_shared_state.instances["sync"]
}
static async linkAccount() {
if (!window) {
throw new Error("This method is only available in the browser.")
}
const { data } = await request({
instance: TidalService.api_instance,
method: "GET",
url: `/services/tidal/create_link`,
})
if (data.auth_url) {
window.open(data.auth_url, "_blank")
}
return data
}
static async unlinkAccount() {
if (!window) {
throw new Error("This method is only available in the browser.")
}
const { data } = await request({
instance: TidalService.api_instance,
method: "POST",
url: `/services/tidal/delete_link`,
})
return data
}
static async isActive() {
if (!window) {
throw new Error("This method is only available in the browser.")
}
const { data } = await request({
instance: TidalService.api_instance,
method: "GET",
url: `/services/tidal/is_active`,
})
return data
}
static async getCurrentUser() {
const { data } = await request({
instance: TidalService.api_instance,
method: "GET",
url: `/services/tidal/current`,
})
return data
}
static async getPlaybackUrl(track_id) {
const { data } = await request({
instance: TidalService.api_instance,
method: "GET",
url: `/services/tidal/playback/${track_id}`,
})
return data
}
static async getTrackManifest(track_id) {
const { data } = await request({
instance: TidalService.api_instance,
method: "GET",
url: `/services/tidal/manifest/${track_id}`,
})
return data
}
static async getMyFavoriteTracks({
limit = 50,
offset = 0,
} = {}) {
const { data } = await request({
instance: TidalService.api_instance,
method: "GET",
url: `/services/tidal/favorites/tracks`,
params: {
limit,
offset,
},
})
return data
}
static async getMyFavoritePlaylists({
limit = 50,
offset = 0,
} = {}) {
const { data } = await request({
instance: TidalService.api_instance,
method: "GET",
url: `/services/tidal/favorites/playlists`,
params: {
limit,
offset,
},
})
return data
}
static async getPlaylistData({
playlist_id,
resolve_items = false,
limit = 50,
offset = 0,
}) {
const { data } = await request({
instance: TidalService.api_instance,
method: "GET",
url: `/services/tidal/playlist/${playlist_id}/data`,
params: {
limit,
offset,
resolve_items,
},
})
return data
}
static async getPlaylistItems({
playlist_id,
resolve_items = false,
limit = 50,
offset = 0,
}) {
const { data } = await request({
instance: TidalService.api_instance,
method: "GET",
url: `/services/tidal/playlist/${playlist_id}/items`,
params: {
limit,
offset,
resolve_items,
},
})
return data
}
static async toggleTrackLike({
track_id,
to,
}) {
const { data } = await request({
instance: TidalService.api_instance,
method: to ? "POST" : "DELETE",
url: `/services/tidal/track/${track_id}/like`,
})
return data
}
}

View File

@ -1,159 +0,0 @@
import SessionModel from "../session"
import request from "../../handlers/request"
export default class User {
static data = async (payload = {}) => {
let {
username,
user_id,
} = payload
if (!username && !user_id) {
user_id = SessionModel.user_id
}
if (username && !user_id) {
// resolve user_id from username
const resolveResponse = await request({
method: "GET",
url: `/user/user_id/${username}`,
})
user_id = resolveResponse.data.user_id
}
const response = await request({
method: "GET",
url: `/user/${user_id}/data`,
})
return response.data
}
static updateData = async (payload) => {
const response = await request({
method: "POST",
url: "/user/self/update_data",
data: {
update: payload,
},
})
return response.data
}
static unsetFullName = async () => {
const response = await request({
method: "DELETE",
url: "/user/self/public_name",
})
return response.data
}
static selfRoles = async () => {
const response = await request({
method: "GET",
url: "/roles/self",
})
return response.data
}
static haveRole = async (role) => {
const roles = await User.selfRoles()
if (!roles) {
return false
}
return Array.isArray(roles) && roles.includes(role)
}
static haveAdmin = async () => {
return User.haveRole("admin")
}
static getUserBadges = async (user_id) => {
if (!user_id) {
user_id = SessionModel.user_id
}
const { data } = await request({
method: "GET",
url: `/badge/user/${user_id}`,
})
return data
}
static changePassword = async (payload) => {
const { currentPassword, newPassword } = payload
const { data } = await request({
method: "POST",
url: "/user/self/update_password",
data: {
currentPassword,
newPassword,
}
})
return data
}
static getUserFollowers = async ({
user_id,
limit = 20,
offset = 0,
}) => {
// if user_id or username is not provided, set with current user
if (!user_id && !username) {
user_id = SessionModel.user_id
}
const { data } = await request({
method: "GET",
url: `/user/${user_id}/followers`,
params: {
limit,
offset,
}
})
return data
}
static getConnectedUsersFollowing = async () => {
const { data } = await request({
method: "GET",
url: "/status/connected/following",
})
return data
}
static checkUsernameAvailability = async (username) => {
const { data } = await request({
method: "GET",
url: `/user/username_available`,
params: {
username,
}
})
return data
}
static checkEmailAvailability = async (email) => {
const { data } = await request({
method: "GET",
url: `/user/email_available`,
params: {
email,
}
})
return data
}
}

View File

@ -1,18 +0,0 @@
import request from "../../handlers/request"
export default class WidgetModel {
static browse = async ({ limit, offset, keywords } = {}) => {
const response = await request({
instance: globalThis.__comty_shared_state.instances["marketplace"],
method: "GET",
url: "/widgets",
params: {
limit,
offset,
keywords: JSON.stringify(keywords),
},
})
return response.data
}
}

View File

@ -1,78 +0,0 @@
function composeRemote(path) {
if (typeof window !== "undefined") {
if (window.localStorage.getItem("comty:use_indev") || window.location.hostname === "indev.comty.app") {
return envOrigins["indev"][path]
}
}
return envOrigins[process.env.NODE_ENV ?? "production"][path]
}
function getCurrentHostname() {
if (typeof window === "undefined") {
return "localhost"
}
return window?.location?.hostname ?? "localhost"
}
const envOrigins = {
"development": {
default: `http://${getCurrentHostname()}:3010`,
chat: `http://${getCurrentHostname()}:3020`,
livestreaming: `http://${getCurrentHostname()}:3030`,
marketplace: `http://${getCurrentHostname()}:3040`,
music: `http://${getCurrentHostname()}:3050`,
files: `http://${getCurrentHostname()}:3060`,
sync: `http://${getCurrentHostname()}:3070`,
},
"indev": {
default: `https://indev_api.comty.app/main`,
chat: `https://indev_api.comty.app/chat`,
livestreaming: `https://indev_api.comty.app/livestreaming`,
marketplace: `https://indev_api.comty.app/marketplace`,
music: `https://indev_api.comty.app/music`,
files: `https://indev_api.comty.app/files`,
sync: `https://indev_api.comty.app/sync`,
},
"production": {
default: "https://api.comty.app",
chat: `https://chat_api.comty.app`,
livestreaming: `https://livestreaming_api.comty.app`,
marketplace: `https://marketplace_api.comty.app`,
music: `https://music_api.comty.app`,
files: `https://files_api.comty.app`,
sync: `https://sync_api.comty.app`,
}
}
export default {
default: {
origin: composeRemote("default"),
hasWebsocket: true,
},
chat: {
origin: composeRemote("chat"),
hasWebsocket: true,
},
music: {
origin: composeRemote("music"),
hasWebsocket: true,
},
livestreaming: {
origin: composeRemote("livestreaming"),
hasWebsocket: false,
},
marketplace: {
origin: composeRemote("marketplace"),
hasWebsocket: false,
},
files: {
origin: composeRemote("files"),
hasWebsocket: false,
},
sync: {
origin: composeRemote("sync"),
hasWebsocket: false,
}
}

View File

@ -14,6 +14,7 @@ export default {
verified: { type: Boolean, default: false },
badges: { type: Array, default: [] },
links: { type: Array, default: [] },
location: { type: String, default: null },
birthday: { type: Date, default: null },
}
}

View File

@ -9,14 +9,20 @@ import { Observable } from "@gullerya/object-observer"
import chalk from "chalk"
import Spinnies from "spinnies"
import chokidar from "chokidar"
import IPCRouter from "linebridge/src/server/classes/IPCRouter"
import fastify from "fastify"
import { createProxyMiddleware } from "http-proxy-middleware"
import { dots as DefaultSpinner } from "spinnies/spinners.json"
import IPCRouter from "linebridge/src/server/classes/IPCRouter"
import getInternalIp from "./lib/getInternalIp"
import comtyAscii from "./ascii"
import pkg from "./package.json"
import cors from "linebridge/src/server/middlewares/cors"
import { onExit } from "signal-exit"
const bootloaderBin = path.resolve(__dirname, "boot")
const servicesPath = path.resolve(__dirname, "services")
@ -45,6 +51,7 @@ async function scanServices() {
return finalServices
}
let internal_proxy = null
let allReady = false
let selectedProcessInstance = null
let internalIp = null
@ -158,6 +165,10 @@ async function getIgnoredFiles(cwd) {
}
async function handleAllReady() {
if (allReady) {
return false
}
console.clear()
allReady = true
@ -199,29 +210,53 @@ async function handleServiceExit(id, code, err) {
serviceRegistry[id].ready = false
}
// PROCESS HANDLERS
async function handleProcessExit(error, code) {
if (error) {
console.error(error)
async function registerProxy(_path, target, pathRewrite) {
if (internal_proxy.proxys.has(_path)) {
console.warn(`Proxy already registered [${_path}], skipping...`)
return false
}
console.log(`\nPreparing to exit...`)
console.log(`🔗 Registering path proxy [${_path}] -> [${target}]`)
for await (let instance of instancePool) {
console.log(`🛑 Killing ${instance.id} [${instance.instance.pid}]`)
await instance.instance.kill()
}
internal_proxy.proxys.add(_path)
return 0
internal_proxy.use(_path, createProxyMiddleware({
target: target,
changeOrigin: true,
pathRewrite: pathRewrite,
ws: true,
logLevel: "silent",
}))
return true
}
async function handleIPCData(id, data) {
if (data.type === "log") {
console.log(`[${id}] ${data.message}`)
async function handleIPCData(service_id, msg) {
if (msg.type === "log") {
console.log(`[${service_id}] ${msg.message}`)
}
if (data.status === "ready") {
await handleServiceStarted(id)
if (msg.status === "ready") {
await handleServiceStarted(service_id)
}
if (msg.type === "router:register") {
if (msg.data.path_overrides) {
for await (let pathOverride of msg.data.path_overrides) {
await registerProxy(
`/${pathOverride}`,
`http://${internalIp}:${msg.data.listen.port}/${pathOverride}`,
{
[`^/${pathOverride}`]: "",
}
)
}
} else {
await registerProxy(
`/${service_id}`,
`http://${msg.data.listen.ip}:${msg.data.listen.port}`
)
}
}
}
@ -305,6 +340,31 @@ function createServiceLogTransformer({ id, color = "bgCyan" }) {
async function main() {
internalIp = await getInternalIp()
internal_proxy = fastify()
internal_proxy.proxys = new Set()
await internal_proxy.register(require("@fastify/middie"))
await internal_proxy.use(cors)
internal_proxy.get("/ping", (request, reply) => {
return reply.send({
status: "ok"
})
})
internal_proxy.get("/", (request, reply) => {
return reply.send({
services: instancePool.map((instance) => {
return {
id: instance.id,
version: instance.version,
}
}),
})
})
console.clear()
console.log(comtyAscii)
console.log(`\nRunning ${chalk.bgBlue(`${pkg.name}`)} | ${chalk.bgMagenta(`[v${pkg.version}]`)} | ${internalIp} \n\n\n`)
@ -323,7 +383,7 @@ async function main() {
const instanceFile = path.basename(service)
const instanceBasePath = path.dirname(service)
const { name: id, version, proxy } = require(path.resolve(instanceBasePath, "package.json"))
const { name: id, version } = require(path.resolve(instanceBasePath, "package.json"))
serviceFileReference[instanceFile] = id
@ -333,7 +393,6 @@ async function main() {
version: version,
file: instanceFile,
cwd: instanceBasePath,
proxy: proxy,
buffer: [],
ready: false,
}
@ -413,14 +472,29 @@ async function main() {
return command_fn.fn(callback, ...args)
}
}).on("exit", () => {
process.exit(0)
})
await internal_proxy.listen({
host: "0.0.0.0",
port: 9000
})
onExit((code, signal) => {
console.clear()
console.log(`\n🛑 Preparing to exit...`)
console.log(`Stoping proxy...`)
internal_proxy.close()
console.log(`Kill all ${instancePool.length} instances...`)
for (let instance of instancePool) {
console.log(`Killing ${instance.id} [${instance.instance.pid}]`)
instance.instance.kill()
}
})
}
process.on("exit", handleProcessExit)
process.on("SIGINT", handleProcessExit)
process.on("uncaughtException", handleProcessExit)
process.on("unhandledRejection", handleProcessExit)
main()

View File

@ -1,120 +0,0 @@
require("dotenv").config()
const path = require("path")
const { registerBaseAliases } = require("linebridge/dist/server")
const { webcrypto: crypto } = require("crypto")
const infisical = require("infisical-node")
global.isProduction = process.env.NODE_ENV === "production"
globalThis["__root"] = path.resolve(process.cwd())
globalThis["__src"] = path.resolve(globalThis["__root"], global.isProduction ? "dist" : "src")
const customAliases = {
"root": globalThis["__root"],
"src": globalThis["__src"],
"@shared-classes": path.resolve(globalThis["__src"], "_shared/classes"),
"@services": path.resolve(globalThis["__src"], "services"),
}
if (!global.isProduction) {
customAliases["comty.js"] = path.resolve(globalThis["__src"], "../../comty.js/src")
customAliases["@shared-classes"] = path.resolve(globalThis["__src"], "shared-classes")
}
if (process.env.USE_LINKED_SHARED) {
customAliases["@shared-classes"] = path.resolve(globalThis["__src"], "shared-classes")
}
registerBaseAliases(globalThis["__src"], customAliases)
// patches
const { Buffer } = require("buffer")
global.b64Decode = (data) => {
return Buffer.from(data, "base64").toString("utf-8")
}
global.b64Encode = (data) => {
return Buffer.from(data, "utf-8").toString("base64")
}
global.nanoid = (t = 21) => crypto.getRandomValues(new Uint8Array(t)).reduce(((t, e) => t += (e &= 63) < 36 ? e.toString(36) : e < 62 ? (e - 26).toString(36).toUpperCase() : e > 62 ? "-" : "_"), "");
Array.prototype.updateFromObjectKeys = function (obj) {
this.forEach((value, index) => {
if (obj[value] !== undefined) {
this[index] = obj[value]
}
})
return this
}
global.toBoolean = (value) => {
if (typeof value === "boolean") {
return value
}
if (typeof value === "string") {
return value.toLowerCase() === "true"
}
return false
}
async function injectEnvFromInfisical() {
const envMode = global.FORCE_ENV ?? global.isProduction ? "prod" : "dev"
console.log(`🔑 Injecting env variables from INFISICAL in [${envMode}] mode...`)
const client = new infisical({
token: process.env.INFISICAL_TOKEN,
})
const secrets = await client.getAllSecrets({
environment: envMode,
attachToProcessEnv: false,
})
// inject to process.env
secrets.forEach((secret) => {
if (!(process.env[secret.secretName])) {
process.env[secret.secretName] = secret.secretValue
}
})
}
function handleExit(instance, code) {
if (instance.server) {
if (typeof instance.server.close === "function") {
instance.server.close()
}
}
return process.exit(code)
}
async function main({
force_infisical,
} = {}) {
const API = require(path.resolve(globalThis["__src"], "api.js")).default
if (force_infisical || process.env.INFISICAL_TOKEN) {
await injectEnvFromInfisical()
}
const instance = new API()
process.on("exit", () => handleExit(instance, 0))
process.on("SIGINT", () => handleExit(instance, 0))
process.on("uncaughtException", () => handleExit(instance, 1))
process.on("unhandledRejection", () => handleExit(instance, 1))
await instance.initialize()
return instance
}
main().catch((error) => {
console.error(`🆘 [FATAL ERROR] >`, error)
})

View File

@ -0,0 +1,38 @@
import AuthToken from "../../classes/AuthToken"
import { User } from "../../db_models"
export default async (socket, token, err) => {
try {
const validation = await AuthToken.validate(token)
if (!validation.valid) {
if (validation.error) {
return err(`auth:server_error`)
}
return err(`auth:token_invalid`)
}
const userData = await User.findById(validation.data.user_id).catch((err) => {
console.error(`[${socket.id}] failed to get user data caused by server error`, err)
return null
})
if (!userData) {
return err(`auth:user_failed`)
}
socket.userData = userData
socket.token = token
socket.session = validation.data
return {
token: token,
username: userData.username,
user_id: userData._id,
}
} catch (error) {
return err(`auth:authentification_failed`, error)
}
}

View File

@ -12,6 +12,8 @@
"run:prod": "cross-env NODE_ENV=production node ./dist/index.js"
},
"dependencies": {
"@fastify/cors": "^9.0.1",
"@fastify/middie": "^8.3.0",
"@gullerya/object-observer": "^6.1.3",
"@infisical/sdk": "^2.1.8",
"@ragestudio/hermes": "^0.1.1",
@ -21,12 +23,14 @@
"cli-color": "^2.0.3",
"clui": "^0.3.6",
"dotenv": "^16.4.4",
"fastify": "^4.26.2",
"http-proxy-middleware": "^2.0.6",
"jsonwebtoken": "^9.0.2",
"linebridge": "^0.16.0",
"linebridge": "^0.18.1",
"module-alias": "^2.2.3",
"p-map": "^4.0.0",
"p-queue": "^7.3.4",
"signal-exit": "^4.1.0",
"spinnies": "^0.5.1"
},
"devDependencies": {

View File

@ -4,4 +4,6 @@ export default class Account {
static loginStrategy = require("./methods/loginStrategy").default
static changePassword = require("./methods/changePassword").default
static create = require("./methods/create").default
static sessions = require("./methods/sessions").default
static deleteSession = require("./methods/deleteSession").default
}

View File

@ -0,0 +1,28 @@
import { Session } from "@db_models"
export default async (payload = {}) => {
const { user_id, token } = payload
if (!user_id) {
throw new OperationError(400, "user_id not provided")
}
if (!token) {
throw new OperationError(400, "token not provided")
}
const session = await Session.findOne({
user_id,
token
})
if (!session) {
throw new OperationError(400, "Session not found")
}
await session.delete()
return {
success: true
}
}

View File

@ -0,0 +1,13 @@
import { Session } from "@db_models"
export default async (payload = {}) => {
const { user_id } = payload
if (!user_id) {
throw new OperationError(400, "user_id not provided")
}
const sessions = await Session.find({ user_id })
return sessions
}

View File

@ -2,5 +2,9 @@
"name": "auth",
"version": "1.0.0",
"main": "index.js",
"license": "MIT"
"license": "MIT",
"proxy":{
"namespace": "/auth",
"port": 3020
}
}

View File

@ -0,0 +1,20 @@
import { User } from "@db_models"
const emailRegex = new RegExp(/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/)
export default async (req) => {
const { username } = req.params
const query = {
username
}
if (emailRegex.test(username)) {
delete query.username
query.email = username
}
return {
exists: !!await User.exists(query)
}
}

View File

@ -1,6 +1,7 @@
import AuthToken from "@shared-classes/AuthToken"
import { UserConfig, MFASession } from "@db_models"
import requiredFields from "@shared-utils/requiredFields"
import obscureEmail from "@shared-utils/obscureEmail"
import Account from "@classes/account"
@ -66,6 +67,8 @@ export default async (req, res) => {
return {
message: `MFA required, using [${mfa.type}] method.`,
method: mfa.type,
sended_to: obscureEmail(user.email),
mfa_required: true,
}
}

View File

@ -0,0 +1,10 @@
import AccountClass from "@classes/account"
export default {
middlewares: ["withAuthentication"],
fn: async (req, res) => {
return await AccountClass.sessions({
user_id: req.auth.session.user_id
})
}
}

View File

@ -0,0 +1,11 @@
import AccountClass from "@classes/account"
export default {
middlewares: ["withAuthentication"],
fn: async (req, res) => {
return await AccountClass.deleteSession({
user_id: req.auth.session.user_id,
token: req.auth.token,
})
}
}

View File

@ -1,8 +1,6 @@
export default {
method: "GET",
route: "/current",
middlewares: ["withAuthentication"],
fn: async (req, res) => {
return res.json(req.currentSession)
return req.auth.session
}
}

View File

@ -3,12 +3,14 @@ import { User } from "@db_models"
import templates from "../templates"
export default async (ctx, data) => {
const user = await User.findById(data.user_id)
const user = await User.findById(data.user_id).select("+email")
if (!user) {
throw new OperationError(404, "User not found")
}
console.log(`Sending MFA code to ${user.email}...`)
const result = await ctx.mailTransporter.sendMail({
from: process.env.SMTP_USERNAME,
to: user.email,

View File

@ -2,7 +2,7 @@ import { User } from "@db_models"
import templates from "../templates"
export default async (ctx, data) => {
const user = await User.findById(data.user_id)
const user = await User.findById(data.user_id).select("+email")
if (!user) {
throw new OperationError(404, "User not found")

View File

@ -2,7 +2,7 @@ import { User } from "@db_models"
import templates from "../templates"
export default async (ctx, data) => {
const user = await User.findById(data.user_id)
const user = await User.findById(data.user_id).select("+email")
if (!user) {
throw new OperationError(404, "User not found")

View File

@ -12,7 +12,7 @@ class API extends Server {
static refName = "files"
static useEngine = "hyper-express"
static routesPath = `${__dirname}/routes`
static listen_port = process.env.HTTP_LISTEN_PORT ?? 3008
static listen_port = process.env.HTTP_LISTEN_PORT ?? 3002
static maxBodyLength = 1000 * 1000 * 1000

View File

@ -1,29 +0,0 @@
import { Schematized } from "@lib"
import newComment from "../services/newComment"
export default {
method: "POST",
route: "/post/:post_id",
middlewares: ["withAuthentication"],
fn: Schematized({
required: ["message"],
select: ["message"],
}, async (req, res) => {
const { post_id } = req.params
const { message } = req.selection
try {
const comment = await newComment({
user_id: req.user._id.toString(),
parent_id: post_id,
message: message,
})
return res.json(comment)
} catch (error) {
return res.status(400).json({
error: error.message,
})
}
})
}

View File

@ -1,21 +0,0 @@
import deleteComment from "../services/deleteComment"
export default {
method: "DELETE",
route: "/post/:post_id/:comment_id",
middlewares: ["withAuthentication"],
fn: async (req, res) => {
const result = await deleteComment({
comment_id: req.params.comment_id,
issuer_id: req.user._id.toString(),
}).catch((err) => {
res.status(500).json({ message: err.message })
return false
})
if (result) {
return res.json(result)
}
}
}

View File

@ -1,21 +0,0 @@
import getComments from "../services/getComments"
export default {
method: "GET",
route: "/post/:post_id",
fn: async (req, res) => {
const { post_id } = req.params
const comments = await getComments({ parent_id: post_id }).catch((err) => {
res.status(400).json({
error: err.message,
})
return false
})
if (!comments) return
return res.json(comments)
}
}

View File

@ -1,9 +0,0 @@
import { Controller } from "linebridge/dist/server"
import generateEndpointsFromDir from "linebridge/dist/server/lib/generateEndpointsFromDir"
export default class CommentsController extends Controller {
static refName = "CommentsController"
static useRoute = "/comments"
httpEndpoints = generateEndpointsFromDir(__dirname + "/endpoints")
}

View File

@ -1,32 +0,0 @@
import { Comment } from "@db_models"
import CheckUserAdmin from "../../../lib/checkUserAdmin"
export default async (payload) => {
const { issuer_id, comment_id } = payload
if (!issuer_id) {
throw new Error("Missing issuer_id")
}
if (!comment_id) {
throw new Error("Missing comment_id")
}
const isAdmin = await CheckUserAdmin(issuer_id)
const comment = await Comment.findById(comment_id)
if (!comment) {
throw new Error("Comment not found")
}
if (comment.user_id !== issuer_id && !isAdmin) {
throw new Error("You can't delete this comment, cause you are not the owner.")
}
await comment.delete()
global.engine.ws.io.of("/").emit(`post.delete.comment.${comment.parent_id.toString()}`, comment_id)
return comment
}

View File

@ -1,25 +0,0 @@
import { User, Comment } from "@db_models"
export default async (payload = {}) => {
const { parent_id } = payload
if (!parent_id) {
throw new Error("Missing parent_id")
}
// get comments by descending order
let comments = await Comment.find({ parent_id: parent_id })
.sort({ created_at: -1 })
// fullfill comments with user data
comments = await Promise.all(comments.map(async comment => {
const user = await User.findById(comment.user_id)
return {
...comment.toObject(),
user: user.toObject(),
}
}))
return comments
}

View File

@ -1,34 +0,0 @@
import { User, Comment } from "@db_models"
export default async (payload) => {
const { parent_id, message, user_id } = payload
if (!parent_id) {
throw new Error("Missing parent_id")
}
if (!message) {
throw new Error("Missing message")
}
if (!user_id) {
throw new Error("Missing user_id")
}
const comment = new Comment({
user_id: user_id,
parent_id: parent_id,
message: message,
})
await comment.save()
const userData = await User.findById(user_id)
global.engine.ws.io.of("/").emit(`post.new.comment.${parent_id}`, {
...comment.toObject(),
user: userData.toObject(),
})
return comment
}

View File

@ -1,140 +0,0 @@
import { Controller } from "linebridge/dist/server"
import pmap from "p-map"
import getPosts from "./services/getPosts"
import getGlobalReleases from "./services/getGlobalReleases"
import getReleasesFromFollowing from "./services/getReleasesFromFollowing"
import getPlaylistsFromFollowing from "./services/getPlaylistsFromFollowing"
export default class FeedController extends Controller {
static refName = "FeedController"
static useRoute = "/feed"
httpEndpoints = {
get: {
"/timeline": {
middlewares: ["withAuthentication"],
fn: async (req, res) => {
const for_user_id = req.user?._id.toString()
if (!for_user_id) {
return res.status(400).json({
error: "Invalid user id"
})
}
// fetch posts
let posts = await getPosts({
for_user_id,
limit: req.query?.limit,
skip: req.query?.trim,
})
// add type to posts and playlists
posts = posts.map((data) => {
data.type = "post"
return data
})
let feed = [
...posts,
]
// sort feed
feed.sort((a, b) => {
return new Date(b.created_at) - new Date(a.created_at)
})
return res.json(feed)
}
},
"/music/global": {
middlewares: ["withAuthentication"],
fn: async (req, res) => {
const for_user_id = req.user?._id.toString()
if (!for_user_id) {
return res.status(400).json({
error: "Invalid user id"
})
}
// fetch playlists from global
const result = await getGlobalReleases({
for_user_id,
limit: req.query?.limit,
skip: req.query?.trim,
})
return res.json(result)
}
},
"/music": {
middlewares: ["withAuthentication"],
fn: async (req, res) => {
const for_user_id = req.user?._id.toString()
if (!for_user_id) {
return res.status(400).json({
error: "Invalid user id"
})
}
const searchers = [
getGlobalReleases,
//getReleasesFromFollowing,
//getPlaylistsFromFollowing,
]
let result = await pmap(
searchers,
async (fn, index) => {
const data = await fn({
for_user_id,
limit: req.query?.limit,
skip: req.query?.trim,
})
return data
}, {
concurrency: 3,
},)
result = result.reduce((acc, cur) => {
return [...acc, ...cur]
}, [])
return res.json(result)
}
},
"/posts": {
middlewares: ["withAuthentication"],
fn: async (req, res) => {
const for_user_id = req.user?._id.toString()
if (!for_user_id) {
return res.status(400).json({
error: "Invalid user id"
})
}
let feed = []
// fetch posts
const posts = await getPosts({
for_user_id,
limit: req.query?.limit,
skip: req.query?.trim,
})
feed = feed.concat(posts)
return res.json(feed)
}
},
}
}
}

View File

@ -1,25 +0,0 @@
import { Release } from "@db_models"
export default async (payload) => {
const {
limit = 20,
skip = 0,
} = payload
let releases = await Release.find({
$or: [
{ public: true },
]
})
.sort({ created_at: -1 })
.limit(limit)
.skip(skip)
releases = Promise.all(releases.map(async (release) => {
release = release.toObject()
return release
}))
return releases
}

View File

@ -1,39 +0,0 @@
import { Post, UserFollow } from "@db_models"
import fullfillPostsData from "@utils/fullfillPostsData"
export default async (payload) => {
const {
for_user_id,
limit = 20,
skip = 0,
} = payload
// get post from users that the user follows
const followingUsers = await UserFollow.find({
user_id: for_user_id
})
const followingUserIds = followingUsers.map((followingUser) => followingUser.to)
const fetchPostsFromIds = [
for_user_id,
...followingUserIds,
]
let posts = await Post.find({
user_id: { $in: fetchPostsFromIds }
})
.sort({ created_at: -1 })
.limit(limit)
.skip(skip)
// fullfill data
posts = await fullfillPostsData({
posts,
for_user_id,
skip,
})
return posts
}

View File

@ -1,17 +0,0 @@
import { UserFollow } from "@db_models"
export default {
method: "GET",
route: "/user/:user_id",
middlewares: ["withAuthentication"],
fn: async (req, res) => {
const isFollowed = await UserFollow.findOne({
user_id: req.user._id.toString(),
to: req.params.user_id,
}).catch(() => false)
return res.json({
isFollowed: Boolean(isFollowed),
})
}
}

View File

@ -1,29 +0,0 @@
import { User, UserFollow } from "@db_models"
export default {
method: "GET",
route: "/user/:user_id/followers",
fn: async (req, res) => {
const { limit = 30, offset } = req.query
let followers = []
const follows = await UserFollow.find({
to: req.params.user_id,
})
.limit(limit)
.skip(offset)
for await (const follow of follows) {
const user = await User.findById(follow.user_id)
if (!user) {
continue
}
followers.push(user.toObject())
}
return res.json(followers)
}
}

View File

@ -1,60 +0,0 @@
import { Schematized } from "@lib"
import { User, UserFollow } from "@db_models"
import followUser from "../services/followUser"
import unfollowUser from "../services/unfollowUser"
export default {
method: "POST",
route: "/user/toggle",
middlewares: ["withAuthentication"],
fn: Schematized({
select: ["user_id", "username"],
}, async (req, res) => {
const selfUserId = req.user._id.toString()
let targetUserId = null
let result = null
if (typeof req.selection.user_id === "undefined" && typeof req.selection.username === "undefined") {
return res.status(400).json({ message: "No user_id or username provided" })
}
if (typeof req.selection.user_id !== "undefined") {
targetUserId = req.selection.user_id
} else {
const user = await User.findOne({ username: req.selection.username })
if (!user) {
return res.status(404).json({ message: "User not found" })
}
targetUserId = user._id.toString()
}
// check if already following
const isFollowed = await UserFollow.findOne({
user_id: selfUserId,
to: targetUserId,
})
// if already following, delete
if (isFollowed) {
result = await unfollowUser({
user_id: selfUserId,
to: targetUserId,
}).catch((error) => {
return res.status(500).json({ message: error.message })
})
} else {
result = await followUser({
user_id: selfUserId,
to: targetUserId,
}).catch((error) => {
return res.status(500).json({ message: error.message })
})
}
return res.json(result)
})
}

View File

@ -1,9 +0,0 @@
import { Controller } from "linebridge/dist/server"
import generateEndpointsFromDir from "linebridge/dist/server/lib/generateEndpointsFromDir"
export default class FollowController extends Controller {
static refName = "FollowController"
static useRoute = "/follow"
httpEndpoints = generateEndpointsFromDir(__dirname + "/endpoints")
}

View File

@ -1,48 +0,0 @@
import { User, UserFollow } from "@db_models"
export default async (payload) => {
if (typeof payload.user_id === "undefined") {
throw new Error("No user_id provided")
}
if (typeof payload.to === "undefined") {
throw new Error("No to provided")
}
const user = await User.findById(payload.user_id)
if (!user) {
throw new Error("User not found")
}
const follow = await UserFollow.findOne({
user_id: payload.user_id,
to: payload.to,
})
if (follow) {
throw new Error("Already following")
}
const newFollow = await UserFollow.create({
user_id: payload.user_id,
to: payload.to,
})
await newFollow.save()
global.engine.ws.io.of("/").emit(`user.follow`, {
...user.toObject(),
})
global.engine.ws.io.of("/").emit(`user.follow.${payload.user_id}`, {
...user.toObject(),
})
const followers = await UserFollow.find({
to: payload.to,
})
return {
following: true,
followers: followers,
}
}

View File

@ -1,43 +0,0 @@
import { User, UserFollow } from "@db_models"
export default async (payload) => {
if (typeof payload.user_id === "undefined") {
throw new Error("No user_id provided")
}
if (typeof payload.to === "undefined") {
throw new Error("No to provided")
}
const user = await User.findById(payload.user_id)
if (!user) {
throw new Error("User not found")
}
const follow = await UserFollow.findOne({
user_id: payload.user_id,
to: payload.to,
})
if (!follow) {
throw new Error("Not following")
}
await follow.remove()
global.engine.ws.io.of("/").emit(`user.unfollow`, {
...user.toObject(),
})
global.engine.ws.io.of("/").emit(`user.unfollow.${payload.user_id}`, {
...user.toObject(),
})
const followers = await UserFollow.find({
to: payload.to,
})
return {
following: false,
followers: followers,
}
}

View File

@ -1,47 +0,0 @@
import { Post, } from "@db_models"
import toggleLike from "../../PostsController/services/toggleLike"
export default {
method: "POST",
route: "/:post_id/mok_likes",
middlewares: ["withAuthentication", "onlyAdmin"],
fn: async (req, res) => {
const {
count,
interval = 100,
} = req.body
if (count < 1) {
return res.status(400).json({
error: "Invalid count, must be greater than 0",
})
}
let postData = await Post.findById(req.params.post_id)
if (!postData) {
return res.status(404).json({
error: "Post not found",
})
}
for (let i = 0; i < count; i++) {
const mokUserId = `mok_${i}_${count}`
toggleLike({
post_id: postData._id.toString(),
user_id: mokUserId,
to: true
})
await new Promise((resolve) => setTimeout(resolve, interval ?? 100))
continue
}
return res.status(200).json({
message: "Success",
data: postData
})
}
}

View File

@ -1,10 +0,0 @@
import { Controller } from "linebridge/dist/server"
import generateEndpointsFromDir from "linebridge/dist/server/lib/generateEndpointsFromDir"
export default class ModerationController extends Controller {
static refName = "ModerationController"
static useRoute = "/mod"
static reachable = false
httpEndpoints = generateEndpointsFromDir(__dirname + "/endpoints")
}

View File

@ -1,22 +0,0 @@
import { Schematized } from "@lib"
import { CreatePost } from "../services"
export default {
method: "POST",
route: "/new",
middlewares: ["withAuthentication"],
fn: Schematized({
required: ["timestamp"],
select: ["message", "attachments", "timestamp", "reply_to"],
}, async (req, res) => {
const post = await CreatePost({
user_id: req.user._id.toString(),
message: req.selection.message,
timestamp: req.selection.timestamp,
attachments: req.selection.attachments,
reply_to: req.selection.reply_to,
})
return res.json(post)
})
}

View File

@ -1,26 +0,0 @@
import { DeletePost } from "../services"
export default {
method: "DELETE",
route: "/:post_id",
middlewares: ["withAuthentication"],
fn: async (req, res) => {
const post = await DeletePost({
post_id: req.params.post_id,
by_user_id: req.user._id.toString(),
}).catch((err) => {
res.status(400).json({
error: err.message
})
return false
})
if (!post) return
return res.json({
success: true,
post
})
}
}

View File

@ -1,20 +0,0 @@
import { Schematized } from "@lib"
import { GetPostData } from "../services"
export default {
method: "GET",
route: "/explore",
middlewares: ["withOptionalAuthentication"],
fn: Schematized({
select: ["user_id"]
}, async (req, res) => {
let posts = await GetPostData({
limit: req.query?.limit,
skip: req.query?.trim,
from_user_id: req.query?.user_id,
for_user_id: req.user?._id.toString(),
})
return res.json(posts)
})
}

View File

@ -1,21 +0,0 @@
import { GetPostData } from "../services"
export default {
method: "GET",
route: "/post/:post_id",
middlewares: ["withOptionalAuthentication"],
fn: async (req, res) => {
let post = await GetPostData({
post_id: req.params.post_id,
for_user_id: req.user?._id.toString(),
}).catch((error) => {
res.status(404).json({ error: error.message })
return null
})
if (!post) return
return res.json(post)
}
}

View File

@ -1,17 +0,0 @@
import { GetPostData } from "../services"
export default {
method: "GET",
route: "/user/:user_id",
middlewares: ["withOptionalAuthentication"],
fn: async (req, res) => {
let posts = await GetPostData({
limit: req.query?.limit,
skip: req.query?.trim,
for_user_id: req.user?._id.toString(),
from_user_id: req.params.user_id,
})
return res.json(posts)
}
}

View File

@ -1,29 +0,0 @@
import { Post } from "@db_models"
import fullfillPostsData from "@utils/fullfillPostsData"
export default {
method: "GET",
route: "/post/:post_id/replies",
middlewares: ["withOptionalAuthentication"],
fn: async (req, res) => {
const {
limit = 50,
offset = 0,
} = req.query
let replies = await Post.find({
reply_to: req.params.post_id,
})
.skip(offset)
.limit(limit)
.sort({ created_at: -1 })
replies = await fullfillPostsData({
posts: replies,
for_user_id: req.user?._id.toString(),
skip: offset,
})
return res.json(replies)
}
}

View File

@ -1,20 +0,0 @@
import { Schematized } from "@lib"
import { GetPostData } from "../services"
export default {
method: "GET",
route: "/saved",
middlewares: ["withAuthentication"],
fn: Schematized({
select: ["user_id"]
}, async (req, res) => {
let posts = await GetPostData({
limit: req.query?.limit,
skip: req.query?.trim,
for_user_id: req.user?._id.toString(),
savedOnly: true,
})
return res.json(posts)
})
}

View File

@ -1,29 +0,0 @@
import { Schematized } from "@lib"
import { ToogleLike } from "../services"
export default {
method: "POST",
route: "/:post_id/toggle_like",
middlewares: ["withAuthentication"],
fn: Schematized({
select: ["to"],
}, async (req, res) => {
const post = await ToogleLike({
user_id: req.user._id.toString(),
post_id: req.params.post_id,
to: req.selection.to,
}).catch((err) => {
res.status(400).json({
error: err.message
})
return false
})
if (!post) return
return res.json({
success: true,
post
})
})
}

View File

@ -1,25 +0,0 @@
import { ToogleSavePost } from "../services"
export default {
method: "POST",
route: "/:post_id/toggle_save",
middlewares: ["withAuthentication"],
fn: async (req, res) => {
const post = await ToogleSavePost({
user_id: req.user._id.toString(),
post_id: req.params.post_id,
}).catch((err) => {
res.status(400).json({
error: err.message
})
return false
})
if (!post) return
return res.json({
success: true,
post
})
}
}

View File

@ -1,19 +0,0 @@
import { Controller } from "linebridge/dist/server"
import generateEndpointsFromDir from "linebridge/dist/server/lib/generateEndpointsFromDir"
export default class PostsController extends Controller {
static refName = "PostsController"
static useRoute = "/posts"
httpEndpoints = generateEndpointsFromDir(__dirname + "/endpoints")
// put = {
// "/:post_id": {
// middlewares: ["withAuthentication"],
// fn: (req, res) => {
// // TODO: Implement Post update
// return res.status(501).json({ error: "Not implemented" })
// }
// }
// }
}

View File

@ -1,45 +0,0 @@
import momentTimezone from "moment-timezone"
import { Post } from "@db_models"
import getPostData from "./getPostData"
import flagNsfwByAttachments from "./flagNsfwByAttachments"
export default async (payload) => {
let { user_id, message, attachments, timestamp, reply_to } = payload
// check if is a Array and have at least one element
const isAttachmentsValid = Array.isArray(attachments) && attachments.length > 0
if (!isAttachmentsValid && !message) {
throw new Error("Cannot create a post without message or attachments")
}
if (message) {
message = String(message).toString().trim()
}
const current_timezone = momentTimezone.tz.guess()
const created_at = momentTimezone.tz(Date.now(), current_timezone).format()
const post = new Post({
created_at: created_at,
user_id: typeof user_id === "object" ? user_id.toString() : user_id,
message: message,
attachments: attachments ?? [],
timestamp: timestamp,
reply_to: reply_to,
flags: [],
})
await post.save()
const resultPost = await getPostData({ post_id: post._id.toString() })
global.engine.ws.io.of("/").emit(`post.new`, resultPost)
global.engine.ws.io.of("/").emit(`post.new.${post.user_id}`, resultPost)
// push to background job to check if is NSFW
flagNsfwByAttachments(post._id.toString())
return post
}

View File

@ -1,33 +0,0 @@
import { Post, User } from "@db_models"
export default async (payload) => {
const { post_id, by_user_id } = payload
if (!by_user_id) {
throw new Error("by_user_id not provided")
}
const post = await Post.findById(post_id)
if (!post) {
throw new Error("Post not found")
}
const userData = await User.findById(by_user_id)
if (!userData) {
throw new Error("User not found")
}
const hasAdmin = userData.roles.includes("admin")
// check if user is the owner of the post
if (post.user_id !== by_user_id && !hasAdmin) {
throw new Error("You are not allowed to delete this post")
}
await post.remove()
global.engine.ws.io.of("/").emit(`post.delete`, post_id)
return post.toObject()
}

View File

@ -1,54 +0,0 @@
import { Post } from "@db_models"
import indecentPrediction from "../../../utils/indecent-prediction"
import isNSFW from "../../../utils/is-nsfw"
import modifyPostData from "./modifyPostData"
export default async (post_id) => {
try {
if (!post_id) {
throw new Error("Post ID is required")
}
let post = await Post.findById(post_id)
if (!post) {
throw new Error("Post not found")
}
let flags = []
// run indecentPrediction to all attachments
if (Array.isArray(post.attachments) && post.attachments.length > 0) {
for await (const attachment of post.attachments) {
const prediction = await indecentPrediction({
url: attachment.url,
}).catch((err) => {
console.log("Error while checking", attachment, err)
return null
})
if (prediction) {
const isNsfw = isNSFW(prediction)
if (isNsfw) {
flags.push("nsfw")
}
}
}
}
// if is there new flags update post
if (post.flags !== flags) {
await modifyPostData(post_id, {
flags: flags,
})
}
return flags
} catch (error) {
console.error(error)
return []
}
}

View File

@ -1,71 +0,0 @@
import { Post, SavedPost } from "@db_models"
import fullfillPostsData from "@utils/fullfillPostsData"
export default async (payload) => {
let {
from_user_id,
for_user_id,
post_id,
query = {},
skip = 0,
limit = 20,
sort = { created_at: -1 },
savedOnly = false,
} = payload
let posts = []
let savedPostsIds = []
// if for_user_id is provided, get saved posts
if (for_user_id) {
const savedPosts = await SavedPost.find({ user_id: for_user_id })
.sort({ saved_at: -1 })
savedPostsIds = savedPosts.map((savedPost) => savedPost.post_id)
}
// if from_user_id is provided, get posts from that user
if (from_user_id) {
query.user_id = from_user_id
}
// if savedOnly is true,set to query to get only saved posts
if (savedOnly) {
query._id = { $in: savedPostsIds }
}
if (post_id) {
const post = await Post.findById(post_id).catch(() => false)
posts = [post]
} else {
posts = await Post.find({ ...query })
.sort(sort)
.skip(skip)
.limit(limit)
}
// short posts if is savedOnly argument
if (savedOnly) {
posts.sort((a, b) => {
return (
savedPostsIds.indexOf(a._id.toString()) -
savedPostsIds.indexOf(b._id.toString())
)
})
}
// fullfill data
posts = await fullfillPostsData({
posts,
for_user_id,
skip,
})
// if post_id is specified, return only one post
if (post_id) {
return posts[0]
}
return posts
}

View File

@ -1,8 +0,0 @@
export { default as CreatePost } from "./createPost"
export { default as ToogleLike } from "./toggleLike"
export { default as ToogleSavePost } from "./togglePostSave"
export { default as GetPostData } from "./getPostData"
export { default as DeletePost } from "./deletePost"
export { default as ModifyPostData } from "./modifyPostData"

View File

@ -1,31 +0,0 @@
import { Post } from "@db_models"
import getPostData from "./getPostData"
export default async (post_id, modification) => {
if (!post_id) {
throw new Error("Cannot modify post data: post not found")
}
let post = await getPostData({ post_id: post_id })
if (!post) {
throw new Error("Cannot modify post data: post not found")
}
if (typeof modification === "object") {
const result = await Post.findByIdAndUpdate(post_id, modification)
await result.save()
post = {
...post,
...result.toObject(),
...modification,
}
}
global.engine.ws.io.of("/").emit(`post.dataUpdate`, post)
global.engine.ws.io.of("/").emit(`post.dataUpdate.${post_id}`, post)
return post
}

View File

@ -1,37 +0,0 @@
import { PostLike } from "@db_models"
export default async (payload) => {
let { post_id, user_id, to } = payload
let likeObj = await PostLike.findOne({
post_id,
user_id,
}).catch(() => false)
if (typeof to === "undefined") {
if (likeObj) {
to = false
} else {
to = true
}
}
if (to) {
likeObj = new PostLike({
post_id,
user_id,
})
await likeObj.save()
} else {
await PostLike.findByIdAndDelete(likeObj._id)
}
global.engine.ws.io.of("/").emit(`post.${post_id}.likes.update`, {
to,
post_id,
user_id,
})
return likeObj
}

View File

@ -1,30 +0,0 @@
import { SavedPost } from "@db_models"
export default async (payload) => {
let { post_id, user_id } = payload
if (!post_id || !user_id) {
throw new Error("Missing post_id or user_id")
}
let post = await SavedPost.findOne({ post_id, user_id }).catch((err) => {
return false
})
if (post) {
await SavedPost.findByIdAndDelete(post._id).catch((err) => {
throw new Error("Cannot delete saved post")
})
} else {
post = new SavedPost({
post_id,
user_id,
})
await post.save().catch((err) => {
throw new Error("Cannot save post")
})
}
return post
}

View File

@ -1,115 +0,0 @@
import { Controller } from "linebridge/dist/server"
import { Role, User } from "@db_models"
import { Schematized } from "@lib"
export default class RolesController extends Controller {
static refName = "RolesController"
static useMiddlewares = ["roles"]
httpEndpoints = {
get: {
"/roles": Schematized({
select: ["user_id", "username"],
}, async (req, res) => {
const roles = await Role.find()
return res.json(roles)
}),
"/roles/self": {
middlewares: ["withAuthentication"],
fn: async (req, res) => {
const user = await User.findOne({
_id: req.user._id.toString(),
})
if (!user) {
return res.status(404).json({ error: "No user founded" })
}
return res.json(user.roles)
}
},
},
post: {
"/role": {
middlewares: ["withAuthentication"],
fn: Schematized({
required: ["name"],
select: ["name", "description"],
}, async (req, res) => {
await Role.findOne(req.selection).then((data) => {
if (data) {
return res.status(409).json("This role is already created")
}
let role = new Role({
name: req.selection.name,
description: req.selection.description,
})
role.save()
return res.json(role)
})
})
},
"/update_user_roles": {
middlewares: ["withAuthentication"],
fn: Schematized({
required: ["update"],
select: ["update"],
}, async (req, res) => {
// check if issuer user is admin
if (!req.isAdmin()) {
return res.status(403).json("You do not have administrator permission")
}
if (!Array.isArray(req.selection.update)) {
return res.status(400).json("Invalid update request")
}
req.selection.update.forEach(async (update) => {
const user = await User.findById(update._id).catch(err => {
return false
})
console.log(update.roles)
if (user) {
user.roles = update.roles
await user.save()
}
})
return res.json("done")
}),
},
},
delete: {
"/role": {
middlewares: ["withAuthentication"],
fn: Schematized({
required: ["name"],
select: ["name"],
}, async (req, res) => {
if (req.selection.name === "admin") {
return res.status(409).json("You can't delete admin role")
}
await Role.findOne(req.selection).then((data) => {
if (!data) {
return res.status(404).json("This role is not found")
}
data.remove()
return res.json(data)
})
})
},
},
}
}

View File

@ -1,16 +0,0 @@
export default {
method: "DELETE",
route: "/all",
middlewares: ["withAuthentication"],
fn: async (req, res) => {
const user_id = req.user._id.toString()
const allSessions = await Session.deleteMany({ user_id })
if (allSessions) {
return res.json("done")
}
return res.status(404).json("not found")
}
}

View File

@ -1,27 +0,0 @@
import { Session } from "@db_models"
export default {
method: "DELETE",
route: "/current",
middlewares: ["withAuthentication"],
fn: async (req, res) => {
const token = req.jwtToken
const user_id = req.user._id.toString()
if (typeof token === "undefined") {
return res.status(400).json("Cannot access to token")
}
const session = await Session.findOneAndDelete({ user_id, token })
if (session) {
return res.json({
message: "done",
})
}
return res.status(404).json({
error: "Session not found",
})
},
}

View File

@ -1,13 +0,0 @@
import { Session } from "@db_models"
export default {
method: "GET",
route: "/all",
middlewares: ["withAuthentication"],
fn: async (req, res) => {
const sessions = await Session.find({ user_id: req.user._id.toString() }, { token: 0 })
.sort({ date: -1 })
return res.json(sessions)
},
}

View File

@ -1,19 +0,0 @@
import Token from "@lib/token"
export default {
method: "POST",
route: "/regenerate",
fn: async (req, res) => {
const { expiredToken, refreshToken } = req.body
const token = await Token.regenerate(expiredToken, refreshToken).catch((error) => {
res.status(400).json({ error: error.message })
return null
})
if (!token) return
return res.json({ token })
},
}

View File

@ -1,13 +0,0 @@
import Token from "@lib/token"
export default {
method: "POST",
route: "/validate",
fn: async (req, res) => {
const token = req.body.token
const result = await Token.validate(token)
return res.json(result)
},
}

View File

@ -1,9 +0,0 @@
import { Controller } from "linebridge/dist/server"
import generateEndpointsFromDir from "linebridge/dist/server/lib/generateEndpointsFromDir"
export default class SessionController extends Controller {
static refName = "SessionController"
static useRoute = "/session"
httpEndpoints = generateEndpointsFromDir(__dirname + "/endpoints")
}

View File

@ -1,15 +0,0 @@
import { User } from "@db_models"
export default {
method: "GET",
route: "/email_available",
fn: async (req, res) => {
const user = await User.findOne({
email: req.query.email,
})
return res.json({
available: !user,
})
}
}

Some files were not shown because too many files have changed in this diff Show More