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.
This commit is contained in:
parent
1fe46a03fd
commit
5cfadfd7ac
@ -1,14 +1,52 @@
|
|||||||
const DefaultError = require("./DefaultError")
|
const DefaultError = require("./DefaultError")
|
||||||
const YggdrasilError = require("./YggdrasilError")
|
const YggdrasilError = require("./YggdrasilError")
|
||||||
const SessionError = require("./SessionError")
|
const SessionError = require("./SessionError")
|
||||||
|
const ServiceError = require("./ServiceError")
|
||||||
const logger = require("../modules/logger")
|
const logger = require("../modules/logger")
|
||||||
|
|
||||||
class ValidationError extends DefaultError {
|
class ValidationError extends DefaultError {
|
||||||
constructor(zodResult, config = {}, context = {}) {
|
constructor(result, config = {}, context = {}) {
|
||||||
const formattedErrors = zodResult.error.issues.map(e => ({
|
let formattedErrors = []
|
||||||
field: e.path.join("."),
|
if (result && result.error && Array.isArray(result.error.issues)) {
|
||||||
message: e.message
|
formattedErrors = result.error.issues.flatMap(issue => {
|
||||||
}))
|
if (issue.code === "unrecognized_keys") {
|
||||||
|
return issue.keys.map(key => ({
|
||||||
|
field: [...issue.path, key].join("."),
|
||||||
|
message: "Field not allowed"
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (issue.code === "invalid_union") {
|
||||||
|
return {
|
||||||
|
field: issue.path.join("."),
|
||||||
|
message: "Invalid input format (union mismatch)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
field: issue.path.join("."),
|
||||||
|
message: issue.message
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else if (result instanceof Error) {
|
||||||
|
formattedErrors = [{
|
||||||
|
field: "global",
|
||||||
|
message: result.message
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
else if (typeof result === "string") {
|
||||||
|
formattedErrors = [{
|
||||||
|
field: "global",
|
||||||
|
message: result
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
formattedErrors = [{
|
||||||
|
field: "unknown",
|
||||||
|
message: "Unknown validation error"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
const message = config.message || "Validation failed"
|
const message = config.message || "Validation failed"
|
||||||
const statusCode = config.code || 400
|
const statusCode = config.code || 400
|
||||||
@ -54,11 +92,27 @@ class ValidationError extends DefaultError {
|
|||||||
return err.serialize()
|
return err.serialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
if (this.config.errorFormat === "ServiceError") {
|
||||||
|
const err = new ServiceError(
|
||||||
|
this.code,
|
||||||
|
this.context.path || "",
|
||||||
|
this.config.errorName || "ValidationException",
|
||||||
|
this.message,
|
||||||
|
this.formattedErrors
|
||||||
|
)
|
||||||
|
return err.serialize()
|
||||||
|
}
|
||||||
|
const response = {
|
||||||
code: this.code,
|
code: this.code,
|
||||||
message: this.message,
|
message: this.message,
|
||||||
errors: this.formattedErrors
|
errors: this.formattedErrors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.cause) {
|
||||||
|
response.cause = this.cause
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -45,7 +45,8 @@ async function setupDatabase() {
|
|||||||
name VARCHAR(256) NOT NULL,
|
name VARCHAR(256) NOT NULL,
|
||||||
value VARCHAR(512) NOT NULL,
|
value VARCHAR(512) NOT NULL,
|
||||||
uuid VARCHAR(36) NOT NULL,
|
uuid VARCHAR(36) NOT NULL,
|
||||||
FOREIGN KEY (uuid) REFERENCES players(uuid)
|
UNIQUE KEY unique_property (uuid, name),
|
||||||
|
FOREIGN KEY (uuid) REFERENCES players(uuid) ON DELETE CASCADE
|
||||||
)
|
)
|
||||||
`)
|
`)
|
||||||
logger.log(`${"playersProperties".bold} table ready`, ["MariaDB", "yellow"])
|
logger.log(`${"playersProperties".bold} table ready`, ["MariaDB", "yellow"])
|
||||||
@ -333,6 +334,17 @@ async function setupDatabase() {
|
|||||||
)
|
)
|
||||||
`)
|
`)
|
||||||
logger.log(`${"serverSessions".bold} table ready`, ["MariaDB", "yellow"])
|
logger.log(`${"serverSessions".bold} table ready`, ["MariaDB", "yellow"])
|
||||||
|
|
||||||
|
await conn.query(`SET GLOBAL event_scheduler = ON;`)
|
||||||
|
logger.log("MariaDB Event Scheduler enabled.", ["MariaDB", "yellow"])
|
||||||
|
|
||||||
|
await conn.query(`
|
||||||
|
CREATE EVENT IF NOT EXISTS clean_expired_certificates
|
||||||
|
ON SCHEDULE EVERY 1 HOUR
|
||||||
|
DO
|
||||||
|
DELETE FROM playerCertificates WHERE expiresAt < NOW();
|
||||||
|
`)
|
||||||
|
logger.log(`${"clean_expired_certificates".bold} event ready`, ["MariaDB", "yellow"])
|
||||||
|
|
||||||
logger.log("MariaDB database successfully initialised!", ["MariaDB", "yellow"])
|
logger.log("MariaDB database successfully initialised!", ["MariaDB", "yellow"])
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,11 @@ const { DefaultError } = require("../errors/errors")
|
|||||||
|
|
||||||
async function addPropertyToPlayer(key, value, uuid) {
|
async function addPropertyToPlayer(key, value, uuid) {
|
||||||
try {
|
try {
|
||||||
const sql = `INSERT INTO playersProperties (name, value, uuid) VALUES (?, ?, ?)`
|
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])
|
const result = await database.query(sql, [key, value, uuid])
|
||||||
|
|
||||||
if (result.affectedRows > 0) {
|
if (result.affectedRows > 0) {
|
||||||
|
|||||||
@ -3,7 +3,7 @@ const router = express.Router()
|
|||||||
const userService = require("../../../../services/userService")
|
const userService = require("../../../../services/userService")
|
||||||
const authService = require("../../../../services/authService")
|
const authService = require("../../../../services/authService")
|
||||||
|
|
||||||
router.put("/", async (req, res) => {
|
router.get("/", async (req, res) => {
|
||||||
const player = await authService.verifyAccessToken({ accessToken: req.headers.authorization.replace("Bearer ", "") })
|
const player = await authService.verifyAccessToken({ accessToken: req.headers.authorization.replace("Bearer ", "") })
|
||||||
const nameChangeInformation = await userService.getPlayerNameChangeStatus(player.user.uuid)
|
const nameChangeInformation = await userService.getPlayerNameChangeStatus(player.user.uuid)
|
||||||
return res.status(nameChangeInformation.code).json(nameChangeInformation.data)
|
return res.status(nameChangeInformation.code).json(nameChangeInformation.data)
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
const fs = require("node:fs")
|
||||||
|
const path = require("node:path")
|
||||||
const express = require("express")
|
const express = require("express")
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
const multer = require("multer")
|
const multer = require("multer")
|
||||||
@ -22,7 +24,11 @@ const uploadLimiter = rateLimit({
|
|||||||
max: 20,
|
max: 20,
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false,
|
legacyHeaders: false,
|
||||||
|
validate: {
|
||||||
|
ip: false
|
||||||
|
},
|
||||||
keyGenerator: (req) => {
|
keyGenerator: (req) => {
|
||||||
|
rateLimit.ipKeyGenerator()
|
||||||
return req.headers.authorization || req.ip
|
return req.headers.authorization || req.ip
|
||||||
},
|
},
|
||||||
handler: (req, res, next, options) => {
|
handler: (req, res, next, options) => {
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
const express = require("express")
|
|
||||||
const router = express.Router()
|
|
||||||
const path = require("node:path")
|
|
||||||
const fs = require("node:fs")
|
|
||||||
|
|
||||||
const TEXTURES_DIR = path.join(process.cwd(), "data", "textures")
|
|
||||||
if (!fs.existsSync(TEXTURES_DIR)) {
|
|
||||||
fs.mkdirSync(TEXTURES_DIR, { recursive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
router.use(express.static(TEXTURES_DIR))
|
|
||||||
|
|
||||||
module.exports = router
|
|
||||||
27
routes/textures/texture/[hash].js
Normal file
27
routes/textures/texture/[hash].js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
const express = require("express")
|
||||||
|
const router = express.Router({ mergeParams: true })
|
||||||
|
const path = require("node:path")
|
||||||
|
const fs = require("node:fs")
|
||||||
|
const { DefaultError } = require("../../../errors/errors")
|
||||||
|
|
||||||
|
const TEXTURES_DIR = path.join(process.cwd(), "data", "textures")
|
||||||
|
|
||||||
|
router.get("/", async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const hash = req.params.hash
|
||||||
|
if (!/^[a-f0-9]{64}$/i.test(hash)) {
|
||||||
|
throw new DefaultError(404, "Texture not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
const subDir = hash.substring(0, 2)
|
||||||
|
const filePath = path.join(TEXTURES_DIR, subDir, hash)
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
throw new DefaultError(404, "Texture not found")
|
||||||
|
}
|
||||||
|
res.sendFile(filePath)
|
||||||
|
} catch (err) {
|
||||||
|
return next(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
13
schemas/minecraftservices/minecraft/profile.js
Normal file
13
schemas/minecraftservices/minecraft/profile.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
GET: {
|
||||||
|
headers: z.object({
|
||||||
|
"authorization": z.string().min(1, { message: "Authorization header is required." })
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
code: 401,
|
||||||
|
message: "Unauthorized"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
schemas/minecraftservices/minecraft/profile/capes/active.js
Normal file
28
schemas/minecraftservices/minecraft/profile/capes/active.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
DELETE: {
|
||||||
|
headers: z.object({
|
||||||
|
"authorization": z.string().min(1, { message: "Authorization header is required." })
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
code: 401,
|
||||||
|
message: "Unauthorized"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PUT: {
|
||||||
|
headers: z.object({
|
||||||
|
"content-type": z.string()
|
||||||
|
.regex(/application\/json/i, { message: "Content-Type must be application/json" }),
|
||||||
|
"authorization": z.string().min(1, { message: "Authorization header is required." })
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
capeId: z.string().uuid({ message: "Invalid Cape UUID." })
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
code: 400,
|
||||||
|
message: "profile does not own cape",
|
||||||
|
error: "IllegalArgumentException"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
POST: {
|
||||||
|
headers: z.object({
|
||||||
|
"content-type": z.string()
|
||||||
|
.regex(/application\/json/i, { message: "Content-Type must be application/json" })
|
||||||
|
}),
|
||||||
|
body: z.array(z.string().trim().min(1))
|
||||||
|
.min(1, { message: "RequestPayload is an empty array." })
|
||||||
|
.max(10, { message: "RequestPayload has more than 10 elements." }),
|
||||||
|
error: {
|
||||||
|
code: 400,
|
||||||
|
error: "CONSTRAINT_VIOLATION",
|
||||||
|
errorMessage: "size must be between 1 and 10"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
GET: {
|
||||||
|
params: z.object({
|
||||||
|
username: z.string().min(1)
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
code: 404,
|
||||||
|
message: "Not Found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
schemas/minecraftservices/minecraft/profile/name/[name].js
Normal file
22
schemas/minecraftservices/minecraft/profile/name/[name].js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
const nameSchema = z.string()
|
||||||
|
.min(1)
|
||||||
|
.max(16)
|
||||||
|
.regex(/^[a-zA-Z0-9_]+$/, { message: "Name can only contain alphanumeric characters and underscores." });
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
PUT: {
|
||||||
|
headers: z.object({
|
||||||
|
"authorization": z.string().min(1, { message: "Authorization header is required." })
|
||||||
|
}),
|
||||||
|
params: z.object({
|
||||||
|
name: nameSchema
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
code: 400,
|
||||||
|
error: "CONSTRAINT_VIOLATION",
|
||||||
|
errorMessage: "Invalid profile name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
const nameSchema = z.string()
|
||||||
|
.min(1)
|
||||||
|
.max(16)
|
||||||
|
.regex(/^[a-zA-Z0-9_]+$/, { message: "Name can only contain alphanumeric characters and underscores." });
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
GET: {
|
||||||
|
headers: z.object({
|
||||||
|
"authorization": z.string().min(1, { message: "Authorization header is required." })
|
||||||
|
}),
|
||||||
|
params: z.object({
|
||||||
|
name: nameSchema
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
code: 401,
|
||||||
|
message: "Unauthorized"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
schemas/minecraftservices/minecraft/profile/namechange.js
Normal file
13
schemas/minecraftservices/minecraft/profile/namechange.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
GET: {
|
||||||
|
headers: z.object({
|
||||||
|
"authorization": z.string().min(1, { message: "Authorization header is required." })
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
code: 401,
|
||||||
|
message: "Unauthorized"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
schemas/minecraftservices/minecraft/profile/skins.js
Normal file
24
schemas/minecraftservices/minecraft/profile/skins.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
POST: {
|
||||||
|
headers: z.object({
|
||||||
|
"content-type": z.string()
|
||||||
|
.regex(/application\/json/i, { message: "Content-Type must be application/json" }),
|
||||||
|
"authorization": z.string().min(1, { message: "Authorization header is required." })
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
variant: z.enum(["classic", "slim"], {
|
||||||
|
errorMap: () => ({ message: "Variant must be 'classic' or 'slim'." })
|
||||||
|
}),
|
||||||
|
url: z.string()
|
||||||
|
.url({ message: "Invalid URL format." })
|
||||||
|
.max(2048, { message: "URL is too long." })
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
code: 400,
|
||||||
|
message: "Invalid skin URL or variant.",
|
||||||
|
error: "IllegalArgumentException"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
schemas/minecraftservices/minecraft/profile/skins/active.js
Normal file
13
schemas/minecraftservices/minecraft/profile/skins/active.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
DELETE: {
|
||||||
|
headers: z.object({
|
||||||
|
"authorization": z.string().min(1, { message: "Authorization header is required." })
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
code: 401,
|
||||||
|
message: "Unauthorized"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
schemas/minecraftservices/player/attributes.js
Normal file
29
schemas/minecraftservices/player/attributes.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
GET: {
|
||||||
|
headers: z.object({
|
||||||
|
"authorization": z.string().min(1, { message: "Authorization header is required." })
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
code: 401,
|
||||||
|
message: "Unauthorized"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
POST: {
|
||||||
|
headers: z.object({
|
||||||
|
"content-type": z.string()
|
||||||
|
.regex(/application\/json/i, { message: "Content-Type must be application/json" }),
|
||||||
|
"authorization": z.string().min(1, { message: "Authorization header is required." })
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
profanityFilterPreferences: z.object({
|
||||||
|
profanityFilterOn: z.boolean({ required_error: "profanityFilterOn is required" })
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
code: 400,
|
||||||
|
message: "Invalid attributes format."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
schemas/minecraftservices/player/certificates.js
Normal file
13
schemas/minecraftservices/player/certificates.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
POST: {
|
||||||
|
headers: z.object({
|
||||||
|
"authorization": z.string().min(1, { message: "Authorization header is required." })
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
code: 401,
|
||||||
|
message: "Unauthorized"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
schemas/minecraftservices/privacy/blocklist.js
Normal file
13
schemas/minecraftservices/privacy/blocklist.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
GET: {
|
||||||
|
headers: z.object({
|
||||||
|
"authorization": z.string().min(1, { message: "Authorization header is required." })
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
code: 401,
|
||||||
|
message: "Unauthorized"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
schemas/minecraftservices/privacy/blocklist/[uuid].js
Normal file
28
schemas/minecraftservices/privacy/blocklist/[uuid].js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
PUT: {
|
||||||
|
headers: z.object({
|
||||||
|
"authorization": z.string().min(1, { message: "Authorization header is required." })
|
||||||
|
}),
|
||||||
|
params: z.object({
|
||||||
|
uuid: z.string().uuid({ message: "Invalid UUID." })
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
code: 400,
|
||||||
|
message: "Invalid UUID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DELETE: {
|
||||||
|
headers: z.object({
|
||||||
|
"authorization": z.string().min(1, { message: "Authorization header is required." })
|
||||||
|
}),
|
||||||
|
params: z.object({
|
||||||
|
uuid: z.string().uuid({ message: "Invalid UUID." })
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
code: 400,
|
||||||
|
message: "Invalid UUID"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
schemas/minecraftservices/privileges.js
Normal file
29
schemas/minecraftservices/privileges.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
GET: {
|
||||||
|
headers: z.object({
|
||||||
|
"authorization": z.string().min(1, { message: "Authorization header is required." })
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
code: 401,
|
||||||
|
message: "Unauthorized"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
POST: {
|
||||||
|
headers: z.object({
|
||||||
|
"content-type": z.string()
|
||||||
|
.regex(/application\/json/i, { message: "Content-Type must be application/json" }),
|
||||||
|
"authorization": z.string().min(1, { message: "Authorization header is required." })
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
profanityFilterPreferences: z.object({
|
||||||
|
profanityFilterOn: z.boolean({ required_error: "profanityFilterOn is required" })
|
||||||
|
}).optional()
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
code: 401,
|
||||||
|
message: "Unauthorized"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
schemas/mojangapi/minecraft/profile/lookup/bulk/byname.js
Normal file
18
schemas/mojangapi/minecraft/profile/lookup/bulk/byname.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
POST: {
|
||||||
|
headers: z.object({
|
||||||
|
"content-type": z.string()
|
||||||
|
.regex(/application\/json/i, { message: "Content-Type must be application/json" })
|
||||||
|
}),
|
||||||
|
body: z.array(z.string().trim().min(1))
|
||||||
|
.min(1, { message: "RequestPayload is an empty array." })
|
||||||
|
.max(10, { message: "RequestPayload has more than 10 elements." }),
|
||||||
|
error: {
|
||||||
|
code: 400,
|
||||||
|
error: "CONSTRAINT_VIOLATION",
|
||||||
|
errorMessage: "size must be between 1 and 10"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
schemas/mojangapi/profile/lookup/name/[username].js
Normal file
13
schemas/mojangapi/profile/lookup/name/[username].js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
GET: {
|
||||||
|
params: z.object({
|
||||||
|
username: z.string().min(1, { message: "Username is required." })
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
code: 404,
|
||||||
|
message: "Not Found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
schemas/mojangapi/profiles/minecraft.js
Normal file
18
schemas/mojangapi/profiles/minecraft.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
POST: {
|
||||||
|
headers: z.object({
|
||||||
|
"content-type": z.string()
|
||||||
|
.regex(/application\/json/i, { message: "Content-Type must be application/json" })
|
||||||
|
}),
|
||||||
|
body: z.array(z.string().trim().min(1))
|
||||||
|
.min(1, { message: "RequestPayload is an empty array." })
|
||||||
|
.max(10, { message: "RequestPayload has more than 10 elements." }),
|
||||||
|
error: {
|
||||||
|
code: 400,
|
||||||
|
error: "CONSTRAINT_VIOLATION",
|
||||||
|
errorMessage: "size must be between 1 and 10"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
schemas/mojangapi/user/profiles/[uuid]/names.js
Normal file
18
schemas/mojangapi/user/profiles/[uuid]/names.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
GET: {
|
||||||
|
headers: z.object({
|
||||||
|
"content-type": z.string()
|
||||||
|
.regex(/application\/json/i, { message: "Content-Type must be application/json" })
|
||||||
|
.optional()
|
||||||
|
}),
|
||||||
|
params: z.object({
|
||||||
|
uuid: z.string().uuid({ message: "Invalid UUID format." })
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
code: 204,
|
||||||
|
message: "No content"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
schemas/mojangapi/users/profiles/minecraft/[username].js
Normal file
13
schemas/mojangapi/users/profiles/minecraft/[username].js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
GET: {
|
||||||
|
params: z.object({
|
||||||
|
username: z.string().min(1, { message: "Username is required." })
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
code: 404,
|
||||||
|
message: "Not Found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
schemas/sessionsserver/session/minecraft/profile/[uuid].js
Normal file
16
schemas/sessionsserver/session/minecraft/profile/[uuid].js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
GET: {
|
||||||
|
params: z.object({
|
||||||
|
uuid: z.string().length(32).regex(/^[0-9a-fA-F]+$/, { message: "Invalid UUID (no dashes expected)." })
|
||||||
|
}),
|
||||||
|
query: z.object({
|
||||||
|
unsigned: z.enum(["true", "false"]).optional()
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
code: 204,
|
||||||
|
message: "No content (UUID not found)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -73,12 +73,12 @@ async function getProfile({ uuid, unsigned = false }) {
|
|||||||
const hasValidCape = !!activeCape
|
const hasValidCape = !!activeCape
|
||||||
|
|
||||||
const skinNode = hasValidSkin ? {
|
const skinNode = hasValidSkin ? {
|
||||||
url: activeSkin.url,
|
url: (process.env.TEXTURES_ENDPOINTS || `http://localhost:${process.env.WEB_PORT}/textures/`) + activeSkin.url,
|
||||||
metadata: activeSkin.variant === "SLIM" ? { model: "slim" } : undefined
|
metadata: activeSkin.variant === "SLIM" ? { model: "slim" } : undefined
|
||||||
} : undefined
|
} : undefined
|
||||||
|
|
||||||
const capeNode = hasValidCape ? {
|
const capeNode = hasValidCape ? {
|
||||||
url: activeCape.url
|
url: (process.env.TEXTURES_ENDPOINTS || `http://localhost:${process.env.WEB_PORT}/textures/`) + activeCape.url
|
||||||
} : undefined
|
} : undefined
|
||||||
|
|
||||||
const texturesObject = {
|
const texturesObject = {
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
const fs = require("node:fs/promises")
|
const fs = require("node:fs/promises")
|
||||||
|
const path = require("node:path")
|
||||||
const util = require("node:util")
|
const util = require("node:util")
|
||||||
const logger = require("../modules/logger")
|
const logger = require("../modules/logger")
|
||||||
const crypto = require("node:crypto")
|
const crypto = require("node:crypto")
|
||||||
|
const ssrfcheck = require("ssrfcheck")
|
||||||
const certsManager = require("../modules/certificatesManager")
|
const certsManager = require("../modules/certificatesManager")
|
||||||
const userRepository = require("../repositories/userRepository")
|
const userRepository = require("../repositories/userRepository")
|
||||||
const { DefaultError } = require("../errors/errors")
|
const { DefaultError } = require("../errors/errors")
|
||||||
@ -475,6 +477,7 @@ async function uploadSkin(uuid, fileObject, variant) {
|
|||||||
|
|
||||||
async function uploadSkinFromUrl(uuid, url, variant) {
|
async function uploadSkinFromUrl(uuid, url, variant) {
|
||||||
if (!url) throw new DefaultError(400, "Missing 'url' parameter.")
|
if (!url) throw new DefaultError(400, "Missing 'url' parameter.")
|
||||||
|
if (ssrfcheck.isSSRFSafeURL(url)) throw new DefaultError(400, "Bad request", null)
|
||||||
|
|
||||||
let buffer
|
let buffer
|
||||||
try {
|
try {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user