diff --git a/README.md b/README.md index 7312fef..bb1d799 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,2 @@ -# Base-REST-API - -A robust, modular, and secure REST API boilerplate built with **Node.js** and **Express**. -It features a **recursive file loader** for routes and schemas, along with a powerful validation middleware using **Zod**. - -## 🚀 Features - -- **Automated Loading**: Recursively loads routes and validation schemas from the file system. -- **Strict Validation**: Request bodies and query parameters are validated using [Zod](https://zod.dev/) before reaching the controller. -- **Clean Architecture**: Separation of concerns with `Routes` (HTTP layer), `Services` (Business logic), and `Schemas` (Validation). -- **Security First**: Inputs are stripped of unknown fields automatically. -- **Custom Logger**: Integrated color-coded logging system for development and file logging for production. -- **Error Handling**: Standardized JSON error responses. - -## 📦 Installation - -1. **Clone the repository** - ```bash - git clone https://gitea.azures.fr/azures04/Base-REST-API.git - cd Base-REST-API - ``` \ No newline at end of file +# LentiaServices +A simple web api to check and download game files, get launcher news. \ No newline at end of file diff --git a/errors/errors.js b/errors/errors.js new file mode 100644 index 0000000..da6ff96 --- /dev/null +++ b/errors/errors.js @@ -0,0 +1,3 @@ +module.exports = { + DefaultError: require("./DefaultError") +} \ No newline at end of file diff --git a/modules/logger.js b/modules/logger.js index 7026d44..6b7c6fd 100644 --- a/modules/logger.js +++ b/modules/logger.js @@ -41,7 +41,7 @@ function write($stream, level, color, content, extraLabels = []) { function createLogger(root) { // eslint-disable-next-line no-useless-escape - const fileName = utils.isTrueFromDotEnv("IS_PROD") ? new Date().toLocaleString("fr-FR", { timeZone: "UTC" }).replace(/[\/:]/g, "-").replace(/ /g, "_") : "DEV-LOG" + const fileName = isTrueFromDotEnv("IS_PROD") ? new Date().toLocaleString("fr-FR", { timeZone: "UTC" }).replace(/[\/:]/g, "-").replace(/ /g, "_") : "DEV-LOG" const logsDir = path.join(root, "logs") @@ -76,6 +76,10 @@ function createLogger(root) { } } +function isTrueFromDotEnv(key) { + return (process.env[key] || "").trim().toLowerCase() === "true" +} + function stripColors(string) { if (!string || typeof string !== "string") { return string diff --git a/modules/utils.js b/modules/utils.js index 193ee0d..ad2aea9 100644 --- a/modules/utils.js +++ b/modules/utils.js @@ -1,6 +1,4 @@ -const path = require("node:path") -const Logger = require("./logger") -const logger = Logger.createLogger(path.join(__dirname, "..")) +const logger = require("./logger") function sendValidationError(req, res, zodResult, type, path, errorConfig) { const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress diff --git a/routes/download.js b/routes/download.js new file mode 100644 index 0000000..dda6cb6 --- /dev/null +++ b/routes/download.js @@ -0,0 +1,10 @@ +const express = require("express") +const router = express.Router() +const gameFilesService = require("../services/gameDataService") + +router.get(/.*/, async (req, res) => { + const fileRequest = await gameFilesService.getFile(req.baseUrl.replace("/download/", "")) + return res.status(fileRequest.code).sendFile(fileRequest.path) +}) + +module.exports = router \ No newline at end of file diff --git a/routes/gamefiles.js b/routes/gamefiles.js new file mode 100644 index 0000000..48ac79c --- /dev/null +++ b/routes/gamefiles.js @@ -0,0 +1,10 @@ +const express = require("express") +const router = express.Router() +const gameFilesService = require("../services/gameDataService") + +router.get("", async (req, res) => { + const gameIndex = await gameFilesService.getGameFiles() + return res.status(200).json(gameIndex) +}) + +module.exports = router \ No newline at end of file diff --git a/routes/register.js b/routes/register.js deleted file mode 100644 index 0caddaa..0000000 --- a/routes/register.js +++ /dev/null @@ -1,15 +0,0 @@ -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(200).json({ - code: 200, - message: "User successfully registered", - data: registerResult - }) -}) - -module.exports = router \ No newline at end of file diff --git a/routes/users/[id].js b/routes/users/[id].js deleted file mode 100644 index ddb426b..0000000 --- a/routes/users/[id].js +++ /dev/null @@ -1,17 +0,0 @@ -const express = require("express") -const DefaultError = require("../../errors/DefaultError") -const router = express.Router() - -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 \ No newline at end of file diff --git a/schemas/register.js b/schemas/register.js deleted file mode 100644 index cffb6ab..0000000 --- a/schemas/register.js +++ /dev/null @@ -1,25 +0,0 @@ -const z = require("zod") - -module.exports = { - POST: { - headers: z.object({ - "content-type": z.string().regex(/application\/json/i).optional() - }), - body: 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 request data" - } - } -} \ No newline at end of file diff --git a/schemas/users/[id].js b/schemas/users/[id].js deleted file mode 100644 index 74f95fc..0000000 --- a/schemas/users/[id].js +++ /dev/null @@ -1,15 +0,0 @@ -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" - } - } -} \ No newline at end of file diff --git a/server.js b/server.js index ae30264..dcbfb35 100644 --- a/server.js +++ b/server.js @@ -4,8 +4,7 @@ const app = express() const cors = require("cors") const path = require("node:path") const utils = require("./modules/utils") -const Logger = require("./modules/logger") -const logger = Logger.createLogger(__dirname) +const logger = require("./modules/logger") const helmet = require("helmet") const loader = require("./modules/loader") const DefaultError = require("./errors/DefaultError") diff --git a/services/gameDataService.js b/services/gameDataService.js new file mode 100644 index 0000000..4409362 --- /dev/null +++ b/services/gameDataService.js @@ -0,0 +1,72 @@ +const fs = require("node:fs") +const path = require("node:path") +const crypto = require("node:crypto") +const { DefaultError } = require("../errors/errors") +const { pipeline } = require("node:stream/promises") +const gameDataPath = path.join(process.cwd(), "data", "game") + +function normalizePath($path) { + return $path.split(path.win32.sep).join(path.posix.sep) +} + +async function getFileSha1(filePath) { + const hash = crypto.createHash("sha1") + const readStream = fs.createReadStream(filePath) + + try { + await pipeline(readStream, hash) + return hash.digest("hex") + } catch (error) { + throw new Error(`Erreur lors du calcul du SHA-1 : ${error.message}`); + } +} + +async function getGameFiles() { + const files = await fs.promises.readdir(gameDataPath, { recursive: true }) + const gameFilesIndex = { root: [] } + for (const file of files) { + const filePath = path.join(gameDataPath, file) + const fileMetadata = await fs.promises.stat(filePath) + if (!fileMetadata.isDirectory()) { + const normalizedFilePath = normalizePath(file) + const artifact = { + path: normalizedFilePath, + sha1: await getFileSha1(filePath), + size: fileMetadata.size, + url: (process.env.BASE_POINT || "") + normalizedFilePath + } + const downloadObject = { + downloads: { + artifact, + }, + name: path.parse(normalizedFilePath).base + } + gameFilesIndex.root.push(downloadObject) + } + } + return gameFilesIndex +} + +async function getFile(basePath) { + try { + const fixedPath = path.join(gameDataPath, basePath) + const fileMetadata = await fs.promises.stat(fixedPath) + if (fileMetadata.isDirectory()) { + throw new DefaultError(409, "Can't download a directory", "", "NotDownloadableException") + } + + return { code: 200, path: fixedPath } + } catch (error) { + if (error.code == "ENOENT") { + throw new DefaultError(404, "File not found", "", "NotFoundException") + } else { + throw error + } + } +} + +module.exports = { + getFile, + getFileSha1, + getGameFiles +} \ No newline at end of file diff --git a/services/register.js b/services/register.js deleted file mode 100644 index 1b8b245..0000000 --- a/services/register.js +++ /dev/null @@ -1,19 +0,0 @@ -const crypto = require("node:crypto") -const DefaultError = require("../errors/DefaultError") - -function register({ email, username }) { - const canRegister = true - if (canRegister === true) { - return { - id: crypto.randomUUID(), - username: username, - email: email - } - } else { - throw new DefaultError(418, "I'm a teapot", "", "TeaPotExeception") - } -} - -module.exports = { - register -} \ No newline at end of file diff --git a/tests/register.rest b/tests/register.rest deleted file mode 100644 index 9a8a96d..0000000 --- a/tests/register.rest +++ /dev/null @@ -1,8 +0,0 @@ -POST http://localhost:3000/register HTTP/1.1 -Content-Type: application/json - -{ - "email": "johndoe@exemple.com", - "username": "johndoe", - "password": "Password123" -} \ No newline at end of file diff --git a/tests/users/id.rest b/tests/users/id.rest deleted file mode 100644 index 05b2df5..0000000 --- a/tests/users/id.rest +++ /dev/null @@ -1,2 +0,0 @@ -GET http://localhost:3000/users/johndoe HTTP/1.1 -authorization: Bearer token \ No newline at end of file