Files
HytaleMMORPG/Webapp/server.js
T
gpatruno 96144d809c
Build / build (push) Has been cancelled
update
2026-06-08 23:35:37 +02:00

359 lines
9.4 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);
const deathTime = player.death_time ?? null;
const isDead = deathTime != null && Number(deathTime) > 0;
return {
...player,
classLabel,
raceLabel,
jobs,
powers,
isOnline: player.is_connected === 1,
isDead,
lifeStatus: isDead ? "Mort" : "Vivant",
deathTimeFormatted: isDead ? fmt.formatTimestamp(deathTime) : null,
total_time_play: player.total_time_play ?? 0,
};
}
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}`,
);
}
});