Yggdrasil/repositories/userRepository.js

784 lines
24 KiB
JavaScript

const utils = require("../modules/utils")
const crypto = require("node:crypto")
const database = require("../modules/database")
const { DefaultError } = require("../errors/errors")
async function getSkins(uuid) {
try {
const sql = `
SELECT t.uuid as textureUuid, t.hash, t.url, ps.variant, ps.isSelected
FROM playersSkins ps
JOIN textures t ON ps.assetHash = t.hash
WHERE ps.playerUuid = ?
`
const rows = await database.query(sql, [uuid])
return rows
} catch (error) {
return utils.handleDBError(error)
}
}
async function getCapes(uuid) {
try {
const sql = `
SELECT t.uuid as textureUuid, t.hash, t.url, t.alias, pc.isSelected
FROM playersCapes pc
JOIN textures t ON pc.assetHash = t.hash
WHERE pc.playerUuid = ?
`
const rows = await database.query(sql, [uuid])
return rows
} catch (error) {
return utils.handleDBError(error)
}
}
async function addPropertyToPlayer(key, value, uuid) {
try {
const sql = `
INSERT INTO playersProperties (name, value, uuid)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE value = VALUES(value)
`
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) {
return utils.handleDBError(error)
}
}
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) {
return utils.handleDBError(error)
}
}
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) {
return utils.handleDBError(error)
}
}
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) {
return utils.handleDBError(error)
}
}
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) {
return utils.handleDBError(error)
}
}
async function getPlayerPropertyByValue(key, value) {
try {
const sql = `SELECT * FROM playersProperties WHERE name = ? AND value = ?`
const rows = await database.query(sql, [key, value])
const property = rows[0]
if (!property) {
throw new DefaultError(404, "No property found with this value for the specified key")
}
return {
code: 200,
property: {
name: property.name,
value: property.value,
uuid: property.uuid
}
}
} catch (error) {
return utils.handleDBError(error)
}
}
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 (error) {
return utils.handleDBError(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) {
return utils.handleDBError(error)
}
}
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) {
return utils.handleDBError(error)
}
}
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) {
return utils.handleDBError(error)
}
}
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) {
return utils.handleDBError(error)
}
}
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 reasonKey = ?", [reasonKey])
if (reasonRows.length > 0) {
reasonId = reasonRows[0].id
} else {
const insertReason = await database.query("INSERT INTO banReasons (reasonKey) 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).")
}
return utils.handleDBError(error)
}
}
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) {
return utils.handleDBError(error)
}
}
async function getPlayerBans(uuid) {
try {
const sql = `
SELECT
b.banId,
b.expires,
b.reasonMessage,
r.reasonKey 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) {
return utils.handleDBError(error)
}
}
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")
}
return utils.handleDBError(error)
}
}
async function createTexture(uuid, hash, type, url, alias) {
try {
const sql = `
INSERT INTO textures (uuid, hash, type, url, alias)
VALUES (?, ?, ?, ?, ?)
`
await database.query(sql, [uuid, hash, type, url, alias])
return true
} catch (error) {
if (error.code === 'ER_DUP_ENTRY') {
return false
}
return utils.handleDBError(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) {
return utils.handleDBError(error)
}
}
async function getTextureByHash(hash) {
try {
const sql = "SELECT uuid FROM textures WHERE hash = ?"
const rows = await database.query(sql, [hash])
return rows[0]
} catch (error) {
return utils.handleDBError(error)
}
}
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) {
return utils.handleDBError(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) {
return utils.handleDBError(error)
}
}
async function showCape(uuid, hash) {
try {
await database.query(
"UPDATE playersCapes SET isSelected = 0 WHERE playerUuid = ?",
[uuid]
)
const result = await database.query(
"UPDATE playersCapes SET isSelected = 1 WHERE playerUuid = ? AND assetHash = ?",
[uuid, hash]
)
const affectedRows = Array.isArray(result) ? result[0].affectedRows : result.affectedRows
return { code: 200, changed: affectedRows > 0 }
} catch (error) {
return utils.handleDBError(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) {
return utils.handleDBError(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) {
return utils.handleDBError(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) {
return utils.handleDBError(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) {
return utils.handleDBError(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) {
return utils.handleDBError(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) {
return utils.handleDBError(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) {
return utils.handleDBError(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) {
return utils.handleDBError(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) {
return utils.handleDBError(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) {
return utils.handleDBError(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) {
return utils.handleDBError(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) {
return utils.handleDBError(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) {
return utils.handleDBError(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) {
return utils.handleDBError(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) {
return utils.handleDBError(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) {
return utils.handleDBError(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) {
return utils.handleDBError(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) {
return utils.handleDBError(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) {
return utils.handleDBError(error)
}
}
async function setSkin(uuid, hash, variant) {
try {
const resetSql = `UPDATE playersSkins SET isSelected = 0 WHERE playerUuid = ?`
await database.query(resetSql, [uuid])
const upsertSql = `
INSERT INTO playersSkins (playerUuid, assetHash, variant, isSelected)
VALUES (?, ?, ?, 1)
ON DUPLICATE KEY UPDATE isSelected = 1, variant = VALUES(variant)
`
await database.query(upsertSql, [uuid, hash, variant.toUpperCase()])
return true
} catch (error) {
return utils.handleDBError(error)
}
}
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) {
return utils.handleDBError(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 granted to the player." }
}
throw new DefaultError(500, "Error when assigning the cape.")
} catch (error) {
if (error.code === "ER_DUP_ENTRY") {
throw new DefaultError(409, "The player already possesses this cloak.")
}
return utils.handleDBError(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 removed from player." }
} else {
throw new DefaultError(404, "The player does not own this cloak.")
}
} catch (error) {
return utils.handleDBError(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) {
return utils.handleDBError(error)
}
}
module.exports = {
setSkin,
banUser,
getSkins,
getCapes,
showCape,
hideCape,
resetSkin,
isBlocked,
unbanUser,
blockPlayer,
deleteTexture,
createTexture,
getPlayerBans,
getPlayerMeta,
unblockPlayer,
changeUsername,
getNameHistory,
updatePassword,
getBlockedUuids,
getUsersByNames,
addCapeToPlayer,
getPlayerActions,
getTextureByUuid,
getTextureByHash,
addProfileAction,
getLastNameChange,
getPlayerProperty,
getUuidAndUsername,
checkCapeOwnership,
getProfileByHistory,
getPlayerPrivileges,
getPlayerProperties,
removeProfileAction,
addPropertyToPlayer,
removeCapeFromPlayer,
getProfileByUsername,
getPlayerPreferences,
getPlayerCertificate,
clearAllPlayerActions,
savePlayerCertificate,
updatePlayerPrivileges,
deletePropertyToPlayer,
updatePropertyToPlayer,
getPlayerSettingsSchema,
updatePlayerPreferences,
getPlayerPropertyByValue,
deleteExpiredCertificates,
}