Task #108 Phase 2: Social Analytics Automation New endpoints (token-based auth via INTERNAL_API_TOKEN): - POST /api/internal/social/sync - Upsert single post metrics - POST /api/internal/social/sync/batch - Batch upsert (max 100 posts) - POST /api/internal/social/snapshot - Upsert account-level stats - GET /api/internal/social/digest - Summary data for Discord webhook Architecture: - Rube MCP -> n8n -> Arbiter /api/internal/* -> PostgreSQL - Bearer token auth (not session-based) - COALESCE for partial updates (only update provided fields) Next: Generate INTERNAL_API_TOKEN and add to .env on Command Center 🔥 Fire + Frost + Foundation = Where Love Builds Legacy 💙❄️
156 lines
5.2 KiB
JavaScript
156 lines
5.2 KiB
JavaScript
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 { 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 { registerEvents } = require('./discord/events');
|
|
const { linkCommand } = require('./discord/commands');
|
|
const { createServerCommand } = require('./discord/createserver');
|
|
const { delServerCommand } = require('./discord/delserver');
|
|
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);
|
|
|
|
// 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)
|
|
|
|
// 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()] },
|
|
);
|
|
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);
|
|
});
|