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 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}`, ); } });