Refactor logger usage and add userRepository module

Replaces custom logger instantiation with a shared logger import across modules and routes. Moves player property and privilege management from authRepository to a new userRepository, expanding userRepository with additional user management functions (ban, unban, preferences, privileges, bans). Updates service and route files to use userRepository where appropriate. Adds new session join route and schema, and utility for UUID formatting.
This commit is contained in:
Gilles Lazures 2025-12-24 04:22:43 +01:00
parent 80bca31d9a
commit 2519d8078a
18 changed files with 438 additions and 76 deletions

View File

@ -1,7 +1,8 @@
class SessionError extends Error {
constructor(statusCode, errorMessage, path) {
constructor(statusCode, error, errorMessage, path) {
super(errorMessage)
this.path = path
this.error = error
this.statusCode = statusCode
this.errorMessage = errorMessage
this.isOperational = true
@ -13,6 +14,9 @@ class SessionError extends Error {
path: this.path,
errorMessage: this.errorMessage
}
if (this.error != undefined) {
response.error = this.error
}
return response
}
}

View File

@ -1,7 +1,6 @@
const path = require("node:path")
const DefaultError = require("./DefaultError")
const Logger = require("../modules/logger")
const logger = Logger.createLogger(path.join(__dirname, ".."))
const logger = require("../modules/logger")
class ValidationError extends DefaultError {
constructor(zodResult, config = {}, context = {}) {

View File

@ -1,7 +1,6 @@
const path = require("node:path")
const mariadb = require("mariadb")
const Logger = require("./logger")
const logger = Logger.createLogger(path.join(__dirname, ".."))
const logger = require("./logger")
const crypto = require("node:crypto")
const rootConfig = {
@ -326,7 +325,7 @@ async function setupDatabase() {
await conn.query(`
CREATE TABLE IF NOT EXISTS serverSessions (
uuid VARCHAR(36) PRIMARY KEY,
accessToken VARCHAR(512) NOT NULL,
accessToken TEXT NOT NULL,
serverId VARCHAR(255) NOT NULL,
ip VARCHAR(45) NULL,
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,

View File

@ -85,6 +85,6 @@ function stripColors(string) {
return string.replace(/\x1B\[[0-9;]*[mK]/g, "")
}
module.exports = {
createLogger
}
const logger = createLogger(path.join(__dirname, ".."))
module.exports = logger

View File

@ -1,7 +1,6 @@
const path = require("node:path")
const Logger = require("./logger")
const logger = require("./logger")
const crypto = require("node:crypto")
const logger = Logger.createLogger(path.join(__dirname, ".."))
const certificatesManager = require("./certificatesManager")
const serverKeys = certificatesManager.getKeys()
@ -40,7 +39,22 @@ function signProfileData(dataBase64) {
}
}
function addDashesToUUID(uuid) {
if (typeof uuid !== "string" || uuid.length !== 32) {
return uuid
}
return (
uuid.slice(0, 8) + "-" +
uuid.slice(8, 12) + "-" +
uuid.slice(12, 16) + "-" +
uuid.slice(16, 20) + "-" +
uuid.slice(20)
)
}
module.exports = {
getRegistrationCountryFromIp,
addDashesToUUID,
signProfileData
}

View File

@ -1,6 +1,4 @@
const path = require("node:path")
const Logger = require("../modules/logger")
const logger = Logger.createLogger(path.join(__dirname, ".."))
const logger = require("../modules/logger")
const bcrypt = require("bcryptjs")
const database = require("../modules/database")
const { DefaultError } = require("../errors/errors")
@ -77,39 +75,6 @@ async function checkUsernameAvailability(username) {
return { code: 200, allowed: true }
}
async function addPropertyToPlayer(key, value, uuid) {
try {
const sql = `INSERT INTO playersProperties (name, value, uuid) VALUES (?, ?, ?)`
const result = await database.query(sql, [key, value, uuid])
if (result.affectedRows > 0) {
return { code: 200, key, value, uuid }
} else {
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
}
} catch (error) {
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
}
}
async function deletePropertyToPlayer(key, uuid) {
try {
const sql = `DELETE FROM playersProperties WHERE name = ? AND uuid = ?`
const result = await database.query(sql, [key, uuid])
if (result.affectedRows > 0) {
return { code: 200, key, uuid }
} else {
throw new DefaultError(500, "Property not found for this user/key combination.")
}
} catch (error) {
if (error instanceof DefaultError) throw error
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
}
}
async function insertClientSession(accessToken, clientToken, uuid) {
try {
const sql = `INSERT INTO clientSessions (accessToken, clientToken, uuid) VALUES (?, ?, ?)`
@ -265,10 +230,8 @@ module.exports = {
getClientSession,
revokeAccessTokens,
insertClientSession,
addPropertyToPlayer,
getPlayerProperties,
validateClientSession,
deletePropertyToPlayer,
invalidateClientSession,
validateClientSessionWithoutClientToken
}

View File

@ -1,6 +1,4 @@
const path = require("node:path")
const Logger = require("../modules/logger")
const logger = Logger.createLogger(path.join(__dirname, ".."))
const logger = require("../modules/logger")
const database = require("../modules/database")
const { DefaultError } = require("../errors/errors")

View File

@ -0,0 +1,312 @@
const crypto = require("node:crypto")
const logger = require("../modules/logger")
const database = require("../modules/database")
const { DefaultError } = require("../errors/errors")
async function addPropertyToPlayer(key, value, uuid) {
try {
const sql = `INSERT INTO playersProperties (name, value, uuid) VALUES (?, ?, ?)`
const result = await database.query(sql, [key, value, uuid])
if (result.affectedRows > 0) {
return { code: 200, key, value, uuid }
} else {
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
}
} catch (error) {
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
}
}
async function deletePropertyToPlayer(key, uuid) {
try {
const sql = `DELETE FROM playersProperties WHERE name = ? AND uuid = ?`
const result = await database.query(sql, [key, uuid])
if (result.affectedRows > 0) {
return { code: 200, key, uuid }
} else {
throw new DefaultError(500, "Property not found for this user/key combination.")
}
} catch (error) {
if (error instanceof DefaultError) throw error
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
}
}
async function updatePropertyToPlayer(key, value, uuid) {
try {
const sql = `UPDATE playersProperties SET value = ? WHERE name = ? AND uuid = ?`
const result = await database.query(sql, [value, key, uuid])
if (result.affectedRows > 0) {
return { code: 200, key, value, uuid }
} else {
throw new DefaultError(404, "Property not found for this user/key combination")
}
} catch (error) {
if (error instanceof DefaultError) throw error
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
}
}
async function getPlayerProperties(uuid) {
try {
const sql = `SELECT * FROM playersProperties WHERE uuid = ?`
const rows = await database.query(sql, [uuid])
if (rows.length === 0) {
throw new DefaultError(404, "Properties not found for this user")
}
return {
code: 200,
properties: rows.map(property => ({ name: property.name, value: property.value }))
}
} catch (error) {
if (error instanceof DefaultError) throw error
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
}
}
async function getPlayerProperty(key, uuid) {
try {
const sql = `SELECT * FROM playersProperties WHERE name = ? AND uuid = ?`
const rows = await database.query(sql, [key, uuid])
const property = rows[0]
if (!property) {
throw new DefaultError(404, "Property not found for this user/key combination")
}
return { code: 200, property }
} catch (error) {
if (error instanceof DefaultError) throw error
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
}
}
async function getPlayerSettingsSchema() {
const RAW_SCHEMA_CACHE = {
privileges: {},
preferences: {}
}
try {
const privilegesRows = await database.query("DESCRIBE playersPrivileges")
const preferencesRows = await database.query("DESCRIBE playersPreferences")
RAW_SCHEMA_CACHE.privileges = privilegesRows.map(c => c.Field).filter(n => n !== "uuid")
RAW_SCHEMA_CACHE.preferences = preferencesRows.map(c => c.Field).filter(n => n !== "uuid")
return RAW_SCHEMA_CACHE
} catch (err) {
logger.log("Database Schema Error: " + err.toString(), ["MariaDB", "red"])
throw new DefaultError(500, "Internal Server Error", "Database Schema Error")
}
}
async function updatePlayerPreferences(uuid, updates) {
try {
const keys = Object.keys(updates)
if (keys.length === 0) {
throw new DefaultError(400, "No fields provided for update.")
}
const setClause = keys.map(key => `\`${key}\` = ?`).join(', ')
const sql = `UPDATE playersPreferences SET ${setClause} WHERE uuid = ?`
const values = keys.map(key => updates[key])
values.push(uuid)
const result = await database.query(sql, values)
if (result.affectedRows > 0) {
return {
code: 200,
message: "Preferences updated successfully."
}
} else {
throw new DefaultError(404, "Player preferences not found or no changes made.")
}
} catch (error) {
if (error instanceof DefaultError) throw error
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
}
}
async function getPlayerPreferences(uuid) {
try {
const sql = `SELECT profanityFilter FROM playersPreferences WHERE uuid = ?`
const rows = await database.query(sql, [uuid])
const data = rows[0]
if (data) {
return {
code: 200,
message: "Preferences retrieved successfully.",
data: data
}
} else {
throw new DefaultError(404, "Preferences not found for this UUID.")
}
} catch (error) {
if (error instanceof DefaultError) throw error
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
}
}
async function getPlayerPrivileges(uuid) {
try {
const sql = `
SELECT onlineChat, multiplayerServer, multiplayerRealms, telemetry
FROM playersPrivileges
WHERE uuid = ?
`
const rows = await database.query(sql, [uuid])
const data = rows[0]
if (data) {
return {
code: 200,
message: "Privileges retrieved successfully.",
data: data
}
} else {
throw new DefaultError(404, "Privileges not found for this UUID.")
}
} catch (error) {
if (error instanceof DefaultError) throw error
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
}
}
async function updatePlayerPrivileges(uuid, updates) {
try {
const keys = Object.keys(updates)
if (keys.length === 0) {
throw new DefaultError(404, "No fields provided for update.")
}
const setClause = keys.map(key => `\`${key}\` = ?`).join(', ')
const sql = `UPDATE playersPrivileges SET ${setClause} WHERE uuid = ?`
const values = keys.map(key => updates[key])
values.push(uuid)
const result = await database.query(sql, values)
if (result.affectedRows > 0) {
return {
code: 200,
message: "Privileges updated successfully."
}
} else {
throw new DefaultError(404, "Player privileges not found or no changes made.")
}
} catch (error) {
if (error instanceof DefaultError) throw error
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
}
}
async function banUser(uuid, { reasonKey, reasonMessage, expires = null }) {
try {
if (!uuid || !reasonKey) {
throw new DefaultError(400, "Missing uuid or reasonKey.")
}
let reasonId
const reasonRows = await database.query("SELECT id FROM banReasons WHERE reason_key = ?", [reasonKey])
if (reasonRows.length > 0) {
reasonId = reasonRows[0].id
} else {
const insertReason = await database.query("INSERT INTO banReasons (reason_key) VALUES (?)", [reasonKey])
reasonId = insertReason.insertId
}
const banId = crypto.randomUUID()
const insertSql = `
INSERT INTO bans (banId, uuid, reason, reasonMessage, expires)
VALUES (?, ?, ?, ?, ?)
`
const result = await database.query(insertSql, [banId, uuid, reasonId, reasonMessage || "Banned by operator", expires])
if (result.affectedRows > 0) {
return {
code: 200,
message: "User successfully banned.",
banId: banId
}
} else {
throw new DefaultError(500, "Failed to ban user.")
}
} catch (error) {
if (error instanceof DefaultError) throw error
if (error.code === "ER_NO_REFERENCED_ROW_2" || error.toString().includes("foreign key constraint")) {
throw new DefaultError(404, "User not found (cannot ban a ghost).")
}
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
throw new DefaultError(500, "Internal Server Error", error.toString())
}
}
async function unbanUser(uuid) {
try {
if (!uuid) {
throw new DefaultError(400, "Missing uuid.")
}
const sql = "DELETE FROM bans WHERE uuid = ?"
const result = await database.query(sql, [uuid])
if (result.affectedRows > 0) {
return {
code: 200,
message: "User successfully unbanned.",
count: result.affectedRows
}
} else {
throw new DefaultError(404, "User was not banned.")
}
} catch (error) {
if (error instanceof DefaultError) throw error
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
}
}
async function getPlayerBans(uuid) {
try {
const sql = `
SELECT
b.banId,
b.expires,
b.reasonMessage,
r.reason_key as reason
FROM bans b
JOIN banReasons r ON b.reason = r.id
WHERE b.uuid = ?
ORDER BY b.expires ASC
`
const rows = await database.query(sql, [uuid])
if (rows.length > 0) {
return { code: 200, bans: rows }
} else {
return { code: 204 }
}
} catch (error) {
if (error instanceof DefaultError) throw error
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
}
}
module.exports = {
banUser,
unbanUser,
getPlayerBans,
getPlayerProperty,
getPlayerPrivileges,
getPlayerProperties,
addPropertyToPlayer,
getPlayerPreferences,
updatePlayerPrivileges,
deletePropertyToPlayer,
updatePropertyToPlayer,
getPlayerSettingsSchema,
updatePlayerPreferences,
}

View File

@ -1,11 +1,9 @@
const path = require("path")
const express = require("express")
const router = express.Router()
const { YggdrasilError } = require("../../errors/errors")
const rateLimit = require("express-rate-limit")
const authService = require("../../services/authService")
const Logger = require("../../modules/logger")
const logger = Logger.createLogger(path.join(__dirname, "..", ".."))
const logger = require("../../modules/logger")
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,

View File

@ -1,9 +1,7 @@
const path = require("node:path")
const express = require("express")
const router = express.Router()
const authService = require("../../services/authService")
const Logger = require("../../modules/logger")
const logger = Logger.createLogger(path.join(__dirname, "..", ".."))
const logger = require("../../modules/logger")
const { DefaultError, YggdrasilError } = require("../../errors/errors")
router.post("/", async (req, res) => {

View File

@ -1,9 +1,7 @@
const path = require("node:path")
const express = require("express")
const router = express.Router()
const authService = require("../../services/authService")
const Logger = require("../../modules/logger")
const logger = Logger.createLogger(path.join(__dirname, "..", ".."))
const logger = require("../../modules/logger")
const { DefaultError, YggdrasilError } = require("../../errors/errors")
router.post("/", async (req, res) => {

View File

@ -1,8 +1,6 @@
const path = require("node:path")
const express = require("express")
const router = express.Router()
const Logger = require("../modules/logger")
const logger = Logger.createLogger(path.join(__dirname, ".."))
const logger = require("../modules/logger")
const authService = require("../services/authService")
router.post("/", async (req, res) => {

View File

@ -1,10 +1,8 @@
const path = require("path")
const express = require("express")
const router = express.Router()
const sessionsService = require("../../../../services/sessionsService")
const Logger = require("../../../../modules/logger")
const logger = require("../../../../modules/logger")
const { YggdrasilError, DefaultError } = require("../../../../errors/errors")
const logger = Logger.createLogger(path.join(__dirname, "..", "..", "..", ".."))
router.get("/", async (req, res) => {
const { username, serverId, ip } = req.query

View File

@ -0,0 +1,66 @@
const path = require("path")
const express = require("express")
const router = express.Router()
const utils = require("../../../../modules/utils")
const authService = require("../../../../services/authService")
const sessionsService = require("../../../../services/sessionsService")
const userRepository = require("../../../../repositories/userRepository")
const logger = require("../../../../modules/logger")
const { SessionError, DefaultError } = require("../../../../errors/errors")
router.post("/", async (req, res) => {
const { accessToken, selectedProfile, serverId } = req.body
try {
const verificationResult = await authService.verifyAccessToken({ accessToken })
const tokenUuid = verificationResult.user.uuid
const requestedProfile = utils.addDashesToUUID(selectedProfile)
if (tokenUuid !== requestedProfile) {
throw new SessionError(403, "Forbidden", "You cannot join with a profile that is not yours.", req.originalUrl)
}
const bansResult = await userRepository.getPlayerBans(tokenUuid)
if (bansResult.code === 200 && bansResult.bans && bansResult.bans.length > 0) {
const activeBan = bansResult.bans[0]
throw new SessionError(
403,
"UserBannedException",
activeBan.reasonMessage || "You are banned from multiplayer.",
req.originalUrl
)
}
try {
const privsResult = await userRepository.getPlayerPrivileges(tokenUuid)
if (privsResult.code === 200 && privsResult.data) {
if (!privsResult.data.multiplayerServer) {
throw new SessionError(403, "InsufficientPrivilegesException", "Multiplayer is disabled for your account.", req.originalUrl)
}
}
} catch (privError) {
if (privError instanceof DefaultError && privError.code !== 404) throw privError
}
const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress
await sessionsService.joinServer({
clientToken: verificationResult.session.clientToken,
accessToken,
selectedProfile: requestedProfile,
serverId,
ip
})
logger.log(`Server join success: ${verificationResult.user.username}`, ["SESSION", "green"])
return res.status(204).end()
} catch (err) {
console.log(err)
if (err instanceof SessionError) throw err
if (err instanceof DefaultError) {
const statusCode = err.code === 401 ? 403 : (err.code || 500)
const errorName = "Forbidden"
throw new SessionError(statusCode, errorName, err.message, req.originalUrl)
}
throw new SessionError(500, "Forbidden", "Internal Server Error", req.originalUrl)
}
})
module.exports = router

View File

@ -17,12 +17,12 @@ router.get("", async (req, res) => {
return res.status(200).json(result.data)
}
if (result.code === 204) {
throw new SessionError(404, "Not a valid UUID", req.originalUrl)
throw new SessionError(404, undefined, "Not a valid UUID", req.originalUrl)
}
throw new DefaultError(500, "Unknown error")
throw new DefaultError(500, undefined, "Unknown error", req.originalUrl)
} catch (err) {
const errorMessage = err.message || "Not a valid UUID"
throw new SessionError(400, errorMessage, req.originalUrl)
throw new SessionError(400, undefined, errorMessage, req.originalUrl)
}
})

View File

@ -0,0 +1,19 @@
const z = require("zod")
module.exports = {
GET: {
query: z.object({
username: z.string()
.min(3)
.max(16),
serverId: z.string()
.min(1),
ip: z.string()
.optional()
}),
error: {
code: 204,
message: "Ignored"
}
}
}

View File

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

View File

@ -2,6 +2,7 @@ const jwt = require("jsonwebtoken")
const utils = require("../modules/utils")
const bcrypt = require("bcryptjs")
const crypto = require("node:crypto")
const userRepository = require("../repositories/userRepository")
const authRepository = require("../repositories/authRepository")
const certsManager = require("../modules/certificatesManager")
const { DefaultError } = require("../errors/errors")
@ -30,15 +31,14 @@ async function registerUser({ username, password, email, registrationCountry, pr
const { uuid } = userRegistration
const resolvedCountry = registrationCountry || await utils.getRegistrationCountryFromIp(clientIp)
await authRepository.addPropertyToPlayer("registrationCountry", resolvedCountry || "UNKNOWN", uuid)
await authRepository.addPropertyToPlayer("userPreferredLanguage", preferredLanguage || "fr-FR", uuid)
await userRepository.addPropertyToPlayer("registrationCountry", resolvedCountry || "UNKNOWN", uuid)
await userRepository.addPropertyToPlayer("userPreferredLanguage", preferredLanguage || "fr-FR", uuid)
return { code: 200, message: "User created successfully", uuid }
}
async function authenticate({ identifier, password, clientToken, requireUser }) {
let userResult
try {
userResult = await authRepository.getUser(identifier, true)
} catch (error) {
@ -47,7 +47,6 @@ async function authenticate({ identifier, password, clientToken, requireUser })
}
throw error
}
const passwordValidationProcess = await bcrypt.compare(password, userResult.user.password)
if (!passwordValidationProcess) {
throw new DefaultError(403, "Invalid credentials. Invalid username or password.", "ForbiddenOperationException")