mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 18:44:16 +00:00
implement sync_server
This commit is contained in:
parent
2d464a7fae
commit
13bdccd53d
20
packages/sync_server/.env-example
Executable file
20
packages/sync_server/.env-example
Executable file
@ -0,0 +1,20 @@
|
|||||||
|
DB_HOSTNAME=""
|
||||||
|
DB_NAME=""
|
||||||
|
DB_USER=""
|
||||||
|
DB_PWD=""
|
||||||
|
|
||||||
|
S3_ENDPOINT=""
|
||||||
|
S3_REGION=""
|
||||||
|
S3_PORT=""
|
||||||
|
S3_USE_SSL=""
|
||||||
|
S3_BUCKET=""
|
||||||
|
S3_ACCESS_KEY=""
|
||||||
|
S3_SECRET_KEY=""
|
||||||
|
|
||||||
|
REDIS_HOST=""
|
||||||
|
REDIS_PORT=""
|
||||||
|
REDIS_USERNAME=""
|
||||||
|
REDIS_PASSWORD=""
|
||||||
|
|
||||||
|
COMTY_ACCESS_KEY=""
|
||||||
|
COMTY_SECRET_KEY=""
|
2
packages/sync_server/.gitignore
vendored
Normal file
2
packages/sync_server/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/static
|
||||||
|
shared-classes
|
7
packages/sync_server/.infisical.json
Normal file
7
packages/sync_server/.infisical.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"workspaceId": "64519574a8a691b55e8b361d",
|
||||||
|
"defaultEnvironment": "dev",
|
||||||
|
"gitBranchToEnvironmentMapping": {
|
||||||
|
"master": "prod"
|
||||||
|
}
|
||||||
|
}
|
27
packages/sync_server/Dockerfile
Executable file
27
packages/sync_server/Dockerfile
Executable file
@ -0,0 +1,27 @@
|
|||||||
|
FROM node:16-bullseye-slim
|
||||||
|
|
||||||
|
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
|
||||||
|
|
||||||
|
RUN apt update
|
||||||
|
RUN apt install --no-install-recommends curl python yarn build-essential -y
|
||||||
|
|
||||||
|
RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app
|
||||||
|
|
||||||
|
# Copy comty.js to node_modules
|
||||||
|
WORKDIR /home/node/app
|
||||||
|
USER node
|
||||||
|
|
||||||
|
EXPOSE 3070
|
||||||
|
|
||||||
|
COPY package.json ./
|
||||||
|
COPY --chown=node:node . .
|
||||||
|
|
||||||
|
RUN chmod -R 777 /home/node/app
|
||||||
|
|
||||||
|
RUN export NODE_ENV=production
|
||||||
|
|
||||||
|
RUN yarn global add cross-env
|
||||||
|
RUN yarn install --production
|
||||||
|
RUN yarn build
|
||||||
|
|
||||||
|
CMD ["yarn", "run", "run:prod"]
|
64
packages/sync_server/package.json
Normal file
64
packages/sync_server/package.json
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"name": "@comty/sync_server",
|
||||||
|
"version": "0.54.4",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "corenode-cli build",
|
||||||
|
"dev": "cross-env NODE_ENV=development nodemon --ignore dist/ --exec corenode-node ./src/index.js",
|
||||||
|
"run:prod": "cross-env NODE_ENV=production node ./dist/index.js"
|
||||||
|
},
|
||||||
|
"sharedClasses": {
|
||||||
|
"FileUpload": "src/shared-classes",
|
||||||
|
"CacheService": "src/shared-classes",
|
||||||
|
"ComtyClient": "src/shared-classes",
|
||||||
|
"RedisClient": "src/shared-classes",
|
||||||
|
"StorageClient": "src/shared-classes",
|
||||||
|
"DbManager": "src/shared-classes",
|
||||||
|
"Errors": "src/shared-classes",
|
||||||
|
"DbModels": "src/shared-classes",
|
||||||
|
"SecureSyncEntry": "src/shared-classes",
|
||||||
|
"TidalAPI": "src/shared-classes"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"7zip-min": "^1.4.4",
|
||||||
|
"@corenode/utils": "0.28.26",
|
||||||
|
"@foxify/events": "^2.1.0",
|
||||||
|
"@octokit/rest": "^19.0.7",
|
||||||
|
"axios": "^1.2.5",
|
||||||
|
"bcrypt": "^5.1.0",
|
||||||
|
"busboy": "^1.6.0",
|
||||||
|
"comty.js": "^0.54.0",
|
||||||
|
"connect-mongo": "^4.6.0",
|
||||||
|
"content-range": "^2.0.2",
|
||||||
|
"corenode": "0.28.26",
|
||||||
|
"dotenv": "^16.0.3",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"formidable": "^2.1.1",
|
||||||
|
"hyper-express": "^6.5.9",
|
||||||
|
"jsonwebtoken": "^9.0.0",
|
||||||
|
"linebridge": "0.15.12",
|
||||||
|
"live-directory": "^3.0.3",
|
||||||
|
"luxon": "^3.2.1",
|
||||||
|
"merge-files": "^0.1.2",
|
||||||
|
"mime-types": "^2.1.35",
|
||||||
|
"minio": "^7.0.32",
|
||||||
|
"moment": "^2.29.4",
|
||||||
|
"moment-timezone": "^0.5.40",
|
||||||
|
"mongoose": "^6.9.0",
|
||||||
|
"normalize-url": "^8.0.0",
|
||||||
|
"p-map": "^6.0.0",
|
||||||
|
"p-queue": "^7.3.4",
|
||||||
|
"redis": "^4.6.6",
|
||||||
|
"sharp": "^0.31.3",
|
||||||
|
"split-chunk-merge": "^1.0.0",
|
||||||
|
"sucrase": "^3.32.0",
|
||||||
|
"uglify-js": "^3.17.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"chai": "^4.3.7",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"mocha": "^10.2.0",
|
||||||
|
"nodemon": "^2.0.15"
|
||||||
|
}
|
||||||
|
}
|
142
packages/sync_server/src/api.js
Normal file
142
packages/sync_server/src/api.js
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import fs from "fs"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
|
import DbManager from "@shared-classes/DbManager"
|
||||||
|
import RedisClient from "@shared-classes/RedisClient"
|
||||||
|
import ComtyClient from "@shared-classes/ComtyClient"
|
||||||
|
|
||||||
|
import hyperexpress from "hyper-express"
|
||||||
|
|
||||||
|
import pkg from "../package.json"
|
||||||
|
|
||||||
|
export default class API {
|
||||||
|
static useMiddlewaresOrder = ["useLogger", "useCors", "useAuth"]
|
||||||
|
|
||||||
|
server = global.server = new hyperexpress.Server()
|
||||||
|
|
||||||
|
listenIp = process.env.HTTP_LISTEN_IP ?? "0.0.0.0"
|
||||||
|
listenPort = process.env.HTTP_LISTEN_PORT ?? 3070
|
||||||
|
|
||||||
|
internalRouter = new hyperexpress.Router()
|
||||||
|
|
||||||
|
db = new DbManager()
|
||||||
|
|
||||||
|
ssePools = global.ssePools = {}
|
||||||
|
|
||||||
|
comty = global.comty = ComtyClient({
|
||||||
|
useWs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
redis = global.redis = RedisClient()
|
||||||
|
|
||||||
|
async __registerControllers() {
|
||||||
|
let controllersPath = fs.readdirSync(path.resolve(__dirname, "controllers"))
|
||||||
|
|
||||||
|
for await (const controllerPath of controllersPath) {
|
||||||
|
const controller = require(path.resolve(__dirname, "controllers", controllerPath)).default
|
||||||
|
|
||||||
|
if (!controller) {
|
||||||
|
console.error(`Controller ${controllerPath} not found.`)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const controllerRouter = new hyperexpress.Router()
|
||||||
|
|
||||||
|
const controllerOutput = await controller(controllerRouter)
|
||||||
|
|
||||||
|
if (!controllerOutput) {
|
||||||
|
console.error(`Controller ${controllerPath} returning not valid handler.`)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
this.internalRouter.use(controllerOutput.path ?? "/", controllerOutput.router)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async __registerInternalMiddlewares() {
|
||||||
|
let middlewaresPath = fs.readdirSync(path.resolve(__dirname, "useMiddlewares"))
|
||||||
|
|
||||||
|
// sort middlewares
|
||||||
|
middlewaresPath = middlewaresPath.sort((a, b) => {
|
||||||
|
const aIndex = this.constructor.useMiddlewaresOrder.indexOf(a.replace(".js", ""))
|
||||||
|
const bIndex = this.constructor.useMiddlewaresOrder.indexOf(b.replace(".js", ""))
|
||||||
|
|
||||||
|
if (aIndex === -1) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bIndex === -1) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
return aIndex - bIndex
|
||||||
|
})
|
||||||
|
|
||||||
|
for await (const middlewarePath of middlewaresPath) {
|
||||||
|
const middleware = require(path.resolve(__dirname, "useMiddlewares", middlewarePath)).default
|
||||||
|
|
||||||
|
if (!middleware) {
|
||||||
|
console.error(`Middleware ${middlewarePath} not found.`)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
this.server.use(middleware)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__registerInternalRoutes() {
|
||||||
|
this.server.get("/", (req, res) => {
|
||||||
|
return res.status(200).json({
|
||||||
|
name: pkg.name,
|
||||||
|
version: pkg.version,
|
||||||
|
routes: this.__getRegisteredRoutes()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.server.any("*", (req, res) => {
|
||||||
|
return res.status(404).json({
|
||||||
|
error: "Not found",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
__getRegisteredRoutes() {
|
||||||
|
return this.internalRouter.routes.map((route) => {
|
||||||
|
return {
|
||||||
|
method: route.method,
|
||||||
|
path: route.pattern,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
const startHrTime = process.hrtime()
|
||||||
|
|
||||||
|
// initialize clients
|
||||||
|
await this.db.initialize()
|
||||||
|
await this.redis.initialize()
|
||||||
|
|
||||||
|
// register controllers & middlewares
|
||||||
|
await this.__registerInternalRoutes()
|
||||||
|
await this.__registerControllers()
|
||||||
|
await this.__registerInternalMiddlewares()
|
||||||
|
|
||||||
|
// use internal router
|
||||||
|
this.server.use(this.internalRouter)
|
||||||
|
|
||||||
|
// start server
|
||||||
|
await this.server.listen(this.listenPort, this.listenIp)
|
||||||
|
|
||||||
|
// calculate elapsed time
|
||||||
|
const elapsedHrTime = process.hrtime(startHrTime)
|
||||||
|
const elapsedTimeInMs = elapsedHrTime[0] * 1000 + elapsedHrTime[1] / 1e6
|
||||||
|
|
||||||
|
// log server started
|
||||||
|
console.log(`🚀 Server started ready on \n\t - http://${this.listenIp}:${this.listenPort} \n\t - Tooks ${elapsedTimeInMs}ms`)
|
||||||
|
}
|
||||||
|
}
|
11
packages/sync_server/src/controllers/main/index.js
Normal file
11
packages/sync_server/src/controllers/main/index.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import path from "path"
|
||||||
|
import createRoutesFromDirectory from "@utils/createRoutesFromDirectory"
|
||||||
|
|
||||||
|
export default async (router) => {
|
||||||
|
router = createRoutesFromDirectory("routes", path.resolve(__dirname, "routes"), router)
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: "/",
|
||||||
|
router,
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
export default (req, res) => {
|
||||||
|
console.log(res.sse)
|
||||||
|
if (!res.sse) {
|
||||||
|
return res.json({
|
||||||
|
error: "Server event stream is not enabled.",
|
||||||
|
details: "SSE is required for this request."
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
res.sse.open()
|
||||||
|
|
||||||
|
res.sse.uid = req.params.sse_uid
|
||||||
|
global.ssePools[req.params.sse_uid] = res.sse
|
||||||
|
|
||||||
|
const pingInterval = setInterval(() => {
|
||||||
|
res.sse.send("ping")
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
res.once("close", () => {
|
||||||
|
clearInterval(pingInterval)
|
||||||
|
delete global.ssePools[req.params.sse_uid]
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200)
|
||||||
|
res.header('Content-Type', "text/event-stream")
|
||||||
|
res.header('Cache-Control', 'no-cache')
|
||||||
|
res.header('Connection', 'keep-alive')
|
||||||
|
}
|
11
packages/sync_server/src/controllers/services/index.js
Normal file
11
packages/sync_server/src/controllers/services/index.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import path from "path"
|
||||||
|
import createRoutesFromDirectory from "@utils/createRoutesFromDirectory"
|
||||||
|
|
||||||
|
export default async (router) => {
|
||||||
|
router = createRoutesFromDirectory("routes", path.resolve(__dirname, "routes"), router)
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: "/services",
|
||||||
|
router,
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
import SecureSyncEntry from "@shared-classes/SecureSyncEntry"
|
||||||
|
import { AuthorizationError, InternalServerError } from "@shared-classes/Errors"
|
||||||
|
|
||||||
|
import TidalAPI from "@shared-classes/TidalAPI"
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
if (!req.session) {
|
||||||
|
return new AuthorizationError(req, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
const authProcess = await TidalAPI.getAuthUrl()
|
||||||
|
|
||||||
|
if (!authProcess) {
|
||||||
|
return new InternalServerError(req, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkInterval = setInterval(async () => {
|
||||||
|
const response = await TidalAPI.checkAuthStatus(authProcess.device_code).catch(() => {
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
const userData = {
|
||||||
|
id: response.user.userId,
|
||||||
|
email: response.user.email,
|
||||||
|
username: response.user.username,
|
||||||
|
countryCode: response.user.countryCode,
|
||||||
|
}
|
||||||
|
|
||||||
|
// save to SecureSyncEntry
|
||||||
|
await SecureSyncEntry.set(req.session.user_id.toString(), "tidal_user", JSON.stringify(userData))
|
||||||
|
await SecureSyncEntry.set(req.session.user_id.toString(), "tidal_access_token", response.access_token)
|
||||||
|
await SecureSyncEntry.set(req.session.user_id.toString(), "tidal_refresh_token", response.refresh_token)
|
||||||
|
|
||||||
|
return clearInterval(checkInterval)
|
||||||
|
}
|
||||||
|
}, 3000)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
clearInterval(checkInterval)
|
||||||
|
}, authProcess.expires_in * 1000)
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
auth_url: authProcess.url,
|
||||||
|
device_code: authProcess.deviceCode,
|
||||||
|
})
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
import SecureSyncEntry from "@shared-classes/SecureSyncEntry"
|
||||||
|
import { AuthorizationError, InternalServerError, NotFoundError } from "@shared-classes/Errors"
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
if (!req.session) {
|
||||||
|
return new AuthorizationError(req, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = await SecureSyncEntry.get(req.session.user_id.toString(), "tidal_user")
|
||||||
|
|
||||||
|
try {
|
||||||
|
user = JSON.parse(user)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return new NotFoundError(req, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json(user)
|
||||||
|
} catch (error) {
|
||||||
|
return new InternalServerError(req, res)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
import SecureSyncEntry from "@shared-classes/SecureSyncEntry"
|
||||||
|
import { AuthorizationError, NotFoundError } from "@shared-classes/Errors"
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
if (!req.session) {
|
||||||
|
return new AuthorizationError(req, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasUser = await SecureSyncEntry.has(req.session.user_id.toString(), "tidal_user")
|
||||||
|
|
||||||
|
if (!hasUser) {
|
||||||
|
return new NotFoundError(req, res, "This account is not linked to a TIDAL account.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
active: hasUser
|
||||||
|
})
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
import SecureSyncEntry from "@shared-classes/SecureSyncEntry"
|
||||||
|
import { AuthorizationError, InternalServerError, NotFoundError } from "@shared-classes/Errors"
|
||||||
|
|
||||||
|
import TidalAPI from "@shared-classes/TidalAPI"
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
if (!req.session) {
|
||||||
|
return new AuthorizationError(req, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const access_token = await SecureSyncEntry.get(req.session.user_id.toString(), "tidal_access_token")
|
||||||
|
|
||||||
|
if (!access_token) {
|
||||||
|
return new AuthorizationError(req, res, "Its needed to link your TIDAL account to perform this action.")
|
||||||
|
}
|
||||||
|
|
||||||
|
let user_data = await SecureSyncEntry.get(req.session.user_id.toString(), "tidal_user")
|
||||||
|
|
||||||
|
user_data = JSON.parse(user_data)
|
||||||
|
|
||||||
|
const response = await TidalAPI.getTrackManifest({
|
||||||
|
track_id: req.params.track_id,
|
||||||
|
access_token: access_token,
|
||||||
|
country: user_data.countryCode
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
return new NotFoundError(req, res, "Track is not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json(response)
|
||||||
|
} catch (error) {
|
||||||
|
return new InternalServerError(req, res, error)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
import SecureSyncEntry from "@shared-classes/SecureSyncEntry"
|
||||||
|
import { AuthorizationError, InternalServerError, NotFoundError } from "@shared-classes/Errors"
|
||||||
|
|
||||||
|
import TidalAPI from "@shared-classes/TidalAPI"
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
if (!req.session) {
|
||||||
|
return new AuthorizationError(req, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
const access_token = await SecureSyncEntry.get(req.session.user_id.toString(), "tidal_access_token")
|
||||||
|
|
||||||
|
if (!access_token) {
|
||||||
|
return new AuthorizationError(req, res, "Its needed to link your TIDAL account to perform this action.")
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await TidalAPI.getTrackPlaybackUrl({
|
||||||
|
track_id: req.params.track_id,
|
||||||
|
access_token: access_token
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
return new NotFoundError(req, res, "Track is not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json(response)
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
import TidalAPI from "@shared-classes/TidalAPI"
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
const query = req.query
|
||||||
|
|
||||||
|
const response = await TidalAPI.search({
|
||||||
|
query: query.query
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.json(response)
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
import SecureSyncEntry from "@shared-classes/SecureSyncEntry"
|
||||||
|
import { AuthorizationError, InternalServerError } from "@shared-classes/Errors"
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
if (!req.session) {
|
||||||
|
return new AuthorizationError(req, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
await SecureSyncEntry.delete(req.session.user_id.toString(), "tidal_user")
|
||||||
|
await SecureSyncEntry.delete(req.session.user_id.toString(), "tidal_access_token")
|
||||||
|
await SecureSyncEntry.delete(req.session.user_id.toString(), "tidal_refresh_token")
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
}
|
130
packages/sync_server/src/index.js
Normal file
130
packages/sync_server/src/index.js
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import { webcrypto as crypto } from "crypto"
|
||||||
|
import path from "path"
|
||||||
|
import { registerBaseAliases } from "linebridge/dist/server"
|
||||||
|
import infisical from "infisical-node"
|
||||||
|
|
||||||
|
require("dotenv").config()
|
||||||
|
|
||||||
|
global.isProduction = process.env.NODE_ENV === "production"
|
||||||
|
|
||||||
|
globalThis["__root"] = path.resolve(__dirname)
|
||||||
|
|
||||||
|
const customAliases = {
|
||||||
|
"root": globalThis["__root"],
|
||||||
|
"@shared-classes": path.resolve(__dirname, "_shared/classes"),
|
||||||
|
"@services": path.resolve(__dirname, "services"),
|
||||||
|
"@lib": path.resolve(__dirname, "lib"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!global.isProduction) {
|
||||||
|
customAliases["comty.js"] = path.resolve(__dirname, "../../comty.js/src")
|
||||||
|
|
||||||
|
customAliases["@shared-classes"] = path.resolve(__dirname, "shared-classes")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.USE_LINKED_SHARED) {
|
||||||
|
customAliases["@shared-classes"] = path.resolve(__dirname, "shared-classes")
|
||||||
|
}
|
||||||
|
|
||||||
|
registerBaseAliases(undefined, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
import API from "./api"
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
if (process.env.INFISICAL_TOKEN) {
|
||||||
|
console.log(`🔑 Injecting env variables from INFISICAL...`)
|
||||||
|
|
||||||
|
const client = new infisical({
|
||||||
|
token: process.env.INFISICAL_TOKEN,
|
||||||
|
})
|
||||||
|
|
||||||
|
const secrets = await client.getAllSecrets()
|
||||||
|
|
||||||
|
// inject to process.env
|
||||||
|
secrets.forEach((secret) => {
|
||||||
|
process.env[secret.secretName] = secret.secretValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = new API()
|
||||||
|
|
||||||
|
await instance.initialize()
|
||||||
|
|
||||||
|
// kill on process exit
|
||||||
|
process.on("exit", () => {
|
||||||
|
if (typeof instance.server.close === "function") {
|
||||||
|
instance.server.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
// kill on ctrl+c
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
if (typeof instance.server.close === "function") {
|
||||||
|
instance.server.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
// kill on uncaught exceptions
|
||||||
|
process.on("uncaughtException", (error) => {
|
||||||
|
console.error(`🆘 [FATAL ERROR] >`, error)
|
||||||
|
|
||||||
|
if (typeof instance.server.close === "function") {
|
||||||
|
instance.server.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
// kill on unhandled rejections
|
||||||
|
process.on("unhandledRejection", (error) => {
|
||||||
|
console.error(`🆘 [FATAL ERROR] >`, error)
|
||||||
|
|
||||||
|
if (typeof instance.server.close === "function") {
|
||||||
|
instance.server.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(`🆘 [FATAL ERROR] >`, error)
|
||||||
|
})
|
25
packages/sync_server/src/middlewares/withAuth/index.js
Normal file
25
packages/sync_server/src/middlewares/withAuth/index.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
export default async function (req, res, next) {
|
||||||
|
// extract authentification header
|
||||||
|
let auth = req.headers.authorization
|
||||||
|
|
||||||
|
if (!auth) {
|
||||||
|
return res.status(401).json({ error: "Unauthorized, missing token" })
|
||||||
|
}
|
||||||
|
|
||||||
|
auth = auth.replace("Bearer ", "")
|
||||||
|
|
||||||
|
// check if authentification is valid
|
||||||
|
const validation = await comty.rest.session.validateToken(auth).catch((error) => {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!validation.valid) {
|
||||||
|
return res.status(401).json({ error: "Unauthorized" })
|
||||||
|
}
|
||||||
|
|
||||||
|
req.session = validation.session
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
export default async function (req, res, next) {
|
||||||
|
// extract authentification header
|
||||||
|
let auth = req.headers.authorization
|
||||||
|
|
||||||
|
if (!auth) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
auth = auth.replace("Bearer ", "")
|
||||||
|
|
||||||
|
// check if authentification is valid
|
||||||
|
const validation = await comty.rest.session.validateToken(auth).catch((error) => {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!validation.valid) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
req.sessionToken = auth
|
||||||
|
req.session = validation.session
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
55
packages/sync_server/src/middlewares/withWsAuth.js
Normal file
55
packages/sync_server/src/middlewares/withWsAuth.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
export default async (socket, next) => {
|
||||||
|
try {
|
||||||
|
const token = socket.handshake.auth.token
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return next(new Error(`auth:token_missing`))
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = await global.comty.rest.session.validateToken(token).catch((err) => {
|
||||||
|
console.error(`[${socket.id}] failed to validate session caused by server error`, err)
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: err,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!validation.valid) {
|
||||||
|
if (validation.error) {
|
||||||
|
return next(new Error(`auth:server_error`))
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(new Error(`auth:token_invalid`))
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = validation.session
|
||||||
|
|
||||||
|
const userData = await global.comty.rest.user.data({
|
||||||
|
user_id: session.user_id,
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error(`[${socket.id}] failed to get user data caused by server error`, err)
|
||||||
|
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!userData) {
|
||||||
|
return next(new Error(`auth:user_failed`))
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
socket.userData = userData
|
||||||
|
socket.token = token
|
||||||
|
socket.session = session
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
return next(new Error(`auth:decode_failed`))
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[${socket.id}] failed to connect caused by server error`, error)
|
||||||
|
|
||||||
|
next(new Error(`auth:authentification_failed`))
|
||||||
|
}
|
||||||
|
}
|
25
packages/sync_server/src/useMiddlewares/useAuth/index.js
Normal file
25
packages/sync_server/src/useMiddlewares/useAuth/index.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
export default async function (req, res, next) {
|
||||||
|
// extract authentification header
|
||||||
|
let auth = req.headers.authorization
|
||||||
|
|
||||||
|
if (!auth) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
auth = auth.replace("Bearer ", "")
|
||||||
|
|
||||||
|
// check if authentification is valid
|
||||||
|
const validation = await comty.rest.session.validateToken(auth).catch((error) => {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!validation.valid) {
|
||||||
|
return res.status(401).json({ error: "Unauthorized" })
|
||||||
|
}
|
||||||
|
|
||||||
|
req.session = validation.session
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
8
packages/sync_server/src/useMiddlewares/useCors/index.js
Normal file
8
packages/sync_server/src/useMiddlewares/useCors/index.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import cors from "cors"
|
||||||
|
|
||||||
|
export default cors({
|
||||||
|
origin: "*",
|
||||||
|
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "CONNECT", "TRACE"],
|
||||||
|
preflightContinue: false,
|
||||||
|
optionsSuccessStatus: 204,
|
||||||
|
})
|
14
packages/sync_server/src/useMiddlewares/useLogger/index.js
Normal file
14
packages/sync_server/src/useMiddlewares/useLogger/index.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export default (req, res, next) => {
|
||||||
|
const startHrTime = process.hrtime()
|
||||||
|
|
||||||
|
res.on("finish", () => {
|
||||||
|
const elapsedHrTime = process.hrtime(startHrTime)
|
||||||
|
const elapsedTimeInMs = elapsedHrTime[0] * 1000 + elapsedHrTime[1] / 1e6
|
||||||
|
|
||||||
|
res._responseTimeMs = elapsedTimeInMs
|
||||||
|
|
||||||
|
console.log(`${req.method} ${res._status_code ?? res.statusCode ?? 200} ${req.url} ${elapsedTimeInMs}ms`)
|
||||||
|
})
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
12
packages/sync_server/src/utils/composePayloadData/index.js
Normal file
12
packages/sync_server/src/utils/composePayloadData/index.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export default function composePayloadData(socket, data = {}) {
|
||||||
|
return {
|
||||||
|
user: {
|
||||||
|
user_id: socket.userData._id,
|
||||||
|
username: socket.userData.username,
|
||||||
|
fullName: socket.userData.fullName,
|
||||||
|
avatar: socket.userData.avatar,
|
||||||
|
},
|
||||||
|
command_issuer: data.command_issuer ?? socket.userData._id,
|
||||||
|
...data
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
import fs from "fs"
|
||||||
|
|
||||||
|
function createRoutesFromDirectory(startFrom, directoryPath, router) {
|
||||||
|
const files = fs.readdirSync(directoryPath)
|
||||||
|
|
||||||
|
files.forEach((file) => {
|
||||||
|
const filePath = `${directoryPath}/${file}`
|
||||||
|
|
||||||
|
const stat = fs.statSync(filePath)
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
createRoutesFromDirectory(startFrom, filePath, router)
|
||||||
|
} else if (file.endsWith(".js") || file.endsWith(".jsx") || file.endsWith(".ts") || file.endsWith(".tsx")) {
|
||||||
|
let splitedFilePath = filePath.split("/")
|
||||||
|
|
||||||
|
// slice the startFrom path
|
||||||
|
splitedFilePath = splitedFilePath.slice(splitedFilePath.indexOf(startFrom) + 1)
|
||||||
|
|
||||||
|
const method = splitedFilePath[0]
|
||||||
|
|
||||||
|
let route = splitedFilePath.slice(1, splitedFilePath.length).join("/")
|
||||||
|
|
||||||
|
route = route.replace(".jsx", "")
|
||||||
|
route = route.replace(".js", "")
|
||||||
|
route = route.replace(".ts", "")
|
||||||
|
route = route.replace(".tsx", "")
|
||||||
|
|
||||||
|
if (route === "index") {
|
||||||
|
route = "/"
|
||||||
|
} else {
|
||||||
|
route = `/${route}`
|
||||||
|
}
|
||||||
|
|
||||||
|
let handler = require(filePath)
|
||||||
|
|
||||||
|
handler = handler.default || handler
|
||||||
|
|
||||||
|
router[method](route, handler)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createRoutesFromDirectory
|
21
packages/sync_server/src/utils/generateFnHandler/index.js
Normal file
21
packages/sync_server/src/utils/generateFnHandler/index.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export default function generateFnHandler(fn, socket) {
|
||||||
|
return async (...args) => {
|
||||||
|
if (typeof socket === "undefined") {
|
||||||
|
socket = arguments[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
fn(socket, ...args)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[HANDLER_ERROR] ${error.message} >`, error.stack)
|
||||||
|
|
||||||
|
if (typeof socket.emit !== "function") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return socket.emit("error", {
|
||||||
|
message: error.message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
46
packages/sync_server/src/utils/getMiddlewares/index.js
Normal file
46
packages/sync_server/src/utils/getMiddlewares/index.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import fs from "node:fs"
|
||||||
|
import path from "node:path"
|
||||||
|
|
||||||
|
export default async (middlewares, middlewaresPath) => {
|
||||||
|
if (typeof middlewaresPath === "undefined") {
|
||||||
|
middlewaresPath = path.resolve(globalThis["__root"], "middlewares")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(middlewaresPath)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof middlewares === "string") {
|
||||||
|
middlewares = [middlewares]
|
||||||
|
}
|
||||||
|
|
||||||
|
let fns = []
|
||||||
|
|
||||||
|
for await (const middlewareName of middlewares) {
|
||||||
|
const middlewarePath = path.resolve(middlewaresPath, middlewareName)
|
||||||
|
|
||||||
|
if (!fs.existsSync(middlewarePath)) {
|
||||||
|
console.error(`Middleware ${middlewareName} not found.`)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const middleware = require(middlewarePath).default
|
||||||
|
|
||||||
|
if (!middleware) {
|
||||||
|
console.error(`Middleware ${middlewareName} not valid export.`)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof middleware !== "function") {
|
||||||
|
console.error(`Middleware ${middlewareName} not valid function.`)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fns.push(middleware)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fns
|
||||||
|
}
|
20
packages/sync_server/src/utils/resolveUrl/index.js
Normal file
20
packages/sync_server/src/utils/resolveUrl/index.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export default (from, to) => {
|
||||||
|
const resolvedUrl = new URL(to, new URL(from, "resolve://"))
|
||||||
|
|
||||||
|
if (resolvedUrl.protocol === "resolve:") {
|
||||||
|
let { pathname, search, hash } = resolvedUrl
|
||||||
|
|
||||||
|
if (to.includes("@")) {
|
||||||
|
const fromUrl = new URL(from)
|
||||||
|
const toUrl = new URL(to, fromUrl.origin)
|
||||||
|
|
||||||
|
pathname = toUrl.pathname
|
||||||
|
search = toUrl.search
|
||||||
|
hash = toUrl.hash
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathname + search + hash
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedUrl.toString()
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user