Add user GET endpoint, schemas, and security middlewares

Introduces GET /users/:id endpoint with validation schema, adds tests for user and register routes, and applies security middlewares (helmet, hpp, cors) to the server. Also adds ESLint configuration and updates logger with linting comments.
This commit is contained in:
Gilles Lazures 2025-12-22 13:26:13 +01:00
parent 7e1eaf3f1f
commit 335aef34e3
13 changed files with 1182 additions and 24 deletions

View File

@ -16,6 +16,6 @@ It features a **recursive file loader** for routes and schemas, along with a pow
1. **Clone the repository** 1. **Clone the repository**
```bash ```bash
git clone https://gitea.azures.fr/azures04/Base-REST-API.git git clone https://gitea.azures.fr/azures04/Base-REST-API.git
cd Base-REST-API cd Base-REST-API
``` ```

28
eslint.config.js Normal file
View File

@ -0,0 +1,28 @@
const js = require("@eslint/js")
const globals = require("globals")
module.exports = [
{
ignores: ["node_modules", "logs", "coverage", ".env", "*.log"],
},
js.configs.recommended,
{
languageOptions: {
ecmaVersion: "latest",
sourceType: "commonjs",
globals: {
...globals.node,
...globals.jest,
},
},
rules: {
"no-unused-vars": "warn",
"no-undef": "error",
"eqeqeq": "error",
"indent": ["error", 4],
"quotes": ["error", "double"],
"semi": ["error", "never"],
"no-console": "warn",
},
},
]

View File

@ -33,11 +33,13 @@ function write($stream, level, color, content, extraLabels = []) {
} }
} }
// eslint-disable-next-line no-console
console.log(`[${date}] `.magenta + `[${level}]`[color] + consoleLabels + " " + message) console.log(`[${date}] `.magenta + `[${level}]`[color] + consoleLabels + " " + message)
$stream.write(`[${date}] [${level}]${fileLabels} ${stripColors(message)}\n`) $stream.write(`[${date}] [${level}]${fileLabels} ${stripColors(message)}\n`)
} }
function createLogger(root) { function createLogger(root) {
// eslint-disable-next-line no-useless-escape
const fileName = (/false/).test(process.env.IS_PROD.toLowerCase()) ? new Date().toLocaleString("fr-FR", { timeZone: "UTC" }).replace(/[\/:]/g, "-").replace(/ /g, "_") : "DEV-LOG" 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") const logsDir = path.join(root, "logs")
@ -77,6 +79,7 @@ function stripColors(string) {
if (!string || typeof string !== "string") { if (!string || typeof string !== "string") {
return string return string
} }
// eslint-disable-next-line no-control-regex
return string.replace(/\x1B\[[0-9;]*[mK]/g, "") return string.replace(/\x1B\[[0-9;]*[mK]/g, "")
} }

1085
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,18 +17,25 @@
"scripts": { "scripts": {
"start:dev": "nodemon .", "start:dev": "nodemon .",
"start": "node .", "start": "node .",
"test": "echo \"Error: no test specified\" && exit 1" "lint": "eslint .",
"lint:fix": "eslint . --fix"
}, },
"homepage": "https://gitea.azures.fr/azures04/Base-REST-API", "homepage": "https://gitea.azures.fr/azures04/Base-REST-API",
"readme": "https://gitea.azures.fr/azures04/Base-REST-API/src/branch/main/README.md", "readme": "https://gitea.azures.fr/azures04/Base-REST-API/src/branch/main/README.md",
"dependencies": { "dependencies": {
"colors": "^1.4.0", "colors": "^1.4.0",
"cors": "^2.8.5",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"express": "^5.2.1", "express": "^5.2.1",
"helmet": "^8.1.0",
"hpp": "^0.2.3",
"path-to-regexp": "^8.3.0", "path-to-regexp": "^8.3.0",
"zod": "^4.2.0" "zod": "^4.2.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.2",
"eslint": "^9.39.2",
"globals": "^16.5.0",
"nodemon": "^3.1.11" "nodemon": "^3.1.11"
} }
} }

View File

@ -2,18 +2,14 @@ const express = require("express")
const router = express.Router() const router = express.Router()
const registerService = require("../services/register") const registerService = require("../services/register")
router.post("/", async (req, res, next) => { router.post("/", async (req, res) => {
try { const { email, username, password } = req.body
const { email, username, password } = req.body const registerResult = registerService.register({ email, username, password })
const registerResult = registerService.register({ email, username, password }) return res.status(200).json({
return res.status(200).json({ code: 200,
code: 200, message: "User successfully registered",
message: "User successfully registered", data: registerResult
data: registerResult })
})
} catch (error) {
next(error)
}
}) })
module.exports = router module.exports = router

View File

@ -1,8 +1,17 @@
const express = require("express") const express = require("express")
const DefaultError = require("../../errors/DefaultError")
const router = express.Router() const router = express.Router()
router.post("/", async (req, res, next) => { router.get("", async (req, res) => {
const bearer = req.headers.authorization
if (bearer == "Bearer token") {
return res.status(200).json({
id: req.params.id,
username: "johndoe"
})
} else {
throw new DefaultError(403, "Invalid token", "", "InvalidTokenException")
}
}) })
module.exports = router module.exports = router

View File

@ -3,9 +3,6 @@ const z = require("zod")
module.exports = { module.exports = {
POST: { POST: {
headers: 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() "content-type": z.string().regex(/application\/json/i).optional()
}), }),
body: z.object({ body: z.object({

15
schemas/users/[id].js Normal file
View File

@ -0,0 +1,15 @@
const z = require("zod")
module.exports = {
GET: {
headers: z.object({
authorization: z.string()
.startsWith("Bearer ", { message: "Token d'authentification manquant ou invalide." }),
"content-type": z.string().regex(/application\/json/i).optional()
}),
error: {
code: 422,
message: "Invalid request data"
}
}
}

View File

@ -1,9 +1,12 @@
const express = require("express") const express = require("express")
const hpp = require("hpp")
const app = express() const app = express()
const cors = require("cors")
const path = require("node:path") const path = require("node:path")
const utils = require("./modules/utils")
const Logger = require("./modules/logger") const Logger = require("./modules/logger")
const logger = Logger.createLogger(__dirname) const logger = Logger.createLogger(__dirname)
const utils = require("./modules/utils") const helmet = require("helmet")
const loader = require("./modules/loader") const loader = require("./modules/loader")
const DefaultError = require("./errors/DefaultError") const DefaultError = require("./errors/DefaultError")
const path2regex = require("path-to-regexp") const path2regex = require("path-to-regexp")
@ -13,6 +16,10 @@ const schemas = loader.getRecursiveFiles(path.join(__dirname, "schemas"))
const schemaRegistry = {} const schemaRegistry = {}
app.use(hpp())
app.use(helmet())
app.use(cors({ origin: "*" }))
app.use(express.json()) app.use(express.json())
app.use(express.urlencoded({ extended: true })) app.use(express.urlencoded({ extended: true }))

View File

@ -1,8 +1,9 @@
const crypto = require("node:crypto") const crypto = require("node:crypto")
const DefaultError = require("../errors/DefaultError") const DefaultError = require("../errors/DefaultError")
function register({ email, username, password }) { function register({ email, username }) {
if (true) { const canRegister = true
if (canRegister === true) {
return { return {
id: crypto.randomUUID(), id: crypto.randomUUID(),
username: username, username: username,

8
tests/register.rest Normal file
View File

@ -0,0 +1,8 @@
POST http://localhost:3000/register HTTP/1.1
Content-Type: application/json
{
"email": "johndoe@exemple.com",
"username": "johndoe",
"password": "Password123"
}

2
tests/users/id.rest Normal file
View File

@ -0,0 +1,2 @@
GET http://localhost:3000/users/johndoe HTTP/1.1
authorization: Bearer token