Yggdrasil/server.js
azures04 587146d322 Initial project structure and core files
Add base project files including environment example, license, README, .gitignore, error classes, ESLint config, database modules, texture assets, repositories, routes, schemas, services, and server entry point. This establishes the foundational structure for a Yggdrasil-compatible REST API with modular error handling, database setup, and route organization.
2026-01-05 04:42:39 +01:00

168 lines
5.2 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())
app.use(cors({ origin: "*" }))
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
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.all(/.*/, (req, res, next) => {
next(new DefaultError(404, `Can't find ${req.originalUrl} on this server!`, null, "NotFound"))
})
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"])
})