require('dotenv').config(); const express = require('express'); const expressLayouts = require('express-ejs-layouts'); const session = require('express-session'); const PgSession = require('connect-pg-simple')(session); const passport = require('passport'); const DiscordStrategy = require('passport-discord').Strategy; const { Client, GatewayIntentBits, REST, Routes } = require('discord.js'); const csrf = require('csurf'); const cors = require('cors'); const path = require('path'); const { Pool } = require('pg'); const authRoutes = require('./routes/auth'); const adminRoutes = require('./routes/admin/index'); const webhookRoutes = require('./routes/webhook'); const stripeRoutes = require('./routes/stripe'); const apiRoutes = require('./routes/api'); const mvcRoutes = require('./routes/mvc'); const { registerEvents } = require('./discord/events'); const { linkCommand } = require('./discord/commands'); const { createServerCommand } = require('./discord/createserver'); const { delServerCommand } = require('./discord/delserver'); const { tasksCommand } = require('./discord/tasks'); const { verifyMvcCommand } = require('./discord/verifymvc'); const { initCron } = require('./sync/cron'); const discordRoleSync = require('./services/discordRoleSync'); // PostgreSQL connection pool for sessions const pgPool = new Pool({ user: process.env.DB_USER, host: process.env.DB_HOST, database: process.env.DB_NAME, password: process.env.DB_PASSWORD, port: process.env.DB_PORT || 5432 }); // Initialize Discord Client const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers] }); registerEvents(client); // Passport Configuration passport.serializeUser((user, done) => done(null, user)); passport.deserializeUser((obj, done) => done(null, obj)); passport.use(new DiscordStrategy({ clientID: process.env.DISCORD_CLIENT_ID, clientSecret: process.env.DISCORD_CLIENT_SECRET, callbackURL: process.env.REDIRECT_URI, scope: ['identify'] }, (accessToken, refreshToken, profile, done) => { return done(null, profile); })); // Initialize Express App const app = express(); app.set('trust proxy', 1); app.set('view engine', 'ejs'); app.set('views', __dirname + '/views'); // Enable proper layout rendering with express-ejs-layouts app.use(expressLayouts); app.set('layout', 'layout'); // Default layout is views/layout.ejs // HTMX Middleware: Disable layout wrapper for HTMX AJAX requests // HTMX sends HX-Request header - these requests need raw HTML fragments, not full layout app.use((req, res, next) => { if (req.headers['hx-request']) { res.locals.layout = false; } next(); }); // CRITICAL: Stripe webhook needs raw body BEFORE express.json() middleware // Mounted at /webhooks/stripe to avoid conflict with /stripe checkout mount app.use('/webhooks/stripe', stripeRoutes); // Static files (PWA manifest, icons, service worker) app.use(express.static(path.join(__dirname, 'public'))); // Body parsing middleware (comes AFTER webhook route) app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Make Discord client accessible to routes app.locals.client = client; app.use(session({ store: new PgSession({ pool: pgPool, tableName: 'session', createTableIfMissing: true }), secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { secure: process.env.NODE_ENV === 'production', maxAge: 7 * 24 * 60 * 60 * 1000 } })); app.use(passport.initialize()); app.use(passport.session()); // Health Check app.get('/health', (req, res) => { res.json({ status: 'online', uptime: process.uptime(), bot: client.user?.tag || 'not ready' }); }); // Root redirect to admin app.get('/', (req, res) => res.redirect('/admin')); // CSRF Protection (session-based) const csrfProtection = csrf({ cookie: false }); // Register Routes app.use('/auth', authRoutes); app.use('/admin', csrfProtection, adminRoutes); app.use('/webhook', webhookRoutes); app.use('/stripe', stripeRoutes); // Checkout and portal routes (uses JSON body) app.use('/api/internal', apiRoutes); // Internal API for n8n (token-based auth) app.use('/api/mvc', mvcRoutes); // ModpackChecker licensing API (public) // Start Application const PORT = process.env.PORT || 3500; app.listen(PORT, () => { console.log(`🌐 Express server running on port ${PORT}`); console.log(`📍 Admin panel: https://discord-bot.firefrostgaming.com/admin`); client.login(process.env.DISCORD_BOT_TOKEN); }); // Register Slash Commands const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_BOT_TOKEN); (async () => { try { console.log('Refreshing application (/) commands.'); await rest.put( Routes.applicationGuildCommands(process.env.DISCORD_CLIENT_ID, process.env.GUILD_ID), { body: [linkCommand.toJSON(), createServerCommand.toJSON(), delServerCommand.toJSON(), tasksCommand.toJSON(), verifyMvcCommand.toJSON()] }, ); console.log('✅ Successfully reloaded application (/) commands.'); } catch (error) { console.error('Failed to register slash commands:', error); } })(); // Initialize Hourly Cron Job initCron(); console.log('✅ Hourly sync cron initialized.'); // Initialize Server Status Poller (updates Discord status channels every 5 minutes) const serverStatusPoller = require('./services/serverStatusPoller'); serverStatusPoller.start(5); // 5 minute interval console.log('✅ Server status poller initialized (5 min interval).'); // Error handling process.on('unhandledRejection', error => { console.error('❌ Unhandled promise rejection:', error); });