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.
77 lines
2.4 KiB
JavaScript
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 |