Skip to content

Commit 170e9b2

Browse files
committed
Add pagination to server list menu
1 parent 1531ff2 commit 170e9b2

File tree

8 files changed

+615
-137
lines changed

8 files changed

+615
-137
lines changed

client/src/lib/api.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,13 @@ class ApiClient {
9393
search?: string;
9494
plan?: string;
9595
status?: string;
96+
sort?: string;
97+
order?: 'asc' | 'desc';
9698
}) {
9799
const queryParams = new URLSearchParams();
98100
if (params) {
99101
Object.entries(params).forEach(([key, value]) => {
100-
if (value !== undefined) {
102+
if (value !== undefined && value !== null) {
101103
queryParams.append(key, value.toString());
102104
}
103105
});
@@ -115,6 +117,17 @@ class ApiClient {
115117
return this.request(`/servers/${id}/stats`);
116118
}
117119

120+
async updateServerStats(id: string, stats: {
121+
userCount?: number;
122+
ticketCount?: number;
123+
lastActivityAt?: string;
124+
}) {
125+
return this.request(`/servers/${id}/stats`, {
126+
method: 'PUT',
127+
body: JSON.stringify(stats),
128+
});
129+
}
130+
118131
async updateServer(id: string, data: any) {
119132
return this.request(`/servers/${id}`, {
120133
method: 'PUT',

client/src/pages/ServersPage.tsx

Lines changed: 359 additions & 92 deletions
Large diffs are not rendered by default.

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"author": "modl-gg",
2424
"license": "AGPL-3.0-only",
2525
"dependencies": {
26-
"@modl-gg/shared-web": "1.0.1",
26+
"@modl-gg/shared-web": "1.0.2",
2727
"@tanstack/react-query-devtools": "^5.81.2",
2828
"@vitejs/plugin-react": "^4.6.0",
2929
"compression": "^1.7.4",

server/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { updateActivity } from './middleware/authMiddleware';
2121
import EmailService from './services/EmailService';
2222
import PM2LogService from './services/PM2LogService';
2323
import { discordWebhookService } from './services/DiscordWebhookService';
24+
import { webhookConfigService } from './services/WebhookConfigService';
2425
import { serverProvisioningMonitor } from './services/ServerProvisioningMonitor';
2526

2627
// Load environment variables

server/routes/servers.ts

Lines changed: 128 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ router.use(requireAuth);
1919
/**
2020
* GET /api/servers
2121
* Get all servers with pagination and filtering
22+
* Supports: pagination, search, filtering by plan/status, sorting
2223
*/
2324
router.get('/', async (req: Request, res: Response) => {
2425
try {
@@ -32,22 +33,45 @@ router.get('/', async (req: Request, res: Response) => {
3233
order = 'desc'
3334
} = req.query;
3435

35-
const pageNum = parseInt(page as string);
36-
const limitNum = parseInt(limit as string);
36+
// Validate and sanitize pagination parameters
37+
const pageNum = Math.max(1, parseInt(page as string) || 1);
38+
const limitNum = Math.min(100, Math.max(1, parseInt(limit as string) || 20));
3739
const skip = (pageNum - 1) * limitNum;
3840

41+
// Validate sort field - only allow specific fields for security
42+
const allowedSortFields = [
43+
'serverName',
44+
'customDomain',
45+
'adminEmail',
46+
'plan',
47+
'createdAt',
48+
'updatedAt',
49+
'userCount',
50+
'ticketCount',
51+
'provisioningStatus',
52+
'lastActivityAt'
53+
];
54+
const sortField = allowedSortFields.includes(sort as string) ? sort as string : 'createdAt';
55+
const sortOrder = order === 'asc' ? 1 : -1;
56+
3957
// Build filter object
4058
const filter: any = {};
4159

42-
if (search) {
43-
filter.$text = { $search: search as string };
60+
if (search && (search as string).trim()) {
61+
const searchTerm = (search as string).trim();
62+
// Use regex for more flexible search across multiple fields
63+
filter.$or = [
64+
{ serverName: { $regex: searchTerm, $options: 'i' } },
65+
{ customDomain: { $regex: searchTerm, $options: 'i' } },
66+
{ adminEmail: { $regex: searchTerm, $options: 'i' } }
67+
];
4468
}
4569

4670
if (plan && plan !== 'all') {
4771
filter.plan = plan;
4872
}
4973

50-
if (status) {
74+
if (status && status !== 'all') {
5175
switch (status) {
5276
case 'active':
5377
filter.provisioningStatus = 'completed';
@@ -67,42 +91,52 @@ router.get('/', async (req: Request, res: Response) => {
6791

6892
// Build sort object
6993
const sortObj: any = {};
70-
sortObj[sort as string] = order === 'desc' ? -1 : 1;
94+
sortObj[sortField] = sortOrder;
7195

72-
// Execute queries
96+
// Execute queries with better error handling
7397
const ModlServerModel = getModlServerModel();
74-
const [servers, total] = await Promise.all([
75-
ModlServerModel
76-
.find(filter)
77-
.sort(sortObj)
78-
.skip(skip)
79-
.limit(limitNum)
80-
.lean(),
81-
ModlServerModel.countDocuments(filter)
82-
]);
83-
84-
const response: ApiResponse<{
85-
servers: IModlServer[];
86-
pagination: {
87-
page: number;
88-
limit: number;
89-
total: number;
90-
pages: number;
91-
};
92-
}> = {
93-
success: true,
94-
data: {
95-
servers: servers as IModlServer[],
98+
99+
try {
100+
const [servers, total] = await Promise.all([
101+
ModlServerModel
102+
.find(filter)
103+
.sort(sortObj)
104+
.skip(skip)
105+
.limit(limitNum)
106+
.select('-__v -emailVerificationToken -provisioningSignInToken') // Exclude sensitive fields
107+
.lean(),
108+
ModlServerModel.countDocuments(filter)
109+
]);
110+
111+
const response: ApiResponse<{
112+
servers: IModlServer[];
96113
pagination: {
97-
page: pageNum,
98-
limit: limitNum,
99-
total,
100-
pages: Math.ceil(total / limitNum)
114+
page: number;
115+
limit: number;
116+
total: number;
117+
pages: number;
118+
};
119+
}> = {
120+
success: true,
121+
data: {
122+
servers: servers as IModlServer[],
123+
pagination: {
124+
page: pageNum,
125+
limit: limitNum,
126+
total,
127+
pages: Math.ceil(total / limitNum)
128+
}
101129
}
102-
}
103-
};
130+
};
104131

105-
return res.json(response);
132+
return res.json(response);
133+
} catch (queryError) {
134+
console.error('Database query error:', queryError);
135+
return res.status(500).json({
136+
success: false,
137+
error: 'Database query failed'
138+
});
139+
}
106140
} catch (error) {
107141
console.error('Get servers error:', error);
108142
return res.status(500).json({
@@ -247,6 +281,64 @@ router.put('/:id', async (req: Request, res: Response) => {
247281
}
248282
});
249283

284+
/**
285+
* PUT /api/servers/:id/stats
286+
* Update server statistics (userCount, ticketCount, lastActivityAt)
287+
*/
288+
router.put('/:id/stats', async (req: Request, res: Response) => {
289+
try {
290+
const { id } = req.params;
291+
const { userCount, ticketCount, lastActivityAt } = req.body;
292+
293+
// Validate input
294+
const updateData: any = {};
295+
if (typeof userCount === 'number' && userCount >= 0) {
296+
updateData.userCount = userCount;
297+
}
298+
if (typeof ticketCount === 'number' && ticketCount >= 0) {
299+
updateData.ticketCount = ticketCount;
300+
}
301+
if (lastActivityAt) {
302+
updateData.lastActivityAt = new Date(lastActivityAt);
303+
}
304+
305+
if (Object.keys(updateData).length === 0) {
306+
return res.status(400).json({
307+
success: false,
308+
error: 'No valid stats provided to update'
309+
});
310+
}
311+
312+
updateData.updatedAt = new Date();
313+
314+
const ModlServerModel = getModlServerModel();
315+
const server = await ModlServerModel.findByIdAndUpdate(
316+
id,
317+
updateData,
318+
{ new: true, runValidators: true }
319+
);
320+
321+
if (!server) {
322+
return res.status(404).json({
323+
success: false,
324+
error: 'Server not found'
325+
});
326+
}
327+
328+
return res.json({
329+
success: true,
330+
data: server,
331+
message: 'Server statistics updated successfully'
332+
});
333+
} catch (error) {
334+
console.error('Update server stats error:', error);
335+
return res.status(500).json({
336+
success: false,
337+
error: 'Failed to update server statistics'
338+
});
339+
}
340+
});
341+
250342
/**
251343
* DELETE /api/servers/:id
252344
* Delete a server

server/services/DiscordWebhookService.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,28 @@ class DiscordWebhookService {
3939
this.adminRoleId = process.env.DISCORD_ADMIN_ROLE_ID;
4040
}
4141

42+
// Update webhook configuration from panel settings
43+
updateConfig(webhookSettings: {
44+
discordWebhookUrl?: string;
45+
discordAdminRoleId?: string;
46+
botName?: string;
47+
avatarUrl?: string;
48+
enabled?: boolean;
49+
}) {
50+
if (webhookSettings.enabled && webhookSettings.discordWebhookUrl) {
51+
this.webhookUrl = webhookSettings.discordWebhookUrl;
52+
this.adminRoleId = webhookSettings.discordAdminRoleId;
53+
this.botName = webhookSettings.botName || 'MODL Admin';
54+
this.avatarUrl = webhookSettings.avatarUrl || '';
55+
} else {
56+
// Fallback to environment variables if not configured in panel
57+
this.webhookUrl = process.env.DISCORD_WEBHOOK_URL;
58+
this.adminRoleId = process.env.DISCORD_ADMIN_ROLE_ID;
59+
this.botName = 'MODL Admin';
60+
this.avatarUrl = '';
61+
}
62+
}
63+
4264
private getEmbedColor(type: NotificationType): number {
4365
switch (type) {
4466
case NotificationType.ERROR:
@@ -65,7 +87,7 @@ class DiscordWebhookService {
6587
additionalContent?: string
6688
): Promise<void> {
6789
if (!this.webhookUrl) {
68-
console.warn('Discord webhook URL not configured');
90+
// Silently return if webhook not configured - no need to log warnings
6991
return;
7092
}
7193

@@ -110,10 +132,12 @@ class DiscordWebhookService {
110132

111133
if (!response.ok) {
112134
const errorText = await response.text();
113-
console.error('Failed to send Discord webhook:', response.status, errorText);
135+
// Silently fail - webhook errors shouldn't break the main application flow
136+
return;
114137
}
115138
} catch (error) {
116-
console.error('Error sending Discord webhook:', error);
139+
// Silently fail - webhook errors shouldn't break the main application flow
140+
return;
117141
}
118142
}
119143

0 commit comments

Comments
 (0)