Yggdrasil/server.js
2026-01-24 00:14:05 +01:00

178 lines
5.7 KiB
JavaScript

const express = require("express")
const hpp = require("hpp")
const app = express()
const cors = require("cors")
const path = require("node:path")
const utils = require("./modules/utils")
const logger = require("./modules/logger")
const helmet = require("helmet")
const loader = require("./modules/loader")
const DefaultError = require("./errors/DefaultError")
const path2regex = require("path-to-regexp")
const databaseGlobals = require("./modules/databaseGlobals")
const certificates = require("./modules/certificatesManager")
const { ValidationError } = require("./errors/errors")
const routes = loader.getRecursiveFiles(path.join(__dirname, "routes"))
const schemas = loader.getRecursiveFiles(path.join(__dirname, "schemas"))
const schemaRegistry = {}
databaseGlobals.setupDatabase()
certificates.setupKeys()
app.use(hpp())
app.use(helmet({
crossOriginResourcePolicy: { policy: "cross-origin" },
crossOriginEmbedderPolicy: false,
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://cdnjs.cloudflare.com", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com"],
fontSrc: ["'self'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com"],
connectSrc: ["'self'", "https://yggdrasil.azures.fr"],
imgSrc: ["'self'", "data:"],
},
}
}))
app.use(cors({ origin: "*" }))
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
// app.use(cookieParser())
app.set("trust proxy", true)
logger.log("Initializing routes", ["WEB", "yellow"])
for (const schemaFile of schemas) {
try {
const schemaConfig = require(schemaFile)
const routePath = loader.computeRoutePath(path.join(__dirname, "schemas"), schemaFile)
schemaRegistry[routePath] = schemaConfig
logger.log(`${routePath.cyan.bold} schema loaded in memory`, ["WEB", "yellow"])
} catch (error) {
logger.error(error.toString(), ["WEB", "yellow"])
}
}
app.all(/.*/, (req, res, next) => {
let currentPath = req.path
if (currentPath.length > 1 && currentPath.endsWith("/")) {
currentPath = currentPath.slice(0, -1)
}
let schemaConfig = schemaRegistry[currentPath]
let matchedParams = {}
if (!schemaConfig) {
const registeredRoutes = Object.keys(schemaRegistry)
for (const routePattern of registeredRoutes) {
if (!routePattern.includes(":")) {
continue
}
const matcher = path2regex.match(routePattern, { decode: decodeURIComponent })
const result = matcher(currentPath)
if (result) {
schemaConfig = schemaRegistry[routePattern]
matchedParams = result.params
break
}
}
}
if (!schemaConfig || !schemaConfig[req.method]) {
return next()
}
req.params = { ...req.params, ...matchedParams }
const methodConfig = schemaConfig[req.method]
const errorConfig = methodConfig.error || { code: 400, message: "Validation Error" }
const context = {
method: req.method,
path: req.originalUrl,
ip: req.headers["x-forwarded-for"] || req.socket.remoteAddress
}
if (methodConfig.headers) {
const headerResult = methodConfig.headers.safeParse(req.headers)
if (!headerResult.success) {
throw new ValidationError(headerResult, errorConfig, context)
}
}
let dataSchema = null
let dataToValidate = null
let validationType = "body"
if (req.method === "GET" || req.method === "DELETE") {
dataSchema = methodConfig.query
dataToValidate = req.query
validationType = "query"
} else {
dataSchema = methodConfig.body
dataToValidate = req.body
}
if (dataSchema) {
const result = dataSchema.safeParse(dataToValidate)
if (!result.success) {
throw new ValidationError(result, errorConfig, context)
}
if (validationType === "query") {
req.query = result.data
} else {
req.body = result.data
}
}
return next()
})
for (const route of routes) {
try {
const router = require(route)
const routePath = loader.computeRoutePath(path.join(__dirname, "routes"), route)
if (router.stack) {
for (const layer of router.stack) {
if (layer.route && layer.route.methods) {
const method = Object.keys(layer.route.methods).join(", ").toUpperCase()
const innerPath = layer.route.path === "/" ? "" : layer.route.path
const mountPrefix = routePath === "/" ? "" : routePath
const fullDisplayPath = mountPrefix + innerPath
logger.log(`${method.cyan} ${fullDisplayPath.cyan.bold} route registered`, ["WEB", "yellow"])
}
}
}
app.use(routePath, router)
} catch (error) {
logger.error(route, ["WEB", "yellow"])
logger.error(error.toString(), ["WEB", "yellow"])
}
}
app.use((err, req, res, next) => {
const statusCode = err.statusCode || err.code || 500
logger.error(`Error occured on: ${req.originalUrl.bold}`, ["API", "red"])
logger.error(err.message, ["API", "red"])
if (typeof err.serialize === "function") {
return res.status(statusCode).json(err.serialize())
}
return res.status(500).json({
status: "error",
message: "Internal Server Error"
})
})
app.listen(process.env.WEB_PORT || 3000, () => {
logger.log(`Server listening at port : ${process.env.WEB_PORT.bold || 3000}`, ["WEB", "yellow"])
})