Compare commits

...

28 Commits

Author SHA1 Message Date
e242d78864 Update oauth2Service.js 2026-01-24 22:22:33 +01:00
33d54e655a Create success.html 2026-01-24 21:48:14 +01:00
39a566a2f5 Update [name].js 2026-01-24 03:32:36 +01:00
66b3268f8e Update userService.js 2026-01-24 01:42:30 +01:00
9db4d62d78 Update [name].js 2026-01-24 01:37:08 +01:00
492f012519 Update active.js 2026-01-24 00:57:58 +01:00
c5ef3d8181 Update active.js 2026-01-24 00:46:58 +01:00
bf17261fdf Update active.js 2026-01-24 00:46:11 +01:00
bb7b2328ab Update server.js 2026-01-24 00:14:05 +01:00
b6ad724602 Update userRepository.js 2026-01-23 23:13:03 +01:00
dbcb436c9f Refactor skin selection logic and remove DB trigger
Removed the 'unique_active_skin' database trigger from setupDatabase and updated setSkin in userRepository to handle skin selection logic in application code. This change centralizes the logic for ensuring only one active skin per user, improving maintainability and error handling.
2026-01-23 23:07:10 +01:00
aea0b7b016 Delete skins.js 2026-01-23 22:58:02 +01:00
11f930c3d8 Update skins.js 2026-01-23 22:56:37 +01:00
b82c06165a Update skins.js 2026-01-23 22:54:54 +01:00
48ea9f708b Expand skin upload schema and simplify texture storage
Updated the skin upload schema to accept both JSON and multipart/form-data content types, and to allow requests without a URL. Simplified the texture storage path in userService.js by removing subdirectory partitioning based on hash.
2026-01-23 22:52:00 +01:00
3f64c2c897 Update userService.js 2026-01-23 21:49:47 +01:00
c1dbfc58aa Update userService.js 2026-01-23 21:48:45 +01:00
65f2f26325 Update userService.js 2026-01-23 21:42:18 +01:00
f9270c1f9c Update userService.js 2026-01-23 21:41:32 +01:00
6f3231aee7 Update userService.js 2026-01-23 21:40:06 +01:00
4a0fcf65f3 Update userRepository.js 2026-01-23 21:38:54 +01:00
cc8e581cbe Update userRepository.js 2026-01-23 21:38:21 +01:00
b4ae5fa4d9 Update index.js 2026-01-23 21:33:13 +01:00
7018e8c497 Update userRepository.js 2026-01-23 21:30:41 +01:00
8e0a1ab673 Update userRepository.js 2026-01-23 21:29:37 +01:00
e089957db7 Update userRepository.js 2026-01-23 21:28:54 +01:00
c8812c5153 Add getSkins and getCapes methods to user modules
Introduces getSkins and getCapes functions in both userRepository and userService to retrieve player skins and capes from the database. These methods return structured data for use in higher-level application logic.
2026-01-23 21:27:45 +01:00
66db52e7c8 Update server.js 2026-01-19 20:37:12 +01:00
10 changed files with 144 additions and 63 deletions

25
data/static/success.html Normal file
View 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>

View File

@@ -242,20 +242,6 @@ async function setupDatabase() {
logger.log(`defaults skins (steve, alex) ready`, ["MariaDB", "yellow"])
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`)

View File

@@ -3,6 +3,36 @@ const crypto = require("node:crypto")
const database = require("../modules/database")
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) {
try {
const sql = `
@@ -626,19 +656,21 @@ async function getNameHistory(uuid) {
}
async function setSkin(uuid, hash, variant) {
const insertSql = `
INSERT INTO playersSkins (playerUuid, assetHash, variant, isSelected)
VALUES (?, ?, ?, 1)
ON DUPLICATE KEY UPDATE isSelected = 1, variant = ?
`
await database.query(insertSql, [uuid, hash, variant, variant])
const updateSql = `
UPDATE playersSkins
SET isSelected = 0
WHERE playerUuid = ? AND assetHash != ?
`
await database.query(updateSql, [uuid, hash])
return true
try {
const resetSql = `UPDATE playersSkins SET isSelected = 0 WHERE playerUuid = ?`
await database.query(resetSql, [uuid])
const upsertSql = `
INSERT INTO playersSkins (playerUuid, assetHash, variant, isSelected)
VALUES (?, ?, ?, 1)
ON DUPLICATE KEY UPDATE isSelected = 1, variant = VALUES(variant)
`
await database.query(upsertSql, [uuid, hash, variant.toUpperCase()])
return true
} catch (error) {
return utils.handleDBError(error)
}
}
async function updatePassword(uuid, hashedPassword) {
@@ -704,6 +736,8 @@ async function deleteTexture(hash) {
module.exports = {
setSkin,
banUser,
getSkins,
getCapes,
showCape,
hideCape,
resetSkin,

View File

@@ -6,11 +6,19 @@ const authService = require("../../../../../services/authService")
router.delete("/", async (req, res) => {
const player = await authService.verifyAccessToken({ accessToken: req.headers.authorization.replace("Bearer", "").trim() })
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) => {
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)
const [skinsResult, capesResult] = await Promise.all([userService.getSkins(player.user.uuid), userService.getCapes(player.user.uuid)])

View File

@@ -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)])
return res.status(200).json({
id: player.uuid.replace(/-/g, ""),
id: player.user.uuid.replace(/-/g, ""),
name: player.user.username,
skins: skinsResult.data || [],
capes: capesResult.data || []

View File

@@ -1,5 +1,6 @@
const express = require("express")
const authService = require("../../../../../services/authService")
const userService = require("../../../../../services/userService")
const { DefaultError, ServiceError } = require("../../../../../errors/errors")
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 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 capesResult = await userService.getCapes({ uuid: player.uuid })
const skinsResult = await userService.getSkins({ uuid: player.user.uuid })
const capesResult = await userService.getCapes({ uuid: player.user.uuid })
return res.status(200).json({
id: player.uuid.replace(/-/g, ""),
id: player.user.uuid.replace(/-/g, ""),
name: newName,
skins: skinsResult.data || [],
capes: capesResult.data || []

View File

@@ -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"
}
}
}

View File

@@ -21,7 +21,20 @@ databaseGlobals.setupDatabase()
certificates.setupKeys()
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(express.json())

View File

@@ -3,7 +3,7 @@ const oauth2Repository = require("../repositories/oauth2Repository")
const userService = require("./userService")
const authService = require("./authService")
const { StateTypes, Scopes, PromptTypes, ResponseCodeTypes } = require("@mgalacyber/discord-oauth2")
const { DefaultError } = require("../errors/errors")
const { DefaultError, YggdrasilError } = require("../errors/errors")
const oauth2_association = new DiscordOAuth2({
clientId: process.env.DISCORD_CLIENT_ID,
@@ -118,7 +118,7 @@ async function handleLoginCallback(provider, code, requestUser) {
} catch (error) {
if (error.code == 404) {
throw new DefaultError(404, `No ${provider} account linked to any player.`, "NotLinkedError")
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}`)

View File

@@ -5,6 +5,7 @@ const bcrypt = require("bcryptjs")
const logger = require("../modules/logger")
const crypto = require("node:crypto")
const ssrfcheck = require("ssrfcheck")
const authService = require("./authService")
const certsManager = require("../modules/certificatesManager")
const userRepository = require("../repositories/userRepository")
const { DefaultError } = require("../errors/errors")
@@ -13,6 +14,42 @@ const generateKeyPairAsync = util.promisify(crypto.generateKeyPair)
const TEMP_DIR = path.join(process.cwd(), "data", "temp")
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) {
try {
const result = await userRepository.getPlayerProperties(uuid)
@@ -456,8 +493,7 @@ async function uploadSkin(uuid, fileObject, variant) {
const existingTexture = await userRepository.getTextureByHash(hash)
if (!existingTexture) {
const subDir = hash.substring(0, 2)
const targetDir = path.join(TEXTURES_DIR, subDir)
const targetDir = path.join(TEXTURES_DIR)
const targetPath = path.join(targetDir, hash)
await fs.mkdir(targetDir, { recursive: true })
@@ -529,8 +565,10 @@ async function removeCape(uuid, hash) {
module.exports = {
banUser,
getCapes,
showCape,
hideCape,
getSkins,
unbanUser,
resetSkin,
isBlocked,