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.
This commit is contained in:
Gilles Lazures 2026-01-25 21:39:44 +01:00
parent 29cf189a87
commit cf54edf146
15 changed files with 104 additions and 128 deletions

View File

@ -1,21 +1,2 @@
# Base-REST-API # LentiaServices
A simple web api to check and download game files, get launcher news.
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
```

3
errors/errors.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
DefaultError: require("./DefaultError")
}

View File

@ -41,7 +41,7 @@ function write($stream, level, color, content, extraLabels = []) {
function createLogger(root) { function createLogger(root) {
// eslint-disable-next-line no-useless-escape // 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") 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) { function stripColors(string) {
if (!string || typeof string !== "string") { if (!string || typeof string !== "string") {
return string return string

View File

@ -1,6 +1,4 @@
const path = require("node:path") const logger = require("./logger")
const Logger = require("./logger")
const logger = Logger.createLogger(path.join(__dirname, ".."))
function sendValidationError(req, res, zodResult, type, path, errorConfig) { function sendValidationError(req, res, zodResult, type, path, errorConfig) {
const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress

10
routes/download.js Normal file
View File

@ -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

10
routes/gamefiles.js Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -4,8 +4,7 @@ const app = express()
const cors = require("cors") const cors = require("cors")
const path = require("node:path") const path = require("node:path")
const utils = require("./modules/utils") const utils = require("./modules/utils")
const Logger = require("./modules/logger") const logger = require("./modules/logger")
const logger = Logger.createLogger(__dirname)
const helmet = require("helmet") const helmet = require("helmet")
const loader = require("./modules/loader") const loader = require("./modules/loader")
const DefaultError = require("./errors/DefaultError") const DefaultError = require("./errors/DefaultError")

View File

@ -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
}

View File

@ -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
}

View File

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

View File

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