Compare commits
88 Commits
a3eb5ee70c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e242d78864 | |||
| 33d54e655a | |||
| 39a566a2f5 | |||
| 66b3268f8e | |||
| 9db4d62d78 | |||
| 492f012519 | |||
| c5ef3d8181 | |||
| bf17261fdf | |||
| bb7b2328ab | |||
| b6ad724602 | |||
| dbcb436c9f | |||
| aea0b7b016 | |||
| 11f930c3d8 | |||
| b82c06165a | |||
| 48ea9f708b | |||
| 3f64c2c897 | |||
| c1dbfc58aa | |||
| 65f2f26325 | |||
| f9270c1f9c | |||
| 6f3231aee7 | |||
| 4a0fcf65f3 | |||
| cc8e581cbe | |||
| b4ae5fa4d9 | |||
| 7018e8c497 | |||
| 8e0a1ab673 | |||
| e089957db7 | |||
| c8812c5153 | |||
| 66db52e7c8 | |||
| 21fd655a1f | |||
| 074a5dc04b | |||
| 6cf822e603 | |||
| 535c21b971 | |||
| 44a9ff12b1 | |||
| f5df40f264 | |||
| caa318f2c7 | |||
| d6ac0fd8b4 | |||
| 6666025726 | |||
| 57aeb47ed1 | |||
| 4975f7e191 | |||
| c96e728228 | |||
| 308c3b5479 | |||
| 086468405a | |||
| 0b8ab9f194 | |||
| 01e0b94d35 | |||
| 0049ae8ec6 | |||
| 9469822ef9 | |||
| 99598f2b7a | |||
| 85ba96cb6f | |||
| ac6eca3f31 | |||
| 53e58bdb30 | |||
| adddbadbf3 | |||
| b15595ecb1 | |||
| aacfca136c | |||
| 9ac904ba08 | |||
| fab7066ee3 | |||
| 86490ebd2d | |||
| dd04b2014a | |||
| 83d46425b3 | |||
| 7eac539598 | |||
| 9a6b119d37 | |||
| d8318f874f | |||
| 487bd08141 | |||
| 30a0ac3927 | |||
| 21cf0f49e6 | |||
| a31be145cc | |||
| 0e0f176e50 | |||
| 86349bcf4f | |||
| d590ecce6d | |||
| 617e60cf75 | |||
| f1a482c58f | |||
| 69ad2c8f83 | |||
| 71627c7041 | |||
| 88f8ee57e1 | |||
| bfc98243d0 | |||
| c6afafca2a | |||
| b6b7cf7fe0 | |||
| ffa23e35b0 | |||
| c5b6f6c107 | |||
| 5b81f57adb | |||
| bfad2a39c1 | |||
| 439094013d | |||
| da8ab9d488 | |||
| 36a9a0b193 | |||
| 9b36c85974 | |||
| 80fb6c6cd4 | |||
| 947192d997 | |||
| 3cd42103e5 | |||
| e8f58e63cd |
24
.env.example
24
.env.example
@@ -1,12 +1,14 @@
|
|||||||
#Config
|
#Config
|
||||||
WEB_PORT=3000
|
|
||||||
IS_PROD=FALSE
|
IS_PROD=FALSE
|
||||||
|
WEB_PORT=8877
|
||||||
|
SUPPORT_REGISTER=TRUE
|
||||||
|
API_ADMIN_SECRET="oJs8XtVbgY485HTvFNrM@#"
|
||||||
|
|
||||||
#MariaDB
|
#MariaDB
|
||||||
DATABASE_HOST="host"
|
DATABASE_HOST="azures.fr"
|
||||||
DATABASE_USER="username"
|
DATABASE_USER="azures04"
|
||||||
DATABASE_PASSWORD="Password"
|
DATABASE_PASSWORD="0YkeEkLLjBb@#"
|
||||||
DATABASE_NAME="database"
|
DATABASE_NAME="modun"
|
||||||
|
|
||||||
#Mojang API
|
#Mojang API
|
||||||
SUPPORT_UUID_TO_NAME_HISTORY=TRUE
|
SUPPORT_UUID_TO_NAME_HISTORY=TRUE
|
||||||
@@ -18,7 +20,15 @@ SUPPORT_MOJANG_FALLBACK=FALSE #[no_mojang_namespace]
|
|||||||
SUPPORT_MOJANG_TELEMETRY_BLOCKER=TRUE #[enable_mojang_anti_features]
|
SUPPORT_MOJANG_TELEMETRY_BLOCKER=TRUE #[enable_mojang_anti_features]
|
||||||
SUPPORT_PROFILE_KEY=TRUE #[enable_profile_key]
|
SUPPORT_PROFILE_KEY=TRUE #[enable_profile_key]
|
||||||
SUPPORT_ONLY_DEFAULT_USERNAME=true #[username_check]
|
SUPPORT_ONLY_DEFAULT_USERNAME=true #[username_check]
|
||||||
SUPPORT_REGISTER=TRUE
|
|
||||||
REGISTER_ENDPOINT="/register"
|
REGISTER_ENDPOINT="/register"
|
||||||
HOMEPAGE_URL=
|
HOMEPAGE_URL=
|
||||||
SERVER_NAME="Yggdrasil"
|
SERVER_NAME="Yggdrasil"
|
||||||
|
|
||||||
|
#Skin API
|
||||||
|
TEXTURES_ENDPOINTS=https://yggdrasil.azures.fr/textures
|
||||||
|
|
||||||
|
#Discord OAuth2
|
||||||
|
DISCORD_CLIENT_ID="DISCORD_CLIENT_ID"
|
||||||
|
DISCORD_CLIENT_SECRET="SUPER_SECRET"
|
||||||
|
DISCORD_ASSOCIATION_REDIRECT_URL="REMOTE_URL"
|
||||||
|
DISCORD_LOGIN_REDIRECT_URL="LOCAL_URL_FOR_LAUNCHERS"
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -133,4 +133,5 @@ dist
|
|||||||
#Modun Globals
|
#Modun Globals
|
||||||
logs
|
logs
|
||||||
tests
|
tests
|
||||||
data/keys
|
data/keys
|
||||||
|
tests
|
||||||
6
data/static/iziToast.min.js
vendored
Normal file
6
data/static/iziToast.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
43
data/static/register.html
Normal file
43
data/static/register.html
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="
|
||||||
|
default-src 'self';
|
||||||
|
script-src 'self' https://cdnjs.cloudflare.com 'unsafe-inline';
|
||||||
|
style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com;
|
||||||
|
font-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com;
|
||||||
|
connect-src 'self' https://yggdrasil.azures.fr;
|
||||||
|
">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/hung1001/font-awesome-pro@4cac1a6/css/all.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/izitoast/1.4.0/css/iziToast.min.css">
|
||||||
|
<title>Registration</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>
|
||||||
|
<i class="fad fa-shield-alt"></i>
|
||||||
|
Lentia – Yggdrasil
|
||||||
|
</h1>
|
||||||
|
<h3>Page d'inscription</h3>
|
||||||
|
<hr>
|
||||||
|
<div>
|
||||||
|
<input type="email" placeholder="Adresse mail" id="email">
|
||||||
|
<input type="text" placeholder="Nom d'utilisateur" id="username">
|
||||||
|
<input type="password" placeholder="Mot de passe" id="password">
|
||||||
|
<button id="register">
|
||||||
|
Créer mon compte !
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="./iziToast.min.js"></script>
|
||||||
|
<script src="./register.js"></script>
|
||||||
|
<style>
|
||||||
|
div > input {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
29
data/static/register.js
Normal file
29
data/static/register.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
const button = document.querySelector("#register")
|
||||||
|
|
||||||
|
async function register(email, username, password) {
|
||||||
|
const response = await fetch("https://yggdrasil.azures.fr/register", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email,
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const json = await response.json()
|
||||||
|
if (json.code != 200) {
|
||||||
|
return iziToast.error({ title: json.error || "Erreur", message: json.message || "Erreur inconnue" })
|
||||||
|
} else {
|
||||||
|
console.log(json)
|
||||||
|
return iziToast.success({ title: "Succès", message: json.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
const email = document.querySelector("#email").value
|
||||||
|
const username = document.querySelector("#username").value
|
||||||
|
const password = document.querySelector("#password").value
|
||||||
|
register(email, username, password)
|
||||||
|
})
|
||||||
25
data/static/success.html
Normal file
25
data/static/success.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/hung1001/font-awesome-pro@4cac1a6/css/all.css">
|
||||||
|
<title>Registration</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>
|
||||||
|
<i class="fad fa-shield-alt"></i>
|
||||||
|
Lentia – Yggdrasil
|
||||||
|
</h1>
|
||||||
|
<h3>Authentification réussie</h3>
|
||||||
|
<hr>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
Vous pouvez dès à présent fermer cette page et retourner au launcher
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
data/textures/alex.png
Normal file
BIN
data/textures/alex.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
BIN
data/textures/steve.png
Normal file
BIN
data/textures/steve.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -45,7 +45,7 @@ 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,
|
||||||
UNIQUE KEY unique_property (uuid, name),
|
UNIQUE KEY uniqueProperty (uuid, name),
|
||||||
FOREIGN KEY (uuid) REFERENCES players(uuid) ON DELETE CASCADE
|
FOREIGN KEY (uuid) REFERENCES players(uuid) ON DELETE CASCADE
|
||||||
)
|
)
|
||||||
`)
|
`)
|
||||||
@@ -142,7 +142,7 @@ async function setupDatabase() {
|
|||||||
await conn.query(`
|
await conn.query(`
|
||||||
CREATE TABLE IF NOT EXISTS banReasons (
|
CREATE TABLE IF NOT EXISTS banReasons (
|
||||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||||
reason_key VARCHAR(512) UNIQUE NOT NULL
|
reasonKey VARCHAR(512) UNIQUE NOT NULL
|
||||||
)
|
)
|
||||||
`)
|
`)
|
||||||
logger.log(`${"banReasons".bold} table ready`, ["MariaDB", "yellow"])
|
logger.log(`${"banReasons".bold} table ready`, ["MariaDB", "yellow"])
|
||||||
@@ -224,13 +224,13 @@ async function setupDatabase() {
|
|||||||
|
|
||||||
await conn.query(`
|
await conn.query(`
|
||||||
CREATE TABLE IF NOT EXISTS playersCapes (
|
CREATE TABLE IF NOT EXISTS playersCapes (
|
||||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
playerUuid VARCHAR(36) NOT NULL,
|
playerUuid VARCHAR(36) NOT NULL,
|
||||||
assetHash VARCHAR(64) NOT NULL,
|
assetHash VARCHAR(64) NOT NULL,
|
||||||
isSelected TINYINT(1) DEFAULT 0,
|
isSelected TINYINT(1) DEFAULT 0,
|
||||||
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
|
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (playerUuid, assetHash),
|
||||||
FOREIGN KEY (playerUuid) REFERENCES players(uuid) ON DELETE CASCADE,
|
FOREIGN KEY (playerUuid) REFERENCES players(uuid) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (assetHash) REFERENCES textures(hash)
|
FOREIGN KEY (assetHash) REFERENCES textures(hash) ON DELETE CASCADE
|
||||||
)
|
)
|
||||||
`)
|
`)
|
||||||
logger.log(`${"playersCapes".bold} table ready`, ["MariaDB", "yellow"])
|
logger.log(`${"playersCapes".bold} table ready`, ["MariaDB", "yellow"])
|
||||||
@@ -242,36 +242,8 @@ async function setupDatabase() {
|
|||||||
logger.log(`defaults skins (steve, alex) ready`, ["MariaDB", "yellow"])
|
logger.log(`defaults skins (steve, alex) ready`, ["MariaDB", "yellow"])
|
||||||
|
|
||||||
await conn.query(`DROP TRIGGER IF EXISTS unique_active_skin`)
|
await conn.query(`DROP TRIGGER IF EXISTS unique_active_skin`)
|
||||||
await conn.query(`
|
|
||||||
CREATE TRIGGER unique_active_skin
|
|
||||||
AFTER UPDATE ON playersSkins
|
|
||||||
FOR EACH ROW
|
|
||||||
BEGIN
|
|
||||||
IF NEW.isSelected = 1 THEN
|
|
||||||
UPDATE playersSkins
|
|
||||||
SET isSelected = 0
|
|
||||||
WHERE playerUuid = NEW.playerUuid
|
|
||||||
AND assetHash != NEW.assetHash;
|
|
||||||
END IF;
|
|
||||||
END;
|
|
||||||
`)
|
|
||||||
logger.log(`${"unique_active_skin".bold} trigger ready`, ["MariaDB", "yellow"])
|
|
||||||
|
|
||||||
await conn.query(`DROP TRIGGER IF EXISTS unique_active_cape`)
|
await conn.query(`DROP TRIGGER IF EXISTS unique_active_cape`)
|
||||||
await conn.query(`
|
|
||||||
CREATE TRIGGER unique_active_cape
|
|
||||||
AFTER UPDATE ON playersCapes
|
|
||||||
FOR EACH ROW
|
|
||||||
BEGIN
|
|
||||||
IF NEW.isSelected = 1 THEN
|
|
||||||
UPDATE playersCapes
|
|
||||||
SET isSelected = 0
|
|
||||||
WHERE playerUuid = NEW.playerUuid
|
|
||||||
AND id != NEW.id;
|
|
||||||
END IF;
|
|
||||||
END;
|
|
||||||
`)
|
|
||||||
logger.log(`${"unique_active_cape".bold} trigger ready`, ["MariaDB", "yellow"])
|
|
||||||
|
|
||||||
await conn.query(`DROP TRIGGER IF EXISTS auto_assign_random_default_skin`)
|
await conn.query(`DROP TRIGGER IF EXISTS auto_assign_random_default_skin`)
|
||||||
await conn.query(`
|
await conn.query(`
|
||||||
@@ -335,8 +307,20 @@ 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;`)
|
try {
|
||||||
logger.log("MariaDB Event Scheduler enabled.", ["MariaDB", "yellow"])
|
await conn.query(`SET GLOBAL event_scheduler = ON;`)
|
||||||
|
logger.log("MySQL Event Scheduler enabled.", ["MySQL", "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`, ["MySQL", "yellow"])
|
||||||
|
} catch (e) {
|
||||||
|
logger.log("Warning: Could not enable Event Scheduler (permission issue?). Skipping event creation.", ["MySQL", "red"])
|
||||||
|
}
|
||||||
|
|
||||||
await conn.query(`
|
await conn.query(`
|
||||||
CREATE EVENT IF NOT EXISTS clean_expired_certificates
|
CREATE EVENT IF NOT EXISTS clean_expired_certificates
|
||||||
@@ -345,6 +329,47 @@ async function setupDatabase() {
|
|||||||
DELETE FROM playerCertificates WHERE expiresAt < NOW();
|
DELETE FROM playerCertificates WHERE expiresAt < NOW();
|
||||||
`)
|
`)
|
||||||
logger.log(`${"clean_expired_certificates".bold} event ready`, ["MariaDB", "yellow"])
|
logger.log(`${"clean_expired_certificates".bold} event ready`, ["MariaDB", "yellow"])
|
||||||
|
|
||||||
|
await conn.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS apiAdministrators (
|
||||||
|
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
username VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
password TEXT NOT NULL,
|
||||||
|
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
logger.log(`${"apiAdministrators".bold} table ready`, ["MariaDB", "yellow"])
|
||||||
|
|
||||||
|
await conn.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS apiAdministratorsPermissionsList (
|
||||||
|
permissionKey VARCHAR(64) PRIMARY KEY
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
logger.log(`${"apiAdministratorsPermissionsList".bold} table ready`, ["MariaDB", "yellow"])
|
||||||
|
|
||||||
|
await conn.query(`INSERT IGNORE INTO apiAdministratorsPermissionsList(permissionKey) VALUES ("RESET_PLAYER_SKIN"), ("GRANT_PLAYER_CAPE"), ("REMOVE_PLAYER_CAPE"), ("CHANGE_PLAYER_USERNAME"), ("CHANGE_PLAYER_PASSWORD"), ("UPLOAD_CAPE"), ("DELETE_CAPES"), ("PLAYER_BAN"), ("PLAYER_BAN_HISTORY"), ("PLAYER_BAN_STATUS"), ("PLAYER_ACTIONS_LIST"), ("PLAYER_UNBAN"), ("REGISTER_USER")`)
|
||||||
|
logger.log(`${"apiAdministratorsPermissionsList".bold} permissions ready`, ["MariaDB", "yellow"])
|
||||||
|
|
||||||
|
await conn.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS apiAdministratorsPermissions (
|
||||||
|
administratorId INTEGER NOT NULL,
|
||||||
|
permissionKey VARCHAR(64) NOT NULL,
|
||||||
|
PRIMARY KEY (administratorId, permissionKey),
|
||||||
|
FOREIGN KEY (administratorId) REFERENCES apiAdministrators(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (permissionkey) REFERENCES apiAdministratorsPermissionsList(permissionKey) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
logger.log(`${"apiAdministratorsPermissions".bold} table ready`, ["MariaDB", "yellow"])
|
||||||
|
|
||||||
|
await conn.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS oaauth2LinkAttempts (
|
||||||
|
OAuth2LinkId VARCHAR(255) NOT NULL,
|
||||||
|
playerUuid VARCHAR(255) NOT NULL,
|
||||||
|
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (playerUuid) REFERENCES players(uuid) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
logger.log(`${"oaauth2LinkAttempts".bold} table ready`, ["MariaDB", "yellow"])
|
||||||
|
|
||||||
logger.log("MariaDB database successfully initialised!", ["MariaDB", "yellow"])
|
logger.log("MariaDB database successfully initialised!", ["MariaDB", "yellow"])
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
const fs = require("node:fs")
|
const fs = require("node:fs")
|
||||||
const path = require("node:path")
|
const path = require("node:path")
|
||||||
const utils = require("./utils")
|
|
||||||
require("colors")
|
require("colors")
|
||||||
require("dotenv").config({
|
require("dotenv").config({
|
||||||
quiet: true
|
quiet: true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function isTrueFromDotEnv(key) {
|
||||||
|
return (process.env[key] || "").trim().toLowerCase() === "true"
|
||||||
|
}
|
||||||
|
|
||||||
function cleanup($stream) {
|
function cleanup($stream) {
|
||||||
if (!$stream.destroyed) {
|
if (!$stream.destroyed) {
|
||||||
$stream.end()
|
$stream.end()
|
||||||
@@ -41,7 +44,7 @@ function write($stream, level, color, content, extraLabels = []) {
|
|||||||
|
|
||||||
function createLogger(root) {
|
function createLogger(root) {
|
||||||
// eslint-disable-next-line no-useless-escape
|
// eslint-disable-next-line no-useless-escape
|
||||||
const fileName = utils.isTrueFromDotEnv("IS_PROD") ? new Date().toLocaleString("fr-FR", { timeZone: "UTC" }).replace(/[\/:]/g, "-").replace(/ /g, "_") : "DEV-LOG"
|
const fileName = isTrueFromDotEnv("IS_PROD") ? new Date().toLocaleString("fr-FR", { timeZone: "UTC" }).replace(/[\/:]/g, "-").replace(/ /g, "_") : "DEV-LOG"
|
||||||
|
|
||||||
const logsDir = path.join(root, "logs")
|
const logsDir = path.join(root, "logs")
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
const crypto = require("node:crypto")
|
const crypto = require("node:crypto")
|
||||||
|
const logger = require("../modules/logger")
|
||||||
|
const { DefaultError } = require("../errors/errors")
|
||||||
const certificatesManager = require("./certificatesManager")
|
const certificatesManager = require("./certificatesManager")
|
||||||
|
|
||||||
async function getRegistrationCountryFromIp(ipAddress) {
|
async function getRegistrationCountryFromIp(ipAddress) {
|
||||||
@@ -55,7 +57,22 @@ function isTrueFromDotEnv(key) {
|
|||||||
return (process.env[key] || "").trim().toLowerCase() === "true"
|
return (process.env[key] || "").trim().toLowerCase() === "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getUrlParam(url, param) {
|
||||||
|
const urlParams = new URLSearchParams(url)
|
||||||
|
return urlParams.get(param)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDBError(error, errorMessage = "Internal Server Error", code = 500) {
|
||||||
|
if (error instanceof DefaultError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
logger.log(errorMessage.bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
||||||
|
throw new DefaultError(code, errorMessage, "InternalServerError")
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
getUrlParam,
|
||||||
|
handleDBError,
|
||||||
signProfileData,
|
signProfileData,
|
||||||
addDashesToUUID,
|
addDashesToUUID,
|
||||||
isTrueFromDotEnv,
|
isTrueFromDotEnv,
|
||||||
|
|||||||
238
package-lock.json
generated
238
package-lock.json
generated
@@ -1,16 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "base-rest-api",
|
"name": "yggdrasil",
|
||||||
"version": "0.0.1-alpha",
|
"version": "0.0.1-alpha",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "base-rest-api",
|
"name": "yggdrasil",
|
||||||
"version": "0.0.1-alpha",
|
"version": "0.0.1-alpha",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@mgalacyber/discord-oauth2": "^1.9.6",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
@@ -240,6 +242,36 @@
|
|||||||
"url": "https://github.com/sponsors/nzakas"
|
"url": "https://github.com/sponsors/nzakas"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@mgalacyber/discord-oauth2": {
|
||||||
|
"version": "1.9.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mgalacyber/discord-oauth2/-/discord-oauth2-1.9.6.tgz",
|
||||||
|
"integrity": "sha512-sSzi5aikr+32WnVvkfDpSu0SDtBh0anwvWRB0WfsjUrd4/WqfXPXTu0G2kqQOmcvXEQzws1/8wFbDymklUELJw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@mgalacyber/package-notifier": "^1.0.1",
|
||||||
|
"undici": "^6.16.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@mgalacyber/package-notifier": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mgalacyber/package-notifier/-/package-notifier-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-s/QgExSx4RRbVV73hMbBBZvkHjQ6WzOAc7tYJ/82JHMEabwbiQYoIdRJAH6y3MmAKo11+wKFCovHaM6oCPnpYA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@mgalacyber/termbox": "^1.0.0",
|
||||||
|
"axios": "^1.6.7",
|
||||||
|
"string-width": "^2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@mgalacyber/termbox": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mgalacyber/termbox/-/termbox-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-0tQL5lJw9NW1cND5loLgj5hrvJmIIY2vBWVsXsIz4pQEZR3KasMUKBcOxAKclMaoy19Y5sgqge018M67AgqkhA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
@@ -322,6 +354,15 @@
|
|||||||
"url": "https://github.com/sponsors/epoberezkin"
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ansi-regex": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ansi-styles": {
|
"node_modules/ansi-styles": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
@@ -365,6 +406,23 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Python-2.0"
|
"license": "Python-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.13.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||||
|
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.6",
|
||||||
|
"form-data": "^4.0.4",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@@ -607,6 +665,18 @@
|
|||||||
"node": ">=0.1.90"
|
"node": ">=0.1.90"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -660,6 +730,25 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cookie-parser": {
|
||||||
|
"version": "1.4.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
|
||||||
|
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cookie": "0.7.2",
|
||||||
|
"cookie-signature": "1.0.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cookie-parser/node_modules/cookie-signature": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cookie-signature": {
|
"node_modules/cookie-signature": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||||
@@ -721,6 +810,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/denque": {
|
"node_modules/denque": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||||
@@ -819,6 +917,21 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-set-tostringtag": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.6",
|
||||||
|
"has-tostringtag": "^1.0.2",
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/escape-html": {
|
"node_modules/escape-html": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||||
@@ -1181,6 +1294,63 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||||
|
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||||
|
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data/node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data/node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/forwarded": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@@ -1320,6 +1490,21 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/has-tostringtag": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-symbols": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hasown": {
|
"node_modules/hasown": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
@@ -1524,6 +1709,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-fullwidth-code-point": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-glob": {
|
"node_modules/is-glob": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||||
@@ -2157,6 +2351,12 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/pstree.remy": {
|
"node_modules/pstree.remy": {
|
||||||
"version": "1.1.8",
|
"version": "1.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||||
@@ -2498,6 +2698,31 @@
|
|||||||
"safe-buffer": "~5.2.0"
|
"safe-buffer": "~5.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string-width": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-fullwidth-code-point": "^2.0.0",
|
||||||
|
"strip-ansi": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strip-ansi": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/strip-json-comments": {
|
"node_modules/strip-json-comments": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||||
@@ -2596,6 +2821,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/undici": {
|
||||||
|
"version": "6.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
|
||||||
|
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "7.16.0",
|
"version": "7.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "base-rest-api",
|
"name": "yggdrasil",
|
||||||
"version": "0.0.1-alpha",
|
"version": "0.0.1-alpha",
|
||||||
"description": "",
|
"description": "",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://gitea.azures.fr/azures04/Base-REST-API"
|
"url": "https://gitea.azures.fr/azures04/Yggdrasil"
|
||||||
},
|
},
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "azures04",
|
"name": "azures04",
|
||||||
"url": "https://gitea.azures.fr/azures04/Base-REST-API",
|
"url": "https://gitea.azures.fr/azures04/Yggdrasil",
|
||||||
"email": "gilleslazure04@gmail.com"
|
"email": "gilleslazure04@gmail.com"
|
||||||
},
|
},
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
@@ -20,11 +20,13 @@
|
|||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"lint:fix": "eslint . --fix"
|
"lint:fix": "eslint . --fix"
|
||||||
},
|
},
|
||||||
"homepage": "https://gitea.azures.fr/azures04/Base-REST-API",
|
"homepage": "https://gitea.azures.fr/azures04/Yggdrasil",
|
||||||
"readme": "https://gitea.azures.fr/azures04/Base-REST-API/src/branch/main/README.md",
|
"readme": "https://gitea.azures.fr/azures04/Yggdrasil/src/branch/main/README.md",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@mgalacyber/discord-oauth2": "^1.9.6",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
|||||||
139
repositories/adminRepository.js
Normal file
139
repositories/adminRepository.js
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
const utils = require("../modules/utils")
|
||||||
|
const database = require("../modules/database")
|
||||||
|
const { DefaultError } = require("../errors/errors")
|
||||||
|
|
||||||
|
async function getAdminById(id) {
|
||||||
|
try {
|
||||||
|
const sql = "SELECT id, username, createdAt FROM apiAdministrators WHERE id = ?"
|
||||||
|
const rows = await database.query(sql, [id])
|
||||||
|
return rows[0] || null
|
||||||
|
} catch (error) {
|
||||||
|
return utils.handleDBError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createAdmin(username, hashedPassword) {
|
||||||
|
try {
|
||||||
|
const sql = "INSERT INTO apiAdministrators (username, password) VALUES (?, ?)"
|
||||||
|
const result = await database.query(sql, [username, hashedPassword])
|
||||||
|
|
||||||
|
if (result.affectedRows > 0) {
|
||||||
|
return { code: 200, id: result.insertId, username }
|
||||||
|
} else {
|
||||||
|
throw new DefaultError(500, "Failed to create administrator.")
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === "ER_DUP_ENTRY") {
|
||||||
|
throw new DefaultError(409, "Administrator username already exists.")
|
||||||
|
}
|
||||||
|
return utils.handleDBError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hasPermission(adminId, permissionKey) {
|
||||||
|
try {
|
||||||
|
const sql = `
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM apiAdministratorsPermissions
|
||||||
|
WHERE administratorId = ? AND permissionKey = ?
|
||||||
|
`
|
||||||
|
const rows = await database.query(sql, [adminId, permissionKey])
|
||||||
|
return rows[0].count === 1
|
||||||
|
} catch (error) {
|
||||||
|
return utils.handleDBError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assignPermission(adminId, permissionKey) {
|
||||||
|
try {
|
||||||
|
const sql = "INSERT INTO apiAdministratorsPermissions (administratorId, permissionKey) VALUES (?, ?)"
|
||||||
|
const result = await database.query(sql, [adminId, permissionKey])
|
||||||
|
|
||||||
|
return result.affectedRows > 0
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === "ER_DUP_ENTRY") return true
|
||||||
|
return utils.handleDBError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function revokePermission(adminId, permissionKey) {
|
||||||
|
try {
|
||||||
|
const sql = "DELETE FROM apiAdministratorsPermissions WHERE administratorId = ? AND permissionKey = ?"
|
||||||
|
const result = await database.query(sql, [adminId, permissionKey])
|
||||||
|
|
||||||
|
return result.affectedRows > 0
|
||||||
|
} catch (error) {
|
||||||
|
return utils.handleDBError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAdminPermissions(adminId) {
|
||||||
|
try {
|
||||||
|
const sql = `
|
||||||
|
SELECT permissionKey
|
||||||
|
FROM apiAdministratorsPermissions
|
||||||
|
WHERE administratorId = ?
|
||||||
|
`
|
||||||
|
const rows = await database.query(sql, [adminId])
|
||||||
|
return rows.map(r => r.permissionKey)
|
||||||
|
} catch (error) {
|
||||||
|
return utils.handleDBError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateAdminPassword(adminId, newHashedPassword) {
|
||||||
|
try {
|
||||||
|
const sql = "UPDATE apiAdministrators SET password = ? WHERE id = ?"
|
||||||
|
const result = await database.query(sql, [newHashedPassword, adminId])
|
||||||
|
|
||||||
|
if (result.affectedRows > 0) {
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: "Password updated successfully."
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new DefaultError(404, "Administrator not found.")
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return utils.handleDBError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAdminByUsername(username) {
|
||||||
|
try {
|
||||||
|
const sql = "SELECT id, username, password, createdAt FROM apiAdministrators WHERE username = ?"
|
||||||
|
const rows = await database.query(sql, [username])
|
||||||
|
|
||||||
|
return rows[0] || null
|
||||||
|
} catch (error) {
|
||||||
|
return utils.handleDBError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addPlayerAction(playerUuid, actionCode) {
|
||||||
|
try {
|
||||||
|
const cleanUuid = playerUuid.replace(/-/g, "")
|
||||||
|
const sql = "INSERT IGNORE INTO playerProfileActions (uuid, action) VALUES (?, ?)"
|
||||||
|
const result = await database.query(sql, [cleanUuid, actionCode])
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
success: result.affectedRows > 0,
|
||||||
|
message: result.affectedRows > 0 ? "Action taken." : "Action already taken."
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return utils.handleDBError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createAdmin,
|
||||||
|
getAdminById,
|
||||||
|
hasPermission,
|
||||||
|
addPlayerAction,
|
||||||
|
assignPermission,
|
||||||
|
revokePermission,
|
||||||
|
getAdminByUsername,
|
||||||
|
getAdminPermissions,
|
||||||
|
updateAdminPassword
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
const logger = require("../modules/logger")
|
|
||||||
const bcrypt = require("bcryptjs")
|
const bcrypt = require("bcryptjs")
|
||||||
const database = require("../modules/database")
|
const database = require("../modules/database")
|
||||||
|
const utils = require("../modules/utils")
|
||||||
const { DefaultError } = require("../errors/errors")
|
const { DefaultError } = require("../errors/errors")
|
||||||
|
|
||||||
async function getUser(identifier, requirePassword = false) {
|
async function getUser(identifier, requirePassword = false) {
|
||||||
@@ -19,9 +19,7 @@ async function getUser(identifier, requirePassword = false) {
|
|||||||
return { code: 200, user }
|
return { code: 200, user }
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof DefaultError) throw error
|
return utils.handleDBError(error)
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
|
||||||
throw new DefaultError(500, "Internal Server Error", "Please contact an administrator.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,11 +35,7 @@ async function register(email, username, password) {
|
|||||||
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
|
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof DefaultError) throw error
|
return utils.handleDBError(error)
|
||||||
|
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
|
||||||
console.log(error)
|
|
||||||
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +50,7 @@ async function insertClientSession(accessToken, clientToken, uuid) {
|
|||||||
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
|
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
return utils.handleDBError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,9 +64,7 @@ async function getPlayerProperties(uuid) {
|
|||||||
}
|
}
|
||||||
return { code: 200, properties: properties.map(property => { return { name: property.name, value: property.value } }) }
|
return { code: 200, properties: properties.map(property => { return { name: property.name, value: property.value } }) }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof DefaultError) throw error
|
return utils.handleDBError(error)
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
|
||||||
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,9 +83,7 @@ async function getClientSession(accessToken, clientToken) {
|
|||||||
throw new DefaultError(404, "Client session not found")
|
throw new DefaultError(404, "Client session not found")
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof DefaultError) throw error
|
return utils.handleDBError(error)
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
|
||||||
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,9 +102,7 @@ async function validateClientSession(accessToken, clientToken) {
|
|||||||
throw new DefaultError(404, "Client session not found for this accessToken/clientToken combination.")
|
throw new DefaultError(404, "Client session not found for this accessToken/clientToken combination.")
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof DefaultError) throw error
|
return utils.handleDBError(error)
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
|
||||||
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,9 +121,7 @@ async function validateClientSessionWithoutClientToken(accessToken) {
|
|||||||
throw new DefaultError(404, "Client session not found for this accessToken.")
|
throw new DefaultError(404, "Client session not found for this accessToken.")
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof DefaultError) throw error
|
return utils.handleDBError(error)
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
|
||||||
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,9 +139,7 @@ async function invalidateClientSession(accessToken, clientToken) {
|
|||||||
throw new DefaultError(404, "Client session not found for this accessToken/clientToken combination.")
|
throw new DefaultError(404, "Client session not found for this accessToken/clientToken combination.")
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof DefaultError) throw error
|
return utils.handleDBError(error)
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
|
||||||
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,9 +157,7 @@ async function revokeAccessTokens(uuid) {
|
|||||||
throw new DefaultError(404, "No access token found for this user.")
|
throw new DefaultError(404, "No access token found for this user.")
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof DefaultError) throw error
|
return utils.handleDBError(error)
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
|
||||||
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
51
repositories/oauth2Repository.js
Normal file
51
repositories/oauth2Repository.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
const utils = require("../modules/utils")
|
||||||
|
const database = require("../modules/database")
|
||||||
|
|
||||||
|
async function createLinkAttempt(OAuth2LinkId, playerUuid) {
|
||||||
|
try {
|
||||||
|
const sql = `
|
||||||
|
INSERT INTO oaauth2LinkAttempts (OAuth2LinkId, playerUuid)
|
||||||
|
VALUES (?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE playerUuid = VALUES(playerUuid), createdAt = NOW()
|
||||||
|
`
|
||||||
|
const result = await database.query(sql, [OAuth2LinkId, playerUuid])
|
||||||
|
return result.affectedRows > 0
|
||||||
|
} catch (error) {
|
||||||
|
return utils.handleDBError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function popLinkAttempt(OAuth2LinkId) {
|
||||||
|
try {
|
||||||
|
const selectSql = "SELECT playerUuid FROM oaauth2LinkAttempts WHERE OAuth2LinkId = ?"
|
||||||
|
const rows = await database.query(selectSql, [OAuth2LinkId])
|
||||||
|
|
||||||
|
if (rows.length === 0) return null
|
||||||
|
|
||||||
|
const playerUuid = rows[0].playerUuid
|
||||||
|
|
||||||
|
const deleteSql = "DELETE FROM oaauth2LinkAttempts WHERE OAuth2LinkId = ?"
|
||||||
|
await database.query(deleteSql, [OAuth2LinkId])
|
||||||
|
|
||||||
|
return playerUuid
|
||||||
|
} catch (error) {
|
||||||
|
return utils.handleDBError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unlinkProviderAccount(provider, playerUuid) {
|
||||||
|
try {
|
||||||
|
const sql = `DELETE FROM playersProperties WHERE name = '${provider}Id' AND uuid = ?`
|
||||||
|
const result = await database.query(sql, [playerUuid])
|
||||||
|
|
||||||
|
return result.affectedRows > 0
|
||||||
|
} catch (error) {
|
||||||
|
return utils.handleDBError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
popLinkAttempt,
|
||||||
|
createLinkAttempt,
|
||||||
|
unlinkProviderAccount
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
const logger = require("../modules/logger")
|
const utils = require("../modules/utils")
|
||||||
const database = require("../modules/database")
|
const database = require("../modules/database")
|
||||||
const { DefaultError } = require("../errors/errors")
|
const { DefaultError } = require("../errors/errors")
|
||||||
|
|
||||||
@@ -15,9 +15,7 @@ async function insertLegacyClientSessions(sessionId, uuid) {
|
|||||||
throw new DefaultError(500, "Internal Server Error", "Unknown DB Error")
|
throw new DefaultError(500, "Internal Server Error", "Unknown DB Error")
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof DefaultError) throw error
|
return utils.handleDBError(error)
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
|
||||||
throw new DefaultError(500, "Internal Server Error", "Please contact an administrator.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,8 +37,7 @@ async function validateLegacyClientSession(sessionId, uuid) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Please contact an administrator.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,8 +51,7 @@ async function getBlockedServers() {
|
|||||||
blockedServers: blockedServers.map(bannedServer => ({ sha1: bannedServer.hashedIp }))
|
blockedServers: blockedServers.map(bannedServer => ({ sha1: bannedServer.hashedIp }))
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Please contact an administrator.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,11 +65,12 @@ async function getActiveSkin(uuid) {
|
|||||||
`
|
`
|
||||||
const rows = await database.query(sql, [uuid])
|
const rows = await database.query(sql, [uuid])
|
||||||
const skin = rows[0]
|
const skin = rows[0]
|
||||||
|
if (!skin) {
|
||||||
|
throw new DefaultError(404, "Not found", "Not found")
|
||||||
|
}
|
||||||
return { code: 200, data: skin || null }
|
return { code: 200, data: skin || null }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Please contact an administrator.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,11 +84,12 @@ async function getActiveCape(uuid) {
|
|||||||
`
|
`
|
||||||
const rows = await database.query(sql, [uuid])
|
const rows = await database.query(sql, [uuid])
|
||||||
const cape = rows[0]
|
const cape = rows[0]
|
||||||
|
if (!cape) {
|
||||||
|
throw new DefaultError(404, "Not found", "Not found")
|
||||||
|
}
|
||||||
return { code: 200, data: cape || null }
|
return { code: 200, data: cape || null }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Please contact an administrator.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,8 +103,7 @@ async function getProfileActionsList(uuid) {
|
|||||||
|
|
||||||
return { code: 200, data: actions }
|
return { code: 200, data: actions }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Please contact an administrator.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,8 +122,7 @@ async function saveServerSession(uuid, accessToken, serverId, ip) {
|
|||||||
|
|
||||||
return { code: 200, success: result.affectedRows > 0 }
|
return { code: 200, success: result.affectedRows > 0 }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Please contact an administrator.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +131,8 @@ async function getServerSession(uuid, serverId) {
|
|||||||
const sql = `
|
const sql = `
|
||||||
SELECT ip
|
SELECT ip
|
||||||
FROM serverSessions
|
FROM serverSessions
|
||||||
WHERE uuid = ? AND serverId = ?
|
WHERE uuid = ? AND serverId = ?
|
||||||
|
AND createdAt > (NOW() - INTERVAL 30 SECOND)
|
||||||
`
|
`
|
||||||
const rows = await database.query(sql, [uuid, serverId])
|
const rows = await database.query(sql, [uuid, serverId])
|
||||||
const session = rows[0]
|
const session = rows[0]
|
||||||
@@ -146,8 +143,7 @@ async function getServerSession(uuid, serverId) {
|
|||||||
|
|
||||||
return { code: 200, valid: true, ip: session.ip }
|
return { code: 200, valid: true, ip: session.ip }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Please contact an administrator.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,38 @@
|
|||||||
|
const utils = require("../modules/utils")
|
||||||
const crypto = require("node:crypto")
|
const crypto = require("node:crypto")
|
||||||
const logger = require("../modules/logger")
|
|
||||||
const database = require("../modules/database")
|
const database = require("../modules/database")
|
||||||
const { DefaultError } = require("../errors/errors")
|
const { DefaultError } = require("../errors/errors")
|
||||||
|
|
||||||
|
async function getSkins(uuid) {
|
||||||
|
try {
|
||||||
|
const sql = `
|
||||||
|
SELECT t.uuid as textureUuid, t.hash, t.url, ps.variant, ps.isSelected
|
||||||
|
FROM playersSkins ps
|
||||||
|
JOIN textures t ON ps.assetHash = t.hash
|
||||||
|
WHERE ps.playerUuid = ?
|
||||||
|
`
|
||||||
|
const rows = await database.query(sql, [uuid])
|
||||||
|
return rows
|
||||||
|
} catch (error) {
|
||||||
|
return utils.handleDBError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCapes(uuid) {
|
||||||
|
try {
|
||||||
|
const sql = `
|
||||||
|
SELECT t.uuid as textureUuid, t.hash, t.url, t.alias, pc.isSelected
|
||||||
|
FROM playersCapes pc
|
||||||
|
JOIN textures t ON pc.assetHash = t.hash
|
||||||
|
WHERE pc.playerUuid = ?
|
||||||
|
`
|
||||||
|
const rows = await database.query(sql, [uuid])
|
||||||
|
return rows
|
||||||
|
} catch (error) {
|
||||||
|
return utils.handleDBError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function addPropertyToPlayer(key, value, uuid) {
|
async function addPropertyToPlayer(key, value, uuid) {
|
||||||
try {
|
try {
|
||||||
const sql = `
|
const sql = `
|
||||||
@@ -18,8 +48,7 @@ async function addPropertyToPlayer(key, value, uuid) {
|
|||||||
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
|
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,9 +63,7 @@ async function deletePropertyToPlayer(key, uuid) {
|
|||||||
throw new DefaultError(500, "Property not found for this user/key combination.")
|
throw new DefaultError(500, "Property not found for this user/key combination.")
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof DefaultError) throw error
|
return utils.handleDBError(error)
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
|
||||||
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,9 +77,7 @@ async function updatePropertyToPlayer(key, value, uuid) {
|
|||||||
throw new DefaultError(404, "Property not found for this user/key combination")
|
throw new DefaultError(404, "Property not found for this user/key combination")
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof DefaultError) throw error
|
return utils.handleDBError(error)
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
|
||||||
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,9 +93,7 @@ async function getPlayerProperties(uuid) {
|
|||||||
properties: rows.map(property => ({ name: property.name, value: property.value }))
|
properties: rows.map(property => ({ name: property.name, value: property.value }))
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof DefaultError) throw error
|
return utils.handleDBError(error)
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
|
||||||
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,9 +107,30 @@ async function getPlayerProperty(key, uuid) {
|
|||||||
}
|
}
|
||||||
return { code: 200, property }
|
return { code: 200, property }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof DefaultError) throw error
|
return utils.handleDBError(error)
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
}
|
||||||
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
|
}
|
||||||
|
|
||||||
|
async function getPlayerPropertyByValue(key, value) {
|
||||||
|
try {
|
||||||
|
const sql = `SELECT * FROM playersProperties WHERE name = ? AND value = ?`
|
||||||
|
const rows = await database.query(sql, [key, value])
|
||||||
|
const property = rows[0]
|
||||||
|
|
||||||
|
if (!property) {
|
||||||
|
throw new DefaultError(404, "No property found with this value for the specified key")
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
property: {
|
||||||
|
name: property.name,
|
||||||
|
value: property.value,
|
||||||
|
uuid: property.uuid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return utils.handleDBError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,9 +145,8 @@ async function getPlayerSettingsSchema() {
|
|||||||
RAW_SCHEMA_CACHE.privileges = privilegesRows.map(c => c.Field).filter(n => n !== "uuid")
|
RAW_SCHEMA_CACHE.privileges = privilegesRows.map(c => c.Field).filter(n => n !== "uuid")
|
||||||
RAW_SCHEMA_CACHE.preferences = preferencesRows.map(c => c.Field).filter(n => n !== "uuid")
|
RAW_SCHEMA_CACHE.preferences = preferencesRows.map(c => c.Field).filter(n => n !== "uuid")
|
||||||
return RAW_SCHEMA_CACHE
|
return RAW_SCHEMA_CACHE
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
logger.log("Database Schema Error: " + err.toString(), ["MariaDB", "red"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Database Schema Error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,9 +170,7 @@ async function updatePlayerPreferences(uuid, updates) {
|
|||||||
throw new DefaultError(404, "Player preferences not found or no changes made.")
|
throw new DefaultError(404, "Player preferences not found or no changes made.")
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof DefaultError) throw error
|
return utils.handleDBError(error)
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
|
||||||
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,9 +190,7 @@ async function getPlayerPreferences(uuid) {
|
|||||||
throw new DefaultError(404, "Preferences not found for this UUID.")
|
throw new DefaultError(404, "Preferences not found for this UUID.")
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof DefaultError) throw error
|
return utils.handleDBError(error)
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
|
||||||
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,9 +213,7 @@ async function getPlayerPrivileges(uuid) {
|
|||||||
throw new DefaultError(404, "Privileges not found for this UUID.")
|
throw new DefaultError(404, "Privileges not found for this UUID.")
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof DefaultError) throw error
|
return utils.handleDBError(error)
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
|
||||||
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,9 +237,7 @@ async function updatePlayerPrivileges(uuid, updates) {
|
|||||||
throw new DefaultError(404, "Player privileges not found or no changes made.")
|
throw new DefaultError(404, "Player privileges not found or no changes made.")
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof DefaultError) throw error
|
return utils.handleDBError(error)
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
|
||||||
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,11 +248,11 @@ async function banUser(uuid, { reasonKey, reasonMessage, expires = null }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let reasonId
|
let reasonId
|
||||||
const reasonRows = await database.query("SELECT id FROM banReasons WHERE reason_key = ?", [reasonKey])
|
const reasonRows = await database.query("SELECT id FROM banReasons WHERE reasonKey = ?", [reasonKey])
|
||||||
if (reasonRows.length > 0) {
|
if (reasonRows.length > 0) {
|
||||||
reasonId = reasonRows[0].id
|
reasonId = reasonRows[0].id
|
||||||
} else {
|
} else {
|
||||||
const insertReason = await database.query("INSERT INTO banReasons (reason_key) VALUES (?)", [reasonKey])
|
const insertReason = await database.query("INSERT INTO banReasons (reasonKey) VALUES (?)", [reasonKey])
|
||||||
reasonId = insertReason.insertId
|
reasonId = insertReason.insertId
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,8 +278,7 @@ async function banUser(uuid, { reasonKey, reasonMessage, expires = null }) {
|
|||||||
if (error.code === "ER_NO_REFERENCED_ROW_2" || error.toString().includes("foreign key constraint")) {
|
if (error.code === "ER_NO_REFERENCED_ROW_2" || error.toString().includes("foreign key constraint")) {
|
||||||
throw new DefaultError(404, "User not found (cannot ban a ghost).")
|
throw new DefaultError(404, "User not found (cannot ban a ghost).")
|
||||||
}
|
}
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", error.toString())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,9 +300,7 @@ async function unbanUser(uuid) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof DefaultError) throw error
|
return utils.handleDBError(error)
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
|
||||||
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,7 +311,7 @@ async function getPlayerBans(uuid) {
|
|||||||
b.banId,
|
b.banId,
|
||||||
b.expires,
|
b.expires,
|
||||||
b.reasonMessage,
|
b.reasonMessage,
|
||||||
r.reason_key as reason
|
r.reasonKey as reason
|
||||||
FROM bans b
|
FROM bans b
|
||||||
JOIN banReasons r ON b.reason = r.id
|
JOIN banReasons r ON b.reason = r.id
|
||||||
WHERE b.uuid = ?
|
WHERE b.uuid = ?
|
||||||
@@ -293,9 +325,7 @@ async function getPlayerBans(uuid) {
|
|||||||
return { code: 204 }
|
return { code: 204 }
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof DefaultError) throw error
|
return utils.handleDBError(error)
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
|
||||||
throw new DefaultError(500, "Please contact an administrator.", "InternalServerError")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,8 +343,7 @@ async function changeUsername(uuid, newName) {
|
|||||||
if (error.code === "ER_DUP_ENTRY" || error.errno === 1062) {
|
if (error.code === "ER_DUP_ENTRY" || error.errno === 1062) {
|
||||||
throw new DefaultError(409, "Username already taken", "ForbiddenOperationException")
|
throw new DefaultError(409, "Username already taken", "ForbiddenOperationException")
|
||||||
}
|
}
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", error.toString())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,8 +359,7 @@ async function createTexture(uuid, hash, type, url, alias) {
|
|||||||
if (error.code === 'ER_DUP_ENTRY') {
|
if (error.code === 'ER_DUP_ENTRY') {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,8 +370,7 @@ async function getTextureByUuid(textureUuid) {
|
|||||||
const rows = await database.query(sql, [textureUuid])
|
const rows = await database.query(sql, [textureUuid])
|
||||||
return rows[0] || null
|
return rows[0] || null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,8 +380,7 @@ async function getTextureByHash(hash) {
|
|||||||
const rows = await database.query(sql, [hash])
|
const rows = await database.query(sql, [hash])
|
||||||
return rows[0]
|
return rows[0]
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,8 +399,7 @@ async function resetSkin(uuid, hash, variant) {
|
|||||||
await database.query(updateSql, [hash, uuid])
|
await database.query(updateSql, [hash, uuid])
|
||||||
return { code: 200 }
|
return { code: 200 }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,23 +409,27 @@ async function hideCape(uuid) {
|
|||||||
await database.query(sql, [uuid])
|
await database.query(sql, [uuid])
|
||||||
return { code: 200 }
|
return { code: 200 }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showCape(uuid, hash) {
|
async function showCape(uuid, hash) {
|
||||||
try {
|
try {
|
||||||
const sql = `
|
await database.query(
|
||||||
UPDATE playersCapes
|
"UPDATE playersCapes SET isSelected = 0 WHERE playerUuid = ?",
|
||||||
SET isSelected = (assetHash = ?)
|
[uuid]
|
||||||
WHERE playerUuid = ?
|
)
|
||||||
`
|
|
||||||
const result = await database.query(sql, [hash, uuid])
|
const result = await database.query(
|
||||||
return { code: 200, changed: result.affectedRows > 0 }
|
"UPDATE playersCapes SET isSelected = 1 WHERE playerUuid = ? AND assetHash = ?",
|
||||||
|
[uuid, hash]
|
||||||
|
)
|
||||||
|
|
||||||
|
const affectedRows = Array.isArray(result) ? result[0].affectedRows : result.affectedRows
|
||||||
|
|
||||||
|
return { code: 200, changed: affectedRows > 0 }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Internal Server Error".bold + " : " + error.toString(), ["MariaDB", "yellow"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,7 +439,7 @@ async function checkCapeOwnership(uuid, hash) {
|
|||||||
const rows = await database.query(sql, [uuid, hash])
|
const rows = await database.query(sql, [uuid, hash])
|
||||||
return rows.length > 0
|
return rows.length > 0
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
return utils.handleDBError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,8 +449,7 @@ async function getPlayerMeta(uuid) {
|
|||||||
const rows = await database.query(sql, [uuid])
|
const rows = await database.query(sql, [uuid])
|
||||||
return rows[0]
|
return rows[0]
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,8 +465,7 @@ async function getLastNameChange(uuid) {
|
|||||||
const rows = await database.query(sql, [uuid])
|
const rows = await database.query(sql, [uuid])
|
||||||
return rows[0]
|
return rows[0]
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,8 +475,7 @@ async function getPlayerCertificate(uuid) {
|
|||||||
const rows = await database.query(sql, [uuid])
|
const rows = await database.query(sql, [uuid])
|
||||||
return rows[0]
|
return rows[0]
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,8 +489,7 @@ async function savePlayerCertificate(uuid, privateKey, publicKey, signatureV2, e
|
|||||||
const result = await database.query(sql, [uuid, privateKey, publicKey, signatureV2, expiresAt, refreshedAfter])
|
const result = await database.query(sql, [uuid, privateKey, publicKey, signatureV2, expiresAt, refreshedAfter])
|
||||||
return result.affectedRows > 0
|
return result.affectedRows > 0
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,8 +499,7 @@ async function deleteExpiredCertificates(isoDate) {
|
|||||||
const result = await database.query(sql, [isoDate])
|
const result = await database.query(sql, [isoDate])
|
||||||
return result.affectedRows
|
return result.affectedRows
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,8 +510,7 @@ async function addProfileAction(uuid, actionCode) {
|
|||||||
const result = await database.query(sql, [cleanUuid, actionCode])
|
const result = await database.query(sql, [cleanUuid, actionCode])
|
||||||
return result.affectedRows > 0
|
return result.affectedRows > 0
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,8 +521,7 @@ async function removeProfileAction(uuid, actionCode) {
|
|||||||
const result = await database.query(sql, [cleanUuid, actionCode])
|
const result = await database.query(sql, [cleanUuid, actionCode])
|
||||||
return result.affectedRows
|
return result.affectedRows
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,8 +532,7 @@ async function getPlayerActions(uuid) {
|
|||||||
const rows = await database.query(sql, [cleanUuid])
|
const rows = await database.query(sql, [cleanUuid])
|
||||||
return rows.map(r => r.action)
|
return rows.map(r => r.action)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,8 +543,7 @@ async function clearAllPlayerActions(uuid) {
|
|||||||
const result = await database.query(sql, [cleanUuid])
|
const result = await database.query(sql, [cleanUuid])
|
||||||
return result.affectedRows
|
return result.affectedRows
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -533,8 +553,7 @@ async function blockPlayer(blockerUuid, blockedUuid) {
|
|||||||
const result = await database.query(sql, [blockerUuid, blockedUuid])
|
const result = await database.query(sql, [blockerUuid, blockedUuid])
|
||||||
return result.affectedRows > 0
|
return result.affectedRows > 0
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,8 +563,7 @@ async function unblockPlayer(blockerUuid, blockedUuid) {
|
|||||||
const result = await database.query(sql, [blockerUuid, blockedUuid])
|
const result = await database.query(sql, [blockerUuid, blockedUuid])
|
||||||
return result.affectedRows > 0
|
return result.affectedRows > 0
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -555,8 +573,7 @@ async function getBlockedUuids(blockerUuid) {
|
|||||||
const rows = await database.query(sql, [blockerUuid])
|
const rows = await database.query(sql, [blockerUuid])
|
||||||
return rows.map(r => r.blockedUuid)
|
return rows.map(r => r.blockedUuid)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -566,8 +583,7 @@ async function isBlocked(blockerUuid, targetUuid) {
|
|||||||
const rows = await database.query(sql, [blockerUuid, targetUuid])
|
const rows = await database.query(sql, [blockerUuid, targetUuid])
|
||||||
return rows.length > 0
|
return rows.length > 0
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,8 +598,7 @@ async function getUsersByNames(usernames) {
|
|||||||
const rows = await database.query(sql, uniqueNames)
|
const rows = await database.query(sql, uniqueNames)
|
||||||
return rows
|
return rows
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,8 +609,7 @@ async function getUuidAndUsername(username) {
|
|||||||
|
|
||||||
return rows[0] || null
|
return rows[0] || null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,8 +619,7 @@ async function getProfileByUsername(username) {
|
|||||||
const rows = await database.query(sql, [username])
|
const rows = await database.query(sql, [username])
|
||||||
return rows[0] || null
|
return rows[0] || null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -623,8 +636,7 @@ async function getProfileByHistory(username, isoDate) {
|
|||||||
const rows = await database.query(sql, [username, isoDate])
|
const rows = await database.query(sql, [username, isoDate])
|
||||||
return rows[0] || null
|
return rows[0] || null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -639,44 +651,110 @@ async function getNameHistory(uuid) {
|
|||||||
const rows = await database.query(sql, [uuid])
|
const rows = await database.query(sql, [uuid])
|
||||||
return rows
|
return rows
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Database Error: " + error.toString(), ["MariaDB", "red"])
|
return utils.handleDBError(error)
|
||||||
throw new DefaultError(500, "Internal Server Error", "Database Error")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setSkin(uuid, hash, variant) {
|
async function setSkin(uuid, hash, variant) {
|
||||||
const insertSql = `
|
try {
|
||||||
INSERT INTO playersSkins (playerUuid, assetHash, variant, isSelected)
|
const resetSql = `UPDATE playersSkins SET isSelected = 0 WHERE playerUuid = ?`
|
||||||
VALUES (?, ?, ?, 1)
|
await database.query(resetSql, [uuid])
|
||||||
ON DUPLICATE KEY UPDATE isSelected = 1, variant = ?
|
|
||||||
`
|
const upsertSql = `
|
||||||
await database.query(insertSql, [uuid, hash, variant, variant])
|
INSERT INTO playersSkins (playerUuid, assetHash, variant, isSelected)
|
||||||
const updateSql = `
|
VALUES (?, ?, ?, 1)
|
||||||
UPDATE playersSkins
|
ON DUPLICATE KEY UPDATE isSelected = 1, variant = VALUES(variant)
|
||||||
SET isSelected = 0
|
`
|
||||||
WHERE playerUuid = ? AND assetHash != ?
|
await database.query(upsertSql, [uuid, hash, variant.toUpperCase()])
|
||||||
`
|
|
||||||
await database.query(updateSql, [uuid, hash])
|
return true
|
||||||
return true
|
} catch (error) {
|
||||||
|
return utils.handleDBError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updatePassword(uuid, hashedPassword) {
|
||||||
|
try {
|
||||||
|
const sql = "UPDATE players SET password = ? WHERE uuid = ?"
|
||||||
|
const result = await database.query(sql, [hashedPassword, uuid])
|
||||||
|
|
||||||
|
if (result.affectedRows > 0) {
|
||||||
|
return { code: 200, message: "Password updated successfully" }
|
||||||
|
} else {
|
||||||
|
throw new DefaultError(404, "User not found")
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return utils.handleDBError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addCapeToPlayer(uuid, hash) {
|
||||||
|
try {
|
||||||
|
const sql = `
|
||||||
|
INSERT INTO playersCapes (playerUuid, assetHash, isSelected)
|
||||||
|
VALUES (?, ?, 0)
|
||||||
|
`
|
||||||
|
const result = await database.query(sql, [uuid, hash])
|
||||||
|
|
||||||
|
if (result.affectedRows > 0) {
|
||||||
|
return { code: 200, message: "Cape granted to the player." }
|
||||||
|
}
|
||||||
|
throw new DefaultError(500, "Error when assigning the cape.")
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === "ER_DUP_ENTRY") {
|
||||||
|
throw new DefaultError(409, "The player already possesses this cloak.")
|
||||||
|
}
|
||||||
|
return utils.handleDBError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeCapeFromPlayer(uuid, hash) {
|
||||||
|
try {
|
||||||
|
const sql = "DELETE FROM playersCapes WHERE playerUuid = ? AND assetHash = ?"
|
||||||
|
const result = await database.query(sql, [uuid, hash])
|
||||||
|
|
||||||
|
if (result.affectedRows > 0) {
|
||||||
|
return { code: 200, message: "Cape removed from player." }
|
||||||
|
} else {
|
||||||
|
throw new DefaultError(404, "The player does not own this cloak.")
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return utils.handleDBError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteTexture(hash) {
|
||||||
|
try {
|
||||||
|
const sql = "DELETE FROM textures WHERE hash = ?"
|
||||||
|
const result = await database.query(sql, [hash])
|
||||||
|
return result.affectedRows > 0
|
||||||
|
} catch (error) {
|
||||||
|
return utils.handleDBError(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
setSkin,
|
setSkin,
|
||||||
banUser,
|
banUser,
|
||||||
|
getSkins,
|
||||||
|
getCapes,
|
||||||
showCape,
|
showCape,
|
||||||
hideCape,
|
hideCape,
|
||||||
resetSkin,
|
resetSkin,
|
||||||
isBlocked,
|
isBlocked,
|
||||||
unbanUser,
|
unbanUser,
|
||||||
blockPlayer,
|
blockPlayer,
|
||||||
|
deleteTexture,
|
||||||
createTexture,
|
createTexture,
|
||||||
getPlayerBans,
|
getPlayerBans,
|
||||||
getPlayerMeta,
|
getPlayerMeta,
|
||||||
unblockPlayer,
|
unblockPlayer,
|
||||||
changeUsername,
|
changeUsername,
|
||||||
getNameHistory,
|
getNameHistory,
|
||||||
|
updatePassword,
|
||||||
getBlockedUuids,
|
getBlockedUuids,
|
||||||
getUsersByNames,
|
getUsersByNames,
|
||||||
|
addCapeToPlayer,
|
||||||
getPlayerActions,
|
getPlayerActions,
|
||||||
getTextureByUuid,
|
getTextureByUuid,
|
||||||
getTextureByHash,
|
getTextureByHash,
|
||||||
@@ -690,6 +768,7 @@ module.exports = {
|
|||||||
getPlayerProperties,
|
getPlayerProperties,
|
||||||
removeProfileAction,
|
removeProfileAction,
|
||||||
addPropertyToPlayer,
|
addPropertyToPlayer,
|
||||||
|
removeCapeFromPlayer,
|
||||||
getProfileByUsername,
|
getProfileByUsername,
|
||||||
getPlayerPreferences,
|
getPlayerPreferences,
|
||||||
getPlayerCertificate,
|
getPlayerCertificate,
|
||||||
@@ -700,5 +779,6 @@ module.exports = {
|
|||||||
updatePropertyToPlayer,
|
updatePropertyToPlayer,
|
||||||
getPlayerSettingsSchema,
|
getPlayerSettingsSchema,
|
||||||
updatePlayerPreferences,
|
updatePlayerPreferences,
|
||||||
|
getPlayerPropertyByValue,
|
||||||
deleteExpiredCertificates,
|
deleteExpiredCertificates,
|
||||||
}
|
}
|
||||||
38
routes/admin/ban/index.js
Normal file
38
routes/admin/ban/index.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
const express = require("express")
|
||||||
|
const router = express.Router()
|
||||||
|
const userService = require("../../../services/userService")
|
||||||
|
const adminService = require("../../../services/adminService")
|
||||||
|
|
||||||
|
router.get("/:uuid", adminService.hasPermission("PLAYER_BAN_STATUS"), async (req, res) => {
|
||||||
|
const { uuid } = req.params
|
||||||
|
const banStatus = await userService.getPlayerBanStatus(uuid)
|
||||||
|
return res.status(200).json(banStatus)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.get("/:uuid/actions", adminService.hasPermission("PLAYER_ACTIONS_LIST"), async (req, res) => {
|
||||||
|
const { uuid } = req.params
|
||||||
|
const playerActions = await userService.getPlayerActions(uuid)
|
||||||
|
return res.status(200).json(playerActions)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.get("/:uuid/history", adminService.hasPermission("PLAYER_BAN_HISTORY"), async (req, res) => {
|
||||||
|
const { uuid } = req.params
|
||||||
|
const banHistory = await userService.getPlayerBans(uuid)
|
||||||
|
return res.status(200).json(banHistory)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.put("/:uuid", adminService.hasPermission("PLAYER_BAN"), async (req, res) => {
|
||||||
|
const { reasonKey, reasonMessage, expires } = req.body
|
||||||
|
const ban = await userService.banUser(uuid, { reasonKey, reasonMessage, expires })
|
||||||
|
await adminService.logPlayerAction("BAN")
|
||||||
|
return res.status(200).json(ban)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.delete("/:uuid", adminService.hasPermission("PLAYER_UNBAN"), async (req, res) => {
|
||||||
|
const { uuid } = req.params
|
||||||
|
const ban = await userService.unbanUser(uuid)
|
||||||
|
await adminService.logPlayerAction("UNBAN")
|
||||||
|
return res.status(200).json(ban)
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
19
routes/admin/cosmetics/capes.js
Normal file
19
routes/admin/cosmetics/capes.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
const express = require("express")
|
||||||
|
const path = require("node:path")
|
||||||
|
const multer = require("multer")
|
||||||
|
const router = express.Router()
|
||||||
|
const adminService = require("../../../services/adminService")
|
||||||
|
|
||||||
|
const upload = multer({ dest: path.join(process.cwd(), "data/temp/") })
|
||||||
|
|
||||||
|
router.post("/upload", adminService.hasPermission("UPLOAD_CAPE"), upload.single("file"), async (req, res) => {
|
||||||
|
const result = await adminService.uploadCape(req.file, req.body.alias)
|
||||||
|
res.status(201).json(result)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.delete("/:hash", adminService.hasPermission("DELETE_CAPES"), async (req, res) => {
|
||||||
|
const result = await adminService.deleteCape(req.params.hash)
|
||||||
|
res.status(200).json(result)
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
21
routes/admin/index.js
Normal file
21
routes/admin/index.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const express = require("express")
|
||||||
|
const router = express.Router()
|
||||||
|
const adminService = require("../../services/adminService")
|
||||||
|
|
||||||
|
router.post("/login", async (req, res) => {
|
||||||
|
const { username, password } = req.body
|
||||||
|
const result = await adminService.loginAdmin(username, password)
|
||||||
|
return res.status(200).json(result)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.patch("/password", async (req, res) => {
|
||||||
|
const token = req.headers.authorization.replace("Bearer ", "")
|
||||||
|
const profile = await adminService.getAdminProfileByToken(token)
|
||||||
|
|
||||||
|
const { newPassword } = req.body
|
||||||
|
|
||||||
|
const result = await adminService.changeAdminPassword(profile.id, newPassword)
|
||||||
|
return res.status(200).json(result)
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
12
routes/admin/players/password.js
Normal file
12
routes/admin/players/password.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const express = require("express")
|
||||||
|
const router = express.Router()
|
||||||
|
const userService = require("../../../services/userService")
|
||||||
|
const adminService = require("../../../services/adminService")
|
||||||
|
|
||||||
|
router.patch("/:uuid", adminService.hasPermission("CHANGE_PLAYER_PASSWORD"), async (req, res) => {
|
||||||
|
const { newPassword } = req.body
|
||||||
|
const result = await userService.changePassword(req.params.uuid, newPassword)
|
||||||
|
return res.status(200).json(result)
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
24
routes/admin/players/textures.js
Normal file
24
routes/admin/players/textures.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const express = require("express")
|
||||||
|
const router = express.Router()
|
||||||
|
const userService = require("../../../services/userService")
|
||||||
|
const adminService = require("../../../services/adminService")
|
||||||
|
|
||||||
|
router.delete("/skin/:uuid", adminService.hasPermission("RESET_PLAYER_SKIN"), async (req, res) => {
|
||||||
|
const result = await userService.resetSkin(req.params.uuid)
|
||||||
|
await adminService.logPlayerAction("USING_BANNED_SKIN")
|
||||||
|
return res.status(200).json(result)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.put("/cape/:uuid/:hash", adminService.hasPermission("GRANT_PLAYER_CAPE"), async (req, res) => {
|
||||||
|
const { uuid, hash } = req.params
|
||||||
|
const result = await userService.grantCape(uuid, hash)
|
||||||
|
return res.status(200).json(result)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.delete("/cape/:uuid/:hash", adminService.hasPermission("REMOVE_PLAYER_CAPE"), async (req, res) => {
|
||||||
|
const { uuid, hash } = req.params
|
||||||
|
const result = await userService.removeCape(uuid, hash)
|
||||||
|
return res.status(200).json(result)
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
13
routes/admin/players/username.js
Normal file
13
routes/admin/players/username.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const express = require("express")
|
||||||
|
const router = express.Router()
|
||||||
|
const userService = require("../../../services/userService")
|
||||||
|
const adminService = require("../../../services/adminService")
|
||||||
|
|
||||||
|
router.patch("/:uuid", adminService.hasPermission("CHANGE_PLAYER_USERNAME"), async (req, res) => {
|
||||||
|
const { newUsername } = req.body
|
||||||
|
const result = await userService.changeUsername(req.params.uuid, newUsername)
|
||||||
|
await adminService.logPlayerAction("FORCED_NAME_CHANGE")
|
||||||
|
return res.status(200).json(result)
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
15
routes/auth/provider/discord.js
Normal file
15
routes/auth/provider/discord.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
const express = require("express")
|
||||||
|
const router = express.Router()
|
||||||
|
const oauth2Service = require("../../../services/oauth2Service")
|
||||||
|
|
||||||
|
router.get("/login", async (req, res) => {
|
||||||
|
const redirectObject = await oauth2Service.generateLoginDiscordURL()
|
||||||
|
return res.status(200).redirect(redirectObject.url)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.get("/login/callback", async (req, res) => {
|
||||||
|
const result = await oauth2Service.handleLoginCallback("discord", req.query.code, req.query.requestUser)
|
||||||
|
return res.status(result.code).json(result.response)
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
const express = require("express")
|
const express = require("express")
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
const { YggdrasilError } = require("../../errors/errors")
|
const { YggdrasilError, DefaultError } = require("../../errors/errors")
|
||||||
const rateLimit = require("express-rate-limit")
|
const rateLimit = require("express-rate-limit")
|
||||||
const authService = require("../../services/authService")
|
const authService = require("../../services/authService")
|
||||||
const logger = require("../../modules/logger")
|
const logger = require("../../modules/logger")
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ const utils = require("../modules/utils")
|
|||||||
const serverService = require("../services/serverService")
|
const serverService = require("../services/serverService")
|
||||||
|
|
||||||
if (utils.isTrueFromDotEnv("SUPPORT_AUTHLIB_INJECTOR")) {
|
if (utils.isTrueFromDotEnv("SUPPORT_AUTHLIB_INJECTOR")) {
|
||||||
router.get("", (req, res) => {
|
router.get("/", (req, res) => {
|
||||||
const hostname = req.hostname
|
const hostname = req.hostname
|
||||||
const metadata = serverService.getServerMetadata(hostname)
|
const metadata = serverService.getServerMetadata(hostname)
|
||||||
res.header("X-Authlib-Injector-Date", new Date().toISOString())
|
res.header("X-Authlib-Injector-Date", new Date().toISOString())
|
||||||
return res.status(200).json(metadata)
|
return res.status(200).json(metadata)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
router.get("", (req, res, next) => next())
|
router.get("/", (req, res, next) => next())
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
10
routes/legacy/MinecraftCloaks/[username].js
Normal file
10
routes/legacy/MinecraftCloaks/[username].js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const express = require("express")
|
||||||
|
const router = express.Router({ mergeParams: true })
|
||||||
|
const sessionsService = require("../../../services/sessionsService")
|
||||||
|
|
||||||
|
router.get("", async (req, res) => {
|
||||||
|
const cape = await sessionsService.getActiveCape({ username: req.params.username.replace(".png", "") })
|
||||||
|
return res.redirect(`/textures${cape.data.url}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
10
routes/legacy/MinecraftSkins/[username].js
Normal file
10
routes/legacy/MinecraftSkins/[username].js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const express = require("express")
|
||||||
|
const router = express.Router({ mergeParams: true })
|
||||||
|
const sessionsService = require("../../../services/sessionsService")
|
||||||
|
|
||||||
|
router.get("", async (req, res) => {
|
||||||
|
const cape = await sessionsService.getActiveSkin({ username: req.params.username.replace(".png", "") })
|
||||||
|
return res.redirect(`/textures${cape.data.url}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
24
routes/legacy/checkserver.js
Normal file
24
routes/legacy/checkserver.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const express = require("express")
|
||||||
|
const router = express.Router()
|
||||||
|
const sessionsService = require("../../services/sessionsService")
|
||||||
|
|
||||||
|
router.get("/", async (req, res) => {
|
||||||
|
const { user, serverId } = req.query
|
||||||
|
try {
|
||||||
|
const result = await sessionsService.hasJoinedServer({
|
||||||
|
username: user,
|
||||||
|
serverId,
|
||||||
|
ip: null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.code === 200) {
|
||||||
|
return res.send("YES")
|
||||||
|
} else {
|
||||||
|
return res.send("NO")
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return res.send("NO")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
10
routes/legacy/cloaks/[username].js
Normal file
10
routes/legacy/cloaks/[username].js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const express = require("express")
|
||||||
|
const router = express.Router({ mergeParams: true })
|
||||||
|
const sessionsService = require("../../../services/sessionsService")
|
||||||
|
|
||||||
|
router.get("", async (req, res) => {
|
||||||
|
const cape = await sessionsService.getActiveCape({ username: req.params.username.replace(".png", "") })
|
||||||
|
return res.redirect(`/textures${cape.data.url}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
26
routes/legacy/joinserver.js
Normal file
26
routes/legacy/joinserver.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
const express = require("express")
|
||||||
|
const router = express.Router()
|
||||||
|
const sessionsService = require("../../services/sessionsService")
|
||||||
|
const logger = require("../../modules/logger")
|
||||||
|
|
||||||
|
router.get("/", async (req, res) => {
|
||||||
|
const { user, sessionId, serverId } = req.query
|
||||||
|
const clientIp = req.ip || req.connection.remoteAddress
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sessionsService.joinLegacyServer({
|
||||||
|
name: user,
|
||||||
|
sessionId,
|
||||||
|
serverId,
|
||||||
|
ip: clientIp
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.log(`Legacy Join: ${user} -> ${serverId}`, ["AUTH", "green"])
|
||||||
|
return res.send("OK")
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
return res.send("Bad login")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
36
routes/legacy/login.js
Normal file
36
routes/legacy/login.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
const express = require("express")
|
||||||
|
const router = express.Router()
|
||||||
|
const crypto = require("crypto")
|
||||||
|
const authService = require("../../services/authService")
|
||||||
|
const sessionsService = require("../../services/sessionsService")
|
||||||
|
const logger = require("../../modules/logger")
|
||||||
|
|
||||||
|
router.all("/", async (req, res) => {
|
||||||
|
const { user, password } = { ...req.query, ...req.body }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await authService.authenticate({
|
||||||
|
identifier: user,
|
||||||
|
password,
|
||||||
|
clientToken: "",
|
||||||
|
requireUser: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const profile = result.response.selectedProfile
|
||||||
|
const sessionId = crypto.randomBytes(16).toString("hex")
|
||||||
|
|
||||||
|
await sessionsService.registerLegacySession({
|
||||||
|
uuid: profile.id,
|
||||||
|
sessionId
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.log(`Legacy Login: ${user}`, ["AUTH", "green"])
|
||||||
|
|
||||||
|
const timestamp = Date.now()
|
||||||
|
return res.send(`${timestamp}:deprecated:${profile.name}:${sessionId}:${profile.id}`)
|
||||||
|
} catch (err) {
|
||||||
|
return res.send("Bad login")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
10
routes/legacy/skins/[username].js
Normal file
10
routes/legacy/skins/[username].js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const express = require("express")
|
||||||
|
const router = express.Router({ mergeParams: true })
|
||||||
|
const sessionsService = require("../../../services/sessionsService")
|
||||||
|
|
||||||
|
router.get("", async (req, res) => {
|
||||||
|
const cape = await sessionsService.getActiveSkin({ username: req.params.username.replace(".png", "") })
|
||||||
|
return res.redirect(`/textures${cape.data.url}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
26
routes/link/discord.js
Normal file
26
routes/link/discord.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
const express = require("express")
|
||||||
|
const router = express.Router()
|
||||||
|
const authService = require("../../services/authService")
|
||||||
|
const oauth2Service = require("../../services/oauth2Service")
|
||||||
|
|
||||||
|
router.get("/redirect", async (req, res) => {
|
||||||
|
const accessToken = req.headers.authorization.replace("Bearer ", "")
|
||||||
|
const player = await authService.verifyAccessToken({ accessToken })
|
||||||
|
const redirectObject = await oauth2Service.generateAssociationDiscordURL(player.user.uuid)
|
||||||
|
return res.json({ url: redirectObject.url })
|
||||||
|
})
|
||||||
|
|
||||||
|
router.get("/link", async (req, res) => {
|
||||||
|
const { code, state } = req.query
|
||||||
|
const result = await oauth2Service.handleAssociationCallback("discord", code, state)
|
||||||
|
return res.status(200).json(result)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.delete("/link", async (req, res) => {
|
||||||
|
const accessToken = req.headers.authorization.replace("Bearer ", "")
|
||||||
|
const player = await authService.verifyAccessToken({ accessToken })
|
||||||
|
const result = await oauth2Service.unlinkAccount("discord", player.user.uuid)
|
||||||
|
return res.status(result.code).json(result)
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
@@ -6,11 +6,19 @@ const authService = require("../../../../../services/authService")
|
|||||||
router.delete("/", async (req, res) => {
|
router.delete("/", async (req, res) => {
|
||||||
const player = await authService.verifyAccessToken({ accessToken: req.headers.authorization.replace("Bearer", "").trim() })
|
const player = await authService.verifyAccessToken({ accessToken: req.headers.authorization.replace("Bearer", "").trim() })
|
||||||
await userService.hideCape(player.user.uuid)
|
await userService.hideCape(player.user.uuid)
|
||||||
return res.status(200).send()
|
|
||||||
|
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 || []
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
router.put("/", async (req, res) => {
|
router.put("/", async (req, res) => {
|
||||||
const player = await authService.verifyAccessToken(req.headers.authorization)
|
const player = await authService.verifyAccessToken({ accessToken: req.headers.authorization.replace("Bearer", "").trim() })
|
||||||
|
|
||||||
await userService.showCape(player.user.uuid, req.body.capeId)
|
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)])
|
const [skinsResult, capesResult] = await Promise.all([userService.getSkins(player.user.uuid), userService.getCapes(player.user.uuid)])
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ router.get("/", async (req, res) => {
|
|||||||
const [skinsResult, capesResult] = await Promise.all([userService.getSkins(player.user.uuid), userService.getCapes(player.user.uuid)])
|
const [skinsResult, capesResult] = await Promise.all([userService.getSkins(player.user.uuid), userService.getCapes(player.user.uuid)])
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
id: player.uuid.replace(/-/g, ""),
|
id: player.user.uuid.replace(/-/g, ""),
|
||||||
name: player.user.username,
|
name: player.user.username,
|
||||||
skins: skinsResult.data || [],
|
skins: skinsResult.data || [],
|
||||||
capes: capesResult.data || []
|
capes: capesResult.data || []
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const express = require("express")
|
const express = require("express")
|
||||||
const authService = require("../../../../../services/authService")
|
const authService = require("../../../../../services/authService")
|
||||||
|
const userService = require("../../../../../services/userService")
|
||||||
const { DefaultError, ServiceError } = require("../../../../../errors/errors")
|
const { DefaultError, ServiceError } = require("../../../../../errors/errors")
|
||||||
const router = express.Router({ mergeParams: true })
|
const router = express.Router({ mergeParams: true })
|
||||||
|
|
||||||
@@ -21,13 +22,13 @@ router.put("/", async (req, res) => {
|
|||||||
const player = await authService.verifyAccessToken({ accessToken: req.headers.authorization.replace("Bearer", "").trim() })
|
const player = await authService.verifyAccessToken({ accessToken: req.headers.authorization.replace("Bearer", "").trim() })
|
||||||
const newName = req.params.name
|
const newName = req.params.name
|
||||||
|
|
||||||
await userService.changeUsername(player.uuid, newName)
|
await userService.changeUsername(player.user.uuid, newName)
|
||||||
|
|
||||||
const skinsResult = await userService.getSkins({ uuid: player.uuid })
|
const skinsResult = await userService.getSkins({ uuid: player.user.uuid })
|
||||||
const capesResult = await userService.getCapes({ uuid: player.uuid })
|
const capesResult = await userService.getCapes({ uuid: player.user.uuid })
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
id: player.uuid.replace(/-/g, ""),
|
id: player.user.uuid.replace(/-/g, ""),
|
||||||
name: newName,
|
name: newName,
|
||||||
skins: skinsResult.data || [],
|
skins: skinsResult.data || [],
|
||||||
capes: capesResult.data || []
|
capes: capesResult.data || []
|
||||||
|
|||||||
@@ -1,23 +1,44 @@
|
|||||||
const express = require("express")
|
const express = require("express")
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
const utils = require("../modules/utils")
|
||||||
const logger = require("../modules/logger")
|
const logger = require("../modules/logger")
|
||||||
const authService = require("../services/authService")
|
const authService = require("../services/authService")
|
||||||
|
const adminService = require("../services/adminService")
|
||||||
|
|
||||||
router.post("/", async (req, res) => {
|
if (!utils.isTrueFromDotEnv("SUPPORT_REGISTER")) {
|
||||||
const { username, password, email, registrationCountry, preferredLanguage } = req.body
|
router.post("/", adminService.hasPermission("REGISTER_USER"), async (req, res) => {
|
||||||
const clientIp = req.headers["x-forwarded-for"] || req.connection.remoteAddress
|
const { username, password, email, registrationCountry, preferredLanguage } = req.body
|
||||||
|
const clientIp = req.headers["x-forwarded-for"] || req.connection.remoteAddress
|
||||||
|
|
||||||
const result = await authService.registerUser({
|
const result = await authService.registerUser({
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
email,
|
email,
|
||||||
registrationCountry,
|
registrationCountry,
|
||||||
preferredLanguage,
|
preferredLanguage,
|
||||||
clientIp
|
clientIp
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.log(`New user registered: ${username}`, ["Web", "yellow", "AUTH", "green"])
|
||||||
|
return res.status(200).json(result)
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
router.post("/", async (req, res) => {
|
||||||
|
const { username, password, email, registrationCountry, preferredLanguage } = req.body
|
||||||
|
const clientIp = req.headers["x-forwarded-for"] || req.connection.remoteAddress
|
||||||
|
|
||||||
logger.log(`New user registered: ${username}`, ["Web", "yellow", "AUTH", "green"])
|
const result = await authService.registerUser({
|
||||||
return res.status(200).json(result)
|
username,
|
||||||
})
|
password,
|
||||||
|
email,
|
||||||
|
registrationCountry,
|
||||||
|
preferredLanguage,
|
||||||
|
clientIp
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.log(`New user registered: ${username}`, ["Web", "yellow", "AUTH", "green"])
|
||||||
|
return res.status(200).json(result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
7
routes/static.js
Normal file
7
routes/static.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const path = require("node:path")
|
||||||
|
const expres = require("express")
|
||||||
|
const router = expres.Router()
|
||||||
|
|
||||||
|
router.use(expres.static(path.join(process.cwd(), "data", "static")))
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
@@ -9,12 +9,7 @@ const TEXTURES_DIR = path.join(process.cwd(), "data", "textures")
|
|||||||
router.get("/", async (req, res, next) => {
|
router.get("/", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const hash = req.params.hash
|
const hash = req.params.hash
|
||||||
if (!/^[a-f0-9]{64}$/i.test(hash)) {
|
const filePath = path.join(TEXTURES_DIR, 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)) {
|
if (!fs.existsSync(filePath)) {
|
||||||
throw new DefaultError(404, "Texture not found")
|
throw new DefaultError(404, "Texture not found")
|
||||||
}
|
}
|
||||||
|
|||||||
13
schemas/admin/ban/[uuid]/actions.js
Normal file
13
schemas/admin/ban/[uuid]/actions.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
GET: {
|
||||||
|
headers: z.object({
|
||||||
|
"content-type": z.string().regex(/application\/json/i),
|
||||||
|
"authorization": z.string().startsWith("Bearer ")
|
||||||
|
}),
|
||||||
|
query: z.object({
|
||||||
|
uuid: z.string().uuid()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
13
schemas/admin/ban/[uuid]/history.js
Normal file
13
schemas/admin/ban/[uuid]/history.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
GET: {
|
||||||
|
headers: z.object({
|
||||||
|
"content-type": z.string().regex(/application\/json/i),
|
||||||
|
"authorization": z.string().startsWith("Bearer ")
|
||||||
|
}),
|
||||||
|
query: z.object({
|
||||||
|
uuid: z.string().uuid()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
38
schemas/admin/ban/[uuid]/index.js
Normal file
38
schemas/admin/ban/[uuid]/index.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
const uuidSchema = z.object({
|
||||||
|
uuid: z.string().uuid()
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
GET: {
|
||||||
|
headers: z.object({
|
||||||
|
"content-type": z.string().regex(/application\/json/i),
|
||||||
|
"authorization": z.string().startsWith("Bearer ")
|
||||||
|
}),
|
||||||
|
query: uuidSchema
|
||||||
|
},
|
||||||
|
PUT: {
|
||||||
|
headers: z.object({
|
||||||
|
"content-type": z.string().regex(/application\/json/i),
|
||||||
|
"authorization": z.string().startsWith("Bearer ")
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
reasonKey: z.string().min(1),
|
||||||
|
reasonMessage: z.string().optional(),
|
||||||
|
expires: z.number().int().positive().optional()
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
code: 400,
|
||||||
|
error: "CONSTRAINT_VIOLATION",
|
||||||
|
errorMessage: "Invalid ban format"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DELETE: {
|
||||||
|
headers: z.object({
|
||||||
|
"content-type": z.string().regex(/application\/json/i),
|
||||||
|
"authorization": z.string().startsWith("Bearer ")
|
||||||
|
}),
|
||||||
|
query: uuidSchema
|
||||||
|
}
|
||||||
|
}
|
||||||
9
schemas/admin/cosmetics/capes/[hash].js
Normal file
9
schemas/admin/cosmetics/capes/[hash].js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
DELETE: {
|
||||||
|
headers: z.object({
|
||||||
|
"authorization": z.string().startsWith("Bearer ")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
17
schemas/admin/login.js
Normal file
17
schemas/admin/login.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
POST: {
|
||||||
|
headers: z.object({
|
||||||
|
"content-type": z.string().regex(/application\/json/i),
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
username: z.string()
|
||||||
|
.min(1),
|
||||||
|
password: z.string()
|
||||||
|
.min(8, { message: "The password must be at least 8 characters long." })
|
||||||
|
.regex(/[A-Z]/, { message: "The password must contain a capital letter." })
|
||||||
|
.regex(/[0-9]/, { message: "The password must contain a number." })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
16
schemas/admin/password.js
Normal file
16
schemas/admin/password.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
PATCH: {
|
||||||
|
headers: z.object({
|
||||||
|
"content-type": z.string().regex(/application\/json/i),
|
||||||
|
"authorization": z.string().startsWith("Bearer ")
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
newPassword: z.string()
|
||||||
|
.min(8, { message: "The password must be at least 8 characters long." })
|
||||||
|
.regex(/[A-Z]/, { message: "The password must contain a capital letter." })
|
||||||
|
.regex(/[0-9]/, { message: "The password must contain a number." })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
16
schemas/admin/players/password/[uuid].js
Normal file
16
schemas/admin/players/password/[uuid].js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
PATCH: {
|
||||||
|
headers: z.object({
|
||||||
|
"content-type": z.string().regex(/application\/json/i),
|
||||||
|
"authorization": z.string().startsWith("Bearer ")
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
newPassword: z.string()
|
||||||
|
.min(8, { message: "The password must be at least 8 characters long." })
|
||||||
|
.regex(/[A-Z]/, { message: "The password must contain a capital letter." })
|
||||||
|
.regex(/[0-9]/, { message: "The password must contain a number." }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
14
schemas/admin/players/textures/cape/[uuid]/[hash].js
Normal file
14
schemas/admin/players/textures/cape/[uuid]/[hash].js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
PUT: {
|
||||||
|
headers: z.object({
|
||||||
|
"authorization": z.string().startsWith("Bearer ")
|
||||||
|
})
|
||||||
|
},
|
||||||
|
DELETE: {
|
||||||
|
headers: z.object({
|
||||||
|
"authorization": z.string().startsWith("Bearer ")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
11
schemas/legacy/joinserver.js
Normal file
11
schemas/legacy/joinserver.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
GET: {
|
||||||
|
query: z.object({
|
||||||
|
user: z.string().min(1),
|
||||||
|
sessionId: z.string().min(1),
|
||||||
|
serverId: z.string().min(1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
10
schemas/legacy/legacy/checkserver.js
Normal file
10
schemas/legacy/legacy/checkserver.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
GET: {
|
||||||
|
query: z.object({
|
||||||
|
user: z.string().min(1),
|
||||||
|
serverId: z.string().min(1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
16
schemas/legacy/login.js
Normal file
16
schemas/legacy/login.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
const loginShape = {
|
||||||
|
user: z.string().min(1, { message: "Username required" }),
|
||||||
|
password: z.string().min(1, { message: "Password required" }),
|
||||||
|
version: z.union([z.string(), z.number()]).optional()
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
POST: {
|
||||||
|
body: z.object(loginShape),
|
||||||
|
},
|
||||||
|
GET: {
|
||||||
|
query: z.object(loginShape)
|
||||||
|
}
|
||||||
|
}
|
||||||
24
schemas/link/discord/link.js
Normal file
24
schemas/link/discord/link.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
GET: {
|
||||||
|
query: z.object({
|
||||||
|
code: z.string({ required_error: "Authorisation code required" }),
|
||||||
|
state: z.string({ required_error: "The state parameter is required." })
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
code: 400,
|
||||||
|
message: "Invalid Discord callback settings"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DELETE: {
|
||||||
|
headers: z.object({
|
||||||
|
authorization: z.string({ required_error: "The authentication token is required." })
|
||||||
|
.regex(/^Bearer\s.+/, { message: "Invalid Authorization header format (Bearer token expected)" })
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
code: 401,
|
||||||
|
message: "Authentication required for disassociation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
schemas/link/discord/redirect.js
Normal file
14
schemas/link/discord/redirect.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
const z = require("zod")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
GET: {
|
||||||
|
headers: z.object({
|
||||||
|
authorization: z.string({ required_error: "The authentication token is required." })
|
||||||
|
.regex(/^Bearer\s.+/, { message: "Invalid Authorization header format (Bearer token expected)" })
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
code: 401,
|
||||||
|
message: "Authentication required to generate the link URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
24
server.js
24
server.js
@@ -21,11 +21,25 @@ databaseGlobals.setupDatabase()
|
|||||||
certificates.setupKeys()
|
certificates.setupKeys()
|
||||||
|
|
||||||
app.use(hpp())
|
app.use(hpp())
|
||||||
app.use(helmet())
|
app.use(helmet({
|
||||||
|
crossOriginResourcePolicy: { policy: "cross-origin" },
|
||||||
|
crossOriginEmbedderPolicy: false,
|
||||||
|
contentSecurityPolicy: {
|
||||||
|
directives: {
|
||||||
|
defaultSrc: ["'self'"],
|
||||||
|
scriptSrc: ["'self'", "https://cdnjs.cloudflare.com", "'unsafe-inline'"],
|
||||||
|
styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com"],
|
||||||
|
fontSrc: ["'self'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com"],
|
||||||
|
connectSrc: ["'self'", "https://yggdrasil.azures.fr"],
|
||||||
|
imgSrc: ["'self'", "data:"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}))
|
||||||
app.use(cors({ origin: "*" }))
|
app.use(cors({ origin: "*" }))
|
||||||
|
|
||||||
app.use(express.json())
|
app.use(express.json())
|
||||||
app.use(express.urlencoded({ extended: true }))
|
app.use(express.urlencoded({ extended: true }))
|
||||||
|
// app.use(cookieParser())
|
||||||
|
|
||||||
app.set("trust proxy", true)
|
app.set("trust proxy", true)
|
||||||
|
|
||||||
@@ -142,14 +156,10 @@ for (const route of routes) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app.all(/.*/, (req, res, next) => {
|
|
||||||
next(new DefaultError(404, `Can't find ${req.originalUrl} on this server!`, null, "NotFound"))
|
|
||||||
})
|
|
||||||
|
|
||||||
app.use((err, req, res, next) => {
|
app.use((err, req, res, next) => {
|
||||||
const statusCode = err.statusCode || err.code || 500
|
const statusCode = err.statusCode || err.code || 500
|
||||||
|
|
||||||
logger.log(req.originalUrl)
|
logger.error(`Error occured on: ${req.originalUrl.bold}`, ["API", "red"])
|
||||||
logger.error(err.message, ["API", "red"])
|
logger.error(err.message, ["API", "red"])
|
||||||
|
|
||||||
if (typeof err.serialize === "function") {
|
if (typeof err.serialize === "function") {
|
||||||
@@ -164,4 +174,4 @@ app.use((err, req, res, next) => {
|
|||||||
|
|
||||||
app.listen(process.env.WEB_PORT || 3000, () => {
|
app.listen(process.env.WEB_PORT || 3000, () => {
|
||||||
logger.log(`Server listening at port : ${process.env.WEB_PORT.bold || 3000}`, ["WEB", "yellow"])
|
logger.log(`Server listening at port : ${process.env.WEB_PORT.bold || 3000}`, ["WEB", "yellow"])
|
||||||
})
|
})
|
||||||
|
|||||||
172
services/adminService.js
Normal file
172
services/adminService.js
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
const fs = require("node:fs").promises
|
||||||
|
const path = require("node:path")
|
||||||
|
const jwt = require("jsonwebtoken")
|
||||||
|
const crypto = require("node:crypto")
|
||||||
|
const bcrypt = require("bcryptjs")
|
||||||
|
const userRepository = require("../repositories/userRepository")
|
||||||
|
const adminRepository = require("../repositories/adminRepository")
|
||||||
|
const { DefaultError } = require("../errors/errors")
|
||||||
|
|
||||||
|
const ADMIN_JWT_SECRET = process.env.ADMIN_JWT_SECRET || "udjJLGCOq7m3NmGpdVLJ@#"
|
||||||
|
|
||||||
|
async function registerAdmin(username, plainPassword, permissions = []) {
|
||||||
|
const hashedPassword = await bcrypt.hash(plainPassword, 10)
|
||||||
|
|
||||||
|
const result = await adminRepository.createAdmin(username, hashedPassword)
|
||||||
|
|
||||||
|
if (permissions.length > 0) {
|
||||||
|
for (const perm of permissions) {
|
||||||
|
await adminRepository.assignPermission(result.id, perm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { id: result.id, username, message: "Administrator successfully created." }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkAdminAccess(adminId, requiredPermission) {
|
||||||
|
if (typeof adminId != "number" || !requiredPermission) {
|
||||||
|
throw new DefaultError(400, "Administrator ID or permission missing.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return await adminRepository.hasPermission(adminId, requiredPermission)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changeAdminPassword(adminId, newPlainPassword) {
|
||||||
|
if (!newPlainPassword || newPlainPassword.length < 8) {
|
||||||
|
throw new DefaultError(400, "The password must contain at least 8 characters.")
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashed = await bcrypt.hash(newPlainPassword, 10)
|
||||||
|
return await adminRepository.updateAdminPassword(adminId, hashed)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAdminProfile(adminId) {
|
||||||
|
const admin = await adminRepository.getAdminById(adminId)
|
||||||
|
if (!admin) {
|
||||||
|
throw new DefaultError(404, "Administrator not found.")
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissions = await adminRepository.getAdminPermissions(adminId)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: admin.id,
|
||||||
|
username: admin.username,
|
||||||
|
createdAt: admin.createdAt,
|
||||||
|
permissions: permissions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAdminProfileByToken(accessToken) {
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(accessToken, { complete: true, json: true })
|
||||||
|
return getAdminProfile(decoded.sub)
|
||||||
|
} catch (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function grantPermission(adminId, permissionKey) {
|
||||||
|
return await adminRepository.assignPermission(adminId, permissionKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function revokePermission(adminId, permissionKey) {
|
||||||
|
return await adminRepository.revokePermission(adminId, permissionKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loginAdmin(username, password) {
|
||||||
|
const admin = await adminRepository.getAdminByUsername(username)
|
||||||
|
if (!admin) {
|
||||||
|
throw new DefaultError(403, "Invalid credentials.")
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMatch = await bcrypt.compare(password, admin.password)
|
||||||
|
if (!isMatch) {
|
||||||
|
throw new DefaultError(403, "Invalid credentials.")
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ id: admin.id, username: admin.username, type: "admin" },
|
||||||
|
ADMIN_JWT_SECRET,
|
||||||
|
{ expiresIn: "8h", subject: admin.id.toString(), issuer: "Yggdrasil" }
|
||||||
|
)
|
||||||
|
|
||||||
|
return { token }
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasPermission(requiredPermission) {
|
||||||
|
return async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const authHeader = req.headers.authorization
|
||||||
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||||
|
throw new DefaultError(401, "Admin auth required.")
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader.split(" ")[1]
|
||||||
|
|
||||||
|
const decoded = jwt.verify(token, ADMIN_JWT_SECRET)
|
||||||
|
if (decoded.type !== "admin") {
|
||||||
|
throw new DefaultError(403, "Invalid token.")
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasAccess = await checkAdminAccess(decoded.id, requiredPermission)
|
||||||
|
if (!hasAccess) {
|
||||||
|
throw new DefaultError(403, `Missing permission : ${requiredPermission}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.admin = decoded
|
||||||
|
next()
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
if (err.name === "JsonWebTokenError") {
|
||||||
|
return next(new DefaultError(401, "Invalid session."))
|
||||||
|
}
|
||||||
|
next(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadCape(fileObject, alias = null) {
|
||||||
|
const buffer = await fs.readFile(fileObject.path)
|
||||||
|
const hash = crypto.createHash("sha256").update(buffer).digest("hex")
|
||||||
|
|
||||||
|
const existing = await userRepository.getTextureByHash(hash)
|
||||||
|
if (existing) {
|
||||||
|
await fs.unlink(fileObject.path)
|
||||||
|
throw new DefaultError(409, "Cape already existing.")
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalPath = path.join(process.cwd(), "data/textures", `${hash}`)
|
||||||
|
|
||||||
|
await fs.rename(fileObject.path, finalPath)
|
||||||
|
|
||||||
|
const textureUrl = `/texture/${hash}`
|
||||||
|
await userRepository.createTexture(crypto.randomUUID(), hash, "CAPE", textureUrl, alias)
|
||||||
|
|
||||||
|
return { hash, url: textureUrl }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteCape(hash) {
|
||||||
|
const success = await userRepository.deleteTexture(hash)
|
||||||
|
if (!success) throw new DefaultError(404, "Cape not found.")
|
||||||
|
|
||||||
|
return { message: "Texture removed." }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logPlayerAction(playerUuid, actionCode) {
|
||||||
|
return await adminRepository.addPlayerAction(playerUuid, actionCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
loginAdmin,
|
||||||
|
uploadCape,
|
||||||
|
deleteCape,
|
||||||
|
registerAdmin,
|
||||||
|
hasPermission,
|
||||||
|
getAdminProfile,
|
||||||
|
grantPermission,
|
||||||
|
logPlayerAction,
|
||||||
|
revokePermission,
|
||||||
|
checkAdminAccess,
|
||||||
|
changeAdminPassword,
|
||||||
|
getAdminProfileByToken
|
||||||
|
}
|
||||||
@@ -56,11 +56,12 @@ async function authenticate({ identifier, password, clientToken, requireUser })
|
|||||||
|
|
||||||
delete userResult.user.password
|
delete userResult.user.password
|
||||||
|
|
||||||
const $clientToken = uuidRegex.test(clientToken) ? clientToken : crypto.randomUUID()
|
const $clientToken = clientToken || crypto.randomUUID()
|
||||||
const accessToken = jwt.sign({
|
const accessToken = jwt.sign({
|
||||||
uuid: userResult.user.uuid,
|
uuid: userResult.user.uuid,
|
||||||
username: userResult.user.username,
|
username: userResult.user.username,
|
||||||
clientToken: $clientToken,
|
clientToken: $clientToken,
|
||||||
|
type: "Minecraft"
|
||||||
}, keys.authenticationKeys.private, {
|
}, keys.authenticationKeys.private, {
|
||||||
subject: userResult.user.uuid,
|
subject: userResult.user.uuid,
|
||||||
issuer: "LentiaYggdrasil",
|
issuer: "LentiaYggdrasil",
|
||||||
@@ -105,6 +106,70 @@ async function authenticate({ identifier, password, clientToken, requireUser })
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function authenticateWithoutPassword({ identifier, requireUser }) {
|
||||||
|
let userResult
|
||||||
|
try {
|
||||||
|
userResult = await authRepository.getUser(identifier, true)
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 404) {
|
||||||
|
throw new DefaultError(403, "Invalid credentials. Invalid username or password.", "ForbiddenOperationException")
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
delete userResult.user.password
|
||||||
|
|
||||||
|
const $clientToken = crypto.randomUUID()
|
||||||
|
const accessToken = jwt.sign({
|
||||||
|
uuid: userResult.user.uuid,
|
||||||
|
username: userResult.user.username,
|
||||||
|
clientToken: $clientToken,
|
||||||
|
type: "Minecraft_OAuth2"
|
||||||
|
}, keys.authenticationKeys.private, {
|
||||||
|
subject: userResult.user.uuid,
|
||||||
|
issuer: "LentiaYggdrasil",
|
||||||
|
expiresIn: "1d",
|
||||||
|
algorithm: "RS256"
|
||||||
|
})
|
||||||
|
|
||||||
|
const clientSessionProcess = await authRepository.insertClientSession(accessToken, $clientToken, userResult.user.uuid)
|
||||||
|
|
||||||
|
const userObject = {
|
||||||
|
clientToken: clientSessionProcess.clientToken,
|
||||||
|
accessToken: clientSessionProcess.accessToken,
|
||||||
|
availableProfiles: [{
|
||||||
|
name: userResult.user.username,
|
||||||
|
id: userResult.user.uuid,
|
||||||
|
}],
|
||||||
|
selectedProfile: {
|
||||||
|
name: userResult.user.username,
|
||||||
|
id: userResult.user.uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requireUser) {
|
||||||
|
try {
|
||||||
|
const propertiesRequest = await authRepository.getPlayerProperties(userResult.user.uuid)
|
||||||
|
userObject.user = {
|
||||||
|
username: userResult.user.username,
|
||||||
|
properties: propertiesRequest.properties
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== 404) throw error
|
||||||
|
userObject.user = {
|
||||||
|
username: userResult.user.username,
|
||||||
|
properties: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
response: userObject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async function refreshToken({ previousAccessToken, clientToken, requireUser }) {
|
async function refreshToken({ previousAccessToken, clientToken, requireUser }) {
|
||||||
let sessionCheck
|
let sessionCheck
|
||||||
try {
|
try {
|
||||||
@@ -120,11 +185,12 @@ async function refreshToken({ previousAccessToken, clientToken, requireUser }) {
|
|||||||
|
|
||||||
await authRepository.invalidateClientSession(previousAccessToken, clientToken)
|
await authRepository.invalidateClientSession(previousAccessToken, clientToken)
|
||||||
|
|
||||||
const $clientToken = uuidRegex.test(clientToken) ? clientToken : crypto.randomUUID()
|
const $clientToken = clientToken || crypto.randomUUID()
|
||||||
const newAccessToken = jwt.sign({
|
const newAccessToken = jwt.sign({
|
||||||
uuid: userResult.user.uuid,
|
uuid: userResult.user.uuid,
|
||||||
username: userResult.user.username,
|
username: userResult.user.username,
|
||||||
clientToken: $clientToken,
|
clientToken: $clientToken,
|
||||||
|
type: "Minecraft"
|
||||||
}, keys.authenticationKeys.private, {
|
}, keys.authenticationKeys.private, {
|
||||||
subject: userResult.user.uuid,
|
subject: userResult.user.uuid,
|
||||||
issuer: "LentiaYggdrasil",
|
issuer: "LentiaYggdrasil",
|
||||||
@@ -281,9 +347,10 @@ module.exports = {
|
|||||||
signout,
|
signout,
|
||||||
validate,
|
validate,
|
||||||
invalidate,
|
invalidate,
|
||||||
registerUser,
|
|
||||||
authenticate,
|
authenticate,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
|
registerUser,
|
||||||
verifyAccessToken,
|
verifyAccessToken,
|
||||||
checkUsernameAvailability
|
checkUsernameAvailability,
|
||||||
|
authenticateWithoutPassword,
|
||||||
}
|
}
|
||||||
134
services/oauth2Service.js
Normal file
134
services/oauth2Service.js
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
const { DiscordOAuth2 } = require("@mgalacyber/discord-oauth2")
|
||||||
|
const oauth2Repository = require("../repositories/oauth2Repository")
|
||||||
|
const userService = require("./userService")
|
||||||
|
const authService = require("./authService")
|
||||||
|
const { StateTypes, Scopes, PromptTypes, ResponseCodeTypes } = require("@mgalacyber/discord-oauth2")
|
||||||
|
const { DefaultError, YggdrasilError } = require("../errors/errors")
|
||||||
|
|
||||||
|
const oauth2_association = new DiscordOAuth2({
|
||||||
|
clientId: process.env.DISCORD_CLIENT_ID,
|
||||||
|
clientSecret: process.env.DISCORD_CLIENT_SECRET,
|
||||||
|
redirectUri: process.env.DISCORD_ASSOCIATION_REDIRECT_URL
|
||||||
|
})
|
||||||
|
|
||||||
|
const oauth2_login = new DiscordOAuth2({
|
||||||
|
clientId: process.env.DISCORD_CLIENT_ID,
|
||||||
|
clientSecret: process.env.DISCORD_CLIENT_SECRET,
|
||||||
|
redirectUri: process.env.DISCORD_LOGIN_REDIRECT_URL
|
||||||
|
})
|
||||||
|
|
||||||
|
async function generateAssociationDiscordURL(playerUuid) {
|
||||||
|
const redirectObject = await oauth2_association.GenerateOAuth2Url({
|
||||||
|
state: StateTypes.UserAuth,
|
||||||
|
scope: [
|
||||||
|
Scopes.Identify
|
||||||
|
],
|
||||||
|
prompt: PromptTypes.Consent,
|
||||||
|
responseCode: ResponseCodeTypes.Code,
|
||||||
|
})
|
||||||
|
await oauth2Repository.createLinkAttempt(redirectObject.state, playerUuid)
|
||||||
|
return redirectObject
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAssociationCallback(provider, code, state) {
|
||||||
|
const playerUuid = await oauth2Repository.popLinkAttempt(state)
|
||||||
|
if (!playerUuid) {
|
||||||
|
throw new DefaultError(400, "Invalid or expired session state.", "InvalidStateError")
|
||||||
|
}
|
||||||
|
|
||||||
|
let isProviderAlreadyLinked = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
await userService.getPlayerProperty(playerUuid, `${provider}Id`)
|
||||||
|
isProviderAlreadyLinked = true
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== 404) throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isProviderAlreadyLinked) {
|
||||||
|
throw new DefaultError(409, `Account from ${provider} already linked to that player`, "AlreadyLinkedException")
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tokenResponse = await oauth2_association.GetAccessToken(code)
|
||||||
|
const userProfile = await oauth2_association.UserDataSchema.GetUserProfile(tokenResponse.accessToken)
|
||||||
|
if (!userProfile || !userProfile.id) {
|
||||||
|
throw new DefaultError(500, `Failed to retrieve ${provider} profile.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await userService.addPlayerProperty(playerUuid, `${provider}Id`, userProfile.id)
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: "Account linked successfully",
|
||||||
|
provider: {
|
||||||
|
id: userProfile.id,
|
||||||
|
username: userProfile.username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof DefaultError) throw error
|
||||||
|
throw new DefaultError(500, `${provider} authentication failed: + ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unlinkAccount(provider, playerUuid) {
|
||||||
|
try {
|
||||||
|
const property = await userService.getPlayerProperty(playerUuid, `${provider}Id`).catch(() => null)
|
||||||
|
if (!property) {
|
||||||
|
throw new DefaultError(404, `No ${provider} account linked to this player.`, "NotLinkedError")
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = await oauth2Repository.unlinkProviderAccount(provider, playerUuid)
|
||||||
|
if (!success) {
|
||||||
|
throw new DefaultError(500, "Failed to unlink the account. Please try again.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: `${provider} account successfully unlinked.`
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof DefaultError) throw error;
|
||||||
|
throw new DefaultError(500, "An error occurred during unlinking: " + error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateLoginDiscordURL() {
|
||||||
|
const redirectObject = await oauth2_login.GenerateOAuth2Url({
|
||||||
|
state: StateTypes.UserAuth,
|
||||||
|
scope: [
|
||||||
|
Scopes.Identify
|
||||||
|
],
|
||||||
|
prompt: PromptTypes.Consent,
|
||||||
|
responseCode: ResponseCodeTypes.Code,
|
||||||
|
})
|
||||||
|
return redirectObject
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLoginCallback(provider, code, requestUser) {
|
||||||
|
try {
|
||||||
|
const tokenResponse = await oauth2_login.GetAccessToken(code)
|
||||||
|
const userProfile = await oauth2_login.UserDataSchema.GetUserProfile(tokenResponse.accessToken)
|
||||||
|
if (!userProfile || !userProfile.id) {
|
||||||
|
throw new DefaultError(500, `Failed to retrieve ${provider} profile.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const propertyObject = await userService.getPlayerPropertyByValue(`${provider}Id`, userProfile.id)
|
||||||
|
return await authService.authenticateWithoutPassword({ identifier: propertyObject.property.uuid, requireUser: requestUser || true })
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code == 404) {
|
||||||
|
throw new YggdrasilError(404, "NotLinkedError", `No ${provider} account linked to any player.`)
|
||||||
|
}
|
||||||
|
if (error instanceof DefaultError) throw error
|
||||||
|
throw new DefaultError(500, `${provider} authentication failed: + ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
unlinkAccount,
|
||||||
|
handleLoginCallback,
|
||||||
|
generateLoginDiscordURL,
|
||||||
|
handleAssociationCallback,
|
||||||
|
generateAssociationDiscordURL
|
||||||
|
}
|
||||||
@@ -4,14 +4,14 @@ const package = require("../package.json")
|
|||||||
|
|
||||||
function getServerMetadata(hostname) {
|
function getServerMetadata(hostname) {
|
||||||
const keys = certs.getKeys()
|
const keys = certs.getKeys()
|
||||||
const publicKeyPEM = keys.playerCertificateKeys.public
|
const publicKeyPEM = keys.profilePropertyKeys.public
|
||||||
|
|
||||||
const serverMeta = {
|
const serverMeta = {
|
||||||
meta: {
|
meta: {
|
||||||
serverName: process.env.SERVER_NAME || "Yggdrasil Server",
|
serverName: process.env.SERVER_NAME || "Yggdrasil Server",
|
||||||
implementationName: package.name,
|
implementationName: package.name,
|
||||||
implementationVersion: package.version,
|
implementationVersion: package.version,
|
||||||
|
|
||||||
"feature.legacy_skin_api": utils.isTrueFromDotEnv("SUPPORT_LEGACY_SKIN_API"),
|
"feature.legacy_skin_api": utils.isTrueFromDotEnv("SUPPORT_LEGACY_SKIN_API"),
|
||||||
"feature.no_mojang_namespace": utils.isTrueFromDotEnv("SUPPORT_MOJANG_FALLBACK"),
|
"feature.no_mojang_namespace": utils.isTrueFromDotEnv("SUPPORT_MOJANG_FALLBACK"),
|
||||||
"feature.enable_mojang_anti_features": utils.isTrueFromDotEnv("SUPPORT_MOJANG_TELEMETRY_BLOCKER"),
|
"feature.enable_mojang_anti_features": utils.isTrueFromDotEnv("SUPPORT_MOJANG_TELEMETRY_BLOCKER"),
|
||||||
@@ -36,4 +36,4 @@ function getServerMetadata(hostname) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getServerMetadata
|
getServerMetadata
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ async function getBlockedServers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getProfile({ uuid, unsigned = false }) {
|
async function getProfile({ uuid, unsigned = false }) {
|
||||||
let userResult
|
let userResult, $uuid = utils.addDashesToUUID(uuid)
|
||||||
try {
|
try {
|
||||||
userResult = await authRepository.getUser(uuid, false)
|
userResult = await authRepository.getUser($uuid, false)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === 404) {
|
if (error.code === 404) {
|
||||||
return { code: 204, message: "User not found" }
|
return { code: 204, message: "User 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: (process.env.TEXTURES_ENDPOINTS || `http://localhost:${process.env.WEB_PORT}/textures/`) + 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: (process.env.TEXTURES_ENDPOINTS || `http://localhost:${process.env.WEB_PORT}/textures/`) + activeCape.url
|
url: (process.env.TEXTURES_ENDPOINTS || `http://localhost:${process.env.WEB_PORT}/textures`) + activeCape.url
|
||||||
} : undefined
|
} : undefined
|
||||||
|
|
||||||
const texturesObject = {
|
const texturesObject = {
|
||||||
@@ -122,6 +122,7 @@ async function joinServer({ accessToken, selectedProfile, clientToken, serverId,
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DefaultError(403, "Invalid access token", "ForbiddenOperationException")
|
throw new DefaultError(403, "Invalid access token", "ForbiddenOperationException")
|
||||||
}
|
}
|
||||||
|
|
||||||
await sessionRepository.saveServerSession(selectedProfile, accessToken, serverId, ip)
|
await sessionRepository.saveServerSession(selectedProfile, accessToken, serverId, ip)
|
||||||
return { code: 204 }
|
return { code: 204 }
|
||||||
}
|
}
|
||||||
@@ -152,11 +153,55 @@ async function hasJoinedServer({ username, serverId, ip }) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function joinLegacyServer({ name, sessionId, serverId }) {
|
||||||
|
try {
|
||||||
|
await validateLegacySession({ name, sessionId })
|
||||||
|
} catch (error) {
|
||||||
|
throw new DefaultError(403, "Bad login", "ForbiddenOperationException")
|
||||||
|
}
|
||||||
|
|
||||||
|
const userResult = await authRepository.getUser(name)
|
||||||
|
const uuid = userResult.user.uuid
|
||||||
|
|
||||||
|
await sessionRepository.saveServerSession(uuid, sessionId, serverId, "0.0.0.0")
|
||||||
|
|
||||||
|
return { code: 200, message: "OK" }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getActiveSkin({ username }) {
|
||||||
|
try {
|
||||||
|
const dbUser = await authRepository.getUser(username)
|
||||||
|
const activeSkin = await sessionRepository.getActiveSkin(dbUser.user.uuid)
|
||||||
|
return activeSkin
|
||||||
|
} catch (error) {
|
||||||
|
if (!(error instanceof DefaultError)) {
|
||||||
|
throw new DefaultError(400, "Bad Request", error.toString())
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getActiveCape({ username }) {
|
||||||
|
try {
|
||||||
|
const dbUser = await authRepository.getUser(username)
|
||||||
|
const activeCape = await sessionRepository.getActiveCape(dbUser.user.uuid)
|
||||||
|
return activeCape
|
||||||
|
} catch (error) {
|
||||||
|
if (!(error instanceof DefaultError)) {
|
||||||
|
throw new DefaultError(400, "Bad Request", error.toString())
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getProfile,
|
getProfile,
|
||||||
joinServer,
|
joinServer,
|
||||||
|
getActiveCape,
|
||||||
|
getActiveSkin,
|
||||||
hasJoinedServer,
|
hasJoinedServer,
|
||||||
|
joinLegacyServer,
|
||||||
getBlockedServers,
|
getBlockedServers,
|
||||||
registerLegacySession,
|
registerLegacySession,
|
||||||
validateLegacySession,
|
validateLegacySession,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
const fs = require("node:fs/promises")
|
const fs = require("node:fs/promises")
|
||||||
const path = require("node:path")
|
const path = require("node:path")
|
||||||
const util = require("node:util")
|
const util = require("node:util")
|
||||||
|
const bcrypt = require("bcryptjs")
|
||||||
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 ssrfcheck = require("ssrfcheck")
|
||||||
|
const authService = require("./authService")
|
||||||
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")
|
||||||
@@ -12,6 +14,42 @@ const generateKeyPairAsync = util.promisify(crypto.generateKeyPair)
|
|||||||
const TEMP_DIR = path.join(process.cwd(), "data", "temp")
|
const TEMP_DIR = path.join(process.cwd(), "data", "temp")
|
||||||
const TEXTURES_DIR = path.join(process.cwd(), "data", "textures")
|
const TEXTURES_DIR = path.join(process.cwd(), "data", "textures")
|
||||||
|
|
||||||
|
async function getSkins(uuid) {
|
||||||
|
try {
|
||||||
|
const rawSkins = await userRepository.getSkins(uuid)
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
data: rawSkins.map(r => ({
|
||||||
|
id: r.textureUuid,
|
||||||
|
state: r.isSelected == 1 ? "ACTIVE" : "INACTIVE",
|
||||||
|
url: r.url,
|
||||||
|
variant: r.variant || "CLASSIC"
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCapes(uuid) {
|
||||||
|
try {
|
||||||
|
const rawCapes = await userRepository.getCapes(uuid)
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
data: rawCapes.map(r => ({
|
||||||
|
id: r.textureUuid,
|
||||||
|
state: r.isSelected == 1 ? "ACTIVE" : "INACTIVE",
|
||||||
|
url: r.url,
|
||||||
|
alias: r.alias || "LentiaCustomCape"
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function getPlayerProperties(uuid) {
|
async function getPlayerProperties(uuid) {
|
||||||
try {
|
try {
|
||||||
const result = await userRepository.getPlayerProperties(uuid)
|
const result = await userRepository.getPlayerProperties(uuid)
|
||||||
@@ -28,6 +66,10 @@ async function getPlayerProperty(uuid, key) {
|
|||||||
return await userRepository.getPlayerProperty(key, uuid)
|
return await userRepository.getPlayerProperty(key, uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getPlayerPropertyByValue(key, value) {
|
||||||
|
return await userRepository.getPlayerPropertyByValue(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
async function addPlayerProperty(uuid, key, value) {
|
async function addPlayerProperty(uuid, key, value) {
|
||||||
return await userRepository.addPropertyToPlayer(key, value, uuid)
|
return await userRepository.addPropertyToPlayer(key, value, uuid)
|
||||||
}
|
}
|
||||||
@@ -451,15 +493,14 @@ async function uploadSkin(uuid, fileObject, variant) {
|
|||||||
const existingTexture = await userRepository.getTextureByHash(hash)
|
const existingTexture = await userRepository.getTextureByHash(hash)
|
||||||
|
|
||||||
if (!existingTexture) {
|
if (!existingTexture) {
|
||||||
const subDir = hash.substring(0, 2)
|
const targetDir = path.join(TEXTURES_DIR)
|
||||||
const targetDir = path.join(TEXTURES_DIR, subDir)
|
|
||||||
const targetPath = path.join(targetDir, hash)
|
const targetPath = path.join(targetDir, hash)
|
||||||
|
|
||||||
await fs.mkdir(targetDir, { recursive: true })
|
await fs.mkdir(targetDir, { recursive: true })
|
||||||
await fs.writeFile(targetPath, buffer)
|
await fs.writeFile(targetPath, buffer)
|
||||||
|
|
||||||
const newTextureUuid = crypto.randomUUID()
|
const newTextureUuid = crypto.randomUUID()
|
||||||
const textureUrl = `/texture/${hash}`
|
const textureUrl = `texture/${hash}`
|
||||||
await userRepository.createTexture(newTextureUuid, hash, 'SKIN', textureUrl, null)
|
await userRepository.createTexture(newTextureUuid, hash, 'SKIN', textureUrl, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,15 +539,43 @@ async function uploadSkinFromUrl(uuid, url, variant) {
|
|||||||
return await uploadSkin(uuid, { path: tempPath }, variant)
|
return await uploadSkin(uuid, { path: tempPath }, variant)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function changePassword(uuid, newPlainPassword) {
|
||||||
|
if (!newPlainPassword || newPlainPassword.length < 6) {
|
||||||
|
throw new DefaultError(400, "Password is too short. Minimum 6 characters.")
|
||||||
|
}
|
||||||
|
|
||||||
|
const salt = await bcrypt.genSalt(10)
|
||||||
|
const hashedPassword = await bcrypt.hash(newPlainPassword, salt)
|
||||||
|
|
||||||
|
return await userRepository.updatePassword(uuid, hashedPassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function grantCape(uuid, hash) {
|
||||||
|
const texture = await userRepository.getTextureByHash(hash)
|
||||||
|
if (!texture) {
|
||||||
|
throw new DefaultError(404, "Texture de cape introuvable dans la base globale.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return await userRepository.addCapeToPlayer(uuid, hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeCape(uuid, hash) {
|
||||||
|
return await userRepository.removeCapeFromPlayer(uuid, hash)
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
banUser,
|
banUser,
|
||||||
|
getCapes,
|
||||||
showCape,
|
showCape,
|
||||||
hideCape,
|
hideCape,
|
||||||
|
getSkins,
|
||||||
unbanUser,
|
unbanUser,
|
||||||
resetSkin,
|
resetSkin,
|
||||||
isBlocked,
|
isBlocked,
|
||||||
|
grantCape,
|
||||||
bulkLookup,
|
bulkLookup,
|
||||||
uploadSkin,
|
uploadSkin,
|
||||||
|
removeCape,
|
||||||
blockPlayer,
|
blockPlayer,
|
||||||
getNameUUIDs,
|
getNameUUIDs,
|
||||||
unblockPlayer,
|
unblockPlayer,
|
||||||
@@ -514,6 +583,7 @@ module.exports = {
|
|||||||
getPlayerBans,
|
getPlayerBans,
|
||||||
changeUsername,
|
changeUsername,
|
||||||
getPreferences,
|
getPreferences,
|
||||||
|
changePassword,
|
||||||
getBlockedUuids,
|
getBlockedUuids,
|
||||||
registerTexture,
|
registerTexture,
|
||||||
getLegacyProfile,
|
getLegacyProfile,
|
||||||
@@ -523,6 +593,7 @@ module.exports = {
|
|||||||
getPlayerProperty,
|
getPlayerProperty,
|
||||||
addPlayerProperty,
|
addPlayerProperty,
|
||||||
updatePreferences,
|
updatePreferences,
|
||||||
|
uploadSkinFromUrl,
|
||||||
getSettingsSchema,
|
getSettingsSchema,
|
||||||
getPlayerBanStatus,
|
getPlayerBanStatus,
|
||||||
removeProfileAction,
|
removeProfileAction,
|
||||||
@@ -532,6 +603,7 @@ module.exports = {
|
|||||||
getPlayerCertificate,
|
getPlayerCertificate,
|
||||||
savePlayerCertificate,
|
savePlayerCertificate,
|
||||||
clearAllPlayerActions,
|
clearAllPlayerActions,
|
||||||
|
getPlayerPropertyByValue,
|
||||||
getPlayerNameChangeStatus,
|
getPlayerNameChangeStatus,
|
||||||
getPlayerUsernamesHistory,
|
getPlayerUsernamesHistory,
|
||||||
deleteExpiredCertificates,
|
deleteExpiredCertificates,
|
||||||
|
|||||||
Reference in New Issue
Block a user