Introduces Discord OAuth2 integration for account association and login, including new routes for linking, unlinking, and authenticating via Discord. Adds supporting services, repositories, and schema validation for the OAuth2 flow. Refactors database schema and queries for consistency, and updates dependencies to include required OAuth2 libraries.
169 lines
5.2 KiB
JavaScript
169 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.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.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"])
|
|
})
|