Skip to content

Commit 0a118d7

Browse files
committed
webhook
1 parent 4b5c895 commit 0a118d7

File tree

8 files changed

+613
-34
lines changed

8 files changed

+613
-34
lines changed

server/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import securityRoutes from './routes/security';
2020
import { updateActivity } from './middleware/authMiddleware';
2121
import EmailService from './services/EmailService';
2222
import PM2LogService from './services/PM2LogService';
23+
import { discordWebhookService } from './services/DiscordWebhookService';
24+
import { serverProvisioningMonitor } from './services/ServerProvisioningMonitor';
2325

2426
// Load environment variables
2527
dotenv.config();
@@ -136,6 +138,12 @@ app.use('/api/system', systemRoutes);
136138
app.use('/api/audit', securityRoutes);
137139
app.use('/api/security', securityRoutes);
138140

141+
// Test routes (only in development)
142+
if (NODE_ENV === 'development') {
143+
const testNotificationRoutes = require('./routes/test-notifications').default;
144+
app.use('/api/test-notifications', testNotificationRoutes);
145+
}
146+
139147
// Health check endpoint
140148
app.get('/api/health', (req, res) => {
141149
res.json({
@@ -161,6 +169,13 @@ if (require('fs').existsSync(clientDistPath)) {
161169
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
162170
console.error('Global error handler:', err);
163171

172+
// Send Discord notification for errors
173+
if (discordWebhookService.isConfigured()) {
174+
discordWebhookService.sendPanelError(err, req).catch(error => {
175+
console.error('Failed to send Discord notification:', error);
176+
});
177+
}
178+
164179
res.status(err.status || 500).json({
165180
success: false,
166181
error: NODE_ENV === 'production' ? 'Internal server error' : err.message,
@@ -213,6 +228,10 @@ async function startServer() {
213228

214229
// Initialize PM2 log streaming based on configuration
215230
initializePM2Logging();
231+
232+
// Start server provisioning monitor
233+
serverProvisioningMonitor.start();
234+
console.log(`🔍 Server provisioning monitor started`);
216235
});
217236
} catch (error) {
218237
console.error('Failed to start server:', error);
@@ -224,6 +243,7 @@ async function startServer() {
224243
process.on('SIGTERM', async () => {
225244
console.log('🛑 SIGTERM received, shutting down gracefully');
226245
PM2LogService.stopStreaming();
246+
serverProvisioningMonitor.stop();
227247
io.close();
228248
await mongoose.connection.close();
229249
process.exit(0);
@@ -232,6 +252,7 @@ process.on('SIGTERM', async () => {
232252
process.on('SIGINT', async () => {
233253
console.log('🛑 SIGINT received, shutting down gracefully');
234254
PM2LogService.stopStreaming();
255+
serverProvisioningMonitor.stop();
235256
io.close();
236257
await mongoose.connection.close();
237258
process.exit(0);
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import rateLimit, { RateLimitRequestHandler, Options } from 'express-rate-limit';
2+
import { Request, Response } from 'express';
3+
import { discordWebhookService } from '../services/DiscordWebhookService';
4+
5+
interface CustomRateLimitOptions extends Partial<Options> {
6+
notificationThreshold?: number; // Send notification after N hits
7+
}
8+
9+
export function createRateLimiter(
10+
windowMs: number,
11+
max: number,
12+
message: any,
13+
endpointName: string,
14+
options?: CustomRateLimitOptions
15+
): RateLimitRequestHandler {
16+
const notificationThreshold = options?.notificationThreshold || max;
17+
const hitCounts = new Map<string, number>();
18+
19+
return rateLimit({
20+
windowMs,
21+
max,
22+
message,
23+
standardHeaders: true,
24+
legacyHeaders: false,
25+
skipSuccessfulRequests: false,
26+
keyGenerator: (req: Request) => {
27+
// Use IP address as the key
28+
return req.ip || 'unknown';
29+
},
30+
handler: async (req: Request, res: Response) => {
31+
const key = req.ip || 'unknown';
32+
const userId = (req.session as any)?.adminId || 'Anonymous';
33+
34+
// Track hit counts
35+
const currentHits = (hitCounts.get(key) || 0) + 1;
36+
hitCounts.set(key, currentHits);
37+
38+
// Send Discord notification when threshold is reached
39+
if (currentHits === notificationThreshold && discordWebhookService.isConfigured()) {
40+
discordWebhookService.sendRateLimitNotification(
41+
userId,
42+
`${req.method} ${req.originalUrl}`,
43+
key,
44+
max,
45+
windowMs
46+
).catch(err => console.error('Failed to send rate limit notification:', err));
47+
}
48+
49+
// Reset hit count after window expires
50+
setTimeout(() => {
51+
hitCounts.delete(key);
52+
}, windowMs);
53+
54+
// Send the rate limit response
55+
res.status(429).json(message);
56+
},
57+
...options
58+
});
59+
}
60+
61+
// Export pre-configured rate limiters
62+
export const loginRateLimit = createRateLimiter(
63+
15 * 60 * 1000, // 15 minutes
64+
5, // 5 attempts
65+
{
66+
success: false,
67+
error: 'Too many login attempts, please try again later'
68+
},
69+
'Login Endpoint'
70+
);
71+
72+
export const codeRequestRateLimit = createRateLimiter(
73+
5 * 60 * 1000, // 5 minutes
74+
3, // 3 requests
75+
{
76+
success: false,
77+
error: 'Too many code requests, please try again later'
78+
},
79+
'Code Request Endpoint'
80+
);
81+
82+
export const configUpdateRateLimit = createRateLimiter(
83+
15 * 60 * 1000, // 15 minutes
84+
20, // 20 updates
85+
{
86+
success: false,
87+
error: 'Too many configuration updates, please try again later'
88+
},
89+
'Configuration Update Endpoint',
90+
{ notificationThreshold: 15 } // Notify at 15 hits instead of 20
91+
);

server/routes/auth.ts

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Router, Request, Response } from 'express';
2-
import rateLimit from 'express-rate-limit';
32
import mongoose, { Schema, model, Document, Model } from 'mongoose';
43
import EmailService from '../services/EmailService';
54
import { requireAuth } from '../middleware/authMiddleware';
5+
import { loginRateLimit, codeRequestRateLimit } from '../middleware/rateLimitMiddleware';
66

77
// Define IAdminUser directly in this file
88
interface IAdminUser extends Document {
@@ -29,30 +29,6 @@ const getAdminUserModel = (): Model<IAdminUser> => {
2929

3030
const router = Router();
3131

32-
// Rate limiting for login attempts
33-
const loginRateLimit = rateLimit({
34-
windowMs: 15 * 60 * 1000, // 15 minutes
35-
max: 5, // 5 attempts per window
36-
message: {
37-
success: false,
38-
error: 'Too many login attempts, please try again later'
39-
},
40-
standardHeaders: true,
41-
legacyHeaders: false
42-
});
43-
44-
// Rate limiting for code requests
45-
const codeRequestRateLimit = rateLimit({
46-
windowMs: 5 * 60 * 1000, // 5 minutes
47-
max: 3, // 3 code requests per window
48-
message: {
49-
success: false,
50-
error: 'Too many code requests, please try again later'
51-
},
52-
standardHeaders: true,
53-
legacyHeaders: false
54-
});
55-
5632
/**
5733
* POST /api/auth/request-code
5834
* Request verification code for admin email

server/routes/servers.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Router, Request, Response } from 'express';
22
import mongoose, { Schema, model, Document, Model } from 'mongoose';
33
import { IModlServer as IModlServerShared, ApiResponse, ModlServerSchema } from '@modl-gg/shared-web';
44
import { requireAuth } from '../middleware/authMiddleware';
5+
import { discordWebhookService } from '../services/DiscordWebhookService';
56

67
type IModlServer = IModlServerShared & Document;
78

@@ -353,6 +354,26 @@ router.post('/bulk', async (req: Request, res: Response) => {
353354
}
354355
);
355356
affectedCount = result.modifiedCount;
357+
358+
// Send Discord notifications for suspended servers
359+
if (discordWebhookService.isConfigured() && affectedCount > 0) {
360+
const suspendedServers = await ModlServerModel.find({
361+
_id: { $in: serverIds }
362+
}).select('_id name email plan');
363+
364+
for (const server of suspendedServers) {
365+
discordWebhookService.sendServerProvisioningFailure(
366+
server._id.toString(),
367+
server.name || 'Unnamed Server',
368+
'Server suspended by admin bulk action',
369+
{
370+
'Email': server.email || 'N/A',
371+
'Plan': server.plan || 'N/A',
372+
'Action': 'Bulk Suspend'
373+
}
374+
).catch(err => console.error('Discord notification error:', err));
375+
}
376+
}
356377
break;
357378

358379
case 'activate':

server/routes/system.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { Router, Request, Response } from 'express';
2-
import rateLimit from 'express-rate-limit';
32
import { z } from 'zod';
43
import mongoose, { Schema, model, Document, Model } from 'mongoose';
54
import { ISystemConfig as ISystemConfigShared, SystemConfigSchema } from '@modl-gg/shared-web';
65
import { requireAuth } from '../middleware/authMiddleware';
76
import { logAuditEvent } from './security';
87
import PM2LogService from '../services/PM2LogService';
8+
import { configUpdateRateLimit } from '../middleware/rateLimitMiddleware';
99

1010
type ISystemConfig = ISystemConfigShared & Document;
1111

@@ -114,12 +114,6 @@ export async function getMainConfig(): Promise<ISystemConfig> {
114114
return config!;
115115
}
116116

117-
// Rate limiting configuration
118-
const configRateLimit = rateLimit({
119-
windowMs: 15 * 60 * 1000, // 15 minutes
120-
max: 20, // Increased limit for config changes
121-
message: { error: 'Too many configuration changes' }
122-
});
123117

124118
// Configuration validation schema
125119
const configSchema = z.object({
@@ -190,7 +184,7 @@ router.get('/config', async (req, res) => {
190184
});
191185

192186
// Update system configuration
193-
router.put('/config', configRateLimit, async (req, res) => {
187+
router.put('/config', configUpdateRateLimit, async (req, res) => {
194188
try {
195189
const validatedConfig = configSchema.parse(req.body);
196190

@@ -331,7 +325,7 @@ router.get('/rate-limits', async (req, res) => {
331325
});
332326

333327
// Update rate limits
334-
router.put('/rate-limits', configRateLimit, async (req, res) => {
328+
router.put('/rate-limits', configUpdateRateLimit, async (req, res) => {
335329
try {
336330
const { rateLimitRequests, rateLimitWindow } = req.body;
337331

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { Router, Request, Response } from 'express';
2+
import { requireAuth } from '../middleware/authMiddleware';
3+
import { discordWebhookService, NotificationType } from '../services/DiscordWebhookService';
4+
5+
const router = Router();
6+
7+
// Apply authentication to all test routes
8+
router.use(requireAuth);
9+
10+
/**
11+
* POST /api/test-notifications/discord
12+
* Test Discord webhook notifications
13+
*/
14+
router.post('/discord', async (req: Request, res: Response) => {
15+
try {
16+
const { type = 'all' } = req.body;
17+
18+
if (!discordWebhookService.isConfigured()) {
19+
return res.status(400).json({
20+
success: false,
21+
error: 'Discord webhook is not configured. Please set DISCORD_WEBHOOK_URL environment variable.'
22+
});
23+
}
24+
25+
const notifications = [];
26+
27+
// Test panel error notification
28+
if (type === 'all' || type === 'error') {
29+
await discordWebhookService.sendPanelError(
30+
new Error('Test error: This is a test notification'),
31+
req
32+
);
33+
notifications.push('Panel error notification');
34+
}
35+
36+
// Test server provisioning failure notification
37+
if (type === 'all' || type === 'provisioning') {
38+
await discordWebhookService.sendServerProvisioningFailure(
39+
'test-server-123',
40+
'Test Server',
41+
'Test provisioning failure: This is a test notification',
42+
{
43+
'Email': 'test@example.com',
44+
'Plan': 'premium',
45+
'Test': 'true'
46+
}
47+
);
48+
notifications.push('Server provisioning failure notification');
49+
}
50+
51+
// Test rate limit notification
52+
if (type === 'all' || type === 'ratelimit') {
53+
await discordWebhookService.sendRateLimitNotification(
54+
'test-user-123',
55+
'/api/test-endpoint',
56+
req.ip || '127.0.0.1',
57+
10,
58+
15 * 60 * 1000
59+
);
60+
notifications.push('Rate limit notification');
61+
}
62+
63+
return res.json({
64+
success: true,
65+
message: 'Test notifications sent successfully',
66+
data: {
67+
notificationsSent: notifications,
68+
webhookConfigured: true,
69+
adminRoleConfigured: !!process.env.DISCORD_ADMIN_ROLE_ID
70+
}
71+
});
72+
} catch (error) {
73+
console.error('Test notification error:', error);
74+
return res.status(500).json({
75+
success: false,
76+
error: 'Failed to send test notifications',
77+
details: error instanceof Error ? error.message : 'Unknown error'
78+
});
79+
}
80+
});
81+
82+
/**
83+
* GET /api/test-notifications/status
84+
* Check Discord webhook configuration status
85+
*/
86+
router.get('/status', async (req: Request, res: Response) => {
87+
const status = {
88+
webhookConfigured: discordWebhookService.isConfigured(),
89+
webhookUrl: process.env.DISCORD_WEBHOOK_URL ? 'Set (hidden)' : 'Not set',
90+
adminRoleId: process.env.DISCORD_ADMIN_ROLE_ID ? 'Set (hidden)' : 'Not set',
91+
environment: {
92+
NODE_ENV: process.env.NODE_ENV,
93+
hasWebhookUrl: !!process.env.DISCORD_WEBHOOK_URL,
94+
hasAdminRoleId: !!process.env.DISCORD_ADMIN_ROLE_ID
95+
}
96+
};
97+
98+
return res.json({
99+
success: true,
100+
data: status
101+
});
102+
});
103+
104+
export default router;

0 commit comments

Comments
 (0)