diff --git a/errors/DefaultError.js b/errors/DefaultError.js index a0c7893..4969210 100644 --- a/errors/DefaultError.js +++ b/errors/DefaultError.js @@ -6,6 +6,8 @@ class DefaultError extends Error { this.cause = cause || "Internal Server Error" this.message = message || "Internal Server Error" this.isOperational = true + + Error.captureStackTrace(this, this.constructor) } serialize() { diff --git a/modules/loader.js b/modules/loader.js index 8178dd1..4a2663a 100644 --- a/modules/loader.js +++ b/modules/loader.js @@ -28,20 +28,19 @@ function getRecursiveFiles(dir) { function computeRoutePath(baseDir, filePath) { const relativePath = path.relative(baseDir, filePath) - const normalizedPath = relativePath.replace(/\\/g, "/") + let route = "/" + relativePath.split(path.sep).join("/") - let route = "/" + normalizedPath - - if (route.endsWith("index.js")) { - route = route.replace("index.js", "") - } else { - route = route.replace(".js", "") + if (route.endsWith(".js")) { + route = route.slice(0, -3) + } + if (route.endsWith("/index")) { + route = route.slice(0, -6) } - route = route.replace(/\/{2,}/g, "/") + route = route.replace(/\[([^\]]+)\]/g, ":$1") - if (route.length > 1 && route.endsWith('/')) { - route = route.slice(0, -1) + if (route === "") { + return "/" } return route diff --git a/modules/utils.js b/modules/utils.js new file mode 100644 index 0000000..9385702 --- /dev/null +++ b/modules/utils.js @@ -0,0 +1,30 @@ +const path = require("node:path") +const Logger = require("./logger") +const logger = Logger.createLogger(path.join(__dirname, "..")) + +function sendValidationError(req, res, zodResult, type, path, errorConfig) { + const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress + logger.warn(`Validation failed for ${req.method.cyan} ${path.cyan.bold} (${type}) ` + ``.bold, ["WEB", "yellow"]) + + const response = { + success: false, + message: errorConfig.message || `Validation failed in ${type}`, + errors: zodResult.error.issues.map(e => ({ + field: e.path.join("."), + message: e.message + })) + } + + if (errorConfig) { + const extras = { ...errorConfig } + delete extras.status + delete extras.message + Object.assign(response, extras) + } + + return res.status(errorConfig.status || 400).json(response) +} + +module.exports = { + sendValidationError +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9d95690..bca3493 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "colors": "^1.4.0", "dotenv": "^17.2.3", "express": "^5.2.1", + "path-to-regexp": "^8.3.0", "zod": "^4.2.0" }, "devDependencies": { diff --git a/package.json b/package.json index 463bc02..f0e58cb 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "colors": "^1.4.0", "dotenv": "^17.2.3", "express": "^5.2.1", + "path-to-regexp": "^8.3.0", "zod": "^4.2.0" }, "devDependencies": { diff --git a/routes/users/[id].js b/routes/users/[id].js new file mode 100644 index 0000000..287552d --- /dev/null +++ b/routes/users/[id].js @@ -0,0 +1,8 @@ +const express = require("express") +const router = express.Router() + +router.post("/", async (req, res, next) => { + +}) + +module.exports = router \ No newline at end of file diff --git a/schemas/register.js b/schemas/register.js index e2af884..90fa9bb 100644 --- a/schemas/register.js +++ b/schemas/register.js @@ -1,9 +1,14 @@ const z = require("zod") module.exports = { - errorFormat: "default", POST: { - zod: z.object({ + headers: z.object({ + authorization: z.string() + .startsWith("Bearer ", { message: "Token d'authentification manquant ou invalide." }) + .optional(), + "content-type": z.string().regex(/application\/json/i).optional() + }), + body: z.object({ email: z.string() .email({ message: "Invalid E-Mail format." }) .toLowerCase(), @@ -17,7 +22,7 @@ module.exports = { }), error: { code: 422, - message: "Invalid body request" + message: "Invalid request data" } } } \ No newline at end of file diff --git a/server.js b/server.js index 69b033c..5451b89 100644 --- a/server.js +++ b/server.js @@ -3,7 +3,10 @@ const app = express() const path = require("node:path") const Logger = require("./modules/logger") const logger = Logger.createLogger(__dirname) +const utils = require("./modules/utils") const loader = require("./modules/loader") +const DefaultError = require("./errors/DefaultError") +const path2regex = require("path-to-regexp") const routes = loader.getRecursiveFiles(path.join(__dirname, "routes")) const schemas = loader.getRecursiveFiles(path.join(__dirname, "schemas")) @@ -35,46 +38,69 @@ app.all(/.*/, (req, res, next) => { currentPath = currentPath.slice(0, -1) } - const schemaConfig = schemaRegistry[currentPath] + 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() } - const methodConfig = schemaConfig[req.method] - const zodSchema = methodConfig.zod || methodConfig - const errorConfig = methodConfig.error || { status: 400, message: "Validation Error" } - const dataToValidate = (req.method === "GET" || req.method === "DELETE") ? req.query : req.body - const result = zodSchema.safeParse(dataToValidate) + req.params = { ...req.params, ...matchedParams } - if (result.success) { - if (req.method === "GET" || req.method === "DELETE") { + const methodConfig = schemaConfig[req.method] + const errorConfig = methodConfig.error || { status: 400, message: "Validation Error" } + + if (methodConfig.headers) { + const headerResult = methodConfig.headers.safeParse(req.headers) + if (!headerResult.success) { + return utils.sendValidationError(req, res, headerResult, "headers", currentPath, errorConfig) + } + } + + 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) { + return utils.sendValidationError(req, res, result, validationType, currentPath, errorConfig) + } + if (validationType === "query") { req.query = result.data } else { req.body = result.data } - return next() } - const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress - logger.warn(`Validation failed for ${req.method.cyan} ${currentPath.cyan.bold} ` + ``.bold, ["WEB", "yellow"]) - - const response = { - success: false, - message: errorConfig.message, - errors: result.error.issues.map(e => ({ - field: e.path.join("."), - message: e.message - })) - } - - if (methodConfig.error) { - const extras = { ...methodConfig.error } - delete extras.status - delete extras.message - Object.assign(response, extras) - } - - return res.status(errorConfig.status || 400).json(response) + return next() }) for (const route of routes) { @@ -96,6 +122,10 @@ for (const route of routes) { } } +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 || 500