WHAT THIS ADDS: - Discord role sync on new subscriptions (checkout.session.completed) - Discord role removal on chargebacks (charge.dispute.created) - Grace period expiration job (hourly cron check) - Automatic downgrade to Awakened when grace period expires NEW FILES: - src/services/discordRoleSync.js - Role add/remove/sync functions - src/sync/graceExpiration.js - Grace period expiration processor MODIFIED FILES: - src/routes/stripe.js - Added role sync calls to webhook handlers - src/discord/events.js - Initialize role sync service on bot ready - src/sync/cron.js - Added grace period check to hourly job - src/index.js - Import discordRoleSync service PHILOSOPHY: 'We Don't Kick People Out' - expired grace periods downgrade to permanent Awakened tier (tier 1, lifetime). Users keep community access, just lose premium perks. ROLE MAPPING (tier_level -> role key): 1=the-awakened, 2=fire-elemental, 3=frost-elemental, 4=fire-knight, 5=frost-knight, 6=fire-master, 7=frost-master, 8=fire-legend, 9=frost-legend, 10=the-sovereign CHARGEBACKS: - Immediate role removal - Added to banned_users table - Full audit logging Signed-off-by: Claude (Chronicler #62) <claude@firefrostgaming.com>
147 lines
4.6 KiB
JavaScript
147 lines
4.6 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 { registerEvents } = require('./discord/events');
|
|
const { linkCommand } = require('./discord/commands');
|
|
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)
|
|
|
|
// 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()] },
|
|
);
|
|
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.');
|
|
|
|
// Error handling
|
|
process.on('unhandledRejection', error => {
|
|
console.error('❌ Unhandled promise rejection:', error);
|
|
});
|