Add skin upload and texture management endpoints
Introduces endpoints and logic for uploading Minecraft skins via file or URL, storing textures, and managing player skins. Adds new repository and service methods for texture registration and retrieval, updates authorization handling, and uses process.cwd() for data paths. Also includes static serving of textures and rate limiting for skin uploads.
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
const fs = require("node:fs/promises")
|
||||
const util = require("node:util")
|
||||
const logger = require("../modules/logger")
|
||||
const crypto = require("node:crypto")
|
||||
const certsManager = require("../modules/certificatesManager")
|
||||
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")
|
||||
|
||||
const TEMP_DIR = path.join(process.cwd(), "data", "temp")
|
||||
const TEXTURES_DIR = path.join(process.cwd(), "data", "textures")
|
||||
|
||||
async function getPlayerProperties(uuid) {
|
||||
try {
|
||||
@@ -396,6 +400,101 @@ async function getPlayerUsernamesHistory(uuid) {
|
||||
})
|
||||
}
|
||||
|
||||
async function registerTexture(hash, type, url, alias = null) {
|
||||
const existingTexture = await userRepository.getTextureByHash(hash)
|
||||
|
||||
if (existingTexture) {
|
||||
return {
|
||||
code: 200,
|
||||
textureUuid: existingTexture.uuid,
|
||||
isNew: false
|
||||
}
|
||||
}
|
||||
|
||||
const newUuid = crypto.randomUUID()
|
||||
await userRepository.createTexture(newUuid, hash, type, url, alias)
|
||||
|
||||
return {
|
||||
code: 201,
|
||||
textureUuid: newUuid,
|
||||
isNew: true
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadSkin(uuid, fileObject, variant) {
|
||||
if (!fileObject || !fileObject.path) {
|
||||
throw new DefaultError(400, "No skin file provided.")
|
||||
}
|
||||
|
||||
const tempPath = fileObject.path
|
||||
let buffer
|
||||
|
||||
try {
|
||||
buffer = await fs.readFile(tempPath)
|
||||
if (buffer.length < 8 || !buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]))) {
|
||||
throw new DefaultError(400, "Invalid file format. Only PNG is allowed.")
|
||||
}
|
||||
|
||||
try {
|
||||
const width = buffer.readUInt32BE(16)
|
||||
const height = buffer.readUInt32BE(20)
|
||||
if (width !== 64 || (height !== 64 && height !== 32)) {
|
||||
throw new DefaultError(400, `Invalid skin dimensions. Got ${width}x${height}, expected 64x64 or 64x32.`)
|
||||
}
|
||||
} catch (e) {
|
||||
throw new DefaultError(400, "Could not read image dimensions.")
|
||||
}
|
||||
|
||||
const hash = crypto.createHash("sha256").update(buffer).digest("hex")
|
||||
const existingTexture = await userRepository.getTextureByHash(hash)
|
||||
|
||||
if (!existingTexture) {
|
||||
const subDir = hash.substring(0, 2)
|
||||
const targetDir = path.join(TEXTURES_DIR, subDir)
|
||||
const targetPath = path.join(targetDir, hash)
|
||||
|
||||
await fs.mkdir(targetDir, { recursive: true })
|
||||
await fs.writeFile(targetPath, buffer)
|
||||
|
||||
const newTextureUuid = crypto.randomUUID()
|
||||
const textureUrl = `/texture/${hash}`
|
||||
await userRepository.createTexture(newTextureUuid, hash, 'SKIN', textureUrl, null)
|
||||
}
|
||||
|
||||
const validVariant = (variant === "slim") ? "slim" : "classic"
|
||||
await userRepository.setSkin(uuid, hash, validVariant)
|
||||
|
||||
return { code: 200, message: "Skin uploaded successfully" }
|
||||
|
||||
} catch (error) {
|
||||
throw error
|
||||
} finally {
|
||||
await fs.unlink(tempPath).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadSkinFromUrl(uuid, url, variant) {
|
||||
if (!url) throw new DefaultError(400, "Missing 'url' parameter.")
|
||||
|
||||
let buffer
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) throw new Error("Fetch failed")
|
||||
const arrayBuffer = await response.arrayBuffer()
|
||||
buffer = Buffer.from(arrayBuffer)
|
||||
} catch (err) {
|
||||
throw new DefaultError(400, "Could not download skin from the provided URL.")
|
||||
}
|
||||
|
||||
const tempFileName = crypto.randomBytes(16).toString("hex")
|
||||
const tempPath = path.join(TEMP_DIR, tempFileName)
|
||||
|
||||
await fs.mkdir(TEMP_DIR, { recursive: true })
|
||||
await fs.writeFile(tempPath, buffer)
|
||||
|
||||
return await uploadSkin(uuid, { path: tempPath }, variant)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
banUser,
|
||||
showCape,
|
||||
@@ -404,6 +503,7 @@ module.exports = {
|
||||
resetSkin,
|
||||
isBlocked,
|
||||
bulkLookup,
|
||||
uploadSkin,
|
||||
blockPlayer,
|
||||
getNameUUIDs,
|
||||
unblockPlayer,
|
||||
@@ -412,6 +512,7 @@ module.exports = {
|
||||
changeUsername,
|
||||
getPreferences,
|
||||
getBlockedUuids,
|
||||
registerTexture,
|
||||
getLegacyProfile,
|
||||
addProfileAction,
|
||||
getPlayerActions,
|
||||
|
||||
Reference in New Issue
Block a user