feat: implement access list management with CRUD operations and IP testing

- Added API integration for access lists including listing, creating, updating, deleting, and testing IPs against access lists.
- Created AccessListForm component for creating and editing access lists with validation.
- Developed AccessListSelector component for selecting access lists with detailed display of selected ACL.
- Implemented hooks for managing access lists and handling API interactions.
- Added tests for AccessListSelector and useAccessLists hooks to ensure functionality.
- Enhanced AccessLists page with UI for managing access lists, including create, edit, delete, and test IP features.
This commit is contained in:
Wikid82
2025-11-27 08:55:29 +00:00
parent 486c9b40c1
commit 429de10f0f
30 changed files with 4138 additions and 35 deletions

View File

@@ -0,0 +1,179 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { accessListsApi } from '../accessLists';
import client from '../client';
import type { AccessList } from '../accessLists';
// Mock the client module
vi.mock('../client', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
delete: vi.fn(),
},
}));
describe('accessListsApi', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('list', () => {
it('should fetch all access lists', async () => {
const mockLists: AccessList[] = [
{
id: 1,
uuid: 'test-uuid',
name: 'Test ACL',
description: 'Test description',
type: 'whitelist',
ip_rules: '[{"cidr":"192.168.1.0/24"}]',
country_codes: '',
local_network_only: false,
enabled: true,
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
},
];
vi.mocked(client.get).mockResolvedValueOnce({ data: mockLists });
const result = await accessListsApi.list();
expect(client.get).toHaveBeenCalledWith<[string]>('/access-lists');
expect(result).toEqual(mockLists);
});
});
describe('get', () => {
it('should fetch access list by ID', async () => {
const mockList: AccessList = {
id: 1,
uuid: 'test-uuid',
name: 'Test ACL',
description: 'Test description',
type: 'whitelist',
ip_rules: '[{"cidr":"192.168.1.0/24"}]',
country_codes: '',
local_network_only: false,
enabled: true,
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
};
vi.mocked(client.get).mockResolvedValueOnce({ data: mockList });
const result = await accessListsApi.get(1);
expect(client.get).toHaveBeenCalledWith<[string]>('/access-lists/1');
expect(result).toEqual(mockList);
});
});
describe('create', () => {
it('should create a new access list', async () => {
const newList = {
name: 'New ACL',
description: 'New description',
type: 'whitelist' as const,
ip_rules: '[{"cidr":"10.0.0.0/8"}]',
enabled: true,
};
const mockResponse: AccessList = {
id: 1,
uuid: 'new-uuid',
...newList,
country_codes: '',
local_network_only: false,
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
};
vi.mocked(client.post).mockResolvedValueOnce({ data: mockResponse });
const result = await accessListsApi.create(newList);
expect(client.post).toHaveBeenCalledWith<[string, typeof newList]>('/access-lists', newList);
expect(result).toEqual(mockResponse);
});
});
describe('update', () => {
it('should update an access list', async () => {
const updates = {
name: 'Updated ACL',
enabled: false,
};
const mockResponse: AccessList = {
id: 1,
uuid: 'test-uuid',
name: 'Updated ACL',
description: 'Test description',
type: 'whitelist',
ip_rules: '[{"cidr":"192.168.1.0/24"}]',
country_codes: '',
local_network_only: false,
enabled: false,
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
};
vi.mocked(client.put).mockResolvedValueOnce({ data: mockResponse });
const result = await accessListsApi.update(1, updates);
expect(client.put).toHaveBeenCalledWith<[string, typeof updates]>('/access-lists/1', updates);
expect(result).toEqual(mockResponse);
});
});
describe('delete', () => {
it('should delete an access list', async () => {
vi.mocked(client.delete).mockResolvedValueOnce({ data: undefined });
await accessListsApi.delete(1);
expect(client.delete).toHaveBeenCalledWith<[string]>('/access-lists/1');
});
});
describe('testIP', () => {
it('should test an IP against an access list', async () => {
const mockResponse = {
allowed: true,
reason: 'IP matches whitelist rule',
};
vi.mocked(client.post).mockResolvedValueOnce({ data: mockResponse });
const result = await accessListsApi.testIP(1, '192.168.1.100');
expect(client.post).toHaveBeenCalledWith<[string, { ip_address: string }]>('/access-lists/1/test', {
ip_address: '192.168.1.100',
});
expect(result).toEqual(mockResponse);
});
});
describe('getTemplates', () => {
it('should fetch access list templates', async () => {
const mockTemplates = [
{
name: 'Private Networks',
description: 'RFC1918 private networks',
type: 'whitelist' as const,
ip_rules: '[{"cidr":"10.0.0.0/8"},{"cidr":"172.16.0.0/12"},{"cidr":"192.168.0.0/16"}]',
},
];
vi.mocked(client.get).mockResolvedValueOnce({ data: mockTemplates });
const result = await accessListsApi.getTemplates();
expect(client.get).toHaveBeenCalledWith<[string]>('/access-lists/templates');
expect(result).toEqual(mockTemplates);
});
});
});

View File

@@ -0,0 +1,106 @@
import client from './client';
export interface AccessListRule {
cidr: string;
description: string;
}
export interface AccessList {
id: number;
uuid: string;
name: string;
description: string;
type: 'whitelist' | 'blacklist' | 'geo_whitelist' | 'geo_blacklist';
ip_rules: string; // JSON string of AccessListRule[]
country_codes: string; // Comma-separated
local_network_only: boolean;
enabled: boolean;
created_at: string;
updated_at: string;
}
export interface CreateAccessListRequest {
name: string;
description?: string;
type: 'whitelist' | 'blacklist' | 'geo_whitelist' | 'geo_blacklist';
ip_rules?: string;
country_codes?: string;
local_network_only?: boolean;
enabled?: boolean;
}
export interface TestIPRequest {
ip_address: string;
}
export interface TestIPResponse {
allowed: boolean;
reason: string;
}
export interface AccessListTemplate {
name: string;
description: string;
type: string;
local_network_only?: boolean;
country_codes?: string;
}
export const accessListsApi = {
/**
* Fetch all access lists
*/
async list(): Promise<AccessList[]> {
const response = await client.get<AccessList[]>('/access-lists');
return response.data;
},
/**
* Get a single access list by ID
*/
async get(id: number): Promise<AccessList> {
const response = await client.get<AccessList>(`/access-lists/${id}`);
return response.data;
},
/**
* Create a new access list
*/
async create(data: CreateAccessListRequest): Promise<AccessList> {
const response = await client.post<AccessList>('/access-lists', data);
return response.data;
},
/**
* Update an existing access list
*/
async update(id: number, data: Partial<CreateAccessListRequest>): Promise<AccessList> {
const response = await client.put<AccessList>(`/access-lists/${id}`, data);
return response.data;
},
/**
* Delete an access list
*/
async delete(id: number): Promise<void> {
await client.delete(`/access-lists/${id}`);
},
/**
* Test if an IP address would be allowed/blocked
*/
async testIP(id: number, ipAddress: string): Promise<TestIPResponse> {
const response = await client.post<TestIPResponse>(`/access-lists/${id}/test`, {
ip_address: ipAddress,
});
return response.data;
},
/**
* Get predefined ACL templates
*/
async getTemplates(): Promise<AccessListTemplate[]> {
const response = await client.get<AccessListTemplate[]>('/access-lists/templates');
return response.data;
},
};

View File

@@ -38,6 +38,7 @@ export interface ProxyHost {
enabled: boolean;
certificate_id?: number | null;
certificate?: Certificate | null;
access_list_id?: number | null;
created_at: string;
updated_at: string;
}