added marketplace_server

This commit is contained in:
SrGooglo 2023-05-16 19:40:57 +00:00
parent 13c968ecea
commit c3a9ffe80f
21 changed files with 898 additions and 0 deletions

View File

@ -0,0 +1 @@
/static

View File

@ -0,0 +1,49 @@
{
"name": "@comty/marketplace_server",
"version": "0.45.2",
"main": "dist/index.js",
"scripts": {
"build": "corenode-cli build",
"dev": "cross-env NODE_ENV=development nodemon --ignore dist/ --exec corenode-node ./src/index.js"
},
"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",
"connect-mongo": "^4.6.0",
"content-range": "^2.0.2",
"corenode": "0.28.26",
"dotenv": "^16.0.3",
"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"
}
}

View File

@ -0,0 +1,119 @@
import fs from "fs"
import path from "path"
import DbManager from "@classes/DbManager"
import RedisClient from "@classes/RedisClient"
import StorageClient from "@classes/StorageClient"
import hyperexpress from "hyper-express"
import pkg from "../package.json"
export default class WidgetsAPI {
server = global.server = new hyperexpress.Server()
listenIp = process.env.HTTP_LISTEN_IP ?? "0.0.0.0"
listenPort = process.env.HTTP_LISTEN_PORT ?? 3040
internalRouter = new hyperexpress.Router()
db = new DbManager()
redis = global.redis = RedisClient()
storage = global.storage = StorageClient()
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 handler = controller(new hyperexpress.Router())
if (!handler) {
console.error(`Controller ${controllerPath} returning not valid handler.`)
continue
}
this.internalRouter.use(handler.path ?? "/", handler.router)
continue
}
}
async __registerInternalMiddlewares() {
let middlewaresPath = fs.readdirSync(path.resolve(__dirname, "useMiddlewares"))
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()
await this.storage.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`)
}
}

View File

@ -0,0 +1,58 @@
import mongoose from "mongoose"
function getConnectionConfig(obj) {
const { DB_USER, DB_DRIVER, DB_NAME, DB_PWD, DB_HOSTNAME, DB_PORT } = obj
let auth = [
DB_DRIVER ?? "mongodb",
"://",
]
if (DB_USER && DB_PWD) {
auth.push(`${DB_USER}:${DB_PWD}@`)
}
auth.push(DB_HOSTNAME ?? "localhost")
auth.push(`:${DB_PORT ?? "27017"}`)
if (DB_USER) {
auth.push("/?authMechanism=DEFAULT")
}
auth = auth.join("")
return [
auth,
{
dbName: DB_NAME,
useNewUrlParser: true,
useUnifiedTopology: true,
}
]
}
export default class DBManager {
initialize = async (config) => {
console.log("🔌 Connecting to DB...")
const dbConfig = getConnectionConfig(config ?? process.env)
mongoose.set("strictQuery", false)
const connection = await mongoose.connect(...dbConfig)
.catch((err) => {
console.log(`❌ Failed to connect to DB, retrying...\n`)
console.log(error)
// setTimeout(() => {
// this.initialize()
// }, 1000)
return false
})
if (connection) {
console.log(`✅ Connected to DB.`)
}
}
}

View File

@ -0,0 +1,44 @@
import { createClient } from "redis"
function composeURL() {
// support for auth
let url = "redis://"
if (process.env.REDIS_PASSWORD && process.env.REDIS_USERNAME) {
url += process.env.REDIS_USERNAME + ":" + process.env.REDIS_PASSWORD + "@"
}
url += process.env.REDIS_HOST ?? "localhost"
if (process.env.REDIS_PORT) {
url += ":" + process.env.REDIS_PORT
}
return url
}
export default () => {
let client = createClient({
url: composeURL(),
})
client.initialize = async () => {
console.log("🔌 Connecting to Redis client...")
await client.connect()
return client
}
// handle when client disconnects unexpectedly to avoid main crash
client.on("error", (error) => {
console.error("❌ Redis client error:", error)
})
// handle when client connects
client.on("connect", () => {
console.log("✅ Redis client connected.")
})
return client
}

View File

@ -0,0 +1,94 @@
const Minio = require("minio")
export const generateDefaultBucketPolicy = (payload) => {
const { bucketName } = payload
if (!bucketName) {
throw new Error("bucketName is required")
}
return {
Version: "2012-10-17",
Statement: [
{
Action: [
"s3:GetObject"
],
Effect: "Allow",
Principal: {
AWS: [
"*"
]
},
Resource: [
`arn:aws:s3:::${bucketName}/*`
],
Sid: ""
}
]
}
}
export class StorageClient extends Minio.Client {
constructor(options) {
super(options)
this.defaultBucket = String(options.defaultBucket)
this.defaultRegion = String(options.defaultRegion)
}
composeRemoteURL = (key) => {
return `${this.protocol}//${this.host}:${this.port}/${this.defaultBucket}/${key}`
}
setDefaultBucketPolicy = async (bucketName) => {
const policy = generateDefaultBucketPolicy({ bucketName })
return this.setBucketPolicy(bucketName, JSON.stringify(policy))
}
initialize = async () => {
console.log("🔌 Checking if storage client have default bucket...")
// check connection with s3
const bucketExists = await this.bucketExists(this.defaultBucket).catch(() => {
return false
})
if (!bucketExists) {
console.warn("🪣 Default bucket not exists! Creating new bucket...")
await this.makeBucket(this.defaultBucket, "s3")
// set default bucket policy
await this.setDefaultBucketPolicy(this.defaultBucket)
}
// check if default bucket policy exists
const bucketPolicy = await this.getBucketPolicy(this.defaultBucket).catch(() => {
return null
})
if (!bucketPolicy) {
// set default bucket policy
await this.setDefaultBucketPolicy(this.defaultBucket)
}
console.log("✅ Storage client is ready.")
}
}
export const createStorageClientInstance = (options) => {
return new StorageClient({
...options,
endPoint: process.env.S3_ENDPOINT,
port: Number(process.env.S3_PORT),
useSSL: toBoolean(process.env.S3_USE_SSL),
accessKey: process.env.S3_ACCESS_KEY,
secretKey: process.env.S3_SECRET_KEY,
defaultBucket: process.env.S3_BUCKET,
defaultRegion: process.env.S3_REGION,
})
}
export default createStorageClientInstance

View File

@ -0,0 +1,72 @@
import path from "path"
import fs from "fs"
import LiveDirectory from "live-directory"
function serveStaticFiles(req, res, live_dir) {
const path = req.path.replace("/static", "")
const asset = live_dir.get(path)
if (!asset) {
return res.status(404).send("Not Found")
}
if (asset.cached) {
return res.send(asset.content)
} else {
const readable = asset.stream()
return readable.pipe(res)
}
}
async function serveRemoteStatic(req, res) {
global.storage.getObject(process.env.S3_BUCKET, req.path, (err, dataStream) => {
if (err) {
console.log(err)
return res.status(404).send("Not Found")
}
// on end of stream, dispath res.on("finish")
dataStream.on("end", () => {
res.emit("finish")
return res.end()
})
return dataStream.pipe(res)
})
}
async function syncFolder(dir) {
const files = await fs.promises.readdir(dir)
for await (const file of files) {
const filePath = path.resolve(dir, file)
const stat = fs.statSync(filePath)
if (stat.isDirectory()) {
await syncFolder(filePath)
} else {
const fileContent = await fs.promises.readFile(filePath)
await global.storage.putObject(process.env.S3_BUCKET, filePath.replace(process.cwd(), ""), fileContent)
}
}
}
export default (router) => {
const StaticDirectory = new LiveDirectory(path.resolve(process.cwd(), "static"), {
static: true
})
//const static_dir = path.resolve(process.cwd(), "static")
//syncFolder(static_dir)
router.get("*", (req, res) => serveRemoteStatic(req, res, StaticDirectory))
return {
path: "/static/",
router,
}
}

View File

@ -0,0 +1,14 @@
import path from "path"
import createRoutesFromDirectory from "@utils/createRoutesFromDirectory"
export default (router) => {
// create a file based router
const routesPath = path.resolve(__dirname, "routes")
router = createRoutesFromDirectory("routes", routesPath, router)
return {
path: "/widgets",
router,
}
}

View File

@ -0,0 +1,168 @@
import { Widget } from "@models"
import { transform } from "sucrase"
import UglifyJS from "uglify-js"
import resolveUrl from "@utils/resolveUrl"
import replaceImportsWithRemoteURL from "@utils/replaceImportsWithRemoteURL"
async function compileWidgetCode(widgetCode, manifest, rootURL) {
if (!widgetCode) {
throw new Error("Widget code not defined.")
}
if (!manifest) {
throw new Error("Manifest not defined.")
}
if (!rootURL) {
throw new Error("Root URL not defined.")
}
let renderComponentName = null
let cssFiles = []
// inject react with cdn
widgetCode = `import React from "https://cdn.skypack.dev/react@17?dts" \n${widgetCode}`
widgetCode = await replaceImportsWithRemoteURL(widgetCode, resolveUrl(rootURL, manifest.entryFile))
// remove css imports and append to manifest (Only its used in the entry file)
widgetCode = widgetCode.replace(/import\s+["'](.*)\.css["']/g, (match, p1) => {
cssFiles.push(resolveUrl(rootURL, `${p1}.css`))
return ""
})
// transform jsx to js
widgetCode = await transform(widgetCode, {
transforms: ["jsx"],
//jsxRuntime: "automatic",
//production: true,
}).code
// search export default and get the name of the function/const/class
const exportDefaultRegex = /export\s+default\s+(?:function|const|class)\s+([a-zA-Z0-9]+)/g
const exportDefaultMatch = exportDefaultRegex.exec(widgetCode)
if (exportDefaultMatch) {
renderComponentName = exportDefaultMatch[1]
}
// remove export default keywords
widgetCode = widgetCode.replace("export default", "")
let manifestProcessed = {
...manifest,
}
manifestProcessed.cssFiles = cssFiles
manifestProcessed.entryFile = resolveUrl(rootURL, manifest.entryFile)
manifestProcessed.iconUrl = resolveUrl(rootURL, manifest.iconUrl)
let result = `
${widgetCode}
export default {
manifest: ${JSON.stringify(manifestProcessed)},
renderComponent: ${renderComponentName},
}
`
// minify code
result = UglifyJS.minify(result, {
compress: true,
mangle: true,
}).code
return result
}
export default async (req, res) => {
const widgetId = req.params.widgetId
const useCache = req.query["use-cache"] ? toBoolean(req.query["use-cache"]) : true
//console.log(`📦 Getting widget code [${widgetId}], using cache ${useCache}`)
// try to find Widget
const widget = await Widget.findOne({
_id: widgetId,
}).catch(() => {
return null
})
if (!widget) {
return res.status(404).json({
error: "Widget not found.",
})
}
if (!widget.manifest.entryFile) {
return res.status(404).json({
error: "Widget entry file not defined",
})
}
let widgetCode = null
// get origin from request url
const origin = req.protocol + "://" + req.get("host")
const remotePath = `/static/${widgetId}/src`
const remoteEntyFilePath = resolveUrl(remotePath, widget.manifest.entryFile)
if (useCache) {
widgetCode = await global.redis.get(`widget:${widgetId}`)
}
if (!widgetCode) {
try {
widgetCode = await new Promise(async (resolve, reject) => {
await global.storage.getObject(process.env.S3_BUCKET, remoteEntyFilePath, (error, dataStream) => {
if (error) {
return false
}
let data = ""
dataStream.on("data", (chunk) => {
data += chunk
})
dataStream.on("end", () => {
resolve(data)
})
})
})
} catch (error) {
return res.status(500).json({
error: error.message,
})
}
try {
console.log("🔌 Compiling widget code...")
widgetCode = await compileWidgetCode(widgetCode, widget.manifest, resolveUrl(origin, remotePath))
await global.redis.set(`widget:${widgetId}`, widgetCode)
} catch (error) {
return res.status(500).json({
message: "Unable to transform widget code.",
error: error.message,
})
}
}
if (!widgetCode) {
return res.status(404).json({
error: "Widget not found.",
})
}
res.setHeader("Content-Type", "application/javascript")
return res.status(200).send(widgetCode)
}

View File

@ -0,0 +1,23 @@
import { Widget } from "@models"
export default async (req, res) => {
const widget_id = req.params.widgetId
const widget = await Widget.findOne({
_id: widget_id,
}).catch(() => {
return false
})
if (!widget) {
return res.status(404).json({
error: "Widget not found",
})
}
return res.status(200).json({
...widget.manifest,
user_id: widget.user_id,
_id: widget_id,
})
}

View File

@ -0,0 +1,41 @@
import { Widget } from "@models"
export default async (req, res) => {
let { limit = 20, offset = 0, keywords } = req.query
keywords = JSON.parse(keywords ?? "{}")
// remove empty keywords
Object.keys(keywords).forEach((key) => {
if (keywords[key] === "") {
delete keywords[key]
}
})
console.log("Searching with keywords:", keywords)
const query = {
public: true,
}
// inclide keywords for search in manifest
Object.keys(keywords).forEach((key) => {
query[`manifest.${key}`] = {
$regex: keywords[key],
$options: "i",
}
})
let widgets = await Widget.find(query)
.limit(Number(limit))
.skip(Number(offset))
widgets = widgets.map((widget) => {
widget.manifest._id = widget._id
widget.manifest.user_id = widget.user_id
return widget
})
return res.json(widgets)
}

View File

@ -0,0 +1,15 @@
import { Widget } from "@models"
export default async (req, res) => {
const { user_id } = req.params
const { limit = 20, offset = 0 } = req.query
const widgets = await Widget.find({
user_id,
public: true,
})
.limit(Number(limit))
.skip(Number(offset))
return res.json(widgets)
}

View File

@ -0,0 +1,5 @@
export default (req, res) => {
return res.status(200).json({
test: "Testing endpoints by route files"
})
}

View File

@ -0,0 +1,56 @@
require("dotenv").config()
import { webcrypto as crypto } from "crypto"
import path from "path"
import { registerBaseAliases } from "linebridge/dist/server"
registerBaseAliases(undefined, {
"@services": path.resolve(__dirname, "services"),
})
// 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
}
global.isProduction = process.env.NODE_ENV === "production"
import API from "./api"
async function main() {
const mainAPI = new API()
await mainAPI.initialize()
}
main().catch((error) => {
console.error(`🆘 [FATAL ERROR] >`, error)
})

View File

@ -0,0 +1,19 @@
import mongoose, { Schema } from "mongoose"
import fs from "fs"
import path from "path"
function generateModels() {
let models = {}
const dirs = fs.readdirSync(__dirname).filter(file => file !== "index.js")
dirs.forEach((file) => {
const model = require(path.join(__dirname, file)).default
models[model.name] = mongoose.model(model.name, new Schema(model.schema), model.collection)
})
return models
}
module.exports = generateModels()

View File

@ -0,0 +1,24 @@
export default {
name: "Widget",
collection: "widgets",
schema: {
manifest: {
type: Object,
required: true,
},
user_id: {
type: String,
required: true,
},
public: {
type: Boolean,
default: true,
},
created_at: {
type: Date,
},
updated_at: {
type: Date,
},
}
}

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

View 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()
}

View File

@ -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")) {
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

View File

@ -0,0 +1,18 @@
import resolveUrl from "@utils/resolveUrl"
export default (code, rootURL) => {
const importRegex = /import\s+(?:(?:([\w*\s{},]*)\s+from\s+)?["']([^"']*)["']|["']([^"']*)["'])/g
// replaces all imports with absolute paths
const absoluteImportCode = code.replace(importRegex, (match, p1, p2) => {
let resolved = resolveUrl(rootURL, p2)
if (!p1) {
return `import "${resolved}"`
}
return `import ${p1} from "${resolved}"`
})
return absoluteImportCode
}

View File

@ -0,0 +1,11 @@
export default (from, to) => {
const resolvedUrl = new URL(to, new URL(from, "resolve://"))
if (resolvedUrl.protocol === "resolve:") {
const { pathname, search, hash } = resolvedUrl
return pathname + search + hash
}
return resolvedUrl.toString()
}