From cf54edf146013430e311d2aa564148ad3450032a Mon Sep 17 00:00:00 2001 From: azures04 Date: Sun, 25 Jan 2026 21:39:44 +0100 Subject: [PATCH] Refactor API for game file download and listing Removed user registration and user info endpoints, along with related schemas, services, and tests. Added new routes and service for listing and downloading game files. Updated README to reflect new API purpose. Refactored logger and utils for improved modularity. --- README.md | 23 ++---------- errors/errors.js | 3 ++ modules/logger.js | 6 +++- modules/utils.js | 4 +-- routes/download.js | 10 ++++++ routes/gamefiles.js | 10 ++++++ routes/register.js | 15 -------- routes/users/[id].js | 17 --------- schemas/register.js | 25 ------------- schemas/users/[id].js | 15 -------- server.js | 3 +- services/gameDataService.js | 72 +++++++++++++++++++++++++++++++++++++ services/register.js | 19 ---------- tests/register.rest | 8 ----- tests/users/id.rest | 2 -- 15 files changed, 104 insertions(+), 128 deletions(-) create mode 100644 errors/errors.js create mode 100644 routes/download.js create mode 100644 routes/gamefiles.js delete mode 100644 routes/register.js delete mode 100644 routes/users/[id].js delete mode 100644 schemas/register.js delete mode 100644 schemas/users/[id].js create mode 100644 services/gameDataService.js delete mode 100644 services/register.js delete mode 100644 tests/register.rest delete mode 100644 tests/users/id.rest 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