Skip to content

Commit a167975

Browse files
authored
New dashboard (#62) (#64)
* New dashboard (#62) * Redesign ensures consistent log display * display optimization * Duplicate truncation logic * new dashboard (#65) * Update uniqueId search * ModSecurity Configuration Update Custom Rule * avoid duplicate rules * Nginx validation update
1 parent ed46623 commit a167975

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+6098
-1094
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Logs
22
logs
3+
!apps/api/src/domains/logs
4+
!apps/web/src/components/pages/Logs.tsx
35
*.log
46
npm-debug.log*
57
yarn-debug.log*

apps/api/src/domains/access-lists/dto/access-lists.dto.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { body, param, query } from 'express-validator';
2+
import { isValidIpOrCidr } from '../../acl/utils/validators';
23

34
/**
45
* Validation rules for creating an access list
@@ -38,8 +39,13 @@ export const createAccessListValidation = [
3839
body('allowedIps.*')
3940
.optional()
4041
.trim()
41-
.matches(/^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/)
42-
.withMessage('Each IP must be a valid IPv4 address or CIDR notation'),
42+
.custom((value) => {
43+
if (!value) return true;
44+
if (!isValidIpOrCidr(value)) {
45+
throw new Error('Invalid IP address or CIDR notation. Examples: 192.168.1.1 or 192.168.1.0/24');
46+
}
47+
return true;
48+
}),
4349

4450
body('authUsers')
4551
.optional()
@@ -125,8 +131,13 @@ export const updateAccessListValidation = [
125131
body('allowedIps.*')
126132
.optional()
127133
.trim()
128-
.matches(/^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/)
129-
.withMessage('Each IP must be a valid IPv4 address or CIDR notation'),
134+
.custom((value) => {
135+
if (!value) return true;
136+
if (!isValidIpOrCidr(value)) {
137+
throw new Error('Invalid IP address or CIDR notation. Examples: 192.168.1.1 or 192.168.1.0/24');
138+
}
139+
return true;
140+
}),
130141

131142
body('authUsers')
132143
.optional()

apps/api/src/domains/acl/acl.controller.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,31 @@ export class AclController {
181181
}
182182
}
183183

184+
/**
185+
* Preview ACL configuration without applying
186+
* @route GET /api/acl/preview
187+
*/
188+
async previewAclConfig(req: Request, res: Response): Promise<void> {
189+
try {
190+
const config = await aclService.previewNginxConfig();
191+
192+
res.json({
193+
success: true,
194+
data: {
195+
config,
196+
rulesCount: await aclService.getEnabledRulesCount()
197+
}
198+
});
199+
} catch (error: any) {
200+
logger.error('Failed to preview ACL config:', error);
201+
res.status(500).json({
202+
success: false,
203+
message: 'Failed to preview ACL configuration',
204+
error: error.message
205+
});
206+
}
207+
}
208+
184209
/**
185210
* Apply ACL rules to Nginx
186211
* @route POST /api/acl/apply

apps/api/src/domains/acl/acl.routes.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ router.get('/:id', (req, res) => aclController.getAclRule(req, res));
2828
*/
2929
router.post('/', authorize('admin', 'moderator'), (req, res) => aclController.createAclRule(req, res));
3030

31+
/**
32+
* @route GET /api/acl/preview
33+
* @desc Preview ACL configuration without applying
34+
* @access Private (all roles)
35+
*/
36+
router.get('/preview', (req, res) => aclController.previewAclConfig(req, res));
37+
3138
/**
3239
* @route POST /api/acl/apply
3340
* @desc Apply ACL rules to Nginx

apps/api/src/domains/acl/acl.service.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,21 @@ export class AclService {
105105
return rule;
106106
}
107107

108+
/**
109+
* Preview Nginx configuration without applying
110+
*/
111+
async previewNginxConfig(): Promise<string> {
112+
return aclNginxService.generateAclConfig();
113+
}
114+
115+
/**
116+
* Get count of enabled rules
117+
*/
118+
async getEnabledRulesCount(): Promise<number> {
119+
const rules = await aclRepository.findEnabled();
120+
return rules.length;
121+
}
122+
108123
/**
109124
* Apply ACL rules to Nginx
110125
*/

apps/api/src/domains/acl/dto/create-acl-rule.dto.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { validateAclValue, sanitizeValue } from '../utils/validators';
2+
13
/**
24
* DTO for creating ACL rule
35
*/
@@ -17,34 +19,76 @@ export interface CreateAclRuleDto {
1719
export function validateCreateAclRuleDto(data: any): { isValid: boolean; errors: string[] } {
1820
const errors: string[] = [];
1921

22+
// Validate name
2023
if (!data.name || typeof data.name !== 'string' || !data.name.trim()) {
2124
errors.push('Name is required and must be a non-empty string');
25+
} else if (data.name.length > 100) {
26+
errors.push('Name must not exceed 100 characters');
2227
}
2328

29+
// Validate type
30+
const validTypes = ['whitelist', 'blacklist'];
2431
if (!data.type || typeof data.type !== 'string') {
2532
errors.push('Type is required and must be a string');
33+
} else if (!validTypes.includes(data.type)) {
34+
errors.push(`Type must be one of: ${validTypes.join(', ')}`);
2635
}
2736

37+
// Validate condition field
38+
const validFields = ['ip', 'geoip', 'user_agent', 'url', 'method', 'header'];
2839
if (!data.conditionField || typeof data.conditionField !== 'string') {
2940
errors.push('Condition field is required and must be a string');
41+
} else if (!validFields.includes(data.conditionField)) {
42+
errors.push(`Condition field must be one of: ${validFields.join(', ')}`);
3043
}
3144

45+
// Validate condition operator
46+
const validOperators = ['equals', 'contains', 'regex'];
3247
if (!data.conditionOperator || typeof data.conditionOperator !== 'string') {
3348
errors.push('Condition operator is required and must be a string');
49+
} else if (!validOperators.includes(data.conditionOperator)) {
50+
errors.push(`Condition operator must be one of: ${validOperators.join(', ')}`);
3451
}
3552

53+
// Validate condition value
3654
if (!data.conditionValue || typeof data.conditionValue !== 'string') {
3755
errors.push('Condition value is required and must be a string');
56+
} else if (data.conditionValue.trim().length === 0) {
57+
errors.push('Condition value cannot be empty');
58+
} else {
59+
// Perform field-specific validation
60+
const valueValidation = validateAclValue(
61+
data.conditionField,
62+
data.conditionOperator,
63+
data.conditionValue
64+
);
65+
66+
if (!valueValidation.valid) {
67+
errors.push(valueValidation.error || 'Invalid condition value');
68+
}
3869
}
3970

71+
// Validate action
72+
const validActions = ['allow', 'deny', 'challenge'];
4073
if (!data.action || typeof data.action !== 'string') {
4174
errors.push('Action is required and must be a string');
75+
} else if (!validActions.includes(data.action)) {
76+
errors.push(`Action must be one of: ${validActions.join(', ')}`);
4277
}
4378

79+
// Validate enabled
4480
if (data.enabled !== undefined && typeof data.enabled !== 'boolean') {
4581
errors.push('Enabled must be a boolean');
4682
}
4783

84+
// Validate type-action combinations
85+
if (data.type === 'whitelist' && data.action === 'deny') {
86+
errors.push('Whitelist rules should use "allow" action, not "deny"');
87+
}
88+
if (data.type === 'blacklist' && data.action === 'allow') {
89+
errors.push('Blacklist rules should use "deny" action, not "allow"');
90+
}
91+
4892
return {
4993
isValid: errors.length === 0,
5094
errors

apps/api/src/domains/acl/dto/update-acl-rule.dto.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { validateAclValue } from '../utils/validators';
2+
13
/**
24
* DTO for updating ACL rule
35
*/
@@ -17,34 +19,78 @@ export interface UpdateAclRuleDto {
1719
export function validateUpdateAclRuleDto(data: any): { isValid: boolean; errors: string[] } {
1820
const errors: string[] = [];
1921

22+
// Validate name
2023
if (data.name !== undefined && (typeof data.name !== 'string' || !data.name.trim())) {
2124
errors.push('Name must be a non-empty string');
25+
} else if (data.name && data.name.length > 100) {
26+
errors.push('Name must not exceed 100 characters');
2227
}
2328

29+
// Validate type
30+
const validTypes = ['whitelist', 'blacklist'];
2431
if (data.type !== undefined && typeof data.type !== 'string') {
2532
errors.push('Type must be a string');
33+
} else if (data.type && !validTypes.includes(data.type)) {
34+
errors.push(`Type must be one of: ${validTypes.join(', ')}`);
2635
}
2736

37+
// Validate condition field
38+
const validFields = ['ip', 'geoip', 'user_agent', 'url', 'method', 'header'];
2839
if (data.conditionField !== undefined && typeof data.conditionField !== 'string') {
2940
errors.push('Condition field must be a string');
41+
} else if (data.conditionField && !validFields.includes(data.conditionField)) {
42+
errors.push(`Condition field must be one of: ${validFields.join(', ')}`);
3043
}
3144

45+
// Validate condition operator
46+
const validOperators = ['equals', 'contains', 'regex'];
3247
if (data.conditionOperator !== undefined && typeof data.conditionOperator !== 'string') {
3348
errors.push('Condition operator must be a string');
49+
} else if (data.conditionOperator && !validOperators.includes(data.conditionOperator)) {
50+
errors.push(`Condition operator must be one of: ${validOperators.join(', ')}`);
3451
}
3552

36-
if (data.conditionValue !== undefined && typeof data.conditionValue !== 'string') {
37-
errors.push('Condition value must be a string');
53+
// Validate condition value with field-specific validation
54+
if (data.conditionValue !== undefined) {
55+
if (typeof data.conditionValue !== 'string') {
56+
errors.push('Condition value must be a string');
57+
} else if (data.conditionValue.trim().length === 0) {
58+
errors.push('Condition value cannot be empty');
59+
} else if (data.conditionField && data.conditionOperator) {
60+
// Perform field-specific validation if we have all required fields
61+
const valueValidation = validateAclValue(
62+
data.conditionField,
63+
data.conditionOperator,
64+
data.conditionValue
65+
);
66+
67+
if (!valueValidation.valid) {
68+
errors.push(valueValidation.error || 'Invalid condition value');
69+
}
70+
}
3871
}
3972

73+
// Validate action
74+
const validActions = ['allow', 'deny', 'challenge'];
4075
if (data.action !== undefined && typeof data.action !== 'string') {
4176
errors.push('Action must be a string');
77+
} else if (data.action && !validActions.includes(data.action)) {
78+
errors.push(`Action must be one of: ${validActions.join(', ')}`);
4279
}
4380

81+
// Validate enabled
4482
if (data.enabled !== undefined && typeof data.enabled !== 'boolean') {
4583
errors.push('Enabled must be a boolean');
4684
}
4785

86+
// Validate type-action combinations
87+
if (data.type === 'whitelist' && data.action === 'deny') {
88+
errors.push('Whitelist rules should use "allow" action, not "deny"');
89+
}
90+
if (data.type === 'blacklist' && data.action === 'allow') {
91+
errors.push('Blacklist rules should use "deny" action, not "allow"');
92+
}
93+
4894
return {
4995
isValid: errors.length === 0,
5096
errors

0 commit comments

Comments
 (0)