From 13bdccd53d6ee608610af95d2638b9c5e2785384 Mon Sep 17 00:00:00 2001 From: SrGooglo Date: Thu, 27 Jul 2023 00:05:49 +0000 Subject: [PATCH] implement `sync_server` --- packages/sync_server/.env-example | 20 +++ packages/sync_server/.gitignore | 2 + packages/sync_server/.infisical.json | 7 + packages/sync_server/Dockerfile | 27 ++++ packages/sync_server/package.json | 64 ++++++++ packages/sync_server/src/api.js | 142 ++++++++++++++++++ .../sync_server/src/controllers/main/index.js | 11 ++ .../main/routes/get/sse_event/:sse_uid.js | 28 ++++ .../src/controllers/services/index.js | 11 ++ .../services/routes/get/tidal/create_link.js | 47 ++++++ .../services/routes/get/tidal/current.js | 22 +++ .../services/routes/get/tidal/is_active.js | 18 +++ .../routes/get/tidal/manifest/:track_id.js | 36 +++++ .../routes/get/tidal/playback/:track_id.js | 27 ++++ .../services/routes/get/tidal/search.js | 11 ++ .../services/routes/post/tidal/delete_link.js | 16 ++ packages/sync_server/src/index.js | 130 ++++++++++++++++ .../src/middlewares/withAuth/index.js | 25 +++ .../src/middlewares/withOptionalAuth/index.js | 26 ++++ .../sync_server/src/middlewares/withWsAuth.js | 55 +++++++ .../src/useMiddlewares/useAuth/index.js | 25 +++ .../src/useMiddlewares/useCors/index.js | 8 + .../src/useMiddlewares/useLogger/index.js | 14 ++ .../src/utils/composePayloadData/index.js | 12 ++ .../utils/createRoutesFromDirectory/index.js | 45 ++++++ .../src/utils/generateFnHandler/index.js | 21 +++ .../src/utils/getMiddlewares/index.js | 46 ++++++ .../sync_server/src/utils/resolveUrl/index.js | 20 +++ 28 files changed, 916 insertions(+) create mode 100755 packages/sync_server/.env-example create mode 100644 packages/sync_server/.gitignore create mode 100644 packages/sync_server/.infisical.json create mode 100755 packages/sync_server/Dockerfile create mode 100644 packages/sync_server/package.json create mode 100644 packages/sync_server/src/api.js create mode 100644 packages/sync_server/src/controllers/main/index.js create mode 100644 packages/sync_server/src/controllers/main/routes/get/sse_event/:sse_uid.js create mode 100644 packages/sync_server/src/controllers/services/index.js create mode 100644 packages/sync_server/src/controllers/services/routes/get/tidal/create_link.js create mode 100644 packages/sync_server/src/controllers/services/routes/get/tidal/current.js create mode 100644 packages/sync_server/src/controllers/services/routes/get/tidal/is_active.js create mode 100644 packages/sync_server/src/controllers/services/routes/get/tidal/manifest/:track_id.js create mode 100644 packages/sync_server/src/controllers/services/routes/get/tidal/playback/:track_id.js create mode 100644 packages/sync_server/src/controllers/services/routes/get/tidal/search.js create mode 100644 packages/sync_server/src/controllers/services/routes/post/tidal/delete_link.js create mode 100644 packages/sync_server/src/index.js create mode 100644 packages/sync_server/src/middlewares/withAuth/index.js create mode 100644 packages/sync_server/src/middlewares/withOptionalAuth/index.js create mode 100644 packages/sync_server/src/middlewares/withWsAuth.js create mode 100644 packages/sync_server/src/useMiddlewares/useAuth/index.js create mode 100644 packages/sync_server/src/useMiddlewares/useCors/index.js create mode 100644 packages/sync_server/src/useMiddlewares/useLogger/index.js create mode 100644 packages/sync_server/src/utils/composePayloadData/index.js create mode 100644 packages/sync_server/src/utils/createRoutesFromDirectory/index.js create mode 100644 packages/sync_server/src/utils/generateFnHandler/index.js create mode 100644 packages/sync_server/src/utils/getMiddlewares/index.js create mode 100644 packages/sync_server/src/utils/resolveUrl/index.js diff --git a/packages/sync_server/.env-example b/packages/sync_server/.env-example new file mode 100755 index 00000000..0119f1bd --- /dev/null +++ b/packages/sync_server/.env-example @@ -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="" \ No newline at end of file diff --git a/packages/sync_server/.gitignore b/packages/sync_server/.gitignore new file mode 100644 index 00000000..0dd1cacd --- /dev/null +++ b/packages/sync_server/.gitignore @@ -0,0 +1,2 @@ +/static +shared-classes \ No newline at end of file diff --git a/packages/sync_server/.infisical.json b/packages/sync_server/.infisical.json new file mode 100644 index 00000000..fb927147 --- /dev/null +++ b/packages/sync_server/.infisical.json @@ -0,0 +1,7 @@ +{ + "workspaceId": "64519574a8a691b55e8b361d", + "defaultEnvironment": "dev", + "gitBranchToEnvironmentMapping": { + "master": "prod" + } +} \ No newline at end of file diff --git a/packages/sync_server/Dockerfile b/packages/sync_server/Dockerfile new file mode 100755 index 00000000..5f409399 --- /dev/null +++ b/packages/sync_server/Dockerfile @@ -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"] \ No newline at end of file diff --git a/packages/sync_server/package.json b/packages/sync_server/package.json new file mode 100644 index 00000000..330e6eba --- /dev/null +++ b/packages/sync_server/package.json @@ -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" + } +} \ No newline at end of file diff --git a/packages/sync_server/src/api.js b/packages/sync_server/src/api.js new file mode 100644 index 00000000..901fb47d --- /dev/null +++ b/packages/sync_server/src/api.js @@ -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`) + } +} \ No newline at end of file diff --git a/packages/sync_server/src/controllers/main/index.js b/packages/sync_server/src/controllers/main/index.js new file mode 100644 index 00000000..6ce9343e --- /dev/null +++ b/packages/sync_server/src/controllers/main/index.js @@ -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, + } +} \ No newline at end of file diff --git a/packages/sync_server/src/controllers/main/routes/get/sse_event/:sse_uid.js b/packages/sync_server/src/controllers/main/routes/get/sse_event/:sse_uid.js new file mode 100644 index 00000000..63569896 --- /dev/null +++ b/packages/sync_server/src/controllers/main/routes/get/sse_event/:sse_uid.js @@ -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') +} \ No newline at end of file diff --git a/packages/sync_server/src/controllers/services/index.js b/packages/sync_server/src/controllers/services/index.js new file mode 100644 index 00000000..e30b2052 --- /dev/null +++ b/packages/sync_server/src/controllers/services/index.js @@ -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, + } +} \ No newline at end of file diff --git a/packages/sync_server/src/controllers/services/routes/get/tidal/create_link.js b/packages/sync_server/src/controllers/services/routes/get/tidal/create_link.js new file mode 100644 index 00000000..b5de570a --- /dev/null +++ b/packages/sync_server/src/controllers/services/routes/get/tidal/create_link.js @@ -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, + }) +} \ No newline at end of file diff --git a/packages/sync_server/src/controllers/services/routes/get/tidal/current.js b/packages/sync_server/src/controllers/services/routes/get/tidal/current.js new file mode 100644 index 00000000..7143396d --- /dev/null +++ b/packages/sync_server/src/controllers/services/routes/get/tidal/current.js @@ -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) + } +} \ No newline at end of file diff --git a/packages/sync_server/src/controllers/services/routes/get/tidal/is_active.js b/packages/sync_server/src/controllers/services/routes/get/tidal/is_active.js new file mode 100644 index 00000000..13407d79 --- /dev/null +++ b/packages/sync_server/src/controllers/services/routes/get/tidal/is_active.js @@ -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 + }) +} \ No newline at end of file diff --git a/packages/sync_server/src/controllers/services/routes/get/tidal/manifest/:track_id.js b/packages/sync_server/src/controllers/services/routes/get/tidal/manifest/:track_id.js new file mode 100644 index 00000000..9756c003 --- /dev/null +++ b/packages/sync_server/src/controllers/services/routes/get/tidal/manifest/:track_id.js @@ -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) + } +} \ No newline at end of file diff --git a/packages/sync_server/src/controllers/services/routes/get/tidal/playback/:track_id.js b/packages/sync_server/src/controllers/services/routes/get/tidal/playback/:track_id.js new file mode 100644 index 00000000..e801ebec --- /dev/null +++ b/packages/sync_server/src/controllers/services/routes/get/tidal/playback/:track_id.js @@ -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) +} \ No newline at end of file diff --git a/packages/sync_server/src/controllers/services/routes/get/tidal/search.js b/packages/sync_server/src/controllers/services/routes/get/tidal/search.js new file mode 100644 index 00000000..3dd115a4 --- /dev/null +++ b/packages/sync_server/src/controllers/services/routes/get/tidal/search.js @@ -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) +} \ No newline at end of file diff --git a/packages/sync_server/src/controllers/services/routes/post/tidal/delete_link.js b/packages/sync_server/src/controllers/services/routes/post/tidal/delete_link.js new file mode 100644 index 00000000..6b62f093 --- /dev/null +++ b/packages/sync_server/src/controllers/services/routes/post/tidal/delete_link.js @@ -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 + }) +} \ No newline at end of file diff --git a/packages/sync_server/src/index.js b/packages/sync_server/src/index.js new file mode 100644 index 00000000..587c7229 --- /dev/null +++ b/packages/sync_server/src/index.js @@ -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) +}) \ No newline at end of file diff --git a/packages/sync_server/src/middlewares/withAuth/index.js b/packages/sync_server/src/middlewares/withAuth/index.js new file mode 100644 index 00000000..44ee47ff --- /dev/null +++ b/packages/sync_server/src/middlewares/withAuth/index.js @@ -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() +} \ No newline at end of file diff --git a/packages/sync_server/src/middlewares/withOptionalAuth/index.js b/packages/sync_server/src/middlewares/withOptionalAuth/index.js new file mode 100644 index 00000000..cbc4bb49 --- /dev/null +++ b/packages/sync_server/src/middlewares/withOptionalAuth/index.js @@ -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() +} \ No newline at end of file diff --git a/packages/sync_server/src/middlewares/withWsAuth.js b/packages/sync_server/src/middlewares/withWsAuth.js new file mode 100644 index 00000000..1e85e4de --- /dev/null +++ b/packages/sync_server/src/middlewares/withWsAuth.js @@ -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`)) + } +} \ No newline at end of file diff --git a/packages/sync_server/src/useMiddlewares/useAuth/index.js b/packages/sync_server/src/useMiddlewares/useAuth/index.js new file mode 100644 index 00000000..df7123f5 --- /dev/null +++ b/packages/sync_server/src/useMiddlewares/useAuth/index.js @@ -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 +} \ No newline at end of file diff --git a/packages/sync_server/src/useMiddlewares/useCors/index.js b/packages/sync_server/src/useMiddlewares/useCors/index.js new file mode 100644 index 00000000..7ebac0fc --- /dev/null +++ b/packages/sync_server/src/useMiddlewares/useCors/index.js @@ -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, +}) \ No newline at end of file diff --git a/packages/sync_server/src/useMiddlewares/useLogger/index.js b/packages/sync_server/src/useMiddlewares/useLogger/index.js new file mode 100644 index 00000000..5e9c64f4 --- /dev/null +++ b/packages/sync_server/src/useMiddlewares/useLogger/index.js @@ -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() +} \ No newline at end of file diff --git a/packages/sync_server/src/utils/composePayloadData/index.js b/packages/sync_server/src/utils/composePayloadData/index.js new file mode 100644 index 00000000..de5761f5 --- /dev/null +++ b/packages/sync_server/src/utils/composePayloadData/index.js @@ -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 + } +} \ No newline at end of file diff --git a/packages/sync_server/src/utils/createRoutesFromDirectory/index.js b/packages/sync_server/src/utils/createRoutesFromDirectory/index.js new file mode 100644 index 00000000..f27647a0 --- /dev/null +++ b/packages/sync_server/src/utils/createRoutesFromDirectory/index.js @@ -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 \ No newline at end of file diff --git a/packages/sync_server/src/utils/generateFnHandler/index.js b/packages/sync_server/src/utils/generateFnHandler/index.js new file mode 100644 index 00000000..4c5962d5 --- /dev/null +++ b/packages/sync_server/src/utils/generateFnHandler/index.js @@ -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, + }) + } + } +} \ No newline at end of file diff --git a/packages/sync_server/src/utils/getMiddlewares/index.js b/packages/sync_server/src/utils/getMiddlewares/index.js new file mode 100644 index 00000000..9a9a07f9 --- /dev/null +++ b/packages/sync_server/src/utils/getMiddlewares/index.js @@ -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 +} \ No newline at end of file diff --git a/packages/sync_server/src/utils/resolveUrl/index.js b/packages/sync_server/src/utils/resolveUrl/index.js new file mode 100644 index 00000000..a9a33785 --- /dev/null +++ b/packages/sync_server/src/utils/resolveUrl/index.js @@ -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() +} \ No newline at end of file