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.
435 lines
12 KiB
JavaScript
435 lines
12 KiB
JavaScript
const userRepository = require("../repositories/userRepository")
|
|
const { DefaultError } = require("../errors/errors")
|
|
const crypto = require("node:crypto")
|
|
const util = require("node:util")
|
|
const generateKeyPairAsync = util.promisify(crypto.generateKeyPair)
|
|
const certsManager = require("../modules/certificatesManager")
|
|
const logger = require("../modules/logger")
|
|
|
|
async function getPlayerProperties(uuid) {
|
|
try {
|
|
const result = await userRepository.getPlayerProperties(uuid)
|
|
return result
|
|
} catch (error) {
|
|
if (error.code === 404) {
|
|
return { code: 200, properties: [] }
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async function getPlayerProperty(uuid, key) {
|
|
return await userRepository.getPlayerProperty(key, uuid)
|
|
}
|
|
|
|
async function addPlayerProperty(uuid, key, value) {
|
|
return await userRepository.addPropertyToPlayer(key, value, uuid)
|
|
}
|
|
|
|
async function updatePlayerProperty(uuid, key, value) {
|
|
return await userRepository.updatePropertyToPlayer(key, value, uuid)
|
|
}
|
|
|
|
async function deletePlayerProperty(uuid, key) {
|
|
return await userRepository.deletePropertyToPlayer(key, uuid)
|
|
}
|
|
|
|
async function getSettingsSchema() {
|
|
const schema = await userRepository.getPlayerSettingsSchema()
|
|
return {
|
|
code: 200,
|
|
schema
|
|
}
|
|
}
|
|
|
|
async function getPreferences(uuid) {
|
|
return await userRepository.getPlayerPreferences(uuid)
|
|
}
|
|
|
|
async function updatePreferences(uuid, updates) {
|
|
return await userRepository.updatePlayerPreferences(uuid, updates)
|
|
}
|
|
|
|
async function getPrivileges(uuid) {
|
|
return await userRepository.getPlayerPrivileges(uuid)
|
|
}
|
|
|
|
async function updatePrivileges(uuid, updates) {
|
|
return await userRepository.updatePlayerPrivileges(uuid, updates)
|
|
}
|
|
|
|
async function banUser(uuid, { reasonKey, reasonMessage, expires }) {
|
|
if (!reasonKey) {
|
|
throw new DefaultError(400, "A reason key is required to ban a user.")
|
|
}
|
|
|
|
return await userRepository.banUser(uuid, {
|
|
reasonKey,
|
|
reasonMessage: reasonMessage || "Banned by operator",
|
|
expires
|
|
})
|
|
}
|
|
|
|
async function unbanUser(uuid) {
|
|
return await userRepository.unbanUser(uuid)
|
|
}
|
|
|
|
async function getPlayerBans(uuid) {
|
|
const result = await userRepository.getPlayerBans(uuid)
|
|
if (result.code === 204) {
|
|
return { code: 200, bans: [] }
|
|
}
|
|
return result
|
|
}
|
|
|
|
async function changeUsername(uuid, newName) {
|
|
const availability = await authService.checkUsernameAvailability(newName)
|
|
if (availability.status === "DUPLICATE") {
|
|
throw new DefaultError(409, "Username already taken.", "ForbiddenOperationException")
|
|
}
|
|
if (availability.status === "NOT_ALLOWED") {
|
|
throw new DefaultError(400, availability.message || "Invalid username format.", "InvalidUsernameException")
|
|
}
|
|
|
|
return await userRepository.changeUsername(uuid, newName)
|
|
}
|
|
|
|
async function resetSkin(playerUuid) {
|
|
const isSteve = Math.random() < 0.5
|
|
const targetHash = isSteve ? STEVE_HASH : ALEX_HASH
|
|
const variant = isSteve ? "CLASSIC" : "SLIM"
|
|
await userRepository.resetSkin(playerUuid, targetHash, variant)
|
|
return {
|
|
code: 200,
|
|
message: "Skin reset successfully",
|
|
model: variant
|
|
}
|
|
}
|
|
|
|
async function hideCape(playerUuid) {
|
|
await userRepository.hideCape(playerUuid)
|
|
return {
|
|
code: 200,
|
|
message: "Cape hidden"
|
|
}
|
|
}
|
|
|
|
async function showCape(playerUuid, textureUuid) {
|
|
const texture = await userRepository.getTextureByUuid(textureUuid)
|
|
if (!texture) {
|
|
throw new DefaultError(404, "Cape texture not found in server assets.", "TextureNotFoundException")
|
|
}
|
|
const ownsCape = await userRepository.checkCapeOwnership(playerUuid, texture.hash)
|
|
if (!ownsCape) {
|
|
throw new DefaultError(403, "You do not own this cape.", "ForbiddenOperationException")
|
|
}
|
|
await userRepository.showCape(playerUuid, texture.hash)
|
|
|
|
return {
|
|
code: 200,
|
|
message: "Cape showed"
|
|
}
|
|
}
|
|
|
|
async function getPlayerNameChangeStatus(uuid) {
|
|
const player = await userRepository.getPlayerMeta(uuid)
|
|
if (!player) {
|
|
throw new DefaultError(404, "User not found")
|
|
}
|
|
const history = await userRepository.getLastNameChange(uuid)
|
|
const response = {
|
|
changedAt: history ? history.changedAt : player.createdAt,
|
|
createdAt: player.createdAt,
|
|
nameChangeAllowed: !!player.nameChangeAllowed
|
|
}
|
|
|
|
return { code: 200, data: response }
|
|
}
|
|
|
|
async function getPlayerCertificate(uuid) {
|
|
const cert = await userRepository.getPlayerCertificate(uuid)
|
|
if (cert) {
|
|
return { code: 200, data: cert }
|
|
}
|
|
throw new DefaultError(404, "Certificate not found")
|
|
}
|
|
|
|
async function savePlayerCertificate(uuid, keys) {
|
|
const success = await userRepository.savePlayerCertificate(
|
|
uuid,
|
|
keys.privateKey,
|
|
keys.publicKey,
|
|
keys.signatureV2,
|
|
keys.expiresAt,
|
|
keys.refreshedAfter
|
|
)
|
|
|
|
if (success) {
|
|
return { code: 200, message: "Certificate saved" }
|
|
}
|
|
throw new DefaultError(500, "Failed to save certificate")
|
|
}
|
|
|
|
async function deleteExpiredCertificates(isoDate) {
|
|
const count = await userRepository.deleteExpiredCertificates(isoDate)
|
|
return { code: 200, deletedCount: count }
|
|
}
|
|
|
|
async function addProfileAction(uuid, actionCode) {
|
|
const added = await userRepository.addProfileAction(uuid, actionCode)
|
|
return {
|
|
code: 200,
|
|
success: true,
|
|
added: added
|
|
}
|
|
}
|
|
|
|
async function removeProfileAction(uuid, actionCode) {
|
|
const count = await userRepository.removeProfileAction(uuid, actionCode)
|
|
return {
|
|
code: 200,
|
|
deletedCount: count
|
|
}
|
|
}
|
|
|
|
async function getPlayerActions(uuid) {
|
|
const actions = await userRepository.getPlayerActions(uuid)
|
|
return {
|
|
code: 200,
|
|
actions: actions
|
|
}
|
|
}
|
|
|
|
async function clearAllPlayerActions(uuid) {
|
|
const count = await userRepository.clearAllPlayerActions(uuid)
|
|
return {
|
|
code: 200,
|
|
deletedCount: count
|
|
}
|
|
}
|
|
|
|
async function blockPlayer(blockerUuid, blockedUuid) {
|
|
if (blockerUuid === blockedUuid) {
|
|
throw new DefaultError(400, "You cannot block yourself.")
|
|
}
|
|
const changed = await userRepository.blockPlayer(blockerUuid, blockedUuid)
|
|
return { code: 200, changed: changed }
|
|
}
|
|
|
|
async function unblockPlayer(blockerUuid, blockedUuid) {
|
|
const changed = await userRepository.unblockPlayer(blockerUuid, blockedUuid)
|
|
return { code: 200, changed: changed }
|
|
}
|
|
|
|
async function getBlockedUuids(blockerUuid) {
|
|
const list = await userRepository.getBlockedUuids(blockerUuid)
|
|
return { code: 200, data: list }
|
|
}
|
|
|
|
async function isBlocked(blockerUuid, targetUuid) {
|
|
const status = await userRepository.isBlocked(blockerUuid, targetUuid)
|
|
return { code: 200, isBlocked: status }
|
|
}
|
|
|
|
async function getPlayerBanStatus(uuid) {
|
|
const result = await userRepository.getPlayerBans(uuid)
|
|
if (!result || result.code !== 200 || !result.bans || result.bans.length === 0) {
|
|
return { isBanned: false, activeBan: null }
|
|
}
|
|
|
|
const now = new Date()
|
|
const activeBan = result.bans.find(b => !b.expires || new Date(b.expires) > now)
|
|
|
|
if (!activeBan) {
|
|
return { isBanned: false, activeBan: null }
|
|
}
|
|
|
|
return {
|
|
isBanned: true,
|
|
activeBan: {
|
|
banId: activeBan.banId,
|
|
expires: activeBan.expires,
|
|
reason: activeBan.reason,
|
|
reasonMessage: activeBan.reasonMessage
|
|
}
|
|
}
|
|
}
|
|
|
|
async function fetchOrGenerateCertificate(uuid) {
|
|
try {
|
|
const cached = await userRepository.getPlayerCertificate(uuid)
|
|
if (cached) {
|
|
const expiresAtDate = new Date(cached.expiresAt)
|
|
if (expiresAtDate > new Date(Date.now() + 60000)) {
|
|
return {
|
|
code: 200,
|
|
data: {
|
|
keyPair: {
|
|
privateKey: cached.privateKey,
|
|
publicKey: cached.publicKey
|
|
},
|
|
publicKeySignature: cached.publicKeySignatureV2,
|
|
publicKeySignatureV2: cached.publicKeySignatureV2,
|
|
expiresAt: cached.expiresAt,
|
|
refreshedAfter: cached.refreshedAfter
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (error.code !== 404 && error.code !== 500) {
|
|
logger.warn(`Error fetching cache for ${uuid}:` + error.message, ["Certificate", "yellow"])
|
|
}
|
|
}
|
|
|
|
const { privateKey, publicKey } = await generateKeyPairAsync("rsa", {
|
|
modulusLength: 4096,
|
|
publicKeyEncoding: { type: "pkcs1", format: "pem" },
|
|
privateKeyEncoding: { type: "pkcs1", format: "pem" }
|
|
})
|
|
|
|
const now = new Date()
|
|
const expiresAt = new Date(now.getTime() + 48 * 60 * 60 * 1000).toISOString()
|
|
const refreshedAfter = new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString()
|
|
const keys = certsManager.getKeys()
|
|
const serverPrivateKey = keys.playerCertificateKeys.private
|
|
|
|
const signer = crypto.createSign("SHA256")
|
|
signer.update(uuid)
|
|
signer.update(publicKey)
|
|
signer.update(expiresAt)
|
|
|
|
const signatureV2 = signer.sign(serverPrivateKey, "base64")
|
|
await userRepository.savePlayerCertificate(uuid, privateKey, publicKey, signatureV2, expiresAt, refreshedAfter)
|
|
|
|
return {
|
|
code: 200,
|
|
data: {
|
|
keyPair: {
|
|
privateKey: privateKey,
|
|
publicKey: publicKey
|
|
},
|
|
publicKeySignature: signatureV2,
|
|
publicKeySignatureV2: signatureV2,
|
|
expiresAt: expiresAt,
|
|
refreshedAfter: refreshedAfter
|
|
}
|
|
}
|
|
}
|
|
|
|
async function bulkLookup(usernames) {
|
|
if (!Array.isArray(usernames)) {
|
|
throw new DefaultError(400, "Invalid payload. Array of strings expected.")
|
|
}
|
|
|
|
if (usernames.length > 10) {
|
|
throw new DefaultError(400, "Too many usernames provided (max 10).")
|
|
}
|
|
|
|
if (usernames.length === 0) {
|
|
return []
|
|
}
|
|
|
|
const users = await userRepository.getUsersByNames(usernames)
|
|
|
|
return users.map(u => ({
|
|
id: u.uuid.replace(/-/g, ""),
|
|
name: u.username
|
|
}))
|
|
}
|
|
|
|
async function getLegacyProfile(username) {
|
|
const user = await userRepository.getUuidAndUsername(username)
|
|
|
|
if (!user) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
id: user.uuid.replace(/-/g, ""),
|
|
name: user.username
|
|
}
|
|
}
|
|
|
|
async function getNameUUIDs(username, dateInput) {
|
|
let profile
|
|
|
|
if (!dateInput || dateInput == 0) {
|
|
profile = await userRepository.getProfileByUsername(username)
|
|
} else {
|
|
const targetDate = new Date(Number(dateInput)).toISOString()
|
|
profile = await userRepository.getProfileByHistory(username, targetDate)
|
|
}
|
|
|
|
if (!profile) {
|
|
throw new DefaultError(404, "Couldn't find any profile with that name")
|
|
}
|
|
|
|
return {
|
|
code: 200,
|
|
data: {
|
|
id: profile.uuid.replace(/-/g, ""),
|
|
name: profile.username
|
|
}
|
|
}
|
|
}
|
|
|
|
async function getPlayerUsernamesHistory(uuid) {
|
|
const dashedUuid = utils.addDashesToUUID(uuid)
|
|
const history = await userRepository.getNameHistory(dashedUuid)
|
|
if (!history || history.length === 0) {
|
|
throw new DefaultError(404, "User not found")
|
|
}
|
|
|
|
return history.map(entry => {
|
|
const cleanEntry = {
|
|
name: entry.username
|
|
}
|
|
|
|
if (entry.changedAt) {
|
|
const dateObj = new Date(entry.changedAt)
|
|
if (!isNaN(dateObj.getTime())) {
|
|
cleanEntry.changedToAt = dateObj.getTime()
|
|
}
|
|
}
|
|
return cleanEntry
|
|
})
|
|
}
|
|
|
|
module.exports = {
|
|
banUser,
|
|
showCape,
|
|
hideCape,
|
|
unbanUser,
|
|
resetSkin,
|
|
isBlocked,
|
|
bulkLookup,
|
|
blockPlayer,
|
|
getNameUUIDs,
|
|
unblockPlayer,
|
|
getPrivileges,
|
|
getPlayerBans,
|
|
changeUsername,
|
|
getPreferences,
|
|
getBlockedUuids,
|
|
getLegacyProfile,
|
|
addProfileAction,
|
|
getPlayerActions,
|
|
updatePrivileges,
|
|
getPlayerProperty,
|
|
addPlayerProperty,
|
|
updatePreferences,
|
|
getSettingsSchema,
|
|
getPlayerBanStatus,
|
|
removeProfileAction,
|
|
getPlayerProperties,
|
|
updatePlayerProperty,
|
|
deletePlayerProperty,
|
|
getPlayerCertificate,
|
|
savePlayerCertificate,
|
|
clearAllPlayerActions,
|
|
getPlayerNameChangeStatus,
|
|
getPlayerUsernamesHistory,
|
|
deleteExpiredCertificates,
|
|
fetchOrGenerateCertificate,
|
|
} |