First commit
This commit is contained in:
parent
962582e906
commit
28d51ec760
2
.env.example
Normal file
2
.env.example
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
WEB_PORT=3000
|
||||||
|
IS_PROD=FALSE
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -130,3 +130,5 @@ dist
|
|||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.pnp.*
|
||||||
|
|
||||||
|
#logs
|
||||||
|
logs
|
||||||
30
modules/errors.js
Normal file
30
modules/errors.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
function $default(res, errorDetails) {
|
||||||
|
return res.status(400).json({
|
||||||
|
status: "error",
|
||||||
|
message: "Validation Failed",
|
||||||
|
details: errorDetails
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function legacy(res, errorDetails) {
|
||||||
|
return res.status(200).json({
|
||||||
|
success: false,
|
||||||
|
code: 900,
|
||||||
|
err_msg: errorDetails
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function minimal(res, errorDetails) {
|
||||||
|
return res.status(400).send(`ERR:${JSON.stringify(errorDetails)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function secure(res, errorDetails) {
|
||||||
|
return res.status(401).json({ error: "Unauthorized or Invalid Request" })
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
default: $default,
|
||||||
|
legacy,
|
||||||
|
minimal,
|
||||||
|
secure
|
||||||
|
}
|
||||||
53
modules/loader.js
Normal file
53
modules/loader.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
const fs = require("node:fs")
|
||||||
|
const path = require("node:path")
|
||||||
|
|
||||||
|
function getRecursiveFiles(dir) {
|
||||||
|
let results = []
|
||||||
|
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = fs.readdirSync(dir)
|
||||||
|
|
||||||
|
for (const file of list) {
|
||||||
|
const fullPath = path.join(dir, file)
|
||||||
|
const stat = fs.statSync(fullPath)
|
||||||
|
|
||||||
|
if (stat && stat.isDirectory()) {
|
||||||
|
results = results.concat(getRecursiveFiles(fullPath))
|
||||||
|
} else {
|
||||||
|
if (fullPath.endsWith(".js")) {
|
||||||
|
results.push(fullPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeRoutePath(baseDir, filePath) {
|
||||||
|
const relativePath = path.relative(baseDir, filePath)
|
||||||
|
const normalizedPath = relativePath.replace(/\\/g, "/")
|
||||||
|
|
||||||
|
let route = "/" + normalizedPath
|
||||||
|
|
||||||
|
if (route.endsWith("index.js")) {
|
||||||
|
route = route.replace("index.js", "")
|
||||||
|
} else {
|
||||||
|
route = route.replace(".js", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
route = route.replace(/\/{2,}/g, "/")
|
||||||
|
|
||||||
|
if (route.length > 1 && route.endsWith('/')) {
|
||||||
|
route = route.slice(0, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return route
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getRecursiveFiles,
|
||||||
|
computeRoutePath
|
||||||
|
}
|
||||||
85
modules/logger.js
Normal file
85
modules/logger.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
const fs = require("node:fs")
|
||||||
|
const path = require("node:path")
|
||||||
|
require("colors")
|
||||||
|
require("dotenv").config({
|
||||||
|
quiet: true
|
||||||
|
})
|
||||||
|
|
||||||
|
function cleanup($stream) {
|
||||||
|
if (!$stream.destroyed) {
|
||||||
|
$stream.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function write($stream, level, color, content, extraLabels = []) {
|
||||||
|
const date = new Date().toISOString()
|
||||||
|
const message = typeof content === "string" ? content : JSON.stringify(content, null, 2)
|
||||||
|
|
||||||
|
let consoleLabels = ""
|
||||||
|
let fileLabels = ""
|
||||||
|
|
||||||
|
if (Array.isArray(extraLabels) && extraLabels.length > 0) {
|
||||||
|
for (let i = 0; i < extraLabels.length; i += 2) {
|
||||||
|
const labelName = extraLabels[i]
|
||||||
|
const labelColor = extraLabels[i + 1]
|
||||||
|
if (labelName) {
|
||||||
|
fileLabels += ` [${labelName}]`
|
||||||
|
if (labelColor && labelName[labelColor]) {
|
||||||
|
consoleLabels += ` [${labelName[labelColor]}]`
|
||||||
|
} else {
|
||||||
|
consoleLabels += ` [${labelName.white}]`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[${date}] `.magenta + `[${level}]`[color] + consoleLabels + " " + message)
|
||||||
|
$stream.write(`[${date}] [${level}]${fileLabels} ${stripColors(message)}\n`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLogger(root) {
|
||||||
|
const fileName = (/false/).test(process.env.IS_PROD.toLowerCase()) ? new Date().toLocaleString("fr-FR", { timeZone: "UTC" }).replace(/[\/:]/g, "-").replace(/ /g, "_") : "DEV-LOG"
|
||||||
|
|
||||||
|
const logsDir = path.join(root, "logs")
|
||||||
|
|
||||||
|
if (!fs.existsSync(logsDir)) {
|
||||||
|
fs.mkdirSync(logsDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = fs.createWriteStream(path.join(logsDir, `${fileName}.log`), { flags: "a" })
|
||||||
|
|
||||||
|
process.on("exit", () => {
|
||||||
|
cleanup(stream)
|
||||||
|
})
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
cleanup(stream)
|
||||||
|
process.exit()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
log: (content, labels) => {
|
||||||
|
write(stream, "INFO", "green", content, labels)
|
||||||
|
},
|
||||||
|
error: (content, labels) => {
|
||||||
|
write(stream, "ERROR", "red", content, labels)
|
||||||
|
},
|
||||||
|
warn: (content, labels) => {
|
||||||
|
write(stream, "WARN", "yellow", content, labels)
|
||||||
|
},
|
||||||
|
debug: (content, labels) => {
|
||||||
|
write(stream, "DEBUG", "white", content, labels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripColors(string) {
|
||||||
|
if (!string || typeof string !== "string") {
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
return string.replace(/\x1B\[[0-9;]*[mK]/g, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createLogger
|
||||||
|
}
|
||||||
1211
package-lock.json
generated
Normal file
1211
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
package.json
Normal file
33
package.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "base-rest-api",
|
||||||
|
"version": "0.0.1-alpha",
|
||||||
|
"description": "",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://gitea.azures.fr/azures04/Base-REST-API"
|
||||||
|
},
|
||||||
|
"license": "AGPL-3.0-only",
|
||||||
|
"author": {
|
||||||
|
"name": "azures04",
|
||||||
|
"url": "https://gitea.azures.fr/azures04/Base-REST-API",
|
||||||
|
"email": "gilleslazure04@gmail.com"
|
||||||
|
},
|
||||||
|
"type": "commonjs",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start:dev": "nodemon .",
|
||||||
|
"start": "node .",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"homepage": "https://gitea.azures.fr/azures04/Base-REST-API",
|
||||||
|
"readme": "https://gitea.azures.fr/azures04/Base-REST-API/src/branch/main/README.md",
|
||||||
|
"dependencies": {
|
||||||
|
"colors": "^1.4.0",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"express": "^5.2.1",
|
||||||
|
"zod": "^4.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.1.11"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
routes/register.js
Normal file
11
routes/register.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
const express = require("express")
|
||||||
|
const router = express.Router()
|
||||||
|
const registerService = require("../services/register")
|
||||||
|
|
||||||
|
router.post("/", async (req, res) => {
|
||||||
|
const { email, username, password } = req.body
|
||||||
|
const registerResult = registerService.register({ email, username, password })
|
||||||
|
return res.status(registerResult.code).json(registerResult)
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
23
schemas/register.js
Normal file
23
schemas/register.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
errorFormat: "default",
|
||||||
|
POST: {
|
||||||
|
zod: z.object({
|
||||||
|
email: z.string()
|
||||||
|
.email({ message: "Invalid E-Mail format." })
|
||||||
|
.toLowerCase(),
|
||||||
|
username: z.string()
|
||||||
|
.min(3, { message: "The username must be at least 3 characters long." })
|
||||||
|
.max(16, { message: "The username must be no longer than 16 characters." }),
|
||||||
|
password: z.string()
|
||||||
|
.min(8, { message: "The password must be at least 8 characters long." })
|
||||||
|
.regex(/[A-Z]/, { message: "The password must contain a capital letter." })
|
||||||
|
.regex(/[0-9]/, { message: "The password must contain a number." }),
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
code: 422,
|
||||||
|
message: "Invalid body request"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
101
server.js
Normal file
101
server.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
const express = require("express")
|
||||||
|
const app = express()
|
||||||
|
const path = require("node:path")
|
||||||
|
const Logger = require("./modules/logger")
|
||||||
|
const logger = Logger.createLogger(__dirname)
|
||||||
|
const loader = require("./modules/loader")
|
||||||
|
|
||||||
|
const routes = loader.getRecursiveFiles(path.join(__dirname, "routes"))
|
||||||
|
const schemas = loader.getRecursiveFiles(path.join(__dirname, "schemas"))
|
||||||
|
|
||||||
|
const schemaRegistry = {}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
const schemaConfig = schemaRegistry[currentPath]
|
||||||
|
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)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
if (req.method === "GET" || req.method === "DELETE") {
|
||||||
|
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} ` + `<IP:${ip}>`.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)
|
||||||
|
})
|
||||||
|
|
||||||
|
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 subPath = routePath === "/" ? "" : routePath
|
||||||
|
logger.log(`${method.cyan} ${subPath.cyan.bold} route registered`, ["WEB", "yellow"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.use(routePath, router)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error.toString(), ["WEB", "yellow"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.listen(process.env.WEB_PORT || 3000, () => {
|
||||||
|
logger.log(`Server listening at port : ${process.env.WEB_PORT || 3000}`, ["WEB", "yellow"])
|
||||||
|
})
|
||||||
25
services/register.js
Normal file
25
services/register.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
const crypto = require("node:crypto")
|
||||||
|
|
||||||
|
function register({ email, username, password }) {
|
||||||
|
if (true) {
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: "User successfully registered",
|
||||||
|
data: {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
username: username,
|
||||||
|
email: email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
code: 418,
|
||||||
|
error: "I'm a tea pot",
|
||||||
|
message: "Error occured",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
register
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user