diff --git a/modules/databaseGlobals.js b/modules/databaseGlobals.js index 0768aef..d55e327 100644 --- a/modules/databaseGlobals.js +++ b/modules/databaseGlobals.js @@ -345,6 +345,34 @@ async function setupDatabase() { DELETE FROM playerCertificates WHERE expiresAt < NOW(); `) logger.log(`${"clean_expired_certificates".bold} event ready`, ["MariaDB", "yellow"]) + + await conn.query(` + CREATE TABLE IF NOT EXISTS api_administrators ( + id INTEGER PRIMARY KEY AUTO_INCREMENT, + username VARCHAR(255) UNIQUE NOT NULL, + password TEXT NOT NULL, + createdAt DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `) + logger.log(`${"api_administrators".bold} table ready`, ["MariaDB", "yellow"]) + + await conn.query(` + CREATE TABLE IF NOT EXISTS api_administrators_permissions_list ( + permission_key VARCHAR(64) PRIMARY KEY + ) + `) + logger.log(`${"api_administrators_permissions_list".bold} table ready`, ["MariaDB", "yellow"]) + + await conn.query(` + CREATE TABLE IF NOT EXISTS api_administrators_permissions ( + administrator_id INTEGER NOT NULL, + permission_key VARCHAR(64) NOT NULL, + PRIMARY KEY (administrator_id, permission_key), + FOREIGN KEY (administrator_id) REFERENCES api_administrators(id) ON DELETE CASCADE, + FOREIGN KEY (permission_key) REFERENCES api_administrators_permissions_list(permission_key) ON DELETE CASCADE + ) + `) + logger.log(`${"api_administrators_permissions".bold} table ready`, ["MariaDB", "yellow"]) logger.log("MariaDB database successfully initialised!", ["MariaDB", "yellow"]) diff --git a/repositories/adminRepository.js b/repositories/adminRepository.js new file mode 100644 index 0000000..7f2891b --- /dev/null +++ b/repositories/adminRepository.js @@ -0,0 +1,131 @@ +const logger = require("../modules/logger") +const database = require("../modules/database") +const { DefaultError } = require("../errors/errors") + +async function getAdminById(id) { + try { + const sql = "SELECT id, username, createdAt FROM api_administrators WHERE id = ?" + const rows = await database.query(sql, [id]) + return rows[0] || null + } catch (error) { + logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"]) + throw new DefaultError(500, "Internal Server Error", "Database Error") + } +} + +async function createAdmin(username, hashedPassword) { + try { + const sql = "INSERT INTO api_administrators (username, password) VALUES (?, ?)" + const result = await database.query(sql, [username, hashedPassword]) + + if (result.affectedRows > 0) { + return { code: 200, id: result.insertId, username } + } else { + throw new DefaultError(500, "Failed to create administrator.") + } + } catch (error) { + if (error.code === "ER_DUP_ENTRY") { + throw new DefaultError(409, "Administrator username already exists.") + } + logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"]) + throw new DefaultError(500, "Internal Server Error", "Database Error") + } +} + +async function hasPermission(adminId, permissionKey) { + try { + const sql = ` + SELECT COUNT(*) as count + FROM api_administrators_permissions + WHERE administrator_id = ? AND permission_key = ? + ` + const rows = await database.query(sql, [adminId, permissionKey]) + return rows[0].count === 1 + } catch (error) { + logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"]) + throw new DefaultError(500, "Internal Server Error", "Database Error") + } +} + +async function assignPermission(adminId, permissionKey) { + try { + const sql = "INSERT INTO api_administrators_permissions (administrator_id, permission_key) VALUES (?, ?)" + const result = await database.query(sql, [adminId, permissionKey]) + + return result.affectedRows > 0 + } catch (error) { + if (error.code === "ER_DUP_ENTRY") return true + logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"]) + throw new DefaultError(500, "Internal Server Error", "Database Error") + } +} + +async function revokePermission(adminId, permissionKey) { + try { + const sql = "DELETE FROM api_administrators_permissions WHERE administrator_id = ? AND permission_key = ?" + const result = await database.query(sql, [adminId, permissionKey]) + + return result.affectedRows > 0 + } catch (error) { + logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"]) + throw new DefaultError(500, "Internal Server Error", "Database Error") + } +} + +async function getAdminPermissions(adminId) { + try { + const sql = ` + SELECT permission_key + FROM api_administrators_permissions + WHERE administrator_id = ? + ` + const rows = await database.query(sql, [adminId]) + return rows.map(r => r.permission_key) + } catch (error) { + logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"]) + throw new DefaultError(500, "Internal Server Error", "Database Error") + } +} + +async function updateAdminPassword(adminId, newHashedPassword) { + try { + const sql = "UPDATE api_administrators SET password = ? WHERE id = ?" + const result = await database.query(sql, [newHashedPassword, adminId]) + + if (result.affectedRows > 0) { + return { + code: 200, + message: "Password updated successfully." + } + } else { + throw new DefaultError(404, "Administrator not found.") + } + } catch (error) { + if (error instanceof DefaultError) throw error + logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"]) + throw new DefaultError(500, "Internal Server Error", "Database Error") + } +} + +async function getAdminByUsername(username) { + try { + const sql = "SELECT id, username, password, createdAt FROM api_administrators WHERE username = ?" + const rows = await database.query(sql, [username]) + + return rows[0] || null + } catch (error) { + logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"]) + throw new DefaultError(500, "Internal Server Error", "Database Error") + } +} + +module.exports = { + createAdmin, + getAdminById, + hasPermission, + assignPermission, + revokePermission, + getAdminByUsername, + getAdminPermissions, + updateAdminPassword +} \ No newline at end of file diff --git a/repositories/sessionsRepository.js b/repositories/sessionsRepository.js index a8104e7..6961bed 100644 --- a/repositories/sessionsRepository.js +++ b/repositories/sessionsRepository.js @@ -145,7 +145,8 @@ async function getServerSession(uuid, serverId) { const sql = ` SELECT ip FROM serverSessions - WHERE uuid = ? AND serverId = ? + WHERE uuid = ? AND serverId = ? + AND createdAt > (NOW() - INTERVAL 30 SECOND) ` const rows = await database.query(sql, [uuid, serverId]) const session = rows[0] diff --git a/repositories/userRepository.js b/repositories/userRepository.js index e39f273..e6917eb 100644 --- a/repositories/userRepository.js +++ b/repositories/userRepository.js @@ -660,6 +660,72 @@ async function setSkin(uuid, hash, variant) { return true } +async function updatePassword(uuid, hashedPassword) { + try { + const sql = "UPDATE players SET password = ? WHERE uuid = ?" + const result = await database.query(sql, [hashedPassword, uuid]) + + if (result.affectedRows > 0) { + return { code: 200, message: "Password updated successfully" } + } else { + throw new DefaultError(404, "User not found") + } + } catch (error) { + if (error instanceof DefaultError) throw error + logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"]) + throw new DefaultError(500, "Internal Server Error", "Database Error") + } +} + +async function addCapeToPlayer(uuid, hash) { + try { + const sql = ` + INSERT INTO playersCapes (playerUuid, assetHash, isSelected) + VALUES (?, ?, 0) + ` + const result = await database.query(sql, [uuid, hash]) + + if (result.affectedRows > 0) { + return { code: 200, message: "Cape accordée au joueur." } + } + throw new DefaultError(500, "Erreur lors de l'attribution de la cape.") + } catch (error) { + if (error.code === 'ER_DUP_ENTRY') { + throw new DefaultError(409, "Le joueur possède déjà cette cape.") + } + logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"]) + throw new DefaultError(500, "Internal Server Error", "Database Error") + } +} + +async function removeCapeFromPlayer(uuid, hash) { + try { + const sql = "DELETE FROM playersCapes WHERE playerUuid = ? AND assetHash = ?" + const result = await database.query(sql, [uuid, hash]) + + if (result.affectedRows > 0) { + return { code: 200, message: "Cape retirée du joueur." } + } else { + throw new DefaultError(404, "Le joueur ne possède pas cette cape.") + } + } catch (error) { + if (error instanceof DefaultError) throw error + logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"]) + throw new DefaultError(500, "Internal Server Error", "Database Error") + } +} + +async function deleteTexture(hash) { + try { + const sql = "DELETE FROM textures WHERE hash = ?" + const result = await database.query(sql, [hash]) + return result.affectedRows > 0 + } catch (error) { + logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"]) + throw new DefaultError(500, "Internal Server Error", "Database Error") + } +} + module.exports = { setSkin, banUser, @@ -669,14 +735,17 @@ module.exports = { isBlocked, unbanUser, blockPlayer, + deleteTexture, createTexture, getPlayerBans, getPlayerMeta, unblockPlayer, changeUsername, getNameHistory, + updatePassword, getBlockedUuids, getUsersByNames, + addCapeToPlayer, getPlayerActions, getTextureByUuid, getTextureByHash, @@ -690,6 +759,7 @@ module.exports = { getPlayerProperties, removeProfileAction, addPropertyToPlayer, + removeCapeFromPlayer, getProfileByUsername, getPlayerPreferences, getPlayerCertificate, diff --git a/routes/admin/ban/index.js b/routes/admin/ban/index.js new file mode 100644 index 0000000..7ffaa39 --- /dev/null +++ b/routes/admin/ban/index.js @@ -0,0 +1,32 @@ +const express = require("express") +const router = express.Router() +const userService = require("../../../services/userService") +const adminService = require("../../../services/adminService") + +router.get("/:uuid", adminService.hasPermission("PLAYER_BAN_STATUS"), async (req, res) => { + const banStatus = await userService.getPlayerBanStatus(req.params.uuid) + return res.status(200).json(banStatus) +}) + +router.get("/:uuid/actions", adminService.hasPermission("PLAYER_ACTIONS_LIST"), async (req, res) => { + const playerActions = await userService.getPlayerActions(req.params.uuid) + return res.status(200).json(playerActions) +}) + +router.get("/:uuid/history", adminService.hasPermission("PLAYER_BAN_HISTORY"), async (req, res) => { + const banHistory = await userService.getPlayerBans(req.params.uuid) + return res.status(200).json(banHistory) +}) + +router.put("/:uuid", adminService.hasPermission("PLAYER_BAN"), async (req, res) => { + const { reasonKey, reasonMessage, expires } = req.body + const ban = await userService.banUser(req.params.uuid, { reasonKey, reasonMessage, expires }) + return res.status(200).json(ban) +}) + +router.delete("/:uuid", adminService.hasPermission("PLAYER_UNBAN"), async (req, res) => { + const ban = await userService.unbanUser(req.params.uuid) + return res.status(200).json(ban) +}) + +module.exports = router \ No newline at end of file diff --git a/routes/admin/cosmetics/capes.js b/routes/admin/cosmetics/capes.js new file mode 100644 index 0000000..4d75e51 --- /dev/null +++ b/routes/admin/cosmetics/capes.js @@ -0,0 +1,20 @@ +const express = require("express") +const path = require("node:path") +const multer = require("multer") +const router = express.Router() +const userService = require("../../../services/userService") +const adminService = require("../../../services/adminService") + +const upload = multer({ dest: path.join(process.cwd(), "data/temp/") }) + +router.post("/upload", adminService.hasPermission("UPLOAD_CAPE"), upload.single("file"), async (req, res) => { + const result = await adminService.uploadCape(req.file, req.body.alias) + res.status(201).json(result) +}) + +router.delete("/:hash", adminService.hasPermission("DELETE_CAPES"), async (req, res) => { + const result = await userService.deleteGlobalCape(req.params.hash) + res.status(200).json(result) +}) + +module.exports = router \ No newline at end of file diff --git a/routes/admin/index.js b/routes/admin/index.js new file mode 100644 index 0000000..4c2ef61 --- /dev/null +++ b/routes/admin/index.js @@ -0,0 +1,4 @@ +const express = require("express") +const router = express.Router() + +module.exports = router \ No newline at end of file diff --git a/routes/admin/players/password.js b/routes/admin/players/password.js new file mode 100644 index 0000000..6188685 --- /dev/null +++ b/routes/admin/players/password.js @@ -0,0 +1,12 @@ +const express = require("express") +const router = express.Router() +const userService = require("../../../services/userService") +const adminService = require("../../../services/adminService") + +router.patch("/:uuid", adminService.hasPermission("CHANGE_PLAYER_PASSWORD"), async (req, res) => { + const { newPassword } = req.body + const result = await userService.changePassword(req.params.uuid, newPassword) + return res.status(200).json(result) +}) + +module.exports = router \ No newline at end of file diff --git a/routes/admin/players/textures.js b/routes/admin/players/textures.js new file mode 100644 index 0000000..cb2e926 --- /dev/null +++ b/routes/admin/players/textures.js @@ -0,0 +1,23 @@ +const express = require("express") +const router = express.Router() +const userService = require("../../../services/userService") +const adminService = require("../../../services/adminService") + +router.delete("/skin/:uuid", adminService.hasPermission("RESET_PLAYER_SKIN"), async (req, res) => { + const result = await userService.resetSkin(req.params.uuid) + return res.status(200).json(result) +}) + +router.put("/cape/:uuid/:hash", adminService.hasPermission("GRANT_PLAYER_CAPE"), async (req, res) => { + const { uuid, hash } = req.params + const result = await userService.grantCape(uuid, hash) + return res.status(200).json(result) +}) + +router.delete("/cape/:uuid/:hash", adminService.hasPermission("REMOVE_PLAYER_CAPE"), async (req, res) => { + const { uuid, hash } = req.params + const result = await userService.removeCape(uuid, hash) + return res.status(200).json(result) +}) + +module.exports = router \ No newline at end of file diff --git a/routes/admin/players/username.js b/routes/admin/players/username.js new file mode 100644 index 0000000..809ed26 --- /dev/null +++ b/routes/admin/players/username.js @@ -0,0 +1,12 @@ +const express = require("express") +const router = express.Router() +const userService = require("../../../services/userService") +const adminService = require("../../../services/adminService") + +router.patch("/:uuid", adminService.hasPermission("CHANGE_PLAYER_USERNAME"), async (req, res) => { + const { newUsername } = req.body + const result = await userService.changeUsername(req.params.uuid, newUsername) + return res.status(200).json(result) +}) + +module.exports = router \ No newline at end of file diff --git a/routes/register.js b/routes/register.js index d2df668..5ca62fa 100644 --- a/routes/register.js +++ b/routes/register.js @@ -1,23 +1,44 @@ const express = require("express") const router = express.Router() +const utils = require("../modules/utils") const logger = require("../modules/logger") const authService = require("../services/authService") +const adminService = require("../services/adminService") -router.post("/", async (req, res) => { - const { username, password, email, registrationCountry, preferredLanguage } = req.body - const clientIp = req.headers["x-forwarded-for"] || req.connection.remoteAddress +if (utils.isTrueFromDotEnv("SUPPORT_REGISTER")) { + router.post("/", adminService.hasPermission("REGISTER_USER"), async (req, res) => { + const { username, password, email, registrationCountry, preferredLanguage } = req.body + const clientIp = req.headers["x-forwarded-for"] || req.connection.remoteAddress - const result = await authService.registerUser({ - username, - password, - email, - registrationCountry, - preferredLanguage, - clientIp + const result = await authService.registerUser({ + username, + password, + email, + registrationCountry, + preferredLanguage, + clientIp + }) + + logger.log(`New user registered: ${username}`, ["Web", "yellow", "AUTH", "green"]) + return res.status(200).json(result) }) +} else { + router.post("/", async (req, res) => { + const { username, password, email, registrationCountry, preferredLanguage } = req.body + const clientIp = req.headers["x-forwarded-for"] || req.connection.remoteAddress - logger.log(`New user registered: ${username}`, ["Web", "yellow", "AUTH", "green"]) - return res.status(200).json(result) -}) + const result = await authService.registerUser({ + username, + password, + email, + registrationCountry, + preferredLanguage, + clientIp + }) + + logger.log(`New user registered: ${username}`, ["Web", "yellow", "AUTH", "green"]) + return res.status(200).json(result) + }) +} module.exports = router \ No newline at end of file diff --git a/schemas/admin/admin/cosmetics/capes/[hash].js b/schemas/admin/admin/cosmetics/capes/[hash].js new file mode 100644 index 0000000..f518227 --- /dev/null +++ b/schemas/admin/admin/cosmetics/capes/[hash].js @@ -0,0 +1,9 @@ +const z = require("zod") + +module.exports = { + DELETE: { + query: z.object({ + hash: z.string().length(64) + }) + } +} \ No newline at end of file diff --git a/schemas/admin/ban/[uuid]/actions.js b/schemas/admin/ban/[uuid]/actions.js new file mode 100644 index 0000000..d258b18 --- /dev/null +++ b/schemas/admin/ban/[uuid]/actions.js @@ -0,0 +1,9 @@ +const z = require("zod") + +module.exports = { + GET: { + query: z.object({ + uuid: z.string().uuid() + }) + } +} \ No newline at end of file diff --git a/schemas/admin/ban/[uuid]/history.js b/schemas/admin/ban/[uuid]/history.js new file mode 100644 index 0000000..d258b18 --- /dev/null +++ b/schemas/admin/ban/[uuid]/history.js @@ -0,0 +1,9 @@ +const z = require("zod") + +module.exports = { + GET: { + query: z.object({ + uuid: z.string().uuid() + }) + } +} \ No newline at end of file diff --git a/schemas/admin/ban/[uuid]/index.js b/schemas/admin/ban/[uuid]/index.js new file mode 100644 index 0000000..2532a8f --- /dev/null +++ b/schemas/admin/ban/[uuid]/index.js @@ -0,0 +1,26 @@ +const z = require("zod") + +const uuidSchema = z.object({ + uuid: z.string().uuid() +}) + +module.exports = { + GET: { + query: uuidSchema + }, + PUT: { + body: z.object({ + reasonKey: z.string().min(1), + reasonMessage: z.string().optional(), + expires: z.number().int().positive().optional() + }), + error: { + code: 400, + error: "CONSTRAINT_VIOLATION", + errorMessage: "Invalid ban format" + } + }, + DELETE: { + query: uuidSchema + } +} \ No newline at end of file diff --git a/schemas/admin/players/password/[uuid].js b/schemas/admin/players/password/[uuid].js new file mode 100644 index 0000000..ef84376 --- /dev/null +++ b/schemas/admin/players/password/[uuid].js @@ -0,0 +1,12 @@ +const z = require("zod") + +module.exports = { + PATCH: { + body: z.object({ + newPassword: 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." }), + }) + } +} \ No newline at end of file diff --git a/schemas/admin/players/textures/cape/[uuid]/[hash].js b/schemas/admin/players/textures/cape/[uuid]/[hash].js new file mode 100644 index 0000000..1cabf15 --- /dev/null +++ b/schemas/admin/players/textures/cape/[uuid]/[hash].js @@ -0,0 +1,16 @@ +const z = require("zod") + +module.exports = { + PUT: { + query: z.object({ + uuid: z.string().uuid(), + hash: z.string().length(64) + }) + }, + DELETE: { + query: z.object({ + uuid: z.string().uuid(), + hash: z.string().length(64) + }) + } +} \ No newline at end of file diff --git a/services/adminService.js b/services/adminService.js new file mode 100644 index 0000000..3f6f369 --- /dev/null +++ b/services/adminService.js @@ -0,0 +1,145 @@ +const userRepository = require("../repositories/userRepository") +const adminRepository = require("../repositories/adminRepository") +const bcrypt = require("bcryptjs") +const { DefaultError } = require("../errors/errors") + +const ADMIN_JWT_SECRET = process.env.ADMIN_JWT_SECRET || "udjJLGCOq7m3NmGpdVLJ@#" + +async function registerAdmin(username, plainPassword, permissions = []) { + const hashedPassword = await bcrypt.hash(plainPassword, 10) + + const result = await adminRepository.createAdmin(username, hashedPassword) + + if (permissions.length > 0) { + for (const perm of permissions) { + await adminRepository.assignPermission(result.id, perm) + } + } + + return { id: result.id, username, message: "Administrateur créé avec succès." } +} + +async function checkAdminAccess(adminId, requiredPermission) { + if (!adminId || !requiredPermission) { + throw new DefaultError(400, "ID administrateur ou permission manquante.") + } + + return await adminRepository.hasPermission(adminId, requiredPermission) +} + +async function changeAdminPassword(adminId, newPlainPassword) { + if (!newPlainPassword || newPlainPassword.length < 6) { + throw new DefaultError(400, "Le mot de passe doit contenir au moins 6 caractères.") + } + + const hashed = await bcrypt.hash(newPlainPassword, 10) + return await adminRepository.updateAdminPassword(adminId, hashed) +} + +async function getAdminProfile(adminId) { + const admin = await adminRepository.getAdminById(adminId) + if (!admin) { + throw new DefaultError(404, "Administrateur introuvable.") + } + + const permissions = await adminRepository.getAdminPermissions(adminId) + + return { + id: admin.id, + username: admin.username, + createdAt: admin.createdAt, + permissions: permissions + } +} + +async function grantPermission(adminId, permissionKey) { + return await adminRepository.assignPermission(adminId, permissionKey) +} + +async function revokePermission(adminId, permissionKey) { + return await adminRepository.revokePermission(adminId, permissionKey) +} + +async function loginAdmin(username, password) { + const admin = await adminRepository.getAdminByUsername(username) + if (!admin) { + throw new DefaultError(403, "Invalid credentials.") + } + + const isMatch = await bcrypt.compare(password, admin.password) + if (!isMatch) { + throw new DefaultError(403, "Invalid credentials.") + } + + const token = jwt.sign( + { id: admin.id, username: admin.username, type: "admin" }, + ADMIN_JWT_SECRET, + { expiresIn: "8h" } + ) + + return { token } +} + +function hasPermission(requiredPermission) { + return async (req, res, next) => { + try { + const authHeader = req.headers.authorization + if (!authHeader || !authHeader.startsWith("Bearer ")) { + throw new DefaultError(401, "Authentification admin requise.") + } + + const token = authHeader.split(" ")[1] + + const decoded = jwt.verify(token, ADMIN_JWT_SECRET) + if (decoded.type !== "admin") { + throw new DefaultError(403, "Invalid token.") + } + + const hasAccess = await adminService.checkAdminAccess(decoded.id, requiredPermission) + if (!hasAccess) { + throw new DefaultError(403, `Missing permission : ${requiredPermission}`) + } + + req.admin = decoded + next() + + } catch (err) { + if (err.name === "JsonWebTokenError") { + return next(new DefaultError(401, "Invalid session.")) + } + next(err) + } + } +} + +async function uploadCape(fileObject, alias = null) { + const buffer = await fs.readFile(fileObject.path) + const hash = crypto.createHash("sha256").update(buffer).digest("hex") + + const existing = await userRepository.getTextureByHash(hash) + if (existing) throw new DefaultError(409, "Cette cape existe déjà.") + + const textureUrl = `/texture/${hash}` + await userRepository.createTexture(crypto.randomUUID(), hash, 'CAPE', textureUrl, alias) + + return { hash, url: textureUrl } +} + +async function deleteGlobalCape(hash) { + const success = await userRepository.deleteTexture(hash) + if (!success) throw new DefaultError(404, "Cape introuvable.") + + return { message: "Texture supprimée globalement." } +} + +module.exports = { + loginAdmin, + uploadCape, + registerAdmin, + getAdminProfile, + grantPermission, + revokePermission, + checkAdminAccess, + changeAdminPassword, + hasPermission, +} \ No newline at end of file diff --git a/services/sessionsService.js b/services/sessionsService.js index 2bc705f..3283cb7 100644 --- a/services/sessionsService.js +++ b/services/sessionsService.js @@ -122,6 +122,12 @@ async function joinServer({ accessToken, selectedProfile, clientToken, serverId, } catch (error) { throw new DefaultError(403, "Invalid access token", "ForbiddenOperationException") } + + const existingSession = await sessionRepository.getServerSessionByUuid(selectedProfile) + if (existingSession && existingSession.serverId !== serverId) { + throw new DefaultError(403, "Already logged in on another server.", "ForbiddenOperationException") + } + await sessionRepository.saveServerSession(selectedProfile, accessToken, serverId, ip) return { code: 204 } } diff --git a/services/userService.js b/services/userService.js index 91dce07..770b1ae 100644 --- a/services/userService.js +++ b/services/userService.js @@ -1,6 +1,7 @@ const fs = require("node:fs/promises") const path = require("node:path") const util = require("node:util") +const bcrypt = require("bcryptjs") const logger = require("../modules/logger") const crypto = require("node:crypto") const ssrfcheck = require("ssrfcheck") @@ -498,6 +499,30 @@ async function uploadSkinFromUrl(uuid, url, variant) { return await uploadSkin(uuid, { path: tempPath }, variant) } +async function changePassword(uuid, newPlainPassword) { + if (!newPlainPassword || newPlainPassword.length < 6) { + throw new DefaultError(400, "Password is too short. Minimum 6 characters.") + } + + const salt = await bcrypt.genSalt(10) + const hashedPassword = await bcrypt.hash(newPlainPassword, salt) + + return await userRepository.updatePassword(uuid, hashedPassword) +} + +async function grantCape(uuid, hash) { + const texture = await userRepository.getTextureByHash(hash) + if (!texture) { + throw new DefaultError(404, "Texture de cape introuvable dans la base globale.") + } + + return await userRepository.addCapeToPlayer(uuid, hash) +} + +async function removeCape(uuid, hash) { + return await userRepository.removeCapeFromPlayer(uuid, hash) +} + module.exports = { banUser, showCape, @@ -505,8 +530,10 @@ module.exports = { unbanUser, resetSkin, isBlocked, + grantCape, bulkLookup, uploadSkin, + removeCape, blockPlayer, getNameUUIDs, unblockPlayer, @@ -514,6 +541,7 @@ module.exports = { getPlayerBans, changeUsername, getPreferences, + changePassword, getBlockedUuids, registerTexture, getLegacyProfile,