Add Minecraft services API routes and user service

Introduces new routes under /minecraftservices and /mojangapi for profile, skin, cape, blocklist, privileges, and certificate management. Adds a comprehensive userService module to handle user-related operations, and extends userRepository with methods for username changes, skin/cape management, blocking, and profile lookups. Refactors username availability logic into authService, updates error handling, and improves logger and utility functions. Also updates route handlers to use consistent return statements and enhances route registration logging.
This commit is contained in:
2025-12-28 07:15:24 +01:00
parent 228345c859
commit 5dd1de1521
32 changed files with 1235 additions and 62 deletions

View File

@@ -2,7 +2,6 @@ const logger = require("../modules/logger")
const bcrypt = require("bcryptjs")
const database = require("../modules/database")
const { DefaultError } = require("../errors/errors")
const usernameRegex = /^[a-zA-Z0-9_]{3,16}$/
async function getUser(identifier, requirePassword = false) {
try {
@@ -28,19 +27,14 @@ async function getUser(identifier, requirePassword = false) {
async function register(email, username, password) {
try {
const availability = await checkUsernameAvailability(username)
if (availability.allowed) {
const sql = `INSERT INTO players (email, username, password, uuid) VALUES (?, ?, ?, ?)`
const uuid = crypto.randomUUID()
const hashedPassword = await bcrypt.hash(password, 10)
const result = await database.query(sql, [email, username, hashedPassword, uuid])
if (result.affectedRows > 0) {
return { code: 200, email, username, uuid }
} else {
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
}
const sql = `INSERT INTO players (email, username, password, uuid) VALUES (?, ?, ?, ?)`
const uuid = crypto.randomUUID()
const hashedPassword = await bcrypt.hash(password, 10)
const result = await database.query(sql, [email, username, hashedPassword, uuid])
if (result.affectedRows > 0) {
return { code: 200, email, username, uuid }
} else {
throw new DefaultError(415, "Illegal Server Character", availability.message || "INVALID_USERNAME_FORMAT")
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
}
} catch (error) {
if (error instanceof DefaultError) throw error
@@ -51,30 +45,6 @@ async function register(email, username, password) {
}
}
async function checkUsernameAvailability(username) {
if (!usernameRegex.test(username)) {
return { code: 200, allowed: false, message: "Invalid format (3-16 alphanumeric chars)." }
}
const blocklist = await getUsernamesRules()
const normalizedUsername = username.toLowerCase()
for (const entry of blocklist) {
if (entry.type === "literal") {
if (normalizedUsername === entry.value) {
return { code: 200, allowed: false, message: "This username is reserved." }
}
}
else if (entry.type === "regex") {
if (entry.pattern.test(username)) {
return { code: 200, allowed: false, message: "This username contains forbidden patterns." }
}
}
}
return { code: 200, allowed: true }
}
async function insertClientSession(accessToken, clientToken, uuid) {
try {
const sql = `INSERT INTO clientSessions (accessToken, clientToken, uuid) VALUES (?, ?, ?)`
@@ -228,6 +198,7 @@ module.exports = {
getUser,
register,
getClientSession,
getUsernamesRules,
revokeAccessTokens,
insertClientSession,
getPlayerProperties,

View File

@@ -295,18 +295,358 @@ async function getPlayerBans(uuid) {
}
}
async function changeUsername(uuid, newName) {
try {
const sql = "UPDATE players SET username = ? WHERE uuid = ?"
const result = await database.query(sql, [newName, uuid])
if (result.affectedRows > 0) {
return { code: 200, message: "Username changed successfully" }
} else {
throw new DefaultError(404, "User not found")
}
} catch (error) {
if (error instanceof DefaultError) throw error
if (error.code === "ER_DUP_ENTRY" || error.errno === 1062) {
throw new DefaultError(409, "Username already taken", "ForbiddenOperationException")
}
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
throw new DefaultError(500, "Internal Server Error", error.toString())
}
}
async function resetSkin(uuid, hash, variant) {
try {
const insertSql = `
INSERT IGNORE INTO playersSkins (playerUuid, assetHash, variant, isSelected)
VALUES (?, ?, ?, 0)
`
await database.query(insertSql, [uuid, hash, variant])
const updateSql = `
UPDATE playersSkins
SET isSelected = (assetHash = ?)
WHERE playerUuid = ?
`
await database.query(updateSql, [hash, uuid])
return { code: 200 }
} catch (error) {
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
throw new DefaultError(500, "Internal Server Error", "Database Error")
}
}
async function hideCape(uuid) {
try {
const sql = "UPDATE playersCapes SET isSelected = 0 WHERE playerUuid = ?"
await database.query(sql, [uuid])
return { code: 200 }
} catch (error) {
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
throw new DefaultError(500, "Internal Server Error", "Database Error")
}
}
async function getTextureByUuid(textureUuid) {
try {
const sql = "SELECT hash FROM textures WHERE uuid = ?"
const rows = await database.query(sql, [textureUuid])
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 showCape(uuid, hash) {
try {
const sql = `
UPDATE playersCapes
SET isSelected = (assetHash = ?)
WHERE playerUuid = ?
`
const result = await database.query(sql, [hash, uuid])
return { code: 200, changed: 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 checkCapeOwnership(uuid, hash) {
try {
const sql = "SELECT 1 FROM playersCapes WHERE playerUuid = ? AND assetHash = ?"
const rows = await database.query(sql, [uuid, hash])
return rows.length > 0
} catch (error) {
throw new DefaultError(500, "Internal Server Error", "Database Error")
}
}
async function getPlayerMeta(uuid) {
try {
const sql = `SELECT createdAt, nameChangeAllowed FROM players WHERE uuid = ?`
const rows = await database.query(sql, [uuid])
return rows[0]
} catch (error) {
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
throw new DefaultError(500, "Internal Server Error", "Database Error")
}
}
async function getLastNameChange(uuid) {
try {
const sql = `
SELECT changedAt
FROM uuidToNameHistory
WHERE uuid = ? AND changedAt IS NOT NULL
ORDER BY changedAt DESC
LIMIT 1
`
const rows = await database.query(sql, [uuid])
return rows[0]
} catch (error) {
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
throw new DefaultError(500, "Internal Server Error", "Database Error")
}
}
async function getPlayerCertificate(uuid) {
try {
const sql = "SELECT * FROM playerCertificates WHERE uuid = ?"
const rows = await database.query(sql, [uuid])
return rows[0]
} catch (error) {
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
throw new DefaultError(500, "Internal Server Error", "Database Error")
}
}
async function savePlayerCertificate(uuid, privateKey, publicKey, signatureV2, expiresAt, refreshedAfter) {
try {
const sql = `
REPLACE INTO playerCertificates
(uuid, privateKey, publicKey, publicKeySignatureV2, expiresAt, refreshedAfter)
VALUES (?, ?, ?, ?, ?, ?)
`
const result = await database.query(sql, [uuid, privateKey, publicKey, signatureV2, expiresAt, refreshedAfter])
return result.affectedRows > 0
} catch (error) {
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
throw new DefaultError(500, "Internal Server Error", "Database Error")
}
}
async function deleteExpiredCertificates(isoDate) {
try {
const sql = "DELETE FROM playerCertificates WHERE expiresAt < ?"
const result = await database.query(sql, [isoDate])
return result.affectedRows
} catch (error) {
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
throw new DefaultError(500, "Internal Server Error", "Database Error")
}
}
async function addProfileAction(uuid, actionCode) {
try {
const cleanUuid = uuid.replace(/-/g, "")
const sql = "INSERT IGNORE INTO playerProfileActions (uuid, action) VALUES (?, ?)"
const result = await database.query(sql, [cleanUuid, actionCode])
return result.affectedRows > 0
} catch (error) {
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
throw new DefaultError(500, "Internal Server Error", "Database Error")
}
}
async function removeProfileAction(uuid, actionCode) {
try {
const cleanUuid = uuid.replace(/-/g, "")
const sql = "DELETE FROM playerProfileActions WHERE uuid = ? AND action = ?"
const result = await database.query(sql, [cleanUuid, actionCode])
return result.affectedRows
} catch (error) {
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
throw new DefaultError(500, "Internal Server Error", "Database Error")
}
}
async function getPlayerActions(uuid) {
try {
const cleanUuid = uuid.replace(/-/g, "")
const sql = "SELECT action FROM playerProfileActions WHERE uuid = ?"
const rows = await database.query(sql, [cleanUuid])
return rows.map(r => r.action)
} catch (error) {
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
throw new DefaultError(500, "Internal Server Error", "Database Error")
}
}
async function clearAllPlayerActions(uuid) {
try {
const cleanUuid = uuid.replace(/-/g, "")
const sql = "DELETE FROM playerProfileActions WHERE uuid = ?"
const result = await database.query(sql, [cleanUuid])
return result.affectedRows
} catch (error) {
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
throw new DefaultError(500, "Internal Server Error", "Database Error")
}
}
async function blockPlayer(blockerUuid, blockedUuid) {
try {
const sql = `INSERT IGNORE INTO playersBlockslist (blockerUuid, blockedUuid) VALUES (?, ?)`
const result = await database.query(sql, [blockerUuid, blockedUuid])
return result.affectedRows > 0
} catch (error) {
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
throw new DefaultError(500, "Internal Server Error", "Database Error")
}
}
async function unblockPlayer(blockerUuid, blockedUuid) {
try {
const sql = `DELETE FROM playersBlockslist WHERE blockerUuid = ? AND blockedUuid = ?`
const result = await database.query(sql, [blockerUuid, blockedUuid])
return result.affectedRows > 0
} catch (error) {
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
throw new DefaultError(500, "Internal Server Error", "Database Error")
}
}
async function getBlockedUuids(blockerUuid) {
try {
const sql = `SELECT blockedUuid FROM playersBlockslist WHERE blockerUuid = ?`
const rows = await database.query(sql, [blockerUuid])
return rows.map(r => r.blockedUuid)
} catch (error) {
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
throw new DefaultError(500, "Internal Server Error", "Database Error")
}
}
async function isBlocked(blockerUuid, targetUuid) {
try {
const sql = `SELECT 1 FROM playersBlockslist WHERE blockerUuid = ? AND blockedUuid = ? LIMIT 1`
const rows = await database.query(sql, [blockerUuid, targetUuid])
return rows.length > 0
} catch (error) {
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
throw new DefaultError(500, "Internal Server Error", "Database Error")
}
}
async function getUsersByNames(usernames) {
try {
if (!usernames || usernames.length === 0) return []
const uniqueNames = [...new Set(usernames)]
const placeholders = uniqueNames.map(() => "?").join(", ")
const sql = `SELECT uuid, username FROM players WHERE username IN (${placeholders})`
const rows = await database.query(sql, uniqueNames)
return rows
} catch (error) {
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
throw new DefaultError(500, "Internal Server Error", "Database Error")
}
}
async function getUuidAndUsername(username) {
try {
const sql = "SELECT uuid, username FROM players WHERE username = ?"
const rows = await database.query(sql, [username])
return rows[0] || null
} catch (error) {
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
throw new DefaultError(500, "Internal Server Error", "Database Error")
}
}
async function getProfileByUsername(username) {
try {
const sql = "SELECT uuid, username FROM players WHERE username = ?"
const rows = await database.query(sql, [username])
return rows[0] || null
} catch (error) {
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
throw new DefaultError(500, "Internal Server Error", "Database Error")
}
}
async function getProfileByHistory(username, isoDate) {
try {
const sql = `
SELECT uuid, username
FROM uuidToNameHistory
WHERE username = ?
AND (changedAt <= ? OR changedAt IS NULL)
ORDER BY changedAt DESC
LIMIT 1
`
const rows = await database.query(sql, [username, isoDate])
return rows[0] || null
} catch (error) {
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
throw new DefaultError(500, "Internal Server Error", "Database Error")
}
}
async function getNameHistory(uuid) {
try {
const sql = `
SELECT username, changedAt
FROM uuidToNameHistory
WHERE uuid = ?
ORDER BY changedAt ASC
`
const rows = await database.query(sql, [uuid])
return rows
} catch (error) {
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
throw new DefaultError(500, "Internal Server Error", "Database Error")
}
}
module.exports = {
banUser,
showCape,
hideCape,
resetSkin,
isBlocked,
unbanUser,
blockPlayer,
getPlayerBans,
getPlayerMeta,
unblockPlayer,
changeUsername,
getNameHistory,
getBlockedUuids,
getUsersByNames,
getPlayerActions,
getTextureByUuid,
addProfileAction,
getLastNameChange,
getPlayerProperty,
getUuidAndUsername,
checkCapeOwnership,
getProfileByHistory,
getPlayerPrivileges,
getPlayerProperties,
removeProfileAction,
addPropertyToPlayer,
getProfileByUsername,
getPlayerPreferences,
getPlayerCertificate,
clearAllPlayerActions,
savePlayerCertificate,
updatePlayerPrivileges,
deletePropertyToPlayer,
updatePropertyToPlayer,
getPlayerSettingsSchema,
updatePlayerPreferences,
deleteExpiredCertificates,
}