Add Minecraft services API routes and user service
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.
This commit is contained in:
parent
228345c859
commit
5dd1de1521
@ -1,10 +1,12 @@
|
||||
class AccountsAPIError extends Error {
|
||||
constructor(code, path, error, errorMessage) {
|
||||
class ServiceError extends Error {
|
||||
constructor(code, path, error, errorMessage, details = null) {
|
||||
super(errorMessage || error || "Accounts API Error")
|
||||
this.code = code
|
||||
this.path = path
|
||||
this.errorType = error
|
||||
this.errorMessage = errorMessage
|
||||
this.developerMessage = errorMessage
|
||||
this.details = details
|
||||
this.isOperational = true
|
||||
|
||||
Error.captureStackTrace(this, this.constructor)
|
||||
@ -19,14 +21,23 @@ class AccountsAPIError extends Error {
|
||||
|
||||
if (this.errorType && this.errorType.trim() !== "") {
|
||||
response.error = this.errorType
|
||||
response.errorType = this.errorType
|
||||
}
|
||||
|
||||
if (this.details) {
|
||||
response.details = this.details
|
||||
}
|
||||
|
||||
if (this.errorMessage && this.errorMessage.trim() !== "") {
|
||||
response.errorMessage = this.errorMessage
|
||||
}
|
||||
|
||||
if (this.developerMessage && this.developerMessage.trim() !== "") {
|
||||
response.developerMessage = this.developerMessage
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AccountsAPIError
|
||||
module.exports = ServiceError
|
||||
@ -1,4 +1,5 @@
|
||||
const DefaultError = require("./DefaultError")
|
||||
const ServiceError = require("./ServiceError")
|
||||
const SessionError = require("./SessionError")
|
||||
const YggdrasilError = require("./YggdrasilError")
|
||||
const ValidationError = require("./ValidationError")
|
||||
@ -6,6 +7,7 @@ const ValidationError = require("./ValidationError")
|
||||
module.exports = {
|
||||
DefaultError,
|
||||
SessionError,
|
||||
ServiceError,
|
||||
YggdrasilError,
|
||||
ValidationError
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
const fs = require("node:fs")
|
||||
const path = require("node:path")
|
||||
const utils = require("./utils")
|
||||
require("colors")
|
||||
require("dotenv").config({
|
||||
quiet: true
|
||||
@ -42,7 +43,7 @@ function write($stream, level, color, content, extraLabels = []) {
|
||||
|
||||
function createLogger(root) {
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const fileName = (/false/).test(process.env.IS_PROD.toLowerCase()) ? new Date().toLocaleString("fr-FR", { timeZone: "UTC" }).replace(/[\/:]/g, "-").replace(/ /g, "_") : "DEV-LOG"
|
||||
const fileName = utils.isTrueFromDotEnv("IS_PROD") ? new Date().toLocaleString("fr-FR", { timeZone: "UTC" }).replace(/[\/:]/g, "-").replace(/ /g, "_") : "DEV-LOG"
|
||||
|
||||
const logsDir = path.join(root, "logs")
|
||||
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
const path = require("node:path")
|
||||
const logger = require("./logger")
|
||||
const crypto = require("node:crypto")
|
||||
const certificatesManager = require("./certificatesManager")
|
||||
|
||||
@ -53,8 +51,13 @@ function addDashesToUUID(uuid) {
|
||||
)
|
||||
}
|
||||
|
||||
function isTrueFromDotEnv(key) {
|
||||
return (process.env[key] || "").trim().toLowerCase() === "true"
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getRegistrationCountryFromIp,
|
||||
signProfileData,
|
||||
addDashesToUUID,
|
||||
signProfileData
|
||||
isTrueFromDotEnv,
|
||||
getRegistrationCountryFromIp,
|
||||
}
|
||||
@ -2,7 +2,6 @@ const logger = require("../modules/logger")
|
||||
const bcrypt = require("bcryptjs")
|
||||
const database = require("../modules/database")
|
||||
const { DefaultError } = require("../errors/errors")
|
||||
const usernameRegex = /^[a-zA-Z0-9_]{3,16}$/
|
||||
|
||||
async function getUser(identifier, requirePassword = false) {
|
||||
try {
|
||||
@ -28,19 +27,14 @@ async function getUser(identifier, requirePassword = false) {
|
||||
|
||||
async function register(email, username, password) {
|
||||
try {
|
||||
const availability = await checkUsernameAvailability(username)
|
||||
if (availability.allowed) {
|
||||
const sql = `INSERT INTO players (email, username, password, uuid) VALUES (?, ?, ?, ?)`
|
||||
const uuid = crypto.randomUUID()
|
||||
const hashedPassword = await bcrypt.hash(password, 10)
|
||||
const result = await database.query(sql, [email, username, hashedPassword, uuid])
|
||||
if (result.affectedRows > 0) {
|
||||
return { code: 200, email, username, uuid }
|
||||
} else {
|
||||
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
|
||||
}
|
||||
const sql = `INSERT INTO players (email, username, password, uuid) VALUES (?, ?, ?, ?)`
|
||||
const uuid = crypto.randomUUID()
|
||||
const hashedPassword = await bcrypt.hash(password, 10)
|
||||
const result = await database.query(sql, [email, username, hashedPassword, uuid])
|
||||
if (result.affectedRows > 0) {
|
||||
return { code: 200, email, username, uuid }
|
||||
} else {
|
||||
throw new DefaultError(415, "Illegal Server Character", availability.message || "INVALID_USERNAME_FORMAT")
|
||||
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof DefaultError) throw error
|
||||
@ -51,30 +45,6 @@ async function register(email, username, password) {
|
||||
}
|
||||
}
|
||||
|
||||
async function checkUsernameAvailability(username) {
|
||||
if (!usernameRegex.test(username)) {
|
||||
return { code: 200, allowed: false, message: "Invalid format (3-16 alphanumeric chars)." }
|
||||
}
|
||||
|
||||
const blocklist = await getUsernamesRules()
|
||||
const normalizedUsername = username.toLowerCase()
|
||||
|
||||
for (const entry of blocklist) {
|
||||
if (entry.type === "literal") {
|
||||
if (normalizedUsername === entry.value) {
|
||||
return { code: 200, allowed: false, message: "This username is reserved." }
|
||||
}
|
||||
}
|
||||
else if (entry.type === "regex") {
|
||||
if (entry.pattern.test(username)) {
|
||||
return { code: 200, allowed: false, message: "This username contains forbidden patterns." }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { code: 200, allowed: true }
|
||||
}
|
||||
|
||||
async function insertClientSession(accessToken, clientToken, uuid) {
|
||||
try {
|
||||
const sql = `INSERT INTO clientSessions (accessToken, clientToken, uuid) VALUES (?, ?, ?)`
|
||||
@ -228,6 +198,7 @@ module.exports = {
|
||||
getUser,
|
||||
register,
|
||||
getClientSession,
|
||||
getUsernamesRules,
|
||||
revokeAccessTokens,
|
||||
insertClientSession,
|
||||
getPlayerProperties,
|
||||
|
||||
@ -295,18 +295,358 @@ async function getPlayerBans(uuid) {
|
||||
}
|
||||
}
|
||||
|
||||
async function changeUsername(uuid, newName) {
|
||||
try {
|
||||
const sql = "UPDATE players SET username = ? WHERE uuid = ?"
|
||||
const result = await database.query(sql, [newName, uuid])
|
||||
if (result.affectedRows > 0) {
|
||||
return { code: 200, message: "Username changed successfully" }
|
||||
} else {
|
||||
throw new DefaultError(404, "User not found")
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof DefaultError) throw error
|
||||
if (error.code === "ER_DUP_ENTRY" || error.errno === 1062) {
|
||||
throw new DefaultError(409, "Username already taken", "ForbiddenOperationException")
|
||||
}
|
||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
||||
throw new DefaultError(500, "Internal Server Error", error.toString())
|
||||
}
|
||||
}
|
||||
|
||||
async function resetSkin(uuid, hash, variant) {
|
||||
try {
|
||||
const insertSql = `
|
||||
INSERT IGNORE INTO playersSkins (playerUuid, assetHash, variant, isSelected)
|
||||
VALUES (?, ?, ?, 0)
|
||||
`
|
||||
await database.query(insertSql, [uuid, hash, variant])
|
||||
const updateSql = `
|
||||
UPDATE playersSkins
|
||||
SET isSelected = (assetHash = ?)
|
||||
WHERE playerUuid = ?
|
||||
`
|
||||
await database.query(updateSql, [hash, uuid])
|
||||
return { code: 200 }
|
||||
} catch (error) {
|
||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
||||
}
|
||||
}
|
||||
|
||||
async function hideCape(uuid) {
|
||||
try {
|
||||
const sql = "UPDATE playersCapes SET isSelected = 0 WHERE playerUuid = ?"
|
||||
await database.query(sql, [uuid])
|
||||
return { code: 200 }
|
||||
} catch (error) {
|
||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
||||
}
|
||||
}
|
||||
|
||||
async function getTextureByUuid(textureUuid) {
|
||||
try {
|
||||
const sql = "SELECT hash FROM textures WHERE uuid = ?"
|
||||
const rows = await database.query(sql, [textureUuid])
|
||||
return rows[0] || null
|
||||
} catch (error) {
|
||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
||||
}
|
||||
}
|
||||
|
||||
async function showCape(uuid, hash) {
|
||||
try {
|
||||
const sql = `
|
||||
UPDATE playersCapes
|
||||
SET isSelected = (assetHash = ?)
|
||||
WHERE playerUuid = ?
|
||||
`
|
||||
const result = await database.query(sql, [hash, uuid])
|
||||
return { code: 200, changed: result.affectedRows > 0 }
|
||||
} catch (error) {
|
||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
||||
}
|
||||
}
|
||||
|
||||
async function checkCapeOwnership(uuid, hash) {
|
||||
try {
|
||||
const sql = "SELECT 1 FROM playersCapes WHERE playerUuid = ? AND assetHash = ?"
|
||||
const rows = await database.query(sql, [uuid, hash])
|
||||
return rows.length > 0
|
||||
} catch (error) {
|
||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
||||
}
|
||||
}
|
||||
|
||||
async function getPlayerMeta(uuid) {
|
||||
try {
|
||||
const sql = `SELECT createdAt, nameChangeAllowed FROM players WHERE uuid = ?`
|
||||
const rows = await database.query(sql, [uuid])
|
||||
return rows[0]
|
||||
} catch (error) {
|
||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
||||
}
|
||||
}
|
||||
|
||||
async function getLastNameChange(uuid) {
|
||||
try {
|
||||
const sql = `
|
||||
SELECT changedAt
|
||||
FROM uuidToNameHistory
|
||||
WHERE uuid = ? AND changedAt IS NOT NULL
|
||||
ORDER BY changedAt DESC
|
||||
LIMIT 1
|
||||
`
|
||||
const rows = await database.query(sql, [uuid])
|
||||
return rows[0]
|
||||
} catch (error) {
|
||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
||||
}
|
||||
}
|
||||
|
||||
async function getPlayerCertificate(uuid) {
|
||||
try {
|
||||
const sql = "SELECT * FROM playerCertificates WHERE uuid = ?"
|
||||
const rows = await database.query(sql, [uuid])
|
||||
return rows[0]
|
||||
} catch (error) {
|
||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
||||
}
|
||||
}
|
||||
|
||||
async function savePlayerCertificate(uuid, privateKey, publicKey, signatureV2, expiresAt, refreshedAfter) {
|
||||
try {
|
||||
const sql = `
|
||||
REPLACE INTO playerCertificates
|
||||
(uuid, privateKey, publicKey, publicKeySignatureV2, expiresAt, refreshedAfter)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
const result = await database.query(sql, [uuid, privateKey, publicKey, signatureV2, expiresAt, refreshedAfter])
|
||||
return result.affectedRows > 0
|
||||
} catch (error) {
|
||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteExpiredCertificates(isoDate) {
|
||||
try {
|
||||
const sql = "DELETE FROM playerCertificates WHERE expiresAt < ?"
|
||||
const result = await database.query(sql, [isoDate])
|
||||
return result.affectedRows
|
||||
} catch (error) {
|
||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
||||
}
|
||||
}
|
||||
|
||||
async function addProfileAction(uuid, actionCode) {
|
||||
try {
|
||||
const cleanUuid = uuid.replace(/-/g, "")
|
||||
const sql = "INSERT IGNORE INTO playerProfileActions (uuid, action) VALUES (?, ?)"
|
||||
const result = await database.query(sql, [cleanUuid, actionCode])
|
||||
return result.affectedRows > 0
|
||||
} catch (error) {
|
||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
||||
}
|
||||
}
|
||||
|
||||
async function removeProfileAction(uuid, actionCode) {
|
||||
try {
|
||||
const cleanUuid = uuid.replace(/-/g, "")
|
||||
const sql = "DELETE FROM playerProfileActions WHERE uuid = ? AND action = ?"
|
||||
const result = await database.query(sql, [cleanUuid, actionCode])
|
||||
return result.affectedRows
|
||||
} catch (error) {
|
||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
||||
}
|
||||
}
|
||||
|
||||
async function getPlayerActions(uuid) {
|
||||
try {
|
||||
const cleanUuid = uuid.replace(/-/g, "")
|
||||
const sql = "SELECT action FROM playerProfileActions WHERE uuid = ?"
|
||||
const rows = await database.query(sql, [cleanUuid])
|
||||
return rows.map(r => r.action)
|
||||
} catch (error) {
|
||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
||||
}
|
||||
}
|
||||
|
||||
async function clearAllPlayerActions(uuid) {
|
||||
try {
|
||||
const cleanUuid = uuid.replace(/-/g, "")
|
||||
const sql = "DELETE FROM playerProfileActions WHERE uuid = ?"
|
||||
const result = await database.query(sql, [cleanUuid])
|
||||
return result.affectedRows
|
||||
} catch (error) {
|
||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
||||
}
|
||||
}
|
||||
|
||||
async function blockPlayer(blockerUuid, blockedUuid) {
|
||||
try {
|
||||
const sql = `INSERT IGNORE INTO playersBlockslist (blockerUuid, blockedUuid) VALUES (?, ?)`
|
||||
const result = await database.query(sql, [blockerUuid, blockedUuid])
|
||||
return result.affectedRows > 0
|
||||
} catch (error) {
|
||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
||||
}
|
||||
}
|
||||
|
||||
async function unblockPlayer(blockerUuid, blockedUuid) {
|
||||
try {
|
||||
const sql = `DELETE FROM playersBlockslist WHERE blockerUuid = ? AND blockedUuid = ?`
|
||||
const result = await database.query(sql, [blockerUuid, blockedUuid])
|
||||
return result.affectedRows > 0
|
||||
} catch (error) {
|
||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
||||
}
|
||||
}
|
||||
|
||||
async function getBlockedUuids(blockerUuid) {
|
||||
try {
|
||||
const sql = `SELECT blockedUuid FROM playersBlockslist WHERE blockerUuid = ?`
|
||||
const rows = await database.query(sql, [blockerUuid])
|
||||
return rows.map(r => r.blockedUuid)
|
||||
} catch (error) {
|
||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
||||
}
|
||||
}
|
||||
|
||||
async function isBlocked(blockerUuid, targetUuid) {
|
||||
try {
|
||||
const sql = `SELECT 1 FROM playersBlockslist WHERE blockerUuid = ? AND blockedUuid = ? LIMIT 1`
|
||||
const rows = await database.query(sql, [blockerUuid, targetUuid])
|
||||
return rows.length > 0
|
||||
} catch (error) {
|
||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
||||
}
|
||||
}
|
||||
|
||||
async function getUsersByNames(usernames) {
|
||||
try {
|
||||
if (!usernames || usernames.length === 0) return []
|
||||
const uniqueNames = [...new Set(usernames)]
|
||||
|
||||
const placeholders = uniqueNames.map(() => "?").join(", ")
|
||||
const sql = `SELECT uuid, username FROM players WHERE username IN (${placeholders})`
|
||||
|
||||
const rows = await database.query(sql, uniqueNames)
|
||||
return rows
|
||||
} catch (error) {
|
||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
||||
}
|
||||
}
|
||||
|
||||
async function getUuidAndUsername(username) {
|
||||
try {
|
||||
const sql = "SELECT uuid, username FROM players WHERE username = ?"
|
||||
const rows = await database.query(sql, [username])
|
||||
|
||||
return rows[0] || null
|
||||
} catch (error) {
|
||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
||||
}
|
||||
}
|
||||
|
||||
async function getProfileByUsername(username) {
|
||||
try {
|
||||
const sql = "SELECT uuid, username FROM players WHERE username = ?"
|
||||
const rows = await database.query(sql, [username])
|
||||
return rows[0] || null
|
||||
} catch (error) {
|
||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
||||
}
|
||||
}
|
||||
|
||||
async function getProfileByHistory(username, isoDate) {
|
||||
try {
|
||||
const sql = `
|
||||
SELECT uuid, username
|
||||
FROM uuidToNameHistory
|
||||
WHERE username = ?
|
||||
AND (changedAt <= ? OR changedAt IS NULL)
|
||||
ORDER BY changedAt DESC
|
||||
LIMIT 1
|
||||
`
|
||||
const rows = await database.query(sql, [username, isoDate])
|
||||
return rows[0] || null
|
||||
} catch (error) {
|
||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
||||
}
|
||||
}
|
||||
|
||||
async function getNameHistory(uuid) {
|
||||
try {
|
||||
const sql = `
|
||||
SELECT username, changedAt
|
||||
FROM uuidToNameHistory
|
||||
WHERE uuid = ?
|
||||
ORDER BY changedAt ASC
|
||||
`
|
||||
const rows = await database.query(sql, [uuid])
|
||||
return rows
|
||||
} catch (error) {
|
||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
banUser,
|
||||
showCape,
|
||||
hideCape,
|
||||
resetSkin,
|
||||
isBlocked,
|
||||
unbanUser,
|
||||
blockPlayer,
|
||||
getPlayerBans,
|
||||
getPlayerMeta,
|
||||
unblockPlayer,
|
||||
changeUsername,
|
||||
getNameHistory,
|
||||
getBlockedUuids,
|
||||
getUsersByNames,
|
||||
getPlayerActions,
|
||||
getTextureByUuid,
|
||||
addProfileAction,
|
||||
getLastNameChange,
|
||||
getPlayerProperty,
|
||||
getUuidAndUsername,
|
||||
checkCapeOwnership,
|
||||
getProfileByHistory,
|
||||
getPlayerPrivileges,
|
||||
getPlayerProperties,
|
||||
removeProfileAction,
|
||||
addPropertyToPlayer,
|
||||
getProfileByUsername,
|
||||
getPlayerPreferences,
|
||||
getPlayerCertificate,
|
||||
clearAllPlayerActions,
|
||||
savePlayerCertificate,
|
||||
updatePlayerPrivileges,
|
||||
deletePropertyToPlayer,
|
||||
updatePropertyToPlayer,
|
||||
getPlayerSettingsSchema,
|
||||
updatePlayerPreferences,
|
||||
deleteExpiredCertificates,
|
||||
}
|
||||
@ -11,7 +11,7 @@ const limiter = rateLimit({
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
handler: (req, res) => {
|
||||
res.status(429).json({
|
||||
return res.status(429).json({
|
||||
error: "TooManyRequestsException",
|
||||
errorMessage: "Too many login attempts, please try again later."
|
||||
})
|
||||
@ -29,7 +29,7 @@ router.post("/", limiter, async (req, res) => {
|
||||
})
|
||||
|
||||
logger.log(`User authenticated: ${username}`, ["AUTH", "green"])
|
||||
res.status(200).json(result.response)
|
||||
return res.status(200).json(result.response)
|
||||
} catch (err) {
|
||||
if (err instanceof DefaultError) {
|
||||
throw new YggdrasilError( err.code, err.error || "ForbiddenOperationException", err.message, "Invalid credentials")
|
||||
|
||||
@ -8,7 +8,7 @@ router.post("/", async (req, res) => {
|
||||
const { accessToken, clientToken } = req.body
|
||||
try {
|
||||
await authService.invalidate({ accessToken, clientToken })
|
||||
res.sendStatus(204)
|
||||
return res.sendStatus(204)
|
||||
} catch (err) {
|
||||
if (err instanceof DefaultError) {
|
||||
throw new YggdrasilError(err.code, err.error || "ForbiddenOperationException", err.message, "Invalid token.")
|
||||
|
||||
@ -16,9 +16,7 @@ router.post("/", async (req, res) => {
|
||||
|
||||
const profileName = result.response.selectedProfile ? result.response.selectedProfile.name : "Unknown"
|
||||
logger.log(`Session refreshed for: ${profileName}`, ["AUTH", "green"])
|
||||
|
||||
res.status(200).json(result.response)
|
||||
|
||||
return res.status(200).json(result.response)
|
||||
} catch (err) {
|
||||
if (err instanceof DefaultError) {
|
||||
throw new YggdrasilError(err.code, err.error || "ForbiddenOperationException", err.message, "Invalid token.")
|
||||
|
||||
@ -17,8 +17,7 @@ router.post("/", async (req, res) => {
|
||||
await authService.signout({ uuid: userUuid })
|
||||
|
||||
logger.log(`User signed out globally: ${username}`, ["AUTH", "green"])
|
||||
res.sendStatus(204)
|
||||
|
||||
return res.sendStatus(204)
|
||||
} catch (err) {
|
||||
if (err instanceof DefaultError) {
|
||||
throw new YggdrasilError(err.code === 403 ? 403 : 500, err.error || "ForbiddenOperationException", err.message || "Invalid credentials.", "Invalid credentials.")
|
||||
|
||||
@ -8,7 +8,7 @@ router.post("/", async (req, res) => {
|
||||
const { accessToken, clientToken } = req.body
|
||||
try {
|
||||
await authService.validate({ accessToken, clientToken })
|
||||
res.sendStatus(204)
|
||||
return res.sendStatus(204)
|
||||
} catch (err) {
|
||||
if (err instanceof DefaultError) {
|
||||
throw new YggdrasilError(err.code, err.error || "ForbiddenOperationException", err.message, "Invalid token.")
|
||||
|
||||
26
routes/minecraftservices/minecraft/profile/capes/active.js
Normal file
26
routes/minecraftservices/minecraft/profile/capes/active.js
Normal file
@ -0,0 +1,26 @@
|
||||
const express = require("express")
|
||||
const router = express.Router()
|
||||
const userService = require("../../../../../services/userService")
|
||||
const authService = require("../../../../../services/authService")
|
||||
|
||||
router.delete("/", async (req, res) => {
|
||||
const player = await authService.verifyAccessToken({ accessToken: req.headers.authorization })
|
||||
await userService.hideCape(player.user.uuid)
|
||||
return res.status(200).send()
|
||||
})
|
||||
|
||||
router.put("/", async (req, res) => {
|
||||
const player = await authService.verifyAccessToken(req.headers.authorization)
|
||||
|
||||
await userService.showCape(player.user.uuid, req.body.capeId)
|
||||
const [skinsResult, capesResult] = await Promise.all([userService.getSkins(player.user.uuid), userService.getCapes(player.user.uuid)])
|
||||
|
||||
return res.status(200).json({
|
||||
id: player.user.uuid.replace(/-/g, ""),
|
||||
name: player.user.username,
|
||||
skins: skinsResult.data || [],
|
||||
capes: capesResult.data || []
|
||||
})
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
18
routes/minecraftservices/minecraft/profile/index.js
Normal file
18
routes/minecraftservices/minecraft/profile/index.js
Normal file
@ -0,0 +1,18 @@
|
||||
const express = require("express")
|
||||
const router = express.Router()
|
||||
const userService = require("../../../../services/userService")
|
||||
const authService = require("../../../../services/authService")
|
||||
|
||||
router.get("/", async (req, res) => {
|
||||
const player = await authService.verifyAccessToken({ accessToken: req.headers.authorization.replace("Bearer ", "") })
|
||||
const [skinsResult, capesResult] = await Promise.all([userService.getSkins(player.user.uuid), userService.getCapes(player.user.uuid)])
|
||||
|
||||
return res.status(200).json({
|
||||
id: player.uuid.replace(/-/g, ""),
|
||||
name: player.user.username,
|
||||
skins: skinsResult.data || [],
|
||||
capes: capesResult.data || []
|
||||
})
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@ -0,0 +1,10 @@
|
||||
const express = require("express")
|
||||
const userService = require("../../../../../../services/userService")
|
||||
const router = express.Router()
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
const profiles = await userService.bulkLookup(req.body)
|
||||
return res.status(200).json(profiles)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@ -0,0 +1,27 @@
|
||||
const express = require("express")
|
||||
const utils = require("../../../../../../modules/utils")
|
||||
const userService = require("../../../../../../services/userService")
|
||||
const authService = require("../../../../../../services/authService")
|
||||
const { ServiceError } = require("../../../../../../errors/errors")
|
||||
const router = express.Router({ mergeParams: true })
|
||||
|
||||
router.get("", async (req, res) => {
|
||||
const profile = await userService.getLegacyProfile(req.params.username)
|
||||
const isUsernameOK = await authService.checkUsernameAvailability(newName)
|
||||
const at = req.query.at
|
||||
if (at != undefined && utils.isTrueFromDotEnv("SUPPORT_UUID_TO_NAME_HISTORY")) {
|
||||
const history = await userService.getNameUUIDs(parseInt(at))
|
||||
return res.status(history.code).json(history.data)
|
||||
} else {
|
||||
throw new ServiceError(400, req.originalUrl, "IllegalArgumentException", "Invalid timestamp.")
|
||||
}
|
||||
if (isUsernameOK.status != "AVAILABLE") {
|
||||
throw new ServiceError(400, req.originalUrl, "CONSTRAINT_VIOLATION", "Invalid username.")
|
||||
}
|
||||
if (!profile) {
|
||||
return res.status(204).send()
|
||||
}
|
||||
return res.status(200).json(profile)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
44
routes/minecraftservices/minecraft/profile/name/[name].js
Normal file
44
routes/minecraftservices/minecraft/profile/name/[name].js
Normal file
@ -0,0 +1,44 @@
|
||||
const express = require("express")
|
||||
const authService = require("../../../../../services/authService")
|
||||
const { DefaultError, ServiceError } = require("../../../../../errors/errors")
|
||||
const router = express.Router({ mergeParams: true })
|
||||
|
||||
router.get("/available", async (req, res) => {
|
||||
try {
|
||||
await authService.verifyAccessToken({ accessToken: req.headers.authorization.replace("Bearer", "").trim() })
|
||||
const isAvailable = await authService.checkUsernameAvailability(req.params.name)
|
||||
return res.status(200).json({ status: isAvailable.status })
|
||||
} catch (error) {
|
||||
if (error instanceof DefaultError) {
|
||||
throw new ServiceError(error.code, req.originalUrl, null, null, null)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
router.put("/", async (req, res) => {
|
||||
try {
|
||||
const player = await authService.verifyAccessToken({ accessToken: req.headers.authorization.replace("Bearer", "").trim() })
|
||||
const newName = req.params.name
|
||||
|
||||
await userService.changeUsername(player.uuid, newName)
|
||||
|
||||
const skinsResult = await userService.getSkins({ uuid: player.uuid })
|
||||
const capesResult = await userService.getCapes({ uuid: player.uuid })
|
||||
|
||||
return res.status(200).json({
|
||||
id: player.uuid.replace(/-/g, ""),
|
||||
name: newName,
|
||||
skins: skinsResult.data || [],
|
||||
capes: capesResult.data || []
|
||||
})
|
||||
|
||||
} catch (err) {
|
||||
const mcStatus = err.code === 409 ? "DUPLICATE" : (err.code === 400 || err.code === 403) ? "NOT_ALLOWED" : null
|
||||
const finalCode = (mcStatus === "DUPLICATE") ? 403 : (err.code || 500)
|
||||
const errorType = mcStatus ? "FORBIDDEN" : (err.error || "Internal Server Error")
|
||||
throw new ServiceError(finalCode, req.originalUrl, errorType, err.message, mcStatus ? { status: mcStatus } : null)
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
12
routes/minecraftservices/minecraft/profile/namechange.js
Normal file
12
routes/minecraftservices/minecraft/profile/namechange.js
Normal file
@ -0,0 +1,12 @@
|
||||
const express = require("express")
|
||||
const router = express.Router()
|
||||
const userService = require("../../../../services/userService")
|
||||
const authService = require("../../../../services/authService")
|
||||
|
||||
router.put("/", async (req, res) => {
|
||||
const player = await authService.verifyAccessToken({ accessToken: req.headers.authorization.replace("Bearer ", "") })
|
||||
const nameChangeInformation = await userService.getPlayerNameChangeStatus(player.user.uuid)
|
||||
return res.status(nameChangeInformation.code).json(nameChangeInformation.data)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
12
routes/minecraftservices/minecraft/profile/skins/active.js
Normal file
12
routes/minecraftservices/minecraft/profile/skins/active.js
Normal file
@ -0,0 +1,12 @@
|
||||
const express = require("express")
|
||||
const router = express.Router()
|
||||
const userService = require("../../../../../services/userService")
|
||||
const authService = require("../../../../../services/authService")
|
||||
|
||||
router.delete("/", async (req, res) => {
|
||||
const player = await authService.verifyAccessToken({ accessToken: req.headers.authorization })
|
||||
await userService.resetSkin(player.user.uuid)
|
||||
return res.status(200).send()
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
34
routes/minecraftservices/player/attributes.js
Normal file
34
routes/minecraftservices/player/attributes.js
Normal file
@ -0,0 +1,34 @@
|
||||
const express = require("express")
|
||||
const userService = require("../../../services/userService")
|
||||
const authService = require("../../../services/authService")
|
||||
const router = express.Router()
|
||||
|
||||
router.get("", async (req, res) => {
|
||||
const player = await authService.verifyAccessToken({ accessToken: req.headers.authorization.replace("Bearer", "").trim() })
|
||||
|
||||
const [preferencesResult, privilegesResult, banStatus] = await Promise.all([userService.getPreferences(player.user.uuid), userService.getPrivileges(player.user.uuid), userService.getPlayerBanStatus(player.user.uuid)])
|
||||
return res.status(200).json({
|
||||
privileges: privilegesResult.data,
|
||||
...preferencesResult.data,
|
||||
banStatus: {
|
||||
bannedScopes: banStatus.isBanned ? { MULTIPLAYER: banStatus.activeBan } : {}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
router.post("", async (req, res) => {
|
||||
const player = await authService.verifyAccessToken({ accessToken: req.headers.authorization.replace("Bearer", "").trim() })
|
||||
|
||||
await userService.updatePreferences(player.user.uuid, req.body)
|
||||
|
||||
const [preferencesResult, privilegesResult, banStatus] = await Promise.all([userService.getPreferences(player.user.uuid), userService.getPrivileges(player.user.uuid), userService.getPlayerBanStatus(player.user.uuid)])
|
||||
return res.status(200).json({
|
||||
privileges: privilegesResult.data,
|
||||
...preferencesResult.data,
|
||||
banStatus: {
|
||||
bannedScopes: banStatus.isBanned ? { MULTIPLAYER: banStatus.activeBan } : {}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
12
routes/minecraftservices/player/certificates.js
Normal file
12
routes/minecraftservices/player/certificates.js
Normal file
@ -0,0 +1,12 @@
|
||||
const express = require("express")
|
||||
const userService = require("../../../services/userService")
|
||||
const authService = require("../../../services/authService")
|
||||
const router = express.Router()
|
||||
|
||||
router.post("", async (req, res) => {
|
||||
const player = await authService.verifyAccessToken({ accessToken: req.headers.authorization.replace("Bearer", "").trim() })
|
||||
const certificates = await userService.fetchOrGenerateCertificate(player.user.uuid)
|
||||
return res.status(200).json(certificates.data)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
39
routes/minecraftservices/privacy/blocklist.js
Normal file
39
routes/minecraftservices/privacy/blocklist.js
Normal file
@ -0,0 +1,39 @@
|
||||
const express = require("express")
|
||||
const router = express.Router()
|
||||
const utils = require("../../../modules/utils") // Pour addDashesToUUID
|
||||
const authService = require("../../../services/authService")
|
||||
const userService = require("../../../services/userService")
|
||||
|
||||
router.get("/", async (req, res, next) => {
|
||||
const user = await authService.verifyUserFromHeader(req.headers.authorization)
|
||||
const result = await userService.getBlockedUuids(user.uuid)
|
||||
return res.status(200).json({
|
||||
blockedProfiles: result.data || []
|
||||
})
|
||||
})
|
||||
|
||||
router.put("/:uuid", async (req, res, next) => {
|
||||
const user = await authService.verifyUserFromHeader(req.headers.authorization)
|
||||
const targetUuid = utils.addDashesToUUID(req.params.uuid)
|
||||
|
||||
await userService.blockPlayer(user.uuid, targetUuid)
|
||||
|
||||
const result = await userService.getBlockedUuids(user.uuid)
|
||||
return res.status(200).json({
|
||||
blockedProfiles: result.data || []
|
||||
})
|
||||
})
|
||||
|
||||
router.delete("/:uuid", async (req, res, next) => {
|
||||
const user = await authService.verifyUserFromHeader(req.headers.authorization)
|
||||
const targetUuid = utils.addDashesToUUID(req.params.uuid)
|
||||
|
||||
await userService.unblockPlayer(user.uuid, targetUuid)
|
||||
|
||||
const result = await userService.getBlockedUuids(user.uuid)
|
||||
return res.status(200).json({
|
||||
blockedProfiles: result.data || []
|
||||
})
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
34
routes/minecraftservices/privileges.js
Normal file
34
routes/minecraftservices/privileges.js
Normal file
@ -0,0 +1,34 @@
|
||||
const express = require("express")
|
||||
const userService = require("../../services/userService")
|
||||
const authService = require("../../services/authService")
|
||||
const router = express.Router()
|
||||
|
||||
router.get("", async (req, res) => {
|
||||
const player = await authService.verifyAccessToken({ accessToken: req.headers.authorization.replace("Bearer", "").trim() })
|
||||
|
||||
const [preferencesResult, privilegesResult, banStatus] = await Promise.all([userService.getPreferences(player.user.uuid), userService.getPrivileges(player.user.uuid), userService.getPlayerBanStatus(player.user.uuid)])
|
||||
return res.status(200).json({
|
||||
privileges: privilegesResult.data,
|
||||
...preferencesResult.data,
|
||||
banStatus: {
|
||||
bannedScopes: banStatus.isBanned ? { MULTIPLAYER: banStatus.activeBan } : {}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
router.post("", async (req, res) => {
|
||||
const player = await authService.verifyAccessToken({ accessToken: req.headers.authorization.replace("Bearer", "").trim() })
|
||||
|
||||
await userService.updatePreferences(player.user.uuid, req.body)
|
||||
|
||||
const [preferencesResult, privilegesResult, banStatus] = await Promise.all([userService.getPreferences(player.user.uuid), userService.getPrivileges(player.user.uuid), userService.getPlayerBanStatus(player.user.uuid)])
|
||||
return res.status(200).json({
|
||||
privileges: privilegesResult.data,
|
||||
...preferencesResult.data,
|
||||
banStatus: {
|
||||
bannedScopes: banStatus.isBanned ? { MULTIPLAYER: banStatus.activeBan } : {}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@ -2,7 +2,7 @@ const express = require("express")
|
||||
const router = express.Router()
|
||||
|
||||
router.get("/giftcode", (req, res) => {
|
||||
res.status(404).json({
|
||||
return res.status(404).json({
|
||||
path: "/productvoucher/giftcode",
|
||||
errorType: "NOT_FOUND",
|
||||
error: "NOT_FOUND",
|
||||
|
||||
@ -12,7 +12,7 @@ router.get("", (req, res) => {
|
||||
}
|
||||
]
|
||||
}
|
||||
res.status(200).json(publicKeys)
|
||||
return res.status(200).json(publicKeys)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
10
routes/mojangapi/minecraft/profile/lookup/bulk/byname.js
Normal file
10
routes/mojangapi/minecraft/profile/lookup/bulk/byname.js
Normal file
@ -0,0 +1,10 @@
|
||||
const express = require("express")
|
||||
const userService = require("../../../../../../services/userService")
|
||||
const router = express.Router()
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
const profiles = await userService.bulkLookup(req.body)
|
||||
return res.status(200).json(profiles)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
28
routes/mojangapi/profile/lookup/name/[username].js
Normal file
28
routes/mojangapi/profile/lookup/name/[username].js
Normal file
@ -0,0 +1,28 @@
|
||||
const express = require("express")
|
||||
const utils = require("../../../../../modules/utils")
|
||||
const userService = require("../../../../../services/userService")
|
||||
const authService = require("../../../../../services/authService")
|
||||
const { ServiceError } = require("../../../../../errors/errors")
|
||||
const router = express.Router({ mergeParams: true })
|
||||
|
||||
|
||||
router.get("", async (req, res) => {
|
||||
const profile = await userService.getLegacyProfile(req.params.username)
|
||||
const isUsernameOK = await authService.checkUsernameAvailability(newName)
|
||||
const at = req.query.at
|
||||
if (at != undefined && utils.isTrueFromDotEnv("SUPPORT_UUID_TO_NAME_HISTORY")) {
|
||||
const history = await userService.getNameUUIDs(parseInt(at))
|
||||
return res.status(history.code).json(history.data)
|
||||
} else {
|
||||
throw new ServiceError(400, req.originalUrl, "IllegalArgumentException", "Invalid timestamp.")
|
||||
}
|
||||
if (isUsernameOK.status != "AVAILABLE") {
|
||||
throw new ServiceError(400, req.originalUrl, "CONSTRAINT_VIOLATION", "Invalid username.")
|
||||
}
|
||||
if (!profile) {
|
||||
return res.status(204).send()
|
||||
}
|
||||
return res.status(200).json(profile)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
10
routes/mojangapi/profiles/minecraft.js
Normal file
10
routes/mojangapi/profiles/minecraft.js
Normal file
@ -0,0 +1,10 @@
|
||||
const express = require("express")
|
||||
const userService = require("../../../services/userService")
|
||||
const router = express.Router()
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
const profiles = await userService.bulkLookup(req.body)
|
||||
return res.status(200).json(profiles)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
15
routes/mojangapi/user/profiles/[uuid]/names.js
Normal file
15
routes/mojangapi/user/profiles/[uuid]/names.js
Normal file
@ -0,0 +1,15 @@
|
||||
const express = require("express")
|
||||
const utils = require("../../../../../modules/utils")
|
||||
const userService = require("../../../../../services/userService")
|
||||
const { ServiceError } = require("../../../../../errors/errors")
|
||||
const router = express.Router({ mergeParams: true })
|
||||
|
||||
router.get("/", async (req, res) => {
|
||||
if (!utils.isTrueFromDotEnv("SUPPORT_UUID_TO_NAME_HISTORY")) {
|
||||
throw new ServiceError(404, req.originalUrl, "Not found", null, null)
|
||||
}
|
||||
const history = await userService.getPlayerUsernamesHistory(req.params.uuid)
|
||||
return res.status(200).json(history)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
27
routes/mojangapi/users/profiles/minecraft/[username].js
Normal file
27
routes/mojangapi/users/profiles/minecraft/[username].js
Normal file
@ -0,0 +1,27 @@
|
||||
const express = require("express")
|
||||
const utils = require("../../../../../modules/utils")
|
||||
const userService = require("../../../../../services/userService")
|
||||
const authService = require("../../../../../services/authService")
|
||||
const { ServiceError } = require("../../../../../errors/errors")
|
||||
const router = express.Router({ mergeParams: true })
|
||||
|
||||
router.get("", async (req, res) => {
|
||||
const profile = await userService.getLegacyProfile(req.params.username)
|
||||
const isUsernameOK = await authService.checkUsernameAvailability(newName)
|
||||
const at = req.query.at
|
||||
if (at != undefined && utils.isTrueFromDotEnv("SUPPORT_UUID_TO_NAME_HISTORY")) {
|
||||
const history = await userService.getNameUUIDs(parseInt(at))
|
||||
return res.status(history.code).json(history.data)
|
||||
} else {
|
||||
throw new ServiceError(400, req.originalUrl, "IllegalArgumentException", "Invalid timestamp.")
|
||||
}
|
||||
if (isUsernameOK.status != "AVAILABLE") {
|
||||
throw new ServiceError(400, req.originalUrl, "CONSTRAINT_VIOLATION", "Invalid username.")
|
||||
}
|
||||
if (!profile) {
|
||||
return res.status(204).send()
|
||||
}
|
||||
return res.status(200).json(profile)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@ -128,8 +128,10 @@ for (const route of routes) {
|
||||
for (const layer of router.stack) {
|
||||
if (layer.route && layer.route.methods) {
|
||||
const method = Object.keys(layer.route.methods).join(", ").toUpperCase()
|
||||
const subPath = routePath === "/" ? "" : routePath
|
||||
logger.log(`${method.cyan} ${subPath.cyan.bold} route registered`, ["WEB", "yellow"])
|
||||
const innerPath = layer.route.path === "/" ? "" : layer.route.path
|
||||
const mountPrefix = routePath === "/" ? "" : routePath
|
||||
const fullDisplayPath = mountPrefix + innerPath
|
||||
logger.log(`${method.cyan} ${fullDisplayPath.cyan.bold} route registered`, ["WEB", "yellow"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,13 +9,15 @@ const { DefaultError } = require("../errors/errors")
|
||||
|
||||
const keys = certsManager.getKeys()
|
||||
const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/
|
||||
const usernameRegex = /^[a-zA-Z0-9_]{3,16}$/
|
||||
|
||||
async function registerUser({ username, password, email, registrationCountry, preferredLanguage, clientIp }) {
|
||||
try {
|
||||
await authRepository.getUser(username)
|
||||
const availability = await checkUsernameAvailability(username)
|
||||
if (availability.status === "DUPLICATE") {
|
||||
throw new DefaultError(409, "Username taken.", "ForbiddenOperationException")
|
||||
} catch (error) {
|
||||
if (error.code !== 404) throw error
|
||||
}
|
||||
if (availability.status === "NOT_ALLOWED") {
|
||||
throw new DefaultError(400, availability.message || "Username invalid.", "InvalidUsernameException")
|
||||
}
|
||||
|
||||
if (email) {
|
||||
@ -225,6 +227,56 @@ async function verifyAccessToken({ accessToken }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function checkUsernameAvailability(username) {
|
||||
if (!usernameRegex.test(username)) {
|
||||
return {
|
||||
code: 200,
|
||||
status: "NOT_ALLOWED",
|
||||
message: "Invalid characters or length."
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await authRepository.getUser(username)
|
||||
return {
|
||||
code: 200,
|
||||
status: "DUPLICATE",
|
||||
message: "Username is already in use."
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== 404) throw error
|
||||
}
|
||||
|
||||
const blocklist = await authRepository.getUsernamesRules()
|
||||
const normalizedUsername = username.toLowerCase()
|
||||
|
||||
for (const entry of blocklist) {
|
||||
if (entry.type === "literal") {
|
||||
if (normalizedUsername === entry.value) {
|
||||
return {
|
||||
code: 200,
|
||||
status: "NOT_ALLOWED",
|
||||
message: "Username is reserved."
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (entry.type === "regex") {
|
||||
if (entry.pattern.test(username)) {
|
||||
return {
|
||||
code: 200,
|
||||
status: "NOT_ALLOWED",
|
||||
message: "Username contains forbidden words."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
status: "AVAILABLE"
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
signout,
|
||||
validate,
|
||||
@ -233,4 +285,5 @@ module.exports = {
|
||||
authenticate,
|
||||
refreshToken,
|
||||
verifyAccessToken,
|
||||
checkUsernameAvailability
|
||||
}
|
||||
435
services/userService.js
Normal file
435
services/userService.js
Normal file
@ -0,0 +1,435 @@
|
||||
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,
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user