Version 2.0.0 - update of everything
This commit is contained in:
85 files changed
+13065
-2107
No files matched your search
+2072
File diff suppressed because it is too large.
Load diff
+518
-432
File diff suppressed because it is too large.
Load diff
+109
-97
@@ -1,11 +1,35 @@
|
||||
const { LogType } = require('loguix');
|
||||
const { LogType } = require('loguix');
|
||||
const dlog = new LogType("DiscordAuth");
|
||||
const { getWebsiteLink, getClientSecret } = require('../../utils/Database/Configuration');
|
||||
|
||||
// Délai maximal d'une requête vers l'API Discord
|
||||
const REQUEST_TIMEOUT = 10 * 1000;
|
||||
|
||||
/**
|
||||
* Appelle l'API Discord et renvoie le JSON, en levant une erreur si le statut HTTP n'est pas 2xx
|
||||
* (429 / 401 renvoient un JSON {message, retry_after} qui ne doit jamais être pris pour un utilisateur)
|
||||
*/
|
||||
async function discordFetch(url, options = {}) {
|
||||
const resp = await fetch(url, { ...options, signal: AbortSignal.timeout(REQUEST_TIMEOUT) });
|
||||
let body = null;
|
||||
try {
|
||||
body = await resp.json();
|
||||
} catch (e) {
|
||||
body = null;
|
||||
}
|
||||
if (!resp.ok) {
|
||||
const detail = body?.error_description || body?.error || body?.message || resp.statusText;
|
||||
const error = new Error(`HTTP ${resp.status} : ${detail}`);
|
||||
error.status = resp.status;
|
||||
throw error;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<{auth, identity}|"ACCESS_TOKEN_ERROR"|"USER_INFO_ERROR">}
|
||||
*/
|
||||
async function getDiscordUser(sessionId, auth_code) {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const discordBot = require("../../discord/Bot")
|
||||
const discordBotClient = discordBot.getClient()
|
||||
dlog.step.init("discord_auth_" + sessionId, "Authentification Discord de la session :" + sessionId);
|
||||
@@ -18,109 +42,97 @@ async function getDiscordUser(sessionId, auth_code) {
|
||||
params.append("grant_type", "authorization_code");
|
||||
params.append("code", auth_code);
|
||||
params.append("redirect_uri", getWebsiteLink() + "/redirect");
|
||||
params.append("scope", "identify guilds");
|
||||
|
||||
fetch("https://discord.com/api/oauth2/token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: params,
|
||||
}).then(accessTokenResp => accessTokenResp.json()).then(accessToken => {
|
||||
if (accessToken.error) {
|
||||
dlog.step.error("discord_auth_" + sessionId, "Erreur lors de la récupération du token d'accès Discord associé à la session : " + sessionId + " : " + accessToken.error + " : " + accessToken.error_description);
|
||||
resolve("ACCESS_TOKEN_ERROR");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
dlog.log("Récupération réussi du token d'accès Discord associé à la session : " + sessionId);
|
||||
|
||||
fetch("https://discord.com/api/users/@me", {
|
||||
headers: {
|
||||
authorization: `${accessToken.token_type} ${accessToken.access_token}`,
|
||||
},
|
||||
}).then(userResp => userResp.json()).then(user => {
|
||||
dlog.log("Récupération réussi des informations de l'utilisateur Discord associé à la session : " + sessionId + " avec le nom d'utilisateur : " + user.username + " (" + user.id + ")");
|
||||
// Get the guilds of the user
|
||||
fetch("https://discord.com/api/users/@me/guilds", {
|
||||
headers: {
|
||||
authorization: `${accessToken.token_type} ${accessToken.access_token}`,
|
||||
},
|
||||
}).then(guildsResp => guildsResp.json()).then(guilds => {
|
||||
dlog.log("Récupération réussi des guildes de l'utilisateur Discord associé à la session : " + sessionId + " avec le nom d'utilisateur : " + user.username + " (" + user.id + ")");
|
||||
dlog.step.end("discord_auth_" + sessionId)
|
||||
const userData = {
|
||||
auth: accessToken,
|
||||
identity: user,
|
||||
guilds: guilds,
|
||||
}
|
||||
resolve(userData);
|
||||
}).catch(err => {
|
||||
dlog.step.error("discord_auth_" + sessionId, "Erreur lors de la récupération des guildes de l'utilisateur Discord" + " avec le nom d'utilisateur : " + user.username + " (" + user.id + ")" + " associé à la session : " + sessionId + " : " + err);
|
||||
resolve("GUILDS_ERROR");
|
||||
});
|
||||
|
||||
}).catch(err => {
|
||||
dlog.step.error("discord_auth_" + sessionId, "Erreur lors de la récupération des informations de l'utilisateur Discord associé à la session : " + sessionId + " : " + err);
|
||||
resolve( "USER_INFO_ERROR");
|
||||
})
|
||||
|
||||
}).catch(err => {
|
||||
dlog.step.error("discord_auth_" + sessionId, "Erreur lors de la récupération du token d'accès Discord associé à la session : " + sessionId + " : " + err);
|
||||
resolve("ACCESS_TOKEN_ERROR");
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function refreshToken(refresh_token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const discordBot = require("../../discord/Bot")
|
||||
const params = new URLSearchParams();
|
||||
params.append("client_id", discordBot.getClient().user.id);
|
||||
params.append("client_secret", getClientSecret());
|
||||
params.append("grant_type", "refresh_token");
|
||||
params.append("refresh_token", refresh_token);
|
||||
params.append("scope", "identify guilds");
|
||||
|
||||
fetch("https://discord.com/api/oauth2/token", {
|
||||
let accessToken;
|
||||
try {
|
||||
accessToken = await discordFetch("https://discord.com/api/oauth2/token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: params,
|
||||
}).then(accessTokenResp => accessTokenResp.json()).then(accessToken => {
|
||||
if (accessToken.error) {
|
||||
dlog.error("Erreur lors de la récupération du token d'accès Discord : " + accessToken.error + " : " + accessToken.error_description);
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
resolve(accessToken);
|
||||
}).catch(err => {
|
||||
dlog.error("Erreur lors de la récupération du token d'accès Discord : " + err);
|
||||
resolve(null);
|
||||
})
|
||||
})
|
||||
}
|
||||
});
|
||||
if (!accessToken?.access_token || accessToken.error) {
|
||||
throw new Error((accessToken?.error || "Réponse invalide") + " : " + (accessToken?.error_description || ""));
|
||||
}
|
||||
} catch (err) {
|
||||
dlog.step.error("discord_auth_" + sessionId, "Erreur lors de la récupération du token d'accès Discord associé à la session : " + sessionId + " : " + err.message);
|
||||
return "ACCESS_TOKEN_ERROR";
|
||||
}
|
||||
|
||||
dlog.log("Récupération réussi du token d'accès Discord associé à la session : " + sessionId);
|
||||
|
||||
function getUserIdentity(accessToken) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch("https://discord.com/api/users/@me", {
|
||||
// Le scope demandé par le site est "identify" : l'appel /users/@me/guilds était toujours refusé et inutilisé
|
||||
try {
|
||||
const user = await discordFetch("https://discord.com/api/users/@me", {
|
||||
headers: {
|
||||
authorization: `${accessToken.token_type} ${accessToken.access_token}`,
|
||||
},
|
||||
}).then(userResp => userResp.json()).then(user => {
|
||||
if (user.error) {
|
||||
dlog.error("Erreur lors de la récupération des informations de l'utilisateur Discord : " + user.error + " : " + user.error_description);
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
resolve(user);
|
||||
}).catch(err => {
|
||||
dlog.error("Erreur lors de la récupération des informations de l'utilisateur Discord : " + err);
|
||||
resolve(null);
|
||||
})
|
||||
})
|
||||
});
|
||||
if (!user?.id) throw new Error("Identité Discord sans identifiant");
|
||||
dlog.log("Récupération réussi des informations de l'utilisateur Discord associé à la session : " + sessionId + " avec le nom d'utilisateur : " + user.username + " (" + user.id + ")");
|
||||
dlog.step.end("discord_auth_" + sessionId)
|
||||
return {
|
||||
auth: accessToken,
|
||||
identity: user,
|
||||
};
|
||||
} catch (err) {
|
||||
dlog.step.error("discord_auth_" + sessionId, "Erreur lors de la récupération des informations de l'utilisateur Discord associé à la session : " + sessionId + " : " + err.message);
|
||||
return "USER_INFO_ERROR";
|
||||
}
|
||||
}
|
||||
module.exports = {getDiscordUser, refreshToken, getUserIdentity}
|
||||
|
||||
/**
|
||||
* @returns {Promise<Object|null>} nouveau token d'accès, null en cas d'échec
|
||||
*/
|
||||
async function refreshToken(refresh_token) {
|
||||
if (!refresh_token) return null;
|
||||
const discordBot = require("../../discord/Bot")
|
||||
const params = new URLSearchParams();
|
||||
params.append("client_id", discordBot.getClient().user.id);
|
||||
params.append("client_secret", getClientSecret());
|
||||
params.append("grant_type", "refresh_token");
|
||||
params.append("refresh_token", refresh_token);
|
||||
|
||||
try {
|
||||
const accessToken = await discordFetch("https://discord.com/api/oauth2/token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: params,
|
||||
});
|
||||
if (!accessToken?.access_token || accessToken.error) {
|
||||
dlog.error("Erreur lors de la récupération du token d'accès Discord : " + accessToken?.error + " : " + accessToken?.error_description);
|
||||
return null;
|
||||
}
|
||||
return accessToken;
|
||||
} catch (err) {
|
||||
dlog.error("Erreur lors de la récupération du token d'accès Discord : " + err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<Object|null>} identité Discord (/users/@me), null en cas d'échec
|
||||
*/
|
||||
async function getUserIdentity(accessToken) {
|
||||
if (!accessToken?.access_token) return null;
|
||||
try {
|
||||
const user = await discordFetch("https://discord.com/api/users/@me", {
|
||||
headers: {
|
||||
authorization: `${accessToken.token_type} ${accessToken.access_token}`,
|
||||
},
|
||||
});
|
||||
if (!user?.id) {
|
||||
dlog.error("Réponse de Discord sans identifiant utilisateur");
|
||||
return null;
|
||||
}
|
||||
return user;
|
||||
} catch (err) {
|
||||
dlog.error("Erreur lors de la récupération des informations de l'utilisateur Discord : " + err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {getDiscordUser, refreshToken, getUserIdentity}
|
||||
+47
-10
@@ -1,33 +1,70 @@
|
||||
const { LogType } = require('loguix');
|
||||
const { generateSessionId } = require('../../utils/TokenManager');
|
||||
const { generateSessionId } = require('../../utils/TokenManager');
|
||||
const clog = new LogType("Session");
|
||||
|
||||
const sessions = new Array();
|
||||
// Une session OAuth n'est utile que le temps de la redirection Discord : on l'expire après 10 minutes
|
||||
const SESSION_TTL = 10 * 60 * 1000;
|
||||
const CLEAN_INTERVAL = 60 * 1000;
|
||||
|
||||
// sessionId -> date de création (ms)
|
||||
const sessions = new Map();
|
||||
|
||||
/**
|
||||
* Supprime les sessions expirées (OAuth jamais terminé)
|
||||
*/
|
||||
function purgeExpired() {
|
||||
const now = Date.now();
|
||||
let removed = 0;
|
||||
for (const [sessionId, createdAt] of sessions) {
|
||||
if (now - createdAt > SESSION_TTL) {
|
||||
sessions.delete(sessionId);
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
if (removed > 0) clog.log(`${removed} session(s) expirée(s) supprimée(s).`);
|
||||
return removed;
|
||||
}
|
||||
|
||||
setInterval(purgeExpired, CLEAN_INTERVAL).unref();
|
||||
|
||||
function checkSession(sessionId) {
|
||||
return sessions.includes(sessionId);
|
||||
if (typeof sessionId !== "string") return false;
|
||||
const createdAt = sessions.get(sessionId);
|
||||
if (createdAt === undefined) return false;
|
||||
if (Date.now() - createdAt > SESSION_TTL) {
|
||||
sessions.delete(sessionId);
|
||||
clog.warn(`Session ${sessionId} expirée.`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function addSession() {
|
||||
purgeExpired();
|
||||
const sessionId = generateSessionId();
|
||||
if (checkSession(sessionId)) {
|
||||
clog.warn(`Session ${sessionId} non trouvée dans la liste des sessions.`);
|
||||
if (sessions.has(sessionId)) {
|
||||
clog.warn(`La session ${sessionId} existe déjà, génération d'un nouvel identifiant.`);
|
||||
return addSession(); // Recursively generate a new session ID if it already exists
|
||||
}
|
||||
sessions.push(sessionId);
|
||||
sessions.set(sessionId, Date.now());
|
||||
clog.log(`Nouvelle session ${sessionId} ajoutée.`);
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
function removeSession(sessionId) {
|
||||
const index = sessions.indexOf(sessionId);
|
||||
if (index > -1) {
|
||||
sessions.splice(index, 1);
|
||||
if (sessions.delete(sessionId)) {
|
||||
clog.log(`Suppression de la session ${sessionId}.`);
|
||||
} else {
|
||||
clog.warn(`Session ${sessionId} non trouvée dans la liste des sessions.`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {checkSession, addSession, removeSession};
|
||||
/**
|
||||
* Nombre de sessions OAuth en attente (vue d'ensemble de l'administration)
|
||||
*/
|
||||
function getSessionCount() {
|
||||
purgeExpired();
|
||||
return sessions.size;
|
||||
}
|
||||
|
||||
module.exports = {checkSession, addSession, removeSession, getSessionCount};
|
||||
+214
-68
@@ -4,53 +4,86 @@ const { generateToken } = require('../../utils/TokenManager');
|
||||
const { LogType } = require('loguix');
|
||||
const clog = new LogType("User");
|
||||
const discordAuth = require('./DiscordAuth');
|
||||
const e = require('cors');
|
||||
|
||||
const UserDB = new Database("Users", __glob.USERFILE, []);
|
||||
var userList = new Array();
|
||||
|
||||
/**
|
||||
* Ajoute ou retire un label. value === undefined : bascule (ancien comportement).
|
||||
* @returns {boolean} présence du label après l'opération
|
||||
*/
|
||||
function applyLabel(labels, label, value) {
|
||||
const has = labels.includes(label);
|
||||
const wanted = value === undefined ? !has : !!value;
|
||||
if (wanted && !has) labels.push(label);
|
||||
if (!wanted && has) {
|
||||
// Retire toutes les occurrences (doublons éventuels dans users.json)
|
||||
for (let i = labels.length - 1; i >= 0; i--) {
|
||||
if (labels[i] === label) labels.splice(i, 1);
|
||||
}
|
||||
}
|
||||
return wanted;
|
||||
}
|
||||
|
||||
class User {
|
||||
auth;
|
||||
identity;
|
||||
tokens;
|
||||
labels;
|
||||
constructor(auth, identity, tokens, labels) {
|
||||
banInfo;
|
||||
constructor(auth, identity, tokens, labels, banInfo) {
|
||||
this.auth = auth;
|
||||
this.identity = identity;
|
||||
this.tokens = tokens;
|
||||
this.labels = labels;
|
||||
this.tokens = tokens || [];
|
||||
this.labels = labels || [];
|
||||
// Raison et date du bannissement global : {reason, at} ou null
|
||||
this.banInfo = banInfo || null;
|
||||
}
|
||||
|
||||
setAdmin() {
|
||||
/**
|
||||
* @param {boolean} [value] true = admin, false = retiré, undefined = bascule
|
||||
* @returns {boolean|null} nouvel état
|
||||
*/
|
||||
setAdmin(value) {
|
||||
const userInUserList = userList.find(user => user.identity.id === this.identity.id);
|
||||
if (!userInUserList) {
|
||||
clog.warn(`Utilisateur ${this.identity.username} non trouvé dans la liste des utilisateurs.`);
|
||||
return null;
|
||||
}
|
||||
if (userInUserList.labels.includes("ADMIN")) {
|
||||
userInUserList.labels.splice(userInUserList.labels.indexOf("ADMIN"), 1);
|
||||
clog.log(`L'utilisateur ${this.identity.username} n'est plus admin.`);
|
||||
} else {
|
||||
userInUserList.labels.push("ADMIN");
|
||||
const wasAdmin = userInUserList.labels.includes("ADMIN");
|
||||
const isAdmin = applyLabel(userInUserList.labels, "ADMIN", value);
|
||||
if (wasAdmin === isAdmin) return isAdmin;
|
||||
if (isAdmin) {
|
||||
clog.log(`L'utilisateur ${this.identity.username} est maintenant admin.`);
|
||||
} else {
|
||||
clog.log(`L'utilisateur ${this.identity.username} n'est plus admin.`);
|
||||
}
|
||||
saveUsers()
|
||||
return isAdmin;
|
||||
}
|
||||
|
||||
setBan(guildId) {
|
||||
/**
|
||||
* @param {string} guildId
|
||||
* @param {boolean} [value] undefined = bascule
|
||||
* @returns {boolean|null} nouvel état
|
||||
*/
|
||||
setBan(guildId, value) {
|
||||
const userInUserList = userList.find(user => user.identity.id === this.identity.id);
|
||||
if (!userInUserList) {
|
||||
clog.warn(`Utilisateur ${this.identity.username} non trouvé dans la liste des utilisateurs.`);
|
||||
return null;
|
||||
}
|
||||
const banLabel = `BAN_${guildId}`;
|
||||
if (userInUserList.labels.includes(banLabel)) {
|
||||
userInUserList.labels.splice(userInUserList.labels.indexOf(banLabel), 1);
|
||||
clog.log(`L'utilisateur ${this.identity.username} n'est plus banni du serveur ${guildId}.`);
|
||||
} else {
|
||||
userInUserList.labels.push(banLabel);
|
||||
const wasBanned = userInUserList.labels.includes(banLabel);
|
||||
const banned = applyLabel(userInUserList.labels, banLabel, value);
|
||||
if (wasBanned === banned) return banned;
|
||||
if (banned) {
|
||||
clog.log(`L'utilisateur ${this.identity.username} est maintenant banni du serveur ${guildId}.`);
|
||||
} else {
|
||||
clog.log(`L'utilisateur ${this.identity.username} n'est plus banni du serveur ${guildId}.`);
|
||||
}
|
||||
saveUsers()
|
||||
return banned;
|
||||
}
|
||||
|
||||
createToken() {
|
||||
@@ -118,20 +151,36 @@ class User {
|
||||
|
||||
}
|
||||
|
||||
setFullBan() {
|
||||
/**
|
||||
* Bannissement global
|
||||
* @param {boolean} [value] undefined = bascule
|
||||
* @param {string} [reason]
|
||||
* @returns {boolean|null} nouvel état
|
||||
*/
|
||||
setFullBan(value, reason) {
|
||||
const userInUserList = userList.find(user => user.identity.id === this.identity.id);
|
||||
if (!userInUserList) {
|
||||
clog.warn(`Utilisateur ${this.identity.username} non trouvé dans la liste des utilisateurs.`);
|
||||
return null;
|
||||
}
|
||||
if (userInUserList.labels.find(label => label == "BAN")) {
|
||||
userInUserList.labels.splice(userInUserList.labels.indexOf("BAN"), 1);
|
||||
clog.log(`L'utilisateur ${this.identity.username} n'est plus banni.`);
|
||||
const wasBanned = userInUserList.labels.includes("BAN");
|
||||
const banned = applyLabel(userInUserList.labels, "BAN", value);
|
||||
if (banned) {
|
||||
if (!wasBanned || reason) {
|
||||
userInUserList.banInfo = { reason: reason ? String(reason).slice(0, 500) : null, at: new Date().toISOString() };
|
||||
}
|
||||
} else {
|
||||
userInUserList.labels.push("BAN");
|
||||
clog.log(`L'utilisateur ${this.identity.username} est maintenant banni.`);
|
||||
userInUserList.banInfo = null;
|
||||
}
|
||||
if (wasBanned !== banned) {
|
||||
if (banned) {
|
||||
clog.log(`L'utilisateur ${this.identity.username} est maintenant banni.`);
|
||||
} else {
|
||||
clog.log(`L'utilisateur ${this.identity.username} n'est plus banni.`);
|
||||
}
|
||||
}
|
||||
saveUsers()
|
||||
return banned;
|
||||
}
|
||||
|
||||
isBanned(guildId) {
|
||||
@@ -148,6 +197,9 @@ class User {
|
||||
const modLabel = `MOD_${guildId}`;
|
||||
return this.labels.includes(modLabel) || this.isAdmin();
|
||||
}
|
||||
isDeleted() {
|
||||
return this.labels.includes("DELETED");
|
||||
}
|
||||
|
||||
justUpdated() {
|
||||
const userInUserList = userList.find(user => user.identity.id === this.identity.id);
|
||||
@@ -187,13 +239,19 @@ class User {
|
||||
async function refreshAllUserInformation() {
|
||||
await loadUsers();
|
||||
clog.log("Récupération des informations de tous les utilisateurs...");
|
||||
for (const user of userList) {
|
||||
await updateCredientials(user.identity.id);
|
||||
// Une seule sauvegarde à la fin plutôt qu'une réécriture complète du fichier par utilisateur
|
||||
for (const user of [...userList]) {
|
||||
if (!user.auth) continue;
|
||||
await updateCredientials(user.identity.id, false);
|
||||
}
|
||||
saveUsers();
|
||||
}
|
||||
|
||||
async function updateCredientials(id) {
|
||||
/**
|
||||
* @param {string} id
|
||||
* @param {boolean} [save=true] false pour regrouper les sauvegardes (rafraîchissement global)
|
||||
*/
|
||||
async function updateCredientials(id, save = true) {
|
||||
const user = getUserById(id);
|
||||
if (!user) {
|
||||
clog.warn(`Utilisateur ${id} non trouvé.`);
|
||||
@@ -223,7 +281,7 @@ async function updateCredientials(id) {
|
||||
else {
|
||||
clog.warn(`Aucune authentification trouvée pour l'utilisateur ${user.identity.username} (${user.identity.id})`);
|
||||
}
|
||||
saveUsers();
|
||||
if (save) saveUsers();
|
||||
return user.auth;
|
||||
}
|
||||
|
||||
@@ -277,6 +335,11 @@ async function updateIdentity(id) {
|
||||
* @returns {User} user
|
||||
*/
|
||||
async function addUser(auth, identity) {
|
||||
// Une réponse d'erreur de Discord (429, 401...) n'a pas d'id : on ne crée jamais d'utilisateur fantôme
|
||||
if (!identity || typeof identity.id !== "string" || identity.id === "") {
|
||||
clog.error("Ajout d'un utilisateur refusé : identité Discord invalide");
|
||||
return null;
|
||||
}
|
||||
// Check if the user already exists
|
||||
const existingUser = userList.find(user => user.identity.id === identity.id);
|
||||
if (existingUser) {
|
||||
@@ -320,12 +383,12 @@ function removeToken(token) {
|
||||
if (index > -1) {
|
||||
user.tokens.splice(index, 1);
|
||||
saveUsers();
|
||||
clog.log(`Token ${token} supprimé pour l'utilisateur ${user.identity.username}.`);
|
||||
clog.log(`Token supprimé pour l'utilisateur ${user.identity.username}.`);
|
||||
} else {
|
||||
clog.warn(`Token ${token} non trouvé pour l'utilisateur ${user.identity.username}.`);
|
||||
clog.warn(`Token non trouvé pour l'utilisateur ${user.identity.username}.`);
|
||||
}
|
||||
} else {
|
||||
clog.warn(`Utilisateur avec le token "${token}" non trouvé.`);
|
||||
clog.warn(`Utilisateur introuvable pour le token fourni.`);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
@@ -336,7 +399,7 @@ function addToken(id) {
|
||||
const token = generateToken();
|
||||
user.tokens.push(token);
|
||||
saveUsers();
|
||||
clog.log(`Token "${token}" ajouté pour l'utilisateur ${user.identity.username}.`);
|
||||
clog.log(`Token ajouté pour l'utilisateur ${user.identity.username}.`);
|
||||
return token;
|
||||
} else {
|
||||
clog.warn(`Utilisateur ${id} non trouvé.`);
|
||||
@@ -392,74 +455,151 @@ function getSimpleUser(id) {
|
||||
|
||||
// SET LABELS
|
||||
|
||||
async function setAdmin(id) {
|
||||
/**
|
||||
* Toutes les fonctions de labels acceptent une valeur explicite (idempotent) ;
|
||||
* sans valeur, elles basculent l'état comme avant (compatibilité).
|
||||
* @returns {Promise<boolean|null>} nouvel état, null si l'utilisateur est introuvable ou protégé
|
||||
*/
|
||||
async function setAdmin(id, value) {
|
||||
const user = getUserById(id);
|
||||
if (user) {
|
||||
await user.setAdmin();
|
||||
await saveUsers();
|
||||
return user.setAdmin(value);
|
||||
} else {
|
||||
clog.warn(`Utilisateur ${id} non trouvé.`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function setGuildMod(id, guildId) {
|
||||
async function setGuildMod(id, guildId, value) {
|
||||
const user = getUserById(id);
|
||||
if (user) {
|
||||
const modLabel = `MOD_${guildId}`;
|
||||
if (user.labels.includes(modLabel)) {
|
||||
await user.labels.splice(user.labels.indexOf(modLabel), 1);
|
||||
clog.log(`L'utilisateur ${user.identity.username} n'est plus modérateur du serveur ${guildId}.`);
|
||||
} else {
|
||||
await user.labels.push(modLabel);
|
||||
clog.log(`L'utilisateur ${user.identity.username} est maintenant modérateur du serveur ${guildId}.`);
|
||||
const wasMod = user.labels.includes(modLabel);
|
||||
const isMod = applyLabel(user.labels, modLabel, value);
|
||||
if (wasMod !== isMod) {
|
||||
if (isMod) {
|
||||
clog.log(`L'utilisateur ${user.identity.username} est maintenant modérateur du serveur ${guildId}.`);
|
||||
} else {
|
||||
clog.log(`L'utilisateur ${user.identity.username} n'est plus modérateur du serveur ${guildId}.`);
|
||||
}
|
||||
saveUsers();
|
||||
}
|
||||
await saveUsers();
|
||||
return isMod;
|
||||
} else {
|
||||
clog.warn(`Utilisateur ${id} non trouvé.`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function setGuildBan(id, guildId) {
|
||||
async function setGuildBan(id, guildId, value) {
|
||||
const user = getUserById(id);
|
||||
if (user) {
|
||||
if(user.isAdmin()) {
|
||||
if(user.isAdmin() && (value === undefined ? !user.isBanned(guildId) : value)) {
|
||||
clog.warn(`L'utilisateur ${user.identity.username} est admin, il ne peut pas être banni.`);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
await user.setBan(guildId);
|
||||
await saveUsers();
|
||||
return user.setBan(guildId, value);
|
||||
} else {
|
||||
clog.warn(`Utilisateur ${id} non trouvé.`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function setFullBan(id) {
|
||||
async function setFullBan(id, value, reason) {
|
||||
const user = getUserById(id);
|
||||
if (user) {
|
||||
if(user.isAdmin()) {
|
||||
if(user.isAdmin() && (value === undefined ? !user.isFullBanned() : value)) {
|
||||
clog.warn(`L'utilisateur ${user.identity.username} est admin, il ne peut pas être banni.`);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
await user.setFullBan();
|
||||
await saveUsers();
|
||||
return user.setFullBan(value, reason);
|
||||
} else {
|
||||
clog.warn(`Utilisateur ${id} non trouvé.`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function deleteAccount(id) {
|
||||
/**
|
||||
* Révoque toutes les sessions web (tokens) d'un utilisateur
|
||||
* @returns {number} nombre de tokens révoqués
|
||||
*/
|
||||
function revokeTokens(id) {
|
||||
const user = getUserById(id);
|
||||
if (user) {
|
||||
user.labels = user.labels.filter(label => label.includes('BAN'));
|
||||
user.labels.push('DELETED'); // Add a deleted label
|
||||
user.tokens = []; // Clear tokens
|
||||
user.auth = null; // Clear authentication
|
||||
user.identity = { id: user.identity.id, username: user.identity.username }; // Keep only identity information
|
||||
saveUsers();
|
||||
clog.log(`Suppression du compte de l'utilisateur ${user.identity.username}.`);
|
||||
} else {
|
||||
if (!user) {
|
||||
clog.warn(`Utilisateur ${id} non trouvé.`);
|
||||
return 0;
|
||||
}
|
||||
const count = user.tokens?.length || 0;
|
||||
if (count > 0) user.clearToken();
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suppression douce : garde l'entrée et les labels BAN (un banni ne retrouve pas un compte vierge),
|
||||
* ajoute DELETED et efface tokens, authentification et identité détaillée.
|
||||
* @param {string} id
|
||||
* @param {{purge?: boolean}} [options] purge : supprime aussi playlists, historique et médias
|
||||
* @returns {Promise<{playlists: number, history: number, media: number}|null>} éléments purgés, null si introuvable
|
||||
*/
|
||||
async function deleteAccount(id, { purge = false } = {}) {
|
||||
const user = getUserById(id);
|
||||
if (!user) {
|
||||
clog.warn(`Utilisateur ${id} non trouvé.`);
|
||||
return null;
|
||||
}
|
||||
user.labels = user.labels.filter(label => label.includes('BAN'));
|
||||
user.labels.push('DELETED'); // Add a deleted label
|
||||
user.tokens = []; // Clear tokens
|
||||
user.auth = null; // Clear authentication
|
||||
user.identity = { id: user.identity.id, username: user.identity.username }; // Keep only identity information
|
||||
saveUsers();
|
||||
clog.log(`Suppression du compte de l'utilisateur ${user.identity.username}.`);
|
||||
|
||||
// Jetons Google (identifiants, pas des données) : toujours supprimés, révocation chez Google sans attendre
|
||||
try {
|
||||
// Chargement différé : OAuth2.js dépend de ce module
|
||||
require('../../playlists/Google/OAuth2').unlink(id, { revoke: true }).catch((e) => {
|
||||
clog.error(`Suppression du compte Google de ${id} impossible : ${e?.message || e}`);
|
||||
});
|
||||
} catch (e) {
|
||||
clog.error(`Suppression du compte Google de ${id} impossible : ${e?.message || e}`);
|
||||
}
|
||||
|
||||
const purged = { playlists: 0, history: 0, media: 0 };
|
||||
if (!purge) return purged;
|
||||
|
||||
// Chargement différé : évite les dépendances circulaires au démarrage
|
||||
const { getDatabase } = require('../../utils/Database/Database');
|
||||
try {
|
||||
const playlistDB = getDatabase("Playlists");
|
||||
purged.playlists = playlistDB?.data?.[id]?.length || 0;
|
||||
await require('../../playlists/PlaylistManager').deleteUserPlaylists(id);
|
||||
} catch (e) {
|
||||
clog.error(`Purge des playlists de ${id} impossible : ${e.message}`);
|
||||
}
|
||||
try {
|
||||
const historyDB = getDatabase("history");
|
||||
purged.history = historyDB?.data?.[id]?.length || 0;
|
||||
if (historyDB?.data?.[id]) {
|
||||
delete historyDB.data[id];
|
||||
historyDB.save();
|
||||
}
|
||||
} catch (e) {
|
||||
clog.error(`Purge de l'historique de ${id} impossible : ${e.message}`);
|
||||
}
|
||||
try {
|
||||
const mediaDB = getDatabase("media");
|
||||
if (Array.isArray(mediaDB?.data)) {
|
||||
const before = mediaDB.data.length;
|
||||
mediaDB.data = mediaDB.data.filter(m => m.userId !== id);
|
||||
purged.media = before - mediaDB.data.length;
|
||||
if (purged.media > 0) mediaDB.save();
|
||||
}
|
||||
} catch (e) {
|
||||
clog.error(`Purge des médias de ${id} impossible : ${e.message}`);
|
||||
}
|
||||
clog.log(`Purge des données de l'utilisateur ${id} : ${purged.playlists} playlist(s), ${purged.history} entrée(s) d'historique, ${purged.media} média(s).`);
|
||||
return purged;
|
||||
}
|
||||
|
||||
// USERS DB
|
||||
@@ -468,12 +608,15 @@ function loadUsers() {
|
||||
UserDB.load()
|
||||
userList = new Array();
|
||||
for (const user of UserDB.getData()) {
|
||||
if(!user?.identity?.id) {
|
||||
clog.warn("Entrée utilisateur sans identifiant ignorée dans users.json.");
|
||||
continue;
|
||||
}
|
||||
if(user?.labels?.includes("DELETED")) {
|
||||
clog.log(`Utilisateur ${user.identity.id} marqué comme supprimé, ignoré.`);
|
||||
userList.push(new User(null, user.identity, [], user.labels));
|
||||
userList.push(new User(null, user.identity, [], user.labels, user.banInfo));
|
||||
continue; // Skip deleted users
|
||||
}
|
||||
userList.push(new User(user.auth, user.identity, user.tokens, user.labels));
|
||||
userList.push(new User(user.auth, user.identity, user.tokens, user.labels, user.banInfo));
|
||||
}
|
||||
clog.log(`Chargement de ${userList.length} utilisateurs.`);
|
||||
return userList;
|
||||
@@ -481,12 +624,14 @@ function loadUsers() {
|
||||
|
||||
function saveUsers() {
|
||||
UserDB.data = userList.map(user => {
|
||||
return {
|
||||
const data = {
|
||||
auth: user.auth,
|
||||
identity: user.identity,
|
||||
tokens: user.tokens,
|
||||
labels: user.labels,
|
||||
};
|
||||
if (user.banInfo) data.banInfo = user.banInfo;
|
||||
return data;
|
||||
});
|
||||
UserDB.save()
|
||||
clog.log(`Sauvegarde de ${userList.length} utilisateurs.`);
|
||||
@@ -503,9 +648,9 @@ function clearNeedUpdateForUsers() {
|
||||
}
|
||||
|
||||
|
||||
module.exports = {User}
|
||||
module.exports = {
|
||||
addUser,
|
||||
User,
|
||||
addUser,
|
||||
setFullBan,
|
||||
removeUser,
|
||||
getUserByToken,
|
||||
@@ -522,5 +667,6 @@ module.exports = {
|
||||
refreshAllUserInformation,
|
||||
updateIdentity,
|
||||
clearNeedUpdateForUsers,
|
||||
deleteAccount
|
||||
deleteAccount,
|
||||
revokeTokens
|
||||
};
|
||||
Reference in new issue
Block a user