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 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.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.dbStatus = db.getDbStatus(); res.locals.sessionUser = req.session.playerUuid ? { uuid: req.session.playerUuid, displayName: req.session.displayName, } : null; next(); }); 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: true, 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); return res.render("profile/account", { page: "profile", title: "Mon profil", player, group, }); } 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."); } });