353 lines
9.1 KiB
JavaScript
353 lines
9.1 KiB
JavaScript
require("dotenv").config();
|
|
|
|
const express = require("express");
|
|
const session = require("express-session");
|
|
const path = require("path");
|
|
const db = require("./src/db");
|
|
const auth = require("./src/auth");
|
|
const labels = require("./src/labels");
|
|
const fmt = require("./src/format");
|
|
|
|
const inventory = require("./src/inventory");
|
|
const itemIcons = require("./src/itemIcons");
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 3000;
|
|
|
|
app.set("view engine", "ejs");
|
|
app.set("views", path.join(__dirname, "views"));
|
|
app.use(express.static(path.join(__dirname, "public")));
|
|
|
|
app.get("/icons/items/:itemId.png", (req, res) => {
|
|
const itemId = decodeURIComponent(req.params.itemId);
|
|
const data = itemIcons.getIconData(itemId);
|
|
if (!data) {
|
|
res.type("image/svg+xml");
|
|
return res.sendFile(path.join(__dirname, "public/icons/unknown.svg"));
|
|
}
|
|
res.set("Content-Type", "image/png");
|
|
res.set("Cache-Control", "public, max-age=86400");
|
|
res.send(data);
|
|
});
|
|
|
|
app.use(express.urlencoded({ extended: false }));
|
|
|
|
app.use(
|
|
session({
|
|
secret: process.env.SESSION_SECRET || "change-me-in-production",
|
|
resave: false,
|
|
saveUninitialized: false,
|
|
cookie: {
|
|
httpOnly: true,
|
|
maxAge: 7 * 24 * 60 * 60 * 1000,
|
|
sameSite: "lax",
|
|
},
|
|
}),
|
|
);
|
|
|
|
app.use((req, res, next) => {
|
|
res.locals.nav = [
|
|
{ href: "/", label: "Dashboard", key: "dashboard" },
|
|
{ href: "/players", label: "Joueurs", key: "players" },
|
|
{ href: "/market", label: "Marché", key: "market" },
|
|
{ href: "/profile", label: "Mon profil", key: "profile" },
|
|
];
|
|
res.locals.fmt = fmt;
|
|
res.locals.auth = auth;
|
|
res.locals.db = db;
|
|
res.locals.itemIcons = itemIcons;
|
|
res.locals.dbStatus = db.getDbStatus();
|
|
res.locals.sessionUser = req.session.playerUuid
|
|
? {
|
|
uuid: req.session.playerUuid,
|
|
displayName: req.session.displayName,
|
|
}
|
|
: null;
|
|
next();
|
|
});
|
|
|
|
function loadPlayerInventory(player) {
|
|
const characterId = inventory.resolveCharacterId(player);
|
|
const slots = db.getCharacterInventory(characterId);
|
|
return inventory.buildInventoryView(slots);
|
|
}
|
|
|
|
function enrichCharacter(character) {
|
|
if (!character) return null;
|
|
|
|
const classLabel = labels.labelClass(
|
|
character.class_id,
|
|
db.getReferenceLabel("classes", character.class_id),
|
|
);
|
|
const raceLabel = labels.labelRace(
|
|
character.race_id,
|
|
db.getReferenceLabel("races", character.race_id),
|
|
);
|
|
const jobs = labels.parseJsonArray(character.jobs).map(labels.labelJob);
|
|
const powers = labels.parseJsonArray(character.powers).map(labels.labelPower);
|
|
|
|
return {
|
|
...character,
|
|
display_name: character.name ?? character.display_name,
|
|
classLabel,
|
|
raceLabel,
|
|
jobs,
|
|
powers,
|
|
};
|
|
}
|
|
|
|
function loadAccountCharacters(accountUuid) {
|
|
if (db.hasCharactersTable()) {
|
|
const activeCharacterId = db.getActiveCharacterId(accountUuid);
|
|
return db.getCharactersByAccountUuid(accountUuid).map((character) => {
|
|
const enriched = enrichCharacter(character);
|
|
return {
|
|
...enriched,
|
|
character_id: character.id,
|
|
isActive: character.id === activeCharacterId,
|
|
playerInventory: inventory.buildInventoryView(
|
|
db.getCharacterInventory(character.id),
|
|
),
|
|
};
|
|
});
|
|
}
|
|
|
|
const player = enrichPlayer(db.getPlayerByUuid(accountUuid));
|
|
if (!player) {
|
|
return [];
|
|
}
|
|
|
|
return [
|
|
{
|
|
...player,
|
|
character_id: inventory.resolveCharacterId(player),
|
|
isActive: true,
|
|
playerInventory: loadPlayerInventory(player),
|
|
},
|
|
];
|
|
}
|
|
|
|
function enrichPlayer(player) {
|
|
if (!player) return null;
|
|
|
|
const classLabel = labels.labelClass(
|
|
player.class_id,
|
|
db.getReferenceLabel("classes", player.class_id),
|
|
);
|
|
const raceLabel = labels.labelRace(
|
|
player.race_id,
|
|
db.getReferenceLabel("races", player.race_id),
|
|
);
|
|
const jobs = labels.parseJsonArray(player.jobs).map(labels.labelJob);
|
|
const powers = labels.parseJsonArray(player.powers).map(labels.labelPower);
|
|
|
|
return {
|
|
...player,
|
|
classLabel,
|
|
raceLabel,
|
|
jobs,
|
|
powers,
|
|
isOnline: player.is_connected === 1,
|
|
};
|
|
}
|
|
|
|
function requireLogin(req, res, next) {
|
|
if (!req.session.playerUuid) {
|
|
return res.redirect("/profile");
|
|
}
|
|
next();
|
|
}
|
|
|
|
app.get("/", (req, res) => {
|
|
const stats = db.getDashboardStats();
|
|
const onlinePlayers = db.getOnlinePlayers().map(enrichPlayer);
|
|
const recentPlayers = db.getRecentPlayers().map(enrichPlayer);
|
|
|
|
res.render("index", {
|
|
page: "dashboard",
|
|
title: "Dashboard",
|
|
stats,
|
|
onlinePlayers,
|
|
recentPlayers,
|
|
});
|
|
});
|
|
|
|
app.get("/players", (req, res) => {
|
|
const players = db.getAllPlayers().map(enrichPlayer);
|
|
|
|
res.render("players/list", {
|
|
page: "players",
|
|
title: "Joueurs",
|
|
players,
|
|
});
|
|
});
|
|
|
|
app.get("/players/:uuid", (req, res) => {
|
|
const player = enrichPlayer(db.getPlayerByUuid(req.params.uuid));
|
|
if (!player) {
|
|
return res.status(404).render("error", {
|
|
page: "players",
|
|
title: "Joueur introuvable",
|
|
message: "Ce joueur n'existe pas dans la base de données.",
|
|
});
|
|
}
|
|
|
|
const group = db.getGroupForPlayer(player.uuid);
|
|
|
|
res.render("players/profile", {
|
|
page: "players",
|
|
title: player.display_name,
|
|
player,
|
|
group,
|
|
comingSoon: false,
|
|
isOwner: req.session.playerUuid === player.uuid,
|
|
});
|
|
});
|
|
|
|
app.get("/market", (req, res) => {
|
|
res.render("coming-soon", {
|
|
page: "market",
|
|
title: "Marché",
|
|
heading: "Marché du serveur",
|
|
description:
|
|
"La place de marché permettra bientôt d'échanger des objets et consulter les annonces entre joueurs.",
|
|
});
|
|
});
|
|
|
|
app.get("/profile", (req, res) => {
|
|
if (req.session.playerUuid) {
|
|
const player = enrichPlayer(db.getPlayerByUuid(req.session.playerUuid));
|
|
if (!player) {
|
|
req.session.destroy(() => {
|
|
res.redirect("/profile");
|
|
});
|
|
return;
|
|
}
|
|
|
|
const group = db.getGroupForPlayer(player.uuid);
|
|
const characters = loadAccountCharacters(req.session.playerUuid);
|
|
|
|
return res.render("profile/account", {
|
|
page: "profile",
|
|
title: "Mon profil",
|
|
player,
|
|
group,
|
|
characters,
|
|
});
|
|
}
|
|
|
|
res.render("profile/login", {
|
|
page: "profile",
|
|
title: "Connexion",
|
|
error: null,
|
|
pseudo: "",
|
|
needsMigration: db.getDbStatus().exists && !db.hasPasswordColumn(),
|
|
});
|
|
});
|
|
|
|
app.post("/profile/login", async (req, res) => {
|
|
const pseudo = (req.body.pseudo || "").trim();
|
|
const password = req.body.password || "";
|
|
|
|
if (!pseudo || !password) {
|
|
return res.status(400).render("profile/login", {
|
|
page: "profile",
|
|
title: "Connexion",
|
|
error: "Pseudo et mot de passe requis.",
|
|
pseudo,
|
|
needsMigration: db.getDbStatus().exists && !db.hasPasswordColumn(),
|
|
});
|
|
}
|
|
|
|
if (!db.getDbStatus().exists) {
|
|
return res.status(503).render("profile/login", {
|
|
page: "profile",
|
|
title: "Connexion",
|
|
error: "Base de données indisponible.",
|
|
pseudo,
|
|
needsMigration: false,
|
|
});
|
|
}
|
|
|
|
if (!db.hasPasswordColumn()) {
|
|
return res.status(503).render("profile/login", {
|
|
page: "profile",
|
|
title: "Connexion",
|
|
error: "Migration web manquante. Redémarrez le serveur Hytale pour mettre à jour la base.",
|
|
pseudo,
|
|
needsMigration: true,
|
|
});
|
|
}
|
|
|
|
const record = db.getPlayerAuthByDisplayName(pseudo);
|
|
if (!record) {
|
|
return res.status(401).render("profile/login", {
|
|
page: "profile",
|
|
title: "Connexion",
|
|
error: "Pseudo introuvable. Connectez-vous d'abord au serveur Hytale pour créer votre profil.",
|
|
pseudo,
|
|
needsMigration: false,
|
|
});
|
|
}
|
|
|
|
if (!record.password_hash) {
|
|
return res.status(401).render("profile/login", {
|
|
page: "profile",
|
|
title: "Connexion",
|
|
error: "Aucun mot de passe défini. Utilisez /mmorpg password <mot_de_passe> en jeu.",
|
|
pseudo,
|
|
needsMigration: false,
|
|
});
|
|
}
|
|
|
|
const valid = await auth.verifyPassword(password, record.password_hash);
|
|
if (!valid) {
|
|
return res.status(401).render("profile/login", {
|
|
page: "profile",
|
|
title: "Connexion",
|
|
error: "Mot de passe incorrect.",
|
|
pseudo,
|
|
needsMigration: false,
|
|
});
|
|
}
|
|
|
|
req.session.playerUuid = record.uuid;
|
|
req.session.displayName = record.display_name;
|
|
res.redirect("/profile");
|
|
});
|
|
|
|
app.post("/profile/logout", requireLogin, (req, res) => {
|
|
req.session.destroy(() => {
|
|
res.redirect("/profile");
|
|
});
|
|
});
|
|
|
|
app.use((req, res) => {
|
|
res.status(404).render("error", {
|
|
page: "",
|
|
title: "Page introuvable",
|
|
message: "La page demandée n'existe pas.",
|
|
});
|
|
});
|
|
|
|
app.listen(PORT, () => {
|
|
const status = db.getDbStatus();
|
|
console.log(`Orion.ovh Web → http://localhost:${PORT}`);
|
|
if (!status.exists) {
|
|
console.warn(`Base SQLite introuvable : ${status.path}`);
|
|
console.warn("Lancez le serveur Hytale pour créer mmorpg.db à la racine du monorepo.");
|
|
}
|
|
const icons = itemIcons.getStatus();
|
|
if (!icons.available) {
|
|
console.warn("Icônes Hytale indisponibles.");
|
|
if (icons.error) {
|
|
console.warn(` → ${icons.error}`);
|
|
}
|
|
console.warn(" → Définissez HYTALE_ASSETS_ZIP dans Webapp/.env si besoin.");
|
|
} else {
|
|
console.log(
|
|
`Icônes Hytale (${icons.backend}) : ${icons.indexedIcons} PNG indexés, ${icons.indexedItems} items — ${icons.assetsZip}`,
|
|
);
|
|
}
|
|
});
|