azures04 5cfadfd7ac Add validation schemas and improve texture handling
Introduces zod-based validation schemas for Minecraft and Mojang API endpoints. Refactors texture route to support hash-based file serving and removes the old static texture route. Updates database schema for player properties and adds an event to clean expired certificates. Improves ValidationError formatting, adjusts skin/cape URL construction, and adds SSRF protection for skin uploads.
2025-12-28 09:02:10 +01:00

77 lines
2.4 KiB
JavaScript

const fs = require("node:fs")
const path = require("node:path")
const express = require("express")
const router = express.Router()
const multer = require("multer")
const rateLimit = require("express-rate-limit")
const userService = require("../../../../../services/userService")
const authService = require("../../../../../services/authService")
const { DefaultError } = require("../../../../../errors/errors")
const TEMP_DIR = path.join(process.cwd(), "data", "temp")
if (!fs.existsSync(TEMP_DIR)) {
fs.mkdirSync(TEMP_DIR, { recursive: true })
}
const upload = multer({
dest: TEMP_DIR,
limits: { fileSize: 2 * 1024 * 1024 }
})
const uploadLimiter = rateLimit({
windowMs: 60 * 1000,
max: 20,
standardHeaders: true,
legacyHeaders: false,
validate: {
ip: false
},
keyGenerator: (req) => {
rateLimit.ipKeyGenerator()
return req.headers.authorization || req.ip
},
handler: (req, res, next, options) => {
throw new DefaultError(429, "Too many requests. Please try again later.")
}
})
router.post("/", uploadLimiter, async (req, res, next) => {
if (req.is('application/json')) {
try {
const token = req.headers.authorization.replace("Bearer ", "").trim()
const player = await authService.verifyAccessToken({ accessToken: token })
await userService.uploadSkinFromUrl(player.user.uuid, req.body.url, req.body.variant)
return res.status(200).send()
} catch (err) {
return next(err)
}
}
else {
upload.single("file")(req, res, async (err) => {
if (err) return next(err)
try {
if (!req.headers.authorization) {
if (req.file) await fs.promises.unlink(req.file.path).catch(() => {})
throw new DefaultError(401, "Missing Authorization Header")
}
const token = req.headers.authorization.replace("Bearer ", "").trim()
const player = await authService.verifyAccessToken({ accessToken: token })
await userService.uploadSkin(player.user.uuid, req.file, req.body.variant)
return res.status(200).send()
} catch (error) {
if (req.file) await fs.promises.unlink(req.file.path).catch(() => {})
return next(error)
}
})
}
})
module.exports = router