Add admin API, permissions, and player management routes
Introduces admin database tables, repository, and service for managing administrators and permissions. Adds new admin routes for banning players, managing cosmetics (capes), changing player passwords and usernames, and handling player textures. Updates user and session services to support admin actions and permission checks. Adds related schema validation for new endpoints.
This commit is contained in:
145
services/adminService.js
Normal file
145
services/adminService.js
Normal file
@@ -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,
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user