chore: clean .gitignore cache
This commit is contained in:
@@ -1,179 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import client from '../../api/client'
|
||||
import { getBackups, createBackup, restoreBackup, deleteBackup } from '../backups'
|
||||
|
||||
describe('backups api', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('getBackups returns list', async () => {
|
||||
const mockData = [{ filename: 'b1.zip', size: 123, time: '2025-01-01T00:00:00Z' }]
|
||||
vi.spyOn(client, 'get').mockResolvedValueOnce({ data: mockData })
|
||||
const res = await getBackups()
|
||||
expect(res).toEqual(mockData)
|
||||
})
|
||||
|
||||
it('createBackup returns filename', async () => {
|
||||
vi.spyOn(client, 'post').mockResolvedValueOnce({ data: { filename: 'b2.zip' } })
|
||||
const res = await createBackup()
|
||||
expect(res).toEqual({ filename: 'b2.zip' })
|
||||
})
|
||||
|
||||
it('restoreBackup posts to restore endpoint', async () => {
|
||||
const spy = vi.spyOn(client, 'post').mockResolvedValueOnce({})
|
||||
await restoreBackup('b3.zip')
|
||||
expect(spy).toHaveBeenCalledWith('/backups/b3.zip/restore')
|
||||
})
|
||||
|
||||
it('deleteBackup deletes backup', async () => {
|
||||
const spy = vi.spyOn(client, 'delete').mockResolvedValueOnce({})
|
||||
await deleteBackup('b3.zip')
|
||||
expect(spy).toHaveBeenCalledWith('/backups/b3.zip')
|
||||
})
|
||||
})
|
||||
@@ -1,52 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import client from '../client';
|
||||
import { getCertificates, uploadCertificate, deleteCertificate, Certificate } from '../certificates';
|
||||
|
||||
vi.mock('../client', () => ({
|
||||
default: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('certificates API', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockCert: Certificate = {
|
||||
id: 1,
|
||||
domain: 'example.com',
|
||||
issuer: 'Let\'s Encrypt',
|
||||
expires_at: '2023-01-01',
|
||||
status: 'valid',
|
||||
provider: 'letsencrypt',
|
||||
};
|
||||
|
||||
it('getCertificates calls client.get', async () => {
|
||||
vi.mocked(client.get).mockResolvedValue({ data: [mockCert] });
|
||||
const result = await getCertificates();
|
||||
expect(client.get).toHaveBeenCalledWith('/certificates');
|
||||
expect(result).toEqual([mockCert]);
|
||||
});
|
||||
|
||||
it('uploadCertificate calls client.post with FormData', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockCert });
|
||||
const certFile = new File(['cert'], 'cert.pem', { type: 'text/plain' });
|
||||
const keyFile = new File(['key'], 'key.pem', { type: 'text/plain' });
|
||||
|
||||
const result = await uploadCertificate('My Cert', certFile, keyFile);
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/certificates', expect.any(FormData), {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
expect(result).toEqual(mockCert);
|
||||
});
|
||||
|
||||
it('deleteCertificate calls client.delete', async () => {
|
||||
vi.mocked(client.delete).mockResolvedValue({ data: {} });
|
||||
await deleteCertificate(1);
|
||||
expect(client.delete).toHaveBeenCalledWith('/certificates/1');
|
||||
});
|
||||
});
|
||||
@@ -1,507 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import * as consoleEnrollment from '../consoleEnrollment'
|
||||
import client from '../client'
|
||||
|
||||
vi.mock('../client')
|
||||
|
||||
describe('consoleEnrollment API', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('getConsoleStatus', () => {
|
||||
it('should fetch enrollment status with pending state', async () => {
|
||||
const mockStatus = {
|
||||
status: 'pending',
|
||||
tenant: 'my-org',
|
||||
agent_name: 'charon-prod',
|
||||
key_present: true,
|
||||
last_attempt_at: '2025-12-15T09:00:00Z',
|
||||
}
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockStatus })
|
||||
|
||||
const result = await consoleEnrollment.getConsoleStatus()
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/admin/crowdsec/console/status')
|
||||
expect(result).toEqual(mockStatus)
|
||||
expect(result.status).toBe('pending')
|
||||
expect(result.key_present).toBe(true)
|
||||
})
|
||||
|
||||
it('should fetch enrolled status with heartbeat', async () => {
|
||||
const mockStatus = {
|
||||
status: 'enrolled',
|
||||
tenant: 'my-org',
|
||||
agent_name: 'charon-prod',
|
||||
key_present: true,
|
||||
enrolled_at: '2025-12-14T10:00:00Z',
|
||||
last_heartbeat_at: '2025-12-15T09:55:00Z',
|
||||
}
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockStatus })
|
||||
|
||||
const result = await consoleEnrollment.getConsoleStatus()
|
||||
|
||||
expect(result.status).toBe('enrolled')
|
||||
expect(result.enrolled_at).toBeDefined()
|
||||
expect(result.last_heartbeat_at).toBeDefined()
|
||||
})
|
||||
|
||||
it('should fetch failed status with error message', async () => {
|
||||
const mockStatus = {
|
||||
status: 'failed',
|
||||
tenant: 'my-org',
|
||||
agent_name: 'charon-prod',
|
||||
key_present: false,
|
||||
last_error: 'Invalid enrollment key',
|
||||
last_attempt_at: '2025-12-15T09:00:00Z',
|
||||
correlation_id: 'req-abc123',
|
||||
}
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockStatus })
|
||||
|
||||
const result = await consoleEnrollment.getConsoleStatus()
|
||||
|
||||
expect(result.status).toBe('failed')
|
||||
expect(result.last_error).toBe('Invalid enrollment key')
|
||||
expect(result.correlation_id).toBe('req-abc123')
|
||||
expect(result.key_present).toBe(false)
|
||||
})
|
||||
|
||||
it('should fetch status with none state (not enrolled)', async () => {
|
||||
const mockStatus = {
|
||||
status: 'none',
|
||||
key_present: false,
|
||||
}
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockStatus })
|
||||
|
||||
const result = await consoleEnrollment.getConsoleStatus()
|
||||
|
||||
expect(result.status).toBe('none')
|
||||
expect(result.key_present).toBe(false)
|
||||
expect(result.tenant).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should NOT return enrollment key in status response', async () => {
|
||||
const mockStatus = {
|
||||
status: 'enrolled',
|
||||
tenant: 'test-org',
|
||||
agent_name: 'test-agent',
|
||||
key_present: true,
|
||||
enrolled_at: '2025-12-14T10:00:00Z',
|
||||
}
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockStatus })
|
||||
|
||||
const result = await consoleEnrollment.getConsoleStatus()
|
||||
|
||||
// Security test: Ensure key is never exposed
|
||||
expect(result).not.toHaveProperty('enrollment_key')
|
||||
expect(result).not.toHaveProperty('encrypted_enroll_key')
|
||||
expect(result).toHaveProperty('key_present')
|
||||
})
|
||||
|
||||
it('should handle API errors', async () => {
|
||||
const error = new Error('Network error')
|
||||
vi.mocked(client.get).mockRejectedValue(error)
|
||||
|
||||
await expect(consoleEnrollment.getConsoleStatus()).rejects.toThrow('Network error')
|
||||
})
|
||||
|
||||
it('should handle server unavailability', async () => {
|
||||
const error = {
|
||||
response: {
|
||||
status: 503,
|
||||
data: { error: 'Service temporarily unavailable' },
|
||||
},
|
||||
}
|
||||
vi.mocked(client.get).mockRejectedValue(error)
|
||||
|
||||
await expect(consoleEnrollment.getConsoleStatus()).rejects.toEqual(error)
|
||||
})
|
||||
})
|
||||
|
||||
describe('enrollConsole', () => {
|
||||
it('should enroll with valid payload', async () => {
|
||||
const payload = {
|
||||
enrollment_key: 'cs-enroll-abc123xyz',
|
||||
tenant: 'my-org',
|
||||
agent_name: 'charon-prod',
|
||||
force: false,
|
||||
}
|
||||
const mockResponse = {
|
||||
status: 'enrolled',
|
||||
tenant: 'my-org',
|
||||
agent_name: 'charon-prod',
|
||||
key_present: true,
|
||||
enrolled_at: '2025-12-15T10:00:00Z',
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
|
||||
|
||||
const result = await consoleEnrollment.enrollConsole(payload)
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/console/enroll', payload)
|
||||
expect(result).toEqual(mockResponse)
|
||||
expect(result.status).toBe('enrolled')
|
||||
expect(result.enrolled_at).toBeDefined()
|
||||
})
|
||||
|
||||
it('should enroll with minimal payload (no tenant)', async () => {
|
||||
const payload = {
|
||||
enrollment_key: 'cs-enroll-key123',
|
||||
agent_name: 'charon-test',
|
||||
}
|
||||
const mockResponse = {
|
||||
status: 'enrolled',
|
||||
agent_name: 'charon-test',
|
||||
key_present: true,
|
||||
enrolled_at: '2025-12-15T10:00:00Z',
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
|
||||
|
||||
const result = await consoleEnrollment.enrollConsole(payload)
|
||||
|
||||
expect(result.status).toBe('enrolled')
|
||||
expect(result.agent_name).toBe('charon-test')
|
||||
})
|
||||
|
||||
it('should force re-enrollment when force=true', async () => {
|
||||
const payload = {
|
||||
enrollment_key: 'cs-enroll-new-key',
|
||||
agent_name: 'charon-updated',
|
||||
force: true,
|
||||
}
|
||||
const mockResponse = {
|
||||
status: 'enrolled',
|
||||
agent_name: 'charon-updated',
|
||||
key_present: true,
|
||||
enrolled_at: '2025-12-15T10:05:00Z',
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
|
||||
|
||||
const result = await consoleEnrollment.enrollConsole(payload)
|
||||
|
||||
expect(result.status).toBe('enrolled')
|
||||
expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/console/enroll', payload)
|
||||
})
|
||||
|
||||
it('should handle invalid enrollment key format', async () => {
|
||||
const payload = {
|
||||
enrollment_key: 'not-a-valid-key',
|
||||
agent_name: 'test',
|
||||
}
|
||||
const error = {
|
||||
response: {
|
||||
status: 400,
|
||||
data: { error: 'Invalid enrollment key format' },
|
||||
},
|
||||
}
|
||||
vi.mocked(client.post).mockRejectedValue(error)
|
||||
|
||||
await expect(consoleEnrollment.enrollConsole(payload)).rejects.toEqual(error)
|
||||
})
|
||||
|
||||
it('should handle transient network errors during enrollment', async () => {
|
||||
const payload = {
|
||||
enrollment_key: 'cs-enroll-key123',
|
||||
agent_name: 'test-agent',
|
||||
}
|
||||
const error = {
|
||||
response: {
|
||||
status: 503,
|
||||
data: { error: 'CrowdSec Console API temporarily unavailable' },
|
||||
},
|
||||
}
|
||||
vi.mocked(client.post).mockRejectedValue(error)
|
||||
|
||||
await expect(consoleEnrollment.enrollConsole(payload)).rejects.toEqual(error)
|
||||
})
|
||||
|
||||
it('should handle enrollment key expiration', async () => {
|
||||
const payload = {
|
||||
enrollment_key: 'cs-enroll-expired-key',
|
||||
agent_name: 'test',
|
||||
}
|
||||
const mockResponse = {
|
||||
status: 'failed',
|
||||
key_present: false,
|
||||
last_error: 'Enrollment key expired',
|
||||
correlation_id: 'err-expired-123',
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
|
||||
|
||||
const result = await consoleEnrollment.enrollConsole(payload)
|
||||
|
||||
expect(result.status).toBe('failed')
|
||||
expect(result.last_error).toBe('Enrollment key expired')
|
||||
})
|
||||
|
||||
it('should sanitize tenant name with special characters', async () => {
|
||||
const payload = {
|
||||
enrollment_key: 'valid-key',
|
||||
tenant: 'My Org (Production)',
|
||||
agent_name: 'agent1',
|
||||
}
|
||||
const mockResponse = {
|
||||
status: 'enrolled',
|
||||
tenant: 'My Org (Production)',
|
||||
agent_name: 'agent1',
|
||||
key_present: true,
|
||||
enrolled_at: '2025-12-15T10:00:00Z',
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
|
||||
|
||||
const result = await consoleEnrollment.enrollConsole(payload)
|
||||
|
||||
expect(result.status).toBe('enrolled')
|
||||
expect(result.tenant).toBe('My Org (Production)')
|
||||
})
|
||||
|
||||
it('should handle SQL injection attempts in agent_name', async () => {
|
||||
const payload = {
|
||||
enrollment_key: 'valid-key',
|
||||
agent_name: "'; DROP TABLE users; --",
|
||||
}
|
||||
const error = {
|
||||
response: {
|
||||
status: 400,
|
||||
data: { error: 'Invalid agent name format' },
|
||||
},
|
||||
}
|
||||
vi.mocked(client.post).mockRejectedValue(error)
|
||||
|
||||
await expect(consoleEnrollment.enrollConsole(payload)).rejects.toEqual(error)
|
||||
})
|
||||
|
||||
it('should handle CrowdSec not running during enrollment', async () => {
|
||||
const payload = {
|
||||
enrollment_key: 'valid-key',
|
||||
agent_name: 'test',
|
||||
}
|
||||
const error = {
|
||||
response: {
|
||||
status: 500,
|
||||
data: { error: 'CrowdSec is not running. Start CrowdSec before enrolling.' },
|
||||
},
|
||||
}
|
||||
vi.mocked(client.post).mockRejectedValue(error)
|
||||
|
||||
await expect(consoleEnrollment.enrollConsole(payload)).rejects.toEqual(error)
|
||||
})
|
||||
|
||||
it('should return pending status when enrollment is queued', async () => {
|
||||
const payload = {
|
||||
enrollment_key: 'valid-key',
|
||||
agent_name: 'test',
|
||||
}
|
||||
const mockResponse = {
|
||||
status: 'pending',
|
||||
agent_name: 'test',
|
||||
key_present: true,
|
||||
last_attempt_at: '2025-12-15T10:00:00Z',
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
|
||||
|
||||
const result = await consoleEnrollment.enrollConsole(payload)
|
||||
|
||||
expect(result.status).toBe('pending')
|
||||
expect(result.last_attempt_at).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('default export', () => {
|
||||
it('should export all functions', () => {
|
||||
expect(consoleEnrollment.default).toHaveProperty('getConsoleStatus')
|
||||
expect(consoleEnrollment.default).toHaveProperty('enrollConsole')
|
||||
})
|
||||
})
|
||||
|
||||
describe('integration scenarios', () => {
|
||||
it('should handle full enrollment workflow: status → enroll → verify', async () => {
|
||||
// 1. Check initial status (not enrolled)
|
||||
const mockStatusNone = {
|
||||
status: 'none',
|
||||
key_present: false,
|
||||
}
|
||||
vi.mocked(client.get).mockResolvedValueOnce({ data: mockStatusNone })
|
||||
|
||||
const statusBefore = await consoleEnrollment.getConsoleStatus()
|
||||
expect(statusBefore.status).toBe('none')
|
||||
|
||||
// 2. Enroll
|
||||
const enrollPayload = {
|
||||
enrollment_key: 'cs-enroll-valid-key',
|
||||
tenant: 'test-org',
|
||||
agent_name: 'charon-test',
|
||||
}
|
||||
const mockEnrollResponse = {
|
||||
status: 'enrolled',
|
||||
tenant: 'test-org',
|
||||
agent_name: 'charon-test',
|
||||
key_present: true,
|
||||
enrolled_at: '2025-12-15T10:00:00Z',
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValueOnce({ data: mockEnrollResponse })
|
||||
|
||||
const enrollResult = await consoleEnrollment.enrollConsole(enrollPayload)
|
||||
expect(enrollResult.status).toBe('enrolled')
|
||||
|
||||
// 3. Verify status updated
|
||||
const mockStatusEnrolled = {
|
||||
status: 'enrolled',
|
||||
tenant: 'test-org',
|
||||
agent_name: 'charon-test',
|
||||
key_present: true,
|
||||
enrolled_at: '2025-12-15T10:00:00Z',
|
||||
last_heartbeat_at: '2025-12-15T10:01:00Z',
|
||||
}
|
||||
vi.mocked(client.get).mockResolvedValueOnce({ data: mockStatusEnrolled })
|
||||
|
||||
const statusAfter = await consoleEnrollment.getConsoleStatus()
|
||||
expect(statusAfter.status).toBe('enrolled')
|
||||
expect(statusAfter.tenant).toBe('test-org')
|
||||
})
|
||||
|
||||
it('should handle enrollment failure and retry', async () => {
|
||||
// 1. First enrollment attempt fails
|
||||
const payload = {
|
||||
enrollment_key: 'cs-enroll-key',
|
||||
agent_name: 'test',
|
||||
}
|
||||
const networkError = new Error('Network timeout')
|
||||
vi.mocked(client.post).mockRejectedValueOnce(networkError)
|
||||
|
||||
await expect(consoleEnrollment.enrollConsole(payload)).rejects.toThrow('Network timeout')
|
||||
|
||||
// 2. Retry succeeds
|
||||
const mockResponse = {
|
||||
status: 'enrolled',
|
||||
agent_name: 'test',
|
||||
key_present: true,
|
||||
enrolled_at: '2025-12-15T10:05:00Z',
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValueOnce({ data: mockResponse })
|
||||
|
||||
const retryResult = await consoleEnrollment.enrollConsole(payload)
|
||||
expect(retryResult.status).toBe('enrolled')
|
||||
})
|
||||
|
||||
it('should handle status transitions: none → pending → enrolled', async () => {
|
||||
// 1. Initial: none
|
||||
const mockNone = { status: 'none', key_present: false }
|
||||
vi.mocked(client.get).mockResolvedValueOnce({ data: mockNone })
|
||||
const status1 = await consoleEnrollment.getConsoleStatus()
|
||||
expect(status1.status).toBe('none')
|
||||
|
||||
// 2. Enroll (returns pending)
|
||||
const payload = { enrollment_key: 'key', agent_name: 'agent' }
|
||||
const mockPending = {
|
||||
status: 'pending',
|
||||
agent_name: 'agent',
|
||||
key_present: true,
|
||||
last_attempt_at: '2025-12-15T10:00:00Z',
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValueOnce({ data: mockPending })
|
||||
const enrollResult = await consoleEnrollment.enrollConsole(payload)
|
||||
expect(enrollResult.status).toBe('pending')
|
||||
|
||||
// 3. Check status again (now enrolled)
|
||||
const mockEnrolled = {
|
||||
status: 'enrolled',
|
||||
agent_name: 'agent',
|
||||
key_present: true,
|
||||
enrolled_at: '2025-12-15T10:00:30Z',
|
||||
}
|
||||
vi.mocked(client.get).mockResolvedValueOnce({ data: mockEnrolled })
|
||||
const status2 = await consoleEnrollment.getConsoleStatus()
|
||||
expect(status2.status).toBe('enrolled')
|
||||
})
|
||||
|
||||
it('should handle force re-enrollment over existing enrollment', async () => {
|
||||
// 1. Check current enrollment
|
||||
const mockCurrent = {
|
||||
status: 'enrolled',
|
||||
tenant: 'old-org',
|
||||
agent_name: 'old-agent',
|
||||
key_present: true,
|
||||
enrolled_at: '2025-12-14T10:00:00Z',
|
||||
}
|
||||
vi.mocked(client.get).mockResolvedValueOnce({ data: mockCurrent })
|
||||
const currentStatus = await consoleEnrollment.getConsoleStatus()
|
||||
expect(currentStatus.tenant).toBe('old-org')
|
||||
|
||||
// 2. Force re-enrollment
|
||||
const forcePayload = {
|
||||
enrollment_key: 'new-key',
|
||||
tenant: 'new-org',
|
||||
agent_name: 'new-agent',
|
||||
force: true,
|
||||
}
|
||||
const mockForced = {
|
||||
status: 'enrolled',
|
||||
tenant: 'new-org',
|
||||
agent_name: 'new-agent',
|
||||
key_present: true,
|
||||
enrolled_at: '2025-12-15T10:00:00Z',
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValueOnce({ data: mockForced })
|
||||
const forceResult = await consoleEnrollment.enrollConsole(forcePayload)
|
||||
expect(forceResult.tenant).toBe('new-org')
|
||||
})
|
||||
})
|
||||
|
||||
describe('security tests', () => {
|
||||
it('should never log or expose enrollment key', async () => {
|
||||
const payload = {
|
||||
enrollment_key: 'cs-enroll-secret-key-should-never-log',
|
||||
agent_name: 'test',
|
||||
}
|
||||
const mockResponse = {
|
||||
status: 'enrolled',
|
||||
agent_name: 'test',
|
||||
key_present: true,
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
|
||||
|
||||
const result = await consoleEnrollment.enrollConsole(payload)
|
||||
|
||||
// Ensure response never contains the key
|
||||
expect(result).not.toHaveProperty('enrollment_key')
|
||||
expect(JSON.stringify(result)).not.toContain('cs-enroll-secret-key')
|
||||
})
|
||||
|
||||
it('should sanitize error messages to avoid key leakage', async () => {
|
||||
const payload = {
|
||||
enrollment_key: 'cs-enroll-sensitive-key',
|
||||
agent_name: 'test',
|
||||
}
|
||||
const error = {
|
||||
response: {
|
||||
status: 400,
|
||||
data: { error: 'Enrollment failed: invalid key format' },
|
||||
},
|
||||
}
|
||||
vi.mocked(client.post).mockRejectedValue(error)
|
||||
|
||||
try {
|
||||
await consoleEnrollment.enrollConsole(payload)
|
||||
} catch (e: unknown) {
|
||||
// Error message should NOT contain the key
|
||||
const error = e as { response?: { data?: { error?: string } } }
|
||||
expect(error.response?.data?.error).not.toContain('cs-enroll-sensitive-key')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle correlation_id for debugging without exposing keys', async () => {
|
||||
const mockStatus = {
|
||||
status: 'failed',
|
||||
key_present: false,
|
||||
last_error: 'Authentication failed',
|
||||
correlation_id: 'debug-correlation-abc123',
|
||||
}
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockStatus })
|
||||
|
||||
const result = await consoleEnrollment.getConsoleStatus()
|
||||
|
||||
expect(result.correlation_id).toBe('debug-correlation-abc123')
|
||||
expect(result).not.toHaveProperty('enrollment_key')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,119 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import {
|
||||
getCredentials,
|
||||
getCredential,
|
||||
createCredential,
|
||||
updateCredential,
|
||||
deleteCredential,
|
||||
testCredential,
|
||||
enableMultiCredentials,
|
||||
type DNSProviderCredential,
|
||||
type CredentialRequest,
|
||||
type CredentialTestResult,
|
||||
} from '../credentials'
|
||||
import client from '../client'
|
||||
|
||||
vi.mock('../client')
|
||||
|
||||
const mockCredential: DNSProviderCredential = {
|
||||
id: 1,
|
||||
uuid: 'test-uuid-1',
|
||||
dns_provider_id: 1,
|
||||
label: 'Production Credentials',
|
||||
zone_filter: '*.example.com',
|
||||
enabled: true,
|
||||
propagation_timeout: 120,
|
||||
polling_interval: 2,
|
||||
key_version: 1,
|
||||
success_count: 5,
|
||||
failure_count: 0,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
}
|
||||
|
||||
const mockCredentialRequest: CredentialRequest = {
|
||||
label: 'New Credentials',
|
||||
zone_filter: '*.example.com',
|
||||
credentials: { api_token: 'test-token-123' },
|
||||
propagation_timeout: 120,
|
||||
polling_interval: 2,
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
describe('credentials API', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should call getCredentials with correct endpoint', async () => {
|
||||
const mockData = [mockCredential, { ...mockCredential, id: 2, label: 'Secondary' }]
|
||||
vi.mocked(client.get).mockResolvedValue({
|
||||
data: { credentials: mockData, total: 2 },
|
||||
})
|
||||
|
||||
const result = await getCredentials(1)
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/dns-providers/1/credentials')
|
||||
expect(result).toEqual(mockData)
|
||||
expect(result).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should call getCredential with correct endpoint', async () => {
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockCredential })
|
||||
|
||||
const result = await getCredential(1, 1)
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/dns-providers/1/credentials/1')
|
||||
expect(result).toEqual(mockCredential)
|
||||
})
|
||||
|
||||
it('should call createCredential with correct endpoint and data', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockCredential })
|
||||
|
||||
const result = await createCredential(1, mockCredentialRequest)
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/dns-providers/1/credentials', mockCredentialRequest)
|
||||
expect(result).toEqual(mockCredential)
|
||||
})
|
||||
|
||||
it('should call updateCredential with correct endpoint and data', async () => {
|
||||
const updatedCredential = { ...mockCredential, label: 'Updated Label' }
|
||||
vi.mocked(client.put).mockResolvedValue({ data: updatedCredential })
|
||||
|
||||
const result = await updateCredential(1, 1, mockCredentialRequest)
|
||||
|
||||
expect(client.put).toHaveBeenCalledWith('/dns-providers/1/credentials/1', mockCredentialRequest)
|
||||
expect(result).toEqual(updatedCredential)
|
||||
})
|
||||
|
||||
it('should call deleteCredential with correct endpoint', async () => {
|
||||
vi.mocked(client.delete).mockResolvedValue({ data: undefined })
|
||||
|
||||
await deleteCredential(1, 1)
|
||||
|
||||
expect(client.delete).toHaveBeenCalledWith('/dns-providers/1/credentials/1')
|
||||
})
|
||||
|
||||
it('should call testCredential with correct endpoint', async () => {
|
||||
const mockTestResult: CredentialTestResult = {
|
||||
success: true,
|
||||
message: 'Credentials validated successfully',
|
||||
propagation_time_ms: 1200,
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockTestResult })
|
||||
|
||||
const result = await testCredential(1, 1)
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/dns-providers/1/credentials/1/test')
|
||||
expect(result).toEqual(mockTestResult)
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('should call enableMultiCredentials with correct endpoint', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({ data: undefined })
|
||||
|
||||
await enableMultiCredentials(1)
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/dns-providers/1/enable-multi-credentials')
|
||||
})
|
||||
})
|
||||
@@ -1,130 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import * as crowdsec from '../crowdsec'
|
||||
import client from '../client'
|
||||
|
||||
vi.mock('../client')
|
||||
|
||||
describe('crowdsec API', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('startCrowdsec', () => {
|
||||
it('should call POST /admin/crowdsec/start', async () => {
|
||||
const mockData = { success: true }
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await crowdsec.startCrowdsec()
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/start')
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
|
||||
describe('stopCrowdsec', () => {
|
||||
it('should call POST /admin/crowdsec/stop', async () => {
|
||||
const mockData = { success: true }
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await crowdsec.stopCrowdsec()
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/stop')
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
|
||||
describe('statusCrowdsec', () => {
|
||||
it('should call GET /admin/crowdsec/status', async () => {
|
||||
const mockData = { running: true, pid: 1234 }
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await crowdsec.statusCrowdsec()
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/admin/crowdsec/status')
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
|
||||
describe('importCrowdsecConfig', () => {
|
||||
it('should call POST /admin/crowdsec/import with FormData', async () => {
|
||||
const mockFile = new File(['content'], 'config.tar.gz', { type: 'application/gzip' })
|
||||
const mockData = { success: true }
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await crowdsec.importCrowdsecConfig(mockFile)
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith(
|
||||
'/admin/crowdsec/import',
|
||||
expect.any(FormData),
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } }
|
||||
)
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
|
||||
describe('exportCrowdsecConfig', () => {
|
||||
it('should call GET /admin/crowdsec/export with blob responseType', async () => {
|
||||
const mockBlob = new Blob(['data'], { type: 'application/gzip' })
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockBlob })
|
||||
|
||||
const result = await crowdsec.exportCrowdsecConfig()
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/admin/crowdsec/export', { responseType: 'blob' })
|
||||
expect(result).toEqual(mockBlob)
|
||||
})
|
||||
})
|
||||
|
||||
describe('listCrowdsecFiles', () => {
|
||||
it('should call GET /admin/crowdsec/files', async () => {
|
||||
const mockData = { files: ['file1.yaml', 'file2.yaml'] }
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await crowdsec.listCrowdsecFiles()
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/admin/crowdsec/files')
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
|
||||
describe('readCrowdsecFile', () => {
|
||||
it('should call GET /admin/crowdsec/file with encoded path', async () => {
|
||||
const mockData = { content: 'file content' }
|
||||
const path = '/etc/crowdsec/file.yaml'
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await crowdsec.readCrowdsecFile(path)
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith(
|
||||
`/admin/crowdsec/file?path=${encodeURIComponent(path)}`
|
||||
)
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
|
||||
describe('writeCrowdsecFile', () => {
|
||||
it('should call POST /admin/crowdsec/file with path and content', async () => {
|
||||
const mockData = { success: true }
|
||||
const path = '/etc/crowdsec/file.yaml'
|
||||
const content = 'new content'
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await crowdsec.writeCrowdsecFile(path, content)
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/file', { path, content })
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
|
||||
describe('default export', () => {
|
||||
it('should export all functions', () => {
|
||||
expect(crowdsec.default).toHaveProperty('startCrowdsec')
|
||||
expect(crowdsec.default).toHaveProperty('stopCrowdsec')
|
||||
expect(crowdsec.default).toHaveProperty('statusCrowdsec')
|
||||
expect(crowdsec.default).toHaveProperty('importCrowdsecConfig')
|
||||
expect(crowdsec.default).toHaveProperty('exportCrowdsecConfig')
|
||||
expect(crowdsec.default).toHaveProperty('listCrowdsecFiles')
|
||||
expect(crowdsec.default).toHaveProperty('readCrowdsecFile')
|
||||
expect(crowdsec.default).toHaveProperty('writeCrowdsecFile')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,138 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { detectDNSProvider, getDetectionPatterns } from '../dnsDetection'
|
||||
import client from '../client'
|
||||
import type { DetectionResult, NameserverPattern } from '../dnsDetection'
|
||||
|
||||
vi.mock('../client')
|
||||
|
||||
describe('dnsDetection API', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('detectDNSProvider', () => {
|
||||
it('should detect DNS provider successfully', async () => {
|
||||
const mockResponse: DetectionResult = {
|
||||
domain: 'example.com',
|
||||
detected: true,
|
||||
provider_type: 'cloudflare',
|
||||
nameservers: ['ns1.cloudflare.com', 'ns2.cloudflare.com'],
|
||||
confidence: 'high',
|
||||
suggested_provider: {
|
||||
id: 1,
|
||||
uuid: 'test-uuid',
|
||||
name: 'Production Cloudflare',
|
||||
provider_type: 'cloudflare',
|
||||
enabled: true,
|
||||
is_default: true,
|
||||
has_credentials: true,
|
||||
propagation_timeout: 120,
|
||||
polling_interval: 5,
|
||||
success_count: 10,
|
||||
failure_count: 0,
|
||||
created_at: '2026-01-01T00:00:00Z',
|
||||
updated_at: '2026-01-01T00:00:00Z',
|
||||
},
|
||||
}
|
||||
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
|
||||
|
||||
const result = await detectDNSProvider('example.com')
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/dns-providers/detect', { domain: 'example.com' })
|
||||
expect(result).toEqual(mockResponse)
|
||||
expect(result.detected).toBe(true)
|
||||
expect(result.provider_type).toBe('cloudflare')
|
||||
expect(result.confidence).toBe('high')
|
||||
})
|
||||
|
||||
it('should handle detection failure (no provider found)', async () => {
|
||||
const mockResponse: DetectionResult = {
|
||||
domain: 'example.com',
|
||||
detected: false,
|
||||
nameservers: ['ns1.unknown.com', 'ns2.unknown.com'],
|
||||
confidence: 'none',
|
||||
}
|
||||
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
|
||||
|
||||
const result = await detectDNSProvider('example.com')
|
||||
|
||||
expect(result.detected).toBe(false)
|
||||
expect(result.confidence).toBe('none')
|
||||
expect(result.nameservers).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should handle detection error', async () => {
|
||||
const mockResponse: DetectionResult = {
|
||||
domain: 'invalid.domain',
|
||||
detected: false,
|
||||
nameservers: [],
|
||||
confidence: 'none',
|
||||
error: 'Failed to lookup nameservers: domain not found',
|
||||
}
|
||||
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
|
||||
|
||||
const result = await detectDNSProvider('invalid.domain')
|
||||
|
||||
expect(result.detected).toBe(false)
|
||||
expect(result.error).toContain('domain not found')
|
||||
})
|
||||
|
||||
it('should handle network error', async () => {
|
||||
vi.mocked(client.post).mockRejectedValue(new Error('Network error'))
|
||||
|
||||
await expect(detectDNSProvider('example.com')).rejects.toThrow('Network error')
|
||||
})
|
||||
|
||||
it('should handle medium confidence detection', async () => {
|
||||
const mockResponse: DetectionResult = {
|
||||
domain: 'example.com',
|
||||
detected: true,
|
||||
provider_type: 'route53',
|
||||
nameservers: ['ns-123.awsdns-12.com'],
|
||||
confidence: 'medium',
|
||||
}
|
||||
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
|
||||
|
||||
const result = await detectDNSProvider('example.com')
|
||||
|
||||
expect(result.confidence).toBe('medium')
|
||||
expect(result.detected).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getDetectionPatterns', () => {
|
||||
it('should fetch detection patterns successfully', async () => {
|
||||
const mockPatterns: NameserverPattern[] = [
|
||||
{ pattern: '.ns.cloudflare.com', provider_type: 'cloudflare' },
|
||||
{ pattern: '.awsdns', provider_type: 'route53' },
|
||||
{ pattern: '.digitalocean.com', provider_type: 'digitalocean' },
|
||||
]
|
||||
|
||||
vi.mocked(client.get).mockResolvedValue({ data: { patterns: mockPatterns } })
|
||||
|
||||
const result = await getDetectionPatterns()
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/dns-providers/patterns')
|
||||
expect(result).toEqual(mockPatterns)
|
||||
expect(result).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('should handle empty patterns list', async () => {
|
||||
vi.mocked(client.get).mockResolvedValue({ data: { patterns: [] } })
|
||||
|
||||
const result = await getDetectionPatterns()
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('should handle network error when fetching patterns', async () => {
|
||||
vi.mocked(client.get).mockRejectedValue(new Error('Network error'))
|
||||
|
||||
await expect(getDetectionPatterns()).rejects.toThrow('Network error')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,431 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import {
|
||||
getDNSProviders,
|
||||
getDNSProvider,
|
||||
getDNSProviderTypes,
|
||||
createDNSProvider,
|
||||
updateDNSProvider,
|
||||
deleteDNSProvider,
|
||||
testDNSProvider,
|
||||
testDNSProviderCredentials,
|
||||
type DNSProvider,
|
||||
type DNSProviderRequest,
|
||||
type DNSProviderTypeInfo,
|
||||
} from '../dnsProviders'
|
||||
import client from '../client'
|
||||
|
||||
vi.mock('../client')
|
||||
|
||||
const mockProvider: DNSProvider = {
|
||||
id: 1,
|
||||
uuid: 'test-uuid-1',
|
||||
name: 'Cloudflare Production',
|
||||
provider_type: 'cloudflare',
|
||||
enabled: true,
|
||||
is_default: true,
|
||||
has_credentials: true,
|
||||
propagation_timeout: 120,
|
||||
polling_interval: 2,
|
||||
success_count: 5,
|
||||
failure_count: 0,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
}
|
||||
|
||||
const mockProviderType: DNSProviderTypeInfo = {
|
||||
type: 'cloudflare',
|
||||
name: 'Cloudflare',
|
||||
fields: [
|
||||
{
|
||||
name: 'api_token',
|
||||
label: 'API Token',
|
||||
type: 'password',
|
||||
required: true,
|
||||
hint: 'Cloudflare API token with DNS edit permissions',
|
||||
},
|
||||
],
|
||||
documentation_url: 'https://developers.cloudflare.com/api/',
|
||||
}
|
||||
|
||||
describe('getDNSProviders', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('fetches all DNS providers successfully', async () => {
|
||||
const mockProviders = [mockProvider, { ...mockProvider, id: 2, name: 'Secondary' }]
|
||||
vi.mocked(client.get).mockResolvedValue({
|
||||
data: { providers: mockProviders, total: 2 },
|
||||
})
|
||||
|
||||
const result = await getDNSProviders()
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/dns-providers')
|
||||
expect(result).toEqual(mockProviders)
|
||||
expect(result).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('returns empty array when no providers exist', async () => {
|
||||
vi.mocked(client.get).mockResolvedValue({
|
||||
data: { providers: [], total: 0 },
|
||||
})
|
||||
|
||||
const result = await getDNSProviders()
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('handles network errors', async () => {
|
||||
vi.mocked(client.get).mockRejectedValue(new Error('Network error'))
|
||||
|
||||
await expect(getDNSProviders()).rejects.toThrow('Network error')
|
||||
})
|
||||
|
||||
it('handles server errors', async () => {
|
||||
vi.mocked(client.get).mockRejectedValue({ response: { status: 500 } })
|
||||
|
||||
await expect(getDNSProviders()).rejects.toMatchObject({
|
||||
response: { status: 500 },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getDNSProvider', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('fetches single provider by valid ID', async () => {
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockProvider })
|
||||
|
||||
const result = await getDNSProvider(1)
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/dns-providers/1')
|
||||
expect(result).toEqual(mockProvider)
|
||||
})
|
||||
|
||||
it('handles not found error for invalid ID', async () => {
|
||||
vi.mocked(client.get).mockRejectedValue({ response: { status: 404 } })
|
||||
|
||||
await expect(getDNSProvider(999)).rejects.toMatchObject({
|
||||
response: { status: 404 },
|
||||
})
|
||||
})
|
||||
|
||||
it('handles server errors', async () => {
|
||||
vi.mocked(client.get).mockRejectedValue({ response: { status: 500 } })
|
||||
|
||||
await expect(getDNSProvider(1)).rejects.toMatchObject({
|
||||
response: { status: 500 },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getDNSProviderTypes', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('fetches supported provider types with field definitions', async () => {
|
||||
const mockTypes = [
|
||||
mockProviderType,
|
||||
{
|
||||
type: 'route53',
|
||||
name: 'AWS Route 53',
|
||||
fields: [
|
||||
{ name: 'access_key_id', label: 'Access Key ID', type: 'text', required: true },
|
||||
{ name: 'secret_access_key', label: 'Secret Access Key', type: 'password', required: true },
|
||||
],
|
||||
documentation_url: 'https://aws.amazon.com/route53/',
|
||||
} as DNSProviderTypeInfo,
|
||||
]
|
||||
vi.mocked(client.get).mockResolvedValue({
|
||||
data: { types: mockTypes },
|
||||
})
|
||||
|
||||
const result = await getDNSProviderTypes()
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/dns-providers/types')
|
||||
expect(result).toEqual(mockTypes)
|
||||
expect(result).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('handles errors when fetching types', async () => {
|
||||
vi.mocked(client.get).mockRejectedValue(new Error('Failed to fetch types'))
|
||||
|
||||
await expect(getDNSProviderTypes()).rejects.toThrow('Failed to fetch types')
|
||||
})
|
||||
})
|
||||
|
||||
describe('createDNSProvider', () => {
|
||||
const validRequest: DNSProviderRequest = {
|
||||
name: 'New Cloudflare',
|
||||
provider_type: 'cloudflare',
|
||||
credentials: { api_token: 'test-token-123' },
|
||||
propagation_timeout: 120,
|
||||
polling_interval: 2,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('creates provider successfully and returns with ID', async () => {
|
||||
const createdProvider = { ...mockProvider, id: 5, name: 'New Cloudflare' }
|
||||
vi.mocked(client.post).mockResolvedValue({ data: createdProvider })
|
||||
|
||||
const result = await createDNSProvider(validRequest)
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/dns-providers', validRequest)
|
||||
expect(result).toEqual(createdProvider)
|
||||
expect(result.id).toBe(5)
|
||||
})
|
||||
|
||||
it('handles validation error for missing required fields', async () => {
|
||||
vi.mocked(client.post).mockRejectedValue({
|
||||
response: { status: 400, data: { error: 'Missing required field: api_token' } },
|
||||
})
|
||||
|
||||
await expect(
|
||||
createDNSProvider({ ...validRequest, credentials: {} })
|
||||
).rejects.toMatchObject({
|
||||
response: { status: 400 },
|
||||
})
|
||||
})
|
||||
|
||||
it('handles validation error for invalid provider type', async () => {
|
||||
vi.mocked(client.post).mockRejectedValue({
|
||||
response: { status: 400, data: { error: 'Invalid provider type' } },
|
||||
})
|
||||
|
||||
await expect(
|
||||
createDNSProvider({ ...validRequest, provider_type: 'invalid' as DNSProviderRequest['provider_type'] })
|
||||
).rejects.toMatchObject({
|
||||
response: { status: 400 },
|
||||
})
|
||||
})
|
||||
|
||||
it('handles duplicate name error', async () => {
|
||||
vi.mocked(client.post).mockRejectedValue({
|
||||
response: { status: 409, data: { error: 'Provider with this name already exists' } },
|
||||
})
|
||||
|
||||
await expect(createDNSProvider(validRequest)).rejects.toMatchObject({
|
||||
response: { status: 409 },
|
||||
})
|
||||
})
|
||||
|
||||
it('handles server errors', async () => {
|
||||
vi.mocked(client.post).mockRejectedValue({ response: { status: 500 } })
|
||||
|
||||
await expect(createDNSProvider(validRequest)).rejects.toMatchObject({
|
||||
response: { status: 500 },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateDNSProvider', () => {
|
||||
const updateRequest: DNSProviderRequest = {
|
||||
name: 'Updated Name',
|
||||
provider_type: 'cloudflare',
|
||||
credentials: { api_token: 'new-token' },
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('updates provider successfully', async () => {
|
||||
const updatedProvider = { ...mockProvider, name: 'Updated Name' }
|
||||
vi.mocked(client.put).mockResolvedValue({ data: updatedProvider })
|
||||
|
||||
const result = await updateDNSProvider(1, updateRequest)
|
||||
|
||||
expect(client.put).toHaveBeenCalledWith('/dns-providers/1', updateRequest)
|
||||
expect(result).toEqual(updatedProvider)
|
||||
expect(result.name).toBe('Updated Name')
|
||||
})
|
||||
|
||||
it('handles not found error', async () => {
|
||||
vi.mocked(client.put).mockRejectedValue({ response: { status: 404 } })
|
||||
|
||||
await expect(updateDNSProvider(999, updateRequest)).rejects.toMatchObject({
|
||||
response: { status: 404 },
|
||||
})
|
||||
})
|
||||
|
||||
it('handles validation errors', async () => {
|
||||
vi.mocked(client.put).mockRejectedValue({
|
||||
response: { status: 400, data: { error: 'Invalid credentials' } },
|
||||
})
|
||||
|
||||
await expect(updateDNSProvider(1, updateRequest)).rejects.toMatchObject({
|
||||
response: { status: 400 },
|
||||
})
|
||||
})
|
||||
|
||||
it('handles server errors', async () => {
|
||||
vi.mocked(client.put).mockRejectedValue({ response: { status: 500 } })
|
||||
|
||||
await expect(updateDNSProvider(1, updateRequest)).rejects.toMatchObject({
|
||||
response: { status: 500 },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteDNSProvider', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('deletes provider successfully', async () => {
|
||||
vi.mocked(client.delete).mockResolvedValue({ data: undefined })
|
||||
|
||||
await deleteDNSProvider(1)
|
||||
|
||||
expect(client.delete).toHaveBeenCalledWith('/dns-providers/1')
|
||||
})
|
||||
|
||||
it('handles not found error', async () => {
|
||||
vi.mocked(client.delete).mockRejectedValue({ response: { status: 404 } })
|
||||
|
||||
await expect(deleteDNSProvider(999)).rejects.toMatchObject({
|
||||
response: { status: 404 },
|
||||
})
|
||||
})
|
||||
|
||||
it('handles in-use error when provider used by proxy hosts', async () => {
|
||||
vi.mocked(client.delete).mockRejectedValue({
|
||||
response: {
|
||||
status: 409,
|
||||
data: { error: 'Cannot delete provider in use by proxy hosts' },
|
||||
},
|
||||
})
|
||||
|
||||
await expect(deleteDNSProvider(1)).rejects.toMatchObject({
|
||||
response: { status: 409 },
|
||||
})
|
||||
})
|
||||
|
||||
it('handles server errors', async () => {
|
||||
vi.mocked(client.delete).mockRejectedValue({ response: { status: 500 } })
|
||||
|
||||
await expect(deleteDNSProvider(1)).rejects.toMatchObject({
|
||||
response: { status: 500 },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('testDNSProvider', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('returns success result with propagation time', async () => {
|
||||
const successResult = {
|
||||
success: true,
|
||||
message: 'DNS challenge completed successfully',
|
||||
propagation_time_ms: 1500,
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValue({ data: successResult })
|
||||
|
||||
const result = await testDNSProvider(1)
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/dns-providers/1/test')
|
||||
expect(result).toEqual(successResult)
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.propagation_time_ms).toBe(1500)
|
||||
})
|
||||
|
||||
it('returns failure result with error message', async () => {
|
||||
const failureResult = {
|
||||
success: false,
|
||||
error: 'Invalid API token',
|
||||
code: 'AUTH_FAILED',
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValue({ data: failureResult })
|
||||
|
||||
const result = await testDNSProvider(1)
|
||||
|
||||
expect(result).toEqual(failureResult)
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error).toBe('Invalid API token')
|
||||
})
|
||||
|
||||
it('handles not found error', async () => {
|
||||
vi.mocked(client.post).mockRejectedValue({ response: { status: 404 } })
|
||||
|
||||
await expect(testDNSProvider(999)).rejects.toMatchObject({
|
||||
response: { status: 404 },
|
||||
})
|
||||
})
|
||||
|
||||
it('handles server errors', async () => {
|
||||
vi.mocked(client.post).mockRejectedValue({ response: { status: 500 } })
|
||||
|
||||
await expect(testDNSProvider(1)).rejects.toMatchObject({
|
||||
response: { status: 500 },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('testDNSProviderCredentials', () => {
|
||||
const testRequest: DNSProviderRequest = {
|
||||
name: 'Test Provider',
|
||||
provider_type: 'cloudflare',
|
||||
credentials: { api_token: 'test-token' },
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('returns success for valid credentials', async () => {
|
||||
const successResult = {
|
||||
success: true,
|
||||
message: 'Credentials validated successfully',
|
||||
propagation_time_ms: 800,
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValue({ data: successResult })
|
||||
|
||||
const result = await testDNSProviderCredentials(testRequest)
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/dns-providers/test', testRequest)
|
||||
expect(result).toEqual(successResult)
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('returns failure for invalid credentials', async () => {
|
||||
const failureResult = {
|
||||
success: false,
|
||||
error: 'Authentication failed',
|
||||
code: 'INVALID_CREDENTIALS',
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValue({ data: failureResult })
|
||||
|
||||
const result = await testDNSProviderCredentials(testRequest)
|
||||
|
||||
expect(result).toEqual(failureResult)
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('handles validation errors for missing credentials', async () => {
|
||||
vi.mocked(client.post).mockRejectedValue({
|
||||
response: { status: 400, data: { error: 'Missing required field: api_token' } },
|
||||
})
|
||||
|
||||
await expect(
|
||||
testDNSProviderCredentials({ ...testRequest, credentials: {} })
|
||||
).rejects.toMatchObject({
|
||||
response: { status: 400 },
|
||||
})
|
||||
})
|
||||
|
||||
it('handles server errors', async () => {
|
||||
vi.mocked(client.post).mockRejectedValue({ response: { status: 500 } })
|
||||
|
||||
await expect(testDNSProviderCredentials(testRequest)).rejects.toMatchObject({
|
||||
response: { status: 500 },
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,96 +0,0 @@
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import { dockerApi } from '../docker';
|
||||
import client from '../client';
|
||||
|
||||
vi.mock('../client', () => ({
|
||||
default: {
|
||||
get: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('dockerApi', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('listContainers', () => {
|
||||
const mockContainers = [
|
||||
{
|
||||
id: 'abc123',
|
||||
names: ['/container1'],
|
||||
image: 'nginx:latest',
|
||||
state: 'running',
|
||||
status: 'Up 2 hours',
|
||||
network: 'bridge',
|
||||
ip: '172.17.0.2',
|
||||
ports: [{ private_port: 80, public_port: 8080, type: 'tcp' }],
|
||||
},
|
||||
{
|
||||
id: 'def456',
|
||||
names: ['/container2'],
|
||||
image: 'redis:alpine',
|
||||
state: 'running',
|
||||
status: 'Up 1 hour',
|
||||
network: 'bridge',
|
||||
ip: '172.17.0.3',
|
||||
ports: [],
|
||||
},
|
||||
];
|
||||
|
||||
it('fetches containers without parameters', async () => {
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockContainers });
|
||||
|
||||
const result = await dockerApi.listContainers();
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/docker/containers', { params: {} });
|
||||
expect(result).toEqual(mockContainers);
|
||||
});
|
||||
|
||||
it('fetches containers with host parameter', async () => {
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockContainers });
|
||||
|
||||
const result = await dockerApi.listContainers('192.168.1.100');
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/docker/containers', {
|
||||
params: { host: '192.168.1.100' },
|
||||
});
|
||||
expect(result).toEqual(mockContainers);
|
||||
});
|
||||
|
||||
it('fetches containers with serverId parameter', async () => {
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockContainers });
|
||||
|
||||
const result = await dockerApi.listContainers(undefined, 'server-uuid-123');
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/docker/containers', {
|
||||
params: { server_id: 'server-uuid-123' },
|
||||
});
|
||||
expect(result).toEqual(mockContainers);
|
||||
});
|
||||
|
||||
it('fetches containers with both host and serverId parameters', async () => {
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockContainers });
|
||||
|
||||
const result = await dockerApi.listContainers('192.168.1.100', 'server-uuid-123');
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/docker/containers', {
|
||||
params: { host: '192.168.1.100', server_id: 'server-uuid-123' },
|
||||
});
|
||||
expect(result).toEqual(mockContainers);
|
||||
});
|
||||
|
||||
it('returns empty array when no containers', async () => {
|
||||
vi.mocked(client.get).mockResolvedValue({ data: [] });
|
||||
|
||||
const result = await dockerApi.listContainers();
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('handles API error', async () => {
|
||||
vi.mocked(client.get).mockRejectedValue(new Error('Network error'));
|
||||
|
||||
await expect(dockerApi.listContainers()).rejects.toThrow('Network error');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import client from '../client';
|
||||
import { getDomains, createDomain, deleteDomain, Domain } from '../domains';
|
||||
|
||||
vi.mock('../client', () => ({
|
||||
default: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('domains API', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockDomain: Domain = {
|
||||
id: 1,
|
||||
uuid: '123',
|
||||
name: 'example.com',
|
||||
created_at: '2023-01-01',
|
||||
};
|
||||
|
||||
it('getDomains calls client.get', async () => {
|
||||
vi.mocked(client.get).mockResolvedValue({ data: [mockDomain] });
|
||||
const result = await getDomains();
|
||||
expect(client.get).toHaveBeenCalledWith('/domains');
|
||||
expect(result).toEqual([mockDomain]);
|
||||
});
|
||||
|
||||
it('createDomain calls client.post', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockDomain });
|
||||
const result = await createDomain('example.com');
|
||||
expect(client.post).toHaveBeenCalledWith('/domains', { name: 'example.com' });
|
||||
expect(result).toEqual(mockDomain);
|
||||
});
|
||||
|
||||
it('deleteDomain calls client.delete', async () => {
|
||||
vi.mocked(client.delete).mockResolvedValue({ data: {} });
|
||||
await deleteDomain('123');
|
||||
expect(client.delete).toHaveBeenCalledWith('/domains/123');
|
||||
});
|
||||
});
|
||||
@@ -1,95 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import {
|
||||
getEncryptionStatus,
|
||||
rotateEncryptionKey,
|
||||
getRotationHistory,
|
||||
validateKeyConfiguration,
|
||||
type RotationStatus,
|
||||
type RotationResult,
|
||||
type RotationHistoryEntry,
|
||||
type KeyValidationResult,
|
||||
} from '../encryption'
|
||||
import client from '../client'
|
||||
|
||||
vi.mock('../client')
|
||||
|
||||
const mockRotationStatus: RotationStatus = {
|
||||
current_version: 2,
|
||||
next_key_configured: true,
|
||||
legacy_key_count: 1,
|
||||
providers_on_current_version: 5,
|
||||
providers_on_older_versions: 0,
|
||||
}
|
||||
|
||||
const mockRotationResult: RotationResult = {
|
||||
total_providers: 5,
|
||||
success_count: 5,
|
||||
failure_count: 0,
|
||||
duration: '2.5s',
|
||||
new_key_version: 3,
|
||||
}
|
||||
|
||||
const mockHistoryEntry: RotationHistoryEntry = {
|
||||
id: 1,
|
||||
uuid: 'test-uuid-1',
|
||||
actor: 'admin@example.com',
|
||||
action: 'encryption_key_rotated',
|
||||
event_category: 'security',
|
||||
details: 'Rotated from version 1 to version 2',
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
}
|
||||
|
||||
const mockValidationResult: KeyValidationResult = {
|
||||
valid: true,
|
||||
message: 'Key configuration is valid',
|
||||
}
|
||||
|
||||
describe('encryption API', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should call getEncryptionStatus with correct endpoint', async () => {
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockRotationStatus })
|
||||
|
||||
const result = await getEncryptionStatus()
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/admin/encryption/status')
|
||||
expect(result).toEqual(mockRotationStatus)
|
||||
expect(result.current_version).toBe(2)
|
||||
})
|
||||
|
||||
it('should call rotateEncryptionKey with correct endpoint', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockRotationResult })
|
||||
|
||||
const result = await rotateEncryptionKey()
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/admin/encryption/rotate')
|
||||
expect(result).toEqual(mockRotationResult)
|
||||
expect(result.new_key_version).toBe(3)
|
||||
expect(result.success_count).toBe(5)
|
||||
})
|
||||
|
||||
it('should call getRotationHistory with correct endpoint', async () => {
|
||||
const mockHistory = [mockHistoryEntry, { ...mockHistoryEntry, id: 2 }]
|
||||
vi.mocked(client.get).mockResolvedValue({
|
||||
data: { history: mockHistory, total: 2 },
|
||||
})
|
||||
|
||||
const result = await getRotationHistory()
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/admin/encryption/history')
|
||||
expect(result).toEqual(mockHistory)
|
||||
expect(result).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should call validateKeyConfiguration with correct endpoint', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockValidationResult })
|
||||
|
||||
const result = await validateKeyConfiguration()
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/admin/encryption/validate')
|
||||
expect(result).toEqual(mockValidationResult)
|
||||
expect(result.valid).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,218 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { connectLiveLogs } from '../logs';
|
||||
|
||||
// Mock WebSocket
|
||||
class MockWebSocket {
|
||||
url: string;
|
||||
onmessage: ((event: MessageEvent) => void) | null = null;
|
||||
onerror: ((error: Event) => void) | null = null;
|
||||
onclose: ((event: CloseEvent) => void) | null = null;
|
||||
readyState: number = WebSocket.CONNECTING;
|
||||
|
||||
static CONNECTING = 0;
|
||||
static OPEN = 1;
|
||||
static CLOSING = 2;
|
||||
static CLOSED = 3;
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
// Simulate connection opening
|
||||
setTimeout(() => {
|
||||
this.readyState = WebSocket.OPEN;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.readyState = WebSocket.CLOSING;
|
||||
setTimeout(() => {
|
||||
this.readyState = WebSocket.CLOSED;
|
||||
const closeEvent = { code: 1000, reason: '', wasClean: true } as CloseEvent;
|
||||
if (this.onclose) {
|
||||
this.onclose(closeEvent);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
simulateMessage(data: string) {
|
||||
if (this.onmessage) {
|
||||
const event = new MessageEvent('message', { data });
|
||||
this.onmessage(event);
|
||||
}
|
||||
}
|
||||
|
||||
simulateError() {
|
||||
if (this.onerror) {
|
||||
const event = new Event('error');
|
||||
this.onerror(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('logs API - connectLiveLogs', () => {
|
||||
let mockWebSocket: MockWebSocket;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock global WebSocket
|
||||
mockWebSocket = new MockWebSocket('');
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).WebSocket = class MockedWebSocket extends MockWebSocket {
|
||||
constructor(url: string) {
|
||||
super(url);
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
mockWebSocket = this;
|
||||
}
|
||||
} as unknown as typeof WebSocket;
|
||||
|
||||
// Mock window.location
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
protocol: 'http:',
|
||||
host: 'localhost:8080',
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates WebSocket connection with correct URL', () => {
|
||||
connectLiveLogs({}, vi.fn());
|
||||
|
||||
expect(mockWebSocket.url).toBe('ws://localhost:8080/api/v1/logs/live?');
|
||||
});
|
||||
|
||||
it('uses wss protocol when page is https', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
protocol: 'https:',
|
||||
host: 'example.com',
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
|
||||
connectLiveLogs({}, vi.fn());
|
||||
|
||||
expect(mockWebSocket.url).toBe('wss://example.com/api/v1/logs/live?');
|
||||
});
|
||||
|
||||
it('includes filters in query parameters', () => {
|
||||
connectLiveLogs({ level: 'error', source: 'waf' }, vi.fn());
|
||||
|
||||
expect(mockWebSocket.url).toContain('level=error');
|
||||
expect(mockWebSocket.url).toContain('source=waf');
|
||||
});
|
||||
|
||||
it('calls onMessage callback when message is received', () => {
|
||||
const mockOnMessage = vi.fn();
|
||||
connectLiveLogs({}, mockOnMessage);
|
||||
|
||||
const logData = {
|
||||
level: 'info',
|
||||
timestamp: '2025-12-09T10:30:00Z',
|
||||
message: 'Test message',
|
||||
};
|
||||
|
||||
mockWebSocket.simulateMessage(JSON.stringify(logData));
|
||||
|
||||
expect(mockOnMessage).toHaveBeenCalledWith(logData);
|
||||
});
|
||||
|
||||
it('handles JSON parse errors gracefully', () => {
|
||||
const mockOnMessage = vi.fn();
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
connectLiveLogs({}, mockOnMessage);
|
||||
|
||||
mockWebSocket.simulateMessage('invalid json');
|
||||
|
||||
expect(mockOnMessage).not.toHaveBeenCalled();
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to parse log message:', expect.any(Error));
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
// These tests are skipped because the WebSocket mock has timing issues with event handlers
|
||||
// The functionality is covered by E2E tests
|
||||
it.skip('calls onError callback when error occurs', async () => {
|
||||
const mockOnError = vi.fn();
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
connectLiveLogs({}, vi.fn(), mockOnError);
|
||||
|
||||
// Wait for handlers to be set up
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
mockWebSocket.simulateError();
|
||||
|
||||
expect(mockOnError).toHaveBeenCalled();
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('WebSocket error:', expect.any(Event));
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it.skip('calls onClose callback when connection closes', async () => {
|
||||
const mockOnClose = vi.fn();
|
||||
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
connectLiveLogs({}, vi.fn(), undefined, mockOnClose);
|
||||
|
||||
// Wait for handlers to be set up
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
mockWebSocket.close();
|
||||
|
||||
// Wait for the close event to be processed
|
||||
await new Promise(resolve => setTimeout(resolve, 20));
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
consoleLogSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('returns a close function that closes the WebSocket', async () => {
|
||||
const closeConnection = connectLiveLogs({}, vi.fn());
|
||||
|
||||
// Wait for connection to open
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
expect(mockWebSocket.readyState).toBe(WebSocket.OPEN);
|
||||
|
||||
closeConnection();
|
||||
|
||||
expect(mockWebSocket.readyState).toBeGreaterThanOrEqual(WebSocket.CLOSING);
|
||||
});
|
||||
|
||||
it('does not throw when closing already closed connection', () => {
|
||||
const closeConnection = connectLiveLogs({}, vi.fn());
|
||||
|
||||
mockWebSocket.readyState = WebSocket.CLOSED;
|
||||
|
||||
expect(() => closeConnection()).not.toThrow();
|
||||
});
|
||||
|
||||
it('handles missing optional callbacks', () => {
|
||||
// Should not throw with only required onMessage callback
|
||||
expect(() => connectLiveLogs({}, vi.fn())).not.toThrow();
|
||||
|
||||
const mockOnMessage = vi.fn();
|
||||
connectLiveLogs({}, mockOnMessage);
|
||||
|
||||
// Simulate various events
|
||||
mockWebSocket.simulateMessage(JSON.stringify({ level: 'info', timestamp: '2025-12-09T10:30:00Z', message: 'test' }));
|
||||
mockWebSocket.simulateError();
|
||||
|
||||
expect(mockOnMessage).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('processes multiple messages in sequence', () => {
|
||||
const mockOnMessage = vi.fn();
|
||||
connectLiveLogs({}, mockOnMessage);
|
||||
|
||||
const log1 = { level: 'info', timestamp: '2025-12-09T10:30:00Z', message: 'Message 1' };
|
||||
const log2 = { level: 'error', timestamp: '2025-12-09T10:30:01Z', message: 'Message 2' };
|
||||
|
||||
mockWebSocket.simulateMessage(JSON.stringify(log1));
|
||||
mockWebSocket.simulateMessage(JSON.stringify(log2));
|
||||
|
||||
expect(mockOnMessage).toHaveBeenCalledTimes(2);
|
||||
expect(mockOnMessage).toHaveBeenNthCalledWith(1, log1);
|
||||
expect(mockOnMessage).toHaveBeenNthCalledWith(2, log2);
|
||||
});
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import client from '../client'
|
||||
import { downloadLog, getLogContent, getLogs } from '../logs'
|
||||
|
||||
vi.mock('../client', () => ({
|
||||
default: {
|
||||
get: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('logs api http helpers', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { href: 'http://localhost' },
|
||||
writable: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('fetches log list and content with filters', async () => {
|
||||
vi.mocked(client.get).mockResolvedValueOnce({ data: [{ name: 'access.log', size: 10, mod_time: 'now' }] })
|
||||
const logs = await getLogs()
|
||||
expect(logs[0].name).toBe('access.log')
|
||||
expect(client.get).toHaveBeenCalledWith('/logs')
|
||||
|
||||
vi.mocked(client.get).mockResolvedValueOnce({ data: { filename: 'access.log', logs: [], total: 0, limit: 100, offset: 0 } })
|
||||
const resp = await getLogContent('access.log', {
|
||||
search: 'bot',
|
||||
host: 'example.com',
|
||||
status: '500',
|
||||
level: 'error',
|
||||
limit: 50,
|
||||
offset: 5,
|
||||
sort: 'asc',
|
||||
})
|
||||
expect(resp.filename).toBe('access.log')
|
||||
expect(client.get).toHaveBeenCalledWith('/logs/access.log?search=bot&host=example.com&status=500&level=error&limit=50&offset=5&sort=asc')
|
||||
})
|
||||
|
||||
it('downloads log via window location', () => {
|
||||
downloadLog('access.log')
|
||||
expect(window.location.href).toBe('/api/v1/logs/access.log/download')
|
||||
})
|
||||
})
|
||||
@@ -1,230 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import {
|
||||
getChallenge,
|
||||
createChallenge,
|
||||
verifyChallenge,
|
||||
pollChallenge,
|
||||
deleteChallenge,
|
||||
} from '../manualChallenge'
|
||||
import client from '../client'
|
||||
|
||||
vi.mock('../client', () => ({
|
||||
default: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('manualChallenge API', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('getChallenge', () => {
|
||||
it('fetches challenge by provider and challenge ID', async () => {
|
||||
const mockChallenge = {
|
||||
id: 'challenge-uuid',
|
||||
status: 'pending',
|
||||
fqdn: '_acme-challenge.example.com',
|
||||
value: 'test-value',
|
||||
ttl: 300,
|
||||
created_at: '2026-01-11T00:00:00Z',
|
||||
expires_at: '2026-01-11T00:10:00Z',
|
||||
dns_propagated: false,
|
||||
}
|
||||
|
||||
vi.mocked(client.get).mockResolvedValueOnce({ data: mockChallenge })
|
||||
|
||||
const result = await getChallenge(1, 'challenge-uuid')
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith(
|
||||
'/dns-providers/1/manual-challenge/challenge-uuid'
|
||||
)
|
||||
expect(result).toEqual(mockChallenge)
|
||||
})
|
||||
|
||||
it('throws error when challenge not found', async () => {
|
||||
vi.mocked(client.get).mockRejectedValueOnce({
|
||||
response: { status: 404, data: { error: 'Challenge not found' } },
|
||||
})
|
||||
|
||||
await expect(getChallenge(1, 'invalid-uuid')).rejects.toMatchObject({
|
||||
response: { status: 404 },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('createChallenge', () => {
|
||||
it('creates a new challenge for the provider', async () => {
|
||||
const mockChallenge = {
|
||||
id: 'new-challenge-uuid',
|
||||
status: 'created',
|
||||
fqdn: '_acme-challenge.example.com',
|
||||
value: 'generated-value',
|
||||
ttl: 300,
|
||||
created_at: '2026-01-11T00:00:00Z',
|
||||
expires_at: '2026-01-11T00:10:00Z',
|
||||
dns_propagated: false,
|
||||
}
|
||||
|
||||
vi.mocked(client.post).mockResolvedValueOnce({ data: mockChallenge })
|
||||
|
||||
const result = await createChallenge(1, { domain: 'example.com' })
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/dns-providers/1/manual-challenge', {
|
||||
domain: 'example.com',
|
||||
})
|
||||
expect(result).toEqual(mockChallenge)
|
||||
})
|
||||
|
||||
it('throws error when provider not found', async () => {
|
||||
vi.mocked(client.post).mockRejectedValueOnce({
|
||||
response: { status: 404, data: { error: 'Provider not found' } },
|
||||
})
|
||||
|
||||
await expect(createChallenge(999, { domain: 'example.com' })).rejects.toMatchObject({
|
||||
response: { status: 404 },
|
||||
})
|
||||
})
|
||||
|
||||
it('throws error when challenge already in progress', async () => {
|
||||
vi.mocked(client.post).mockRejectedValueOnce({
|
||||
response: { status: 409, data: { code: 'CHALLENGE_IN_PROGRESS' } },
|
||||
})
|
||||
|
||||
await expect(createChallenge(1, { domain: 'example.com' })).rejects.toMatchObject({
|
||||
response: { status: 409 },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('verifyChallenge', () => {
|
||||
it('triggers verification for a challenge', async () => {
|
||||
const mockResult = {
|
||||
success: true,
|
||||
dns_found: true,
|
||||
message: 'TXT record verified successfully',
|
||||
}
|
||||
|
||||
vi.mocked(client.post).mockResolvedValueOnce({ data: mockResult })
|
||||
|
||||
const result = await verifyChallenge(1, 'challenge-uuid')
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith(
|
||||
'/dns-providers/1/manual-challenge/challenge-uuid/verify'
|
||||
)
|
||||
expect(result).toEqual(mockResult)
|
||||
})
|
||||
|
||||
it('returns dns_found false when record not propagated', async () => {
|
||||
const mockResult = {
|
||||
success: false,
|
||||
dns_found: false,
|
||||
message: 'DNS record not found',
|
||||
}
|
||||
|
||||
vi.mocked(client.post).mockResolvedValueOnce({ data: mockResult })
|
||||
|
||||
const result = await verifyChallenge(1, 'challenge-uuid')
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.dns_found).toBe(false)
|
||||
})
|
||||
|
||||
it('throws error when challenge expired', async () => {
|
||||
vi.mocked(client.post).mockRejectedValueOnce({
|
||||
response: { status: 410, data: { code: 'CHALLENGE_EXPIRED' } },
|
||||
})
|
||||
|
||||
await expect(verifyChallenge(1, 'challenge-uuid')).rejects.toMatchObject({
|
||||
response: { status: 410 },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('pollChallenge', () => {
|
||||
it('returns current challenge status', async () => {
|
||||
const mockPoll = {
|
||||
status: 'pending',
|
||||
dns_propagated: false,
|
||||
time_remaining_seconds: 480,
|
||||
last_check_at: '2026-01-11T00:02:00Z',
|
||||
}
|
||||
|
||||
vi.mocked(client.get).mockResolvedValueOnce({ data: mockPoll })
|
||||
|
||||
const result = await pollChallenge(1, 'challenge-uuid')
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith(
|
||||
'/dns-providers/1/manual-challenge/challenge-uuid/poll'
|
||||
)
|
||||
expect(result).toEqual(mockPoll)
|
||||
})
|
||||
|
||||
it('returns verified status when DNS propagated', async () => {
|
||||
const mockPoll = {
|
||||
status: 'verified',
|
||||
dns_propagated: true,
|
||||
time_remaining_seconds: 0,
|
||||
last_check_at: '2026-01-11T00:05:00Z',
|
||||
}
|
||||
|
||||
vi.mocked(client.get).mockResolvedValueOnce({ data: mockPoll })
|
||||
|
||||
const result = await pollChallenge(1, 'challenge-uuid')
|
||||
|
||||
expect(result.status).toBe('verified')
|
||||
expect(result.dns_propagated).toBe(true)
|
||||
})
|
||||
|
||||
it('includes error message when challenge failed', async () => {
|
||||
const mockPoll = {
|
||||
status: 'failed',
|
||||
dns_propagated: false,
|
||||
time_remaining_seconds: 0,
|
||||
last_check_at: '2026-01-11T00:05:00Z',
|
||||
error_message: 'ACME validation failed',
|
||||
}
|
||||
|
||||
vi.mocked(client.get).mockResolvedValueOnce({ data: mockPoll })
|
||||
|
||||
const result = await pollChallenge(1, 'challenge-uuid')
|
||||
|
||||
expect(result.status).toBe('failed')
|
||||
expect(result.error_message).toBe('ACME validation failed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteChallenge', () => {
|
||||
it('deletes/cancels a challenge', async () => {
|
||||
vi.mocked(client.delete).mockResolvedValueOnce({ data: undefined })
|
||||
|
||||
await deleteChallenge(1, 'challenge-uuid')
|
||||
|
||||
expect(client.delete).toHaveBeenCalledWith(
|
||||
'/dns-providers/1/manual-challenge/challenge-uuid'
|
||||
)
|
||||
})
|
||||
|
||||
it('throws error when challenge not found', async () => {
|
||||
vi.mocked(client.delete).mockRejectedValueOnce({
|
||||
response: { status: 404, data: { error: 'Challenge not found' } },
|
||||
})
|
||||
|
||||
await expect(deleteChallenge(1, 'invalid-uuid')).rejects.toMatchObject({
|
||||
response: { status: 404 },
|
||||
})
|
||||
})
|
||||
|
||||
it('throws error when unauthorized', async () => {
|
||||
vi.mocked(client.delete).mockRejectedValueOnce({
|
||||
response: { status: 403, data: { error: 'Unauthorized' } },
|
||||
})
|
||||
|
||||
await expect(deleteChallenge(1, 'challenge-uuid')).rejects.toMatchObject({
|
||||
response: { status: 403 },
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,102 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import client from '../client'
|
||||
import {
|
||||
getProviders,
|
||||
createProvider,
|
||||
updateProvider,
|
||||
deleteProvider,
|
||||
testProvider,
|
||||
getTemplates,
|
||||
previewProvider,
|
||||
getExternalTemplates,
|
||||
createExternalTemplate,
|
||||
updateExternalTemplate,
|
||||
deleteExternalTemplate,
|
||||
previewExternalTemplate,
|
||||
getSecurityNotificationSettings,
|
||||
updateSecurityNotificationSettings,
|
||||
} from '../notifications'
|
||||
|
||||
vi.mock('../client', () => ({
|
||||
default: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('notifications api', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('crud for providers uses correct endpoints', async () => {
|
||||
vi.mocked(client.get).mockResolvedValue({ data: [{ id: '1', name: 'webhook', type: 'webhook', url: 'http://', enabled: true } as never] })
|
||||
vi.mocked(client.post).mockResolvedValue({ data: { id: '2' } })
|
||||
vi.mocked(client.put).mockResolvedValue({ data: { id: '2', name: 'updated' } })
|
||||
|
||||
const providers = await getProviders()
|
||||
expect(providers[0].id).toBe('1')
|
||||
expect(client.get).toHaveBeenCalledWith('/notifications/providers')
|
||||
|
||||
await createProvider({ name: 'x' })
|
||||
expect(client.post).toHaveBeenCalledWith('/notifications/providers', { name: 'x' })
|
||||
|
||||
await updateProvider('2', { name: 'updated' })
|
||||
expect(client.put).toHaveBeenCalledWith('/notifications/providers/2', { name: 'updated' })
|
||||
|
||||
await deleteProvider('2')
|
||||
expect(client.delete).toHaveBeenCalledWith('/notifications/providers/2')
|
||||
|
||||
await testProvider({ id: '2', name: 'test' })
|
||||
expect(client.post).toHaveBeenCalledWith('/notifications/providers/test', { id: '2', name: 'test' })
|
||||
})
|
||||
|
||||
it('templates and previews use merged payloads', async () => {
|
||||
vi.mocked(client.get).mockResolvedValueOnce({ data: [{ id: 't1', name: 'default' }] })
|
||||
const templates = await getTemplates()
|
||||
expect(templates[0].name).toBe('default')
|
||||
expect(client.get).toHaveBeenCalledWith('/notifications/templates')
|
||||
|
||||
vi.mocked(client.post).mockResolvedValueOnce({ data: { preview: 'ok' } })
|
||||
const preview = await previewProvider({ name: 'provider' }, { user: 'alice' })
|
||||
expect(preview).toEqual({ preview: 'ok' })
|
||||
expect(client.post).toHaveBeenCalledWith('/notifications/providers/preview', { name: 'provider', data: { user: 'alice' } })
|
||||
})
|
||||
|
||||
it('external template endpoints shape payloads', async () => {
|
||||
vi.mocked(client.get).mockResolvedValueOnce({ data: [{ id: 'ext', name: 'External' }] })
|
||||
const external = await getExternalTemplates()
|
||||
expect(external[0].id).toBe('ext')
|
||||
expect(client.get).toHaveBeenCalledWith('/notifications/external-templates')
|
||||
|
||||
vi.mocked(client.post).mockResolvedValueOnce({ data: { id: 'ext2' } })
|
||||
await createExternalTemplate({ name: 'n' })
|
||||
expect(client.post).toHaveBeenCalledWith('/notifications/external-templates', { name: 'n' })
|
||||
|
||||
vi.mocked(client.put).mockResolvedValueOnce({ data: { id: 'ext', name: 'updated' } })
|
||||
await updateExternalTemplate('ext', { name: 'updated' })
|
||||
expect(client.put).toHaveBeenCalledWith('/notifications/external-templates/ext', { name: 'updated' })
|
||||
|
||||
await deleteExternalTemplate('ext')
|
||||
expect(client.delete).toHaveBeenCalledWith('/notifications/external-templates/ext')
|
||||
|
||||
vi.mocked(client.post).mockResolvedValueOnce({ data: { rendered: true } })
|
||||
const result = await previewExternalTemplate('ext', 'tpl', { id: 1 })
|
||||
expect(result).toEqual({ rendered: true })
|
||||
expect(client.post).toHaveBeenCalledWith('/notifications/external-templates/preview', { template_id: 'ext', template: 'tpl', data: { id: 1 } })
|
||||
})
|
||||
|
||||
it('reads and updates security notification settings', async () => {
|
||||
vi.mocked(client.get).mockResolvedValueOnce({ data: { enabled: true, min_log_level: 'info', notify_waf_blocks: true } })
|
||||
const settings = await getSecurityNotificationSettings()
|
||||
expect(settings.enabled).toBe(true)
|
||||
expect(client.get).toHaveBeenCalledWith('/notifications/settings/security')
|
||||
|
||||
vi.mocked(client.put).mockResolvedValueOnce({ data: { enabled: false } })
|
||||
const updated = await updateSecurityNotificationSettings({ enabled: false })
|
||||
expect(updated.enabled).toBe(false)
|
||||
expect(client.put).toHaveBeenCalledWith('/notifications/settings/security', { enabled: false })
|
||||
})
|
||||
})
|
||||
@@ -1,465 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import * as presets from '../presets'
|
||||
import client from '../client'
|
||||
|
||||
vi.mock('../client')
|
||||
|
||||
describe('presets API', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('listCrowdsecPresets', () => {
|
||||
it('should fetch presets list with cached flags', async () => {
|
||||
const mockPresets = {
|
||||
presets: [
|
||||
{
|
||||
slug: 'bot-mitigation-essentials',
|
||||
title: 'Bot Mitigation Essentials',
|
||||
summary: 'Core HTTP parsers and scenarios',
|
||||
source: 'hub',
|
||||
tags: ['bots', 'web'],
|
||||
requires_hub: true,
|
||||
available: true,
|
||||
cached: true,
|
||||
cache_key: 'hub-bot-abc123',
|
||||
etag: '"w/12345"',
|
||||
retrieved_at: '2025-12-15T10:00:00Z',
|
||||
},
|
||||
{
|
||||
slug: 'honeypot-friendly-defaults',
|
||||
title: 'Honeypot Friendly Defaults',
|
||||
summary: 'Lightweight defaults for honeypots',
|
||||
source: 'builtin',
|
||||
tags: ['low-noise'],
|
||||
requires_hub: false,
|
||||
available: true,
|
||||
cached: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockPresets })
|
||||
|
||||
const result = await presets.listCrowdsecPresets()
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/admin/crowdsec/presets')
|
||||
expect(result).toEqual(mockPresets)
|
||||
expect(result.presets).toHaveLength(2)
|
||||
expect(result.presets[0].cached).toBe(true)
|
||||
expect(result.presets[0].cache_key).toBe('hub-bot-abc123')
|
||||
expect(result.presets[1].cached).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle empty presets list', async () => {
|
||||
const mockData = { presets: [] }
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await presets.listCrowdsecPresets()
|
||||
|
||||
expect(result.presets).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should handle API errors', async () => {
|
||||
const error = new Error('Network error')
|
||||
vi.mocked(client.get).mockRejectedValue(error)
|
||||
|
||||
await expect(presets.listCrowdsecPresets()).rejects.toThrow('Network error')
|
||||
})
|
||||
|
||||
it('should handle hub API unavailability', async () => {
|
||||
const error = {
|
||||
response: {
|
||||
status: 503,
|
||||
data: { error: 'CrowdSec Hub API unavailable' },
|
||||
},
|
||||
}
|
||||
vi.mocked(client.get).mockRejectedValue(error)
|
||||
|
||||
await expect(presets.listCrowdsecPresets()).rejects.toEqual(error)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getCrowdsecPresets', () => {
|
||||
it('should be an alias for listCrowdsecPresets', async () => {
|
||||
const mockData = { presets: [] }
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await presets.getCrowdsecPresets()
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/admin/crowdsec/presets')
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
|
||||
describe('pullCrowdsecPreset', () => {
|
||||
it('should pull preset and return preview with cache_key', async () => {
|
||||
const mockResponse = {
|
||||
status: 'success',
|
||||
slug: 'bot-mitigation-essentials',
|
||||
preview: '# Bot Mitigation Config\nconfigs:\n collections:\n - crowdsecurity/base-http-scenarios',
|
||||
cache_key: 'hub-bot-xyz789',
|
||||
etag: '"abc123"',
|
||||
retrieved_at: '2025-12-15T10:00:00Z',
|
||||
source: 'hub',
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
|
||||
|
||||
const result = await presets.pullCrowdsecPreset('bot-mitigation-essentials')
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/presets/pull', {
|
||||
slug: 'bot-mitigation-essentials',
|
||||
})
|
||||
expect(result).toEqual(mockResponse)
|
||||
expect(result.status).toBe('success')
|
||||
expect(result.cache_key).toBeDefined()
|
||||
expect(result.preview).toContain('configs:')
|
||||
})
|
||||
|
||||
it('should handle invalid preset slug', async () => {
|
||||
const mockResponse = {
|
||||
status: 'error',
|
||||
slug: 'non-existent-preset',
|
||||
preview: '',
|
||||
cache_key: '',
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
|
||||
|
||||
const result = await presets.pullCrowdsecPreset('non-existent-preset')
|
||||
|
||||
expect(result.status).toBe('error')
|
||||
})
|
||||
|
||||
it('should handle hub API timeout during pull', async () => {
|
||||
const error = {
|
||||
response: {
|
||||
status: 504,
|
||||
data: { error: 'Gateway timeout while fetching from CrowdSec Hub' },
|
||||
},
|
||||
}
|
||||
vi.mocked(client.post).mockRejectedValue(error)
|
||||
|
||||
await expect(presets.pullCrowdsecPreset('bot-mitigation-essentials')).rejects.toEqual(error)
|
||||
})
|
||||
|
||||
it('should handle ETAG validation scenarios', async () => {
|
||||
const mockResponse = {
|
||||
status: 'success',
|
||||
slug: 'bot-mitigation-essentials',
|
||||
preview: '# Cached content',
|
||||
cache_key: 'hub-bot-cached123',
|
||||
etag: '"not-modified"',
|
||||
retrieved_at: '2025-12-14T09:00:00Z',
|
||||
source: 'cache',
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
|
||||
|
||||
const result = await presets.pullCrowdsecPreset('bot-mitigation-essentials')
|
||||
|
||||
expect(result.source).toBe('cache')
|
||||
expect(result.etag).toBe('"not-modified"')
|
||||
})
|
||||
|
||||
it('should handle CrowdSec not running during pull', async () => {
|
||||
const error = {
|
||||
response: {
|
||||
status: 500,
|
||||
data: { error: 'CrowdSec LAPI not available' },
|
||||
},
|
||||
}
|
||||
vi.mocked(client.post).mockRejectedValue(error)
|
||||
|
||||
await expect(presets.pullCrowdsecPreset('bot-mitigation-essentials')).rejects.toEqual(error)
|
||||
})
|
||||
|
||||
it('should encode special characters in preset slug', async () => {
|
||||
const mockResponse = {
|
||||
status: 'success',
|
||||
slug: 'custom/preset-with-slash',
|
||||
preview: '# Custom',
|
||||
cache_key: 'custom-key',
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
|
||||
|
||||
await presets.pullCrowdsecPreset('custom/preset-with-slash')
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/presets/pull', {
|
||||
slug: 'custom/preset-with-slash',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('applyCrowdsecPreset', () => {
|
||||
it('should apply preset with cache_key when available', async () => {
|
||||
const payload = { slug: 'bot-mitigation-essentials', cache_key: 'hub-bot-xyz789' }
|
||||
const mockResponse = {
|
||||
status: 'success',
|
||||
backup: '/data/charon/data/backups/preset-backup-20251215-100000.tar.gz',
|
||||
reload_hint: true,
|
||||
used_cscli: true,
|
||||
cache_key: 'hub-bot-xyz789',
|
||||
slug: 'bot-mitigation-essentials',
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
|
||||
|
||||
const result = await presets.applyCrowdsecPreset(payload)
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/presets/apply', payload)
|
||||
expect(result).toEqual(mockResponse)
|
||||
expect(result.status).toBe('success')
|
||||
expect(result.backup).toBeDefined()
|
||||
expect(result.reload_hint).toBe(true)
|
||||
})
|
||||
|
||||
it('should apply preset without cache_key (fallback mode)', async () => {
|
||||
const payload = { slug: 'honeypot-friendly-defaults' }
|
||||
const mockResponse = {
|
||||
status: 'success',
|
||||
backup: '/data/charon/data/backups/preset-backup-20251215-100100.tar.gz',
|
||||
reload_hint: true,
|
||||
used_cscli: true,
|
||||
slug: 'honeypot-friendly-defaults',
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
|
||||
|
||||
const result = await presets.applyCrowdsecPreset(payload)
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/admin/crowdsec/presets/apply', payload)
|
||||
expect(result.status).toBe('success')
|
||||
expect(result.used_cscli).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle stale cache_key gracefully', async () => {
|
||||
const stalePayload = { slug: 'bot-mitigation-essentials', cache_key: 'old_key_123' }
|
||||
const error = {
|
||||
response: {
|
||||
status: 400,
|
||||
data: { error: 'Cache key mismatch or expired. Please pull the preset again.' },
|
||||
},
|
||||
}
|
||||
vi.mocked(client.post).mockRejectedValue(error)
|
||||
|
||||
await expect(presets.applyCrowdsecPreset(stalePayload)).rejects.toEqual(error)
|
||||
})
|
||||
|
||||
it('should error when applying preset with CrowdSec stopped', async () => {
|
||||
const payload = { slug: 'bot-mitigation-essentials', cache_key: 'valid-key' }
|
||||
const error = {
|
||||
response: {
|
||||
status: 500,
|
||||
data: { error: 'CrowdSec is not running. Start CrowdSec before applying presets.' },
|
||||
},
|
||||
}
|
||||
vi.mocked(client.post).mockRejectedValue(error)
|
||||
|
||||
await expect(presets.applyCrowdsecPreset(payload)).rejects.toEqual(error)
|
||||
})
|
||||
|
||||
it('should handle backup creation failure', async () => {
|
||||
const payload = { slug: 'bot-mitigation-essentials', cache_key: 'valid-key' }
|
||||
const error = {
|
||||
response: {
|
||||
status: 500,
|
||||
data: { error: 'Failed to create backup before applying preset' },
|
||||
},
|
||||
}
|
||||
vi.mocked(client.post).mockRejectedValue(error)
|
||||
|
||||
await expect(presets.applyCrowdsecPreset(payload)).rejects.toEqual(error)
|
||||
})
|
||||
|
||||
it('should handle cscli errors during application', async () => {
|
||||
const payload = { slug: 'invalid-preset' }
|
||||
const error = {
|
||||
response: {
|
||||
status: 500,
|
||||
data: { error: 'cscli hub update failed: exit status 1' },
|
||||
},
|
||||
}
|
||||
vi.mocked(client.post).mockRejectedValue(error)
|
||||
|
||||
await expect(presets.applyCrowdsecPreset(payload)).rejects.toEqual(error)
|
||||
})
|
||||
|
||||
it('should handle payload with force flag', async () => {
|
||||
const payload = { slug: 'bot-mitigation-essentials', cache_key: 'key123' }
|
||||
const mockResponse = {
|
||||
status: 'success',
|
||||
backup: '/data/backups/preset-forced.tar.gz',
|
||||
reload_hint: true,
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
|
||||
|
||||
const result = await presets.applyCrowdsecPreset(payload)
|
||||
|
||||
expect(result.status).toBe('success')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getCrowdsecPresetCache', () => {
|
||||
it('should fetch cached preset preview', async () => {
|
||||
const mockCache = {
|
||||
preview: '# Cached Bot Mitigation Config\nconfigs:\n collections:\n - crowdsecurity/base-http-scenarios',
|
||||
cache_key: 'hub-bot-xyz789',
|
||||
etag: '"abc123"',
|
||||
}
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockCache })
|
||||
|
||||
const result = await presets.getCrowdsecPresetCache('bot-mitigation-essentials')
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith(
|
||||
'/admin/crowdsec/presets/cache/bot-mitigation-essentials'
|
||||
)
|
||||
expect(result).toEqual(mockCache)
|
||||
expect(result.preview).toContain('configs:')
|
||||
expect(result.cache_key).toBe('hub-bot-xyz789')
|
||||
})
|
||||
|
||||
it('should encode special characters in slug', async () => {
|
||||
const mockCache = {
|
||||
preview: '# Custom',
|
||||
cache_key: 'custom-key',
|
||||
}
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockCache })
|
||||
|
||||
await presets.getCrowdsecPresetCache('custom/preset with spaces')
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith(
|
||||
'/admin/crowdsec/presets/cache/custom%2Fpreset%20with%20spaces'
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle cache miss (404)', async () => {
|
||||
const error = {
|
||||
response: {
|
||||
status: 404,
|
||||
data: { error: 'Preset not found in cache' },
|
||||
},
|
||||
}
|
||||
vi.mocked(client.get).mockRejectedValue(error)
|
||||
|
||||
await expect(presets.getCrowdsecPresetCache('non-cached-preset')).rejects.toEqual(error)
|
||||
})
|
||||
|
||||
it('should handle expired cache entries', async () => {
|
||||
const error = {
|
||||
response: {
|
||||
status: 410,
|
||||
data: { error: 'Cache entry expired' },
|
||||
},
|
||||
}
|
||||
vi.mocked(client.get).mockRejectedValue(error)
|
||||
|
||||
await expect(presets.getCrowdsecPresetCache('expired-preset')).rejects.toEqual(error)
|
||||
})
|
||||
|
||||
it('should handle empty preview content', async () => {
|
||||
const mockCache = {
|
||||
preview: '',
|
||||
cache_key: 'empty-key',
|
||||
}
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockCache })
|
||||
|
||||
const result = await presets.getCrowdsecPresetCache('empty-preset')
|
||||
|
||||
expect(result.preview).toBe('')
|
||||
expect(result.cache_key).toBe('empty-key')
|
||||
})
|
||||
})
|
||||
|
||||
describe('default export', () => {
|
||||
it('should export all functions', () => {
|
||||
expect(presets.default).toHaveProperty('listCrowdsecPresets')
|
||||
expect(presets.default).toHaveProperty('getCrowdsecPresets')
|
||||
expect(presets.default).toHaveProperty('pullCrowdsecPreset')
|
||||
expect(presets.default).toHaveProperty('applyCrowdsecPreset')
|
||||
expect(presets.default).toHaveProperty('getCrowdsecPresetCache')
|
||||
})
|
||||
})
|
||||
|
||||
describe('integration scenarios', () => {
|
||||
it('should handle full workflow: list → pull → cache → apply', async () => {
|
||||
// 1. List presets
|
||||
const mockList = {
|
||||
presets: [
|
||||
{
|
||||
slug: 'bot-mitigation-essentials',
|
||||
title: 'Bot Mitigation',
|
||||
summary: 'Core',
|
||||
source: 'hub',
|
||||
requires_hub: true,
|
||||
available: true,
|
||||
cached: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
vi.mocked(client.get).mockResolvedValueOnce({ data: mockList })
|
||||
|
||||
const listResult = await presets.listCrowdsecPresets()
|
||||
expect(listResult.presets[0].cached).toBe(false)
|
||||
|
||||
// 2. Pull preset
|
||||
const mockPull = {
|
||||
status: 'success',
|
||||
slug: 'bot-mitigation-essentials',
|
||||
preview: '# Config',
|
||||
cache_key: 'hub-bot-new123',
|
||||
etag: '"etag1"',
|
||||
retrieved_at: '2025-12-15T10:00:00Z',
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValueOnce({ data: mockPull })
|
||||
|
||||
const pullResult = await presets.pullCrowdsecPreset('bot-mitigation-essentials')
|
||||
expect(pullResult.cache_key).toBe('hub-bot-new123')
|
||||
|
||||
// 3. Verify cache
|
||||
const mockCache = {
|
||||
preview: '# Config',
|
||||
cache_key: 'hub-bot-new123',
|
||||
etag: '"etag1"',
|
||||
}
|
||||
vi.mocked(client.get).mockResolvedValueOnce({ data: mockCache })
|
||||
|
||||
const cacheResult = await presets.getCrowdsecPresetCache('bot-mitigation-essentials')
|
||||
expect(cacheResult.cache_key).toBe(pullResult.cache_key)
|
||||
|
||||
// 4. Apply preset
|
||||
const mockApply = {
|
||||
status: 'success',
|
||||
backup: '/data/backups/preset-backup.tar.gz',
|
||||
reload_hint: true,
|
||||
cache_key: 'hub-bot-new123',
|
||||
slug: 'bot-mitigation-essentials',
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValueOnce({ data: mockApply })
|
||||
|
||||
const applyResult = await presets.applyCrowdsecPreset({
|
||||
slug: 'bot-mitigation-essentials',
|
||||
cache_key: pullResult.cache_key,
|
||||
})
|
||||
expect(applyResult.status).toBe('success')
|
||||
expect(applyResult.backup).toBeDefined()
|
||||
})
|
||||
|
||||
it('should handle network failure mid-workflow', async () => {
|
||||
// Pull succeeds
|
||||
const mockPull = {
|
||||
status: 'success',
|
||||
slug: 'test-preset',
|
||||
preview: '# Test',
|
||||
cache_key: 'test-key',
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValueOnce({ data: mockPull })
|
||||
|
||||
const pullResult = await presets.pullCrowdsecPreset('test-preset')
|
||||
expect(pullResult.cache_key).toBe('test-key')
|
||||
|
||||
// Apply fails due to network
|
||||
const networkError = new Error('Network error')
|
||||
vi.mocked(client.post).mockRejectedValueOnce(networkError)
|
||||
|
||||
await expect(
|
||||
presets.applyCrowdsecPreset({ slug: 'test-preset', cache_key: 'test-key' })
|
||||
).rejects.toThrow('Network error')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,95 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { bulkUpdateACL } from '../proxyHosts';
|
||||
import type { BulkUpdateACLResponse } from '../proxyHosts';
|
||||
|
||||
// Mock the client module
|
||||
const mockPut = vi.fn();
|
||||
vi.mock('../client', () => ({
|
||||
default: {
|
||||
put: (...args: unknown[]) => mockPut(...args),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('proxyHosts bulk operations', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('bulkUpdateACL', () => {
|
||||
it('should apply ACL to multiple hosts', async () => {
|
||||
const mockResponse: BulkUpdateACLResponse = {
|
||||
updated: 3,
|
||||
errors: [],
|
||||
};
|
||||
mockPut.mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const hostUUIDs = ['uuid-1', 'uuid-2', 'uuid-3'];
|
||||
const accessListID = 42;
|
||||
const result = await bulkUpdateACL(hostUUIDs, accessListID);
|
||||
|
||||
expect(mockPut).toHaveBeenCalledWith('/proxy-hosts/bulk-update-acl', {
|
||||
host_uuids: hostUUIDs,
|
||||
access_list_id: accessListID,
|
||||
});
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should remove ACL from hosts when accessListID is null', async () => {
|
||||
const mockResponse: BulkUpdateACLResponse = {
|
||||
updated: 2,
|
||||
errors: [],
|
||||
};
|
||||
mockPut.mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const hostUUIDs = ['uuid-1', 'uuid-2'];
|
||||
const result = await bulkUpdateACL(hostUUIDs, null);
|
||||
|
||||
expect(mockPut).toHaveBeenCalledWith('/proxy-hosts/bulk-update-acl', {
|
||||
host_uuids: hostUUIDs,
|
||||
access_list_id: null,
|
||||
});
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should handle partial failures', async () => {
|
||||
const mockResponse: BulkUpdateACLResponse = {
|
||||
updated: 1,
|
||||
errors: [
|
||||
{ uuid: 'invalid-uuid', error: 'proxy host not found' },
|
||||
],
|
||||
};
|
||||
mockPut.mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const hostUUIDs = ['valid-uuid', 'invalid-uuid'];
|
||||
const accessListID = 10;
|
||||
const result = await bulkUpdateACL(hostUUIDs, accessListID);
|
||||
|
||||
expect(result.updated).toBe(1);
|
||||
expect(result.errors).toHaveLength(1);
|
||||
expect(result.errors[0].uuid).toBe('invalid-uuid');
|
||||
});
|
||||
|
||||
it('should handle empty host list', async () => {
|
||||
const mockResponse: BulkUpdateACLResponse = {
|
||||
updated: 0,
|
||||
errors: [],
|
||||
};
|
||||
mockPut.mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const result = await bulkUpdateACL([], 5);
|
||||
|
||||
expect(mockPut).toHaveBeenCalledWith('/proxy-hosts/bulk-update-acl', {
|
||||
host_uuids: [],
|
||||
access_list_id: 5,
|
||||
});
|
||||
expect(result.updated).toBe(0);
|
||||
});
|
||||
|
||||
it('should propagate API errors', async () => {
|
||||
const error = new Error('Network error');
|
||||
mockPut.mockRejectedValue(error);
|
||||
|
||||
await expect(bulkUpdateACL(['uuid-1'], 1)).rejects.toThrow('Network error');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,91 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import client from '../client';
|
||||
import {
|
||||
getProxyHosts,
|
||||
getProxyHost,
|
||||
createProxyHost,
|
||||
updateProxyHost,
|
||||
deleteProxyHost,
|
||||
testProxyHostConnection,
|
||||
ProxyHost
|
||||
} from '../proxyHosts';
|
||||
|
||||
vi.mock('../client', () => ({
|
||||
default: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('proxyHosts API', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockHost: ProxyHost = {
|
||||
uuid: '123',
|
||||
name: 'Example Host',
|
||||
domain_names: 'example.com',
|
||||
forward_scheme: 'http',
|
||||
forward_host: 'localhost',
|
||||
forward_port: 8080,
|
||||
ssl_forced: true,
|
||||
http2_support: true,
|
||||
hsts_enabled: true,
|
||||
hsts_subdomains: false,
|
||||
block_exploits: false,
|
||||
websocket_support: false,
|
||||
application: 'none',
|
||||
locations: [],
|
||||
enabled: true,
|
||||
created_at: '2023-01-01',
|
||||
updated_at: '2023-01-01',
|
||||
};
|
||||
|
||||
it('getProxyHosts calls client.get', async () => {
|
||||
vi.mocked(client.get).mockResolvedValue({ data: [mockHost] });
|
||||
const result = await getProxyHosts();
|
||||
expect(client.get).toHaveBeenCalledWith('/proxy-hosts');
|
||||
expect(result).toEqual([mockHost]);
|
||||
});
|
||||
|
||||
it('getProxyHost calls client.get with uuid', async () => {
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockHost });
|
||||
const result = await getProxyHost('123');
|
||||
expect(client.get).toHaveBeenCalledWith('/proxy-hosts/123');
|
||||
expect(result).toEqual(mockHost);
|
||||
});
|
||||
|
||||
it('createProxyHost calls client.post', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockHost });
|
||||
const newHost = { domain_names: 'example.com' };
|
||||
const result = await createProxyHost(newHost);
|
||||
expect(client.post).toHaveBeenCalledWith('/proxy-hosts', newHost);
|
||||
expect(result).toEqual(mockHost);
|
||||
});
|
||||
|
||||
it('updateProxyHost calls client.put', async () => {
|
||||
vi.mocked(client.put).mockResolvedValue({ data: mockHost });
|
||||
const updates = { enabled: false };
|
||||
const result = await updateProxyHost('123', updates);
|
||||
expect(client.put).toHaveBeenCalledWith('/proxy-hosts/123', updates);
|
||||
expect(result).toEqual(mockHost);
|
||||
});
|
||||
|
||||
it('deleteProxyHost calls client.delete', async () => {
|
||||
vi.mocked(client.delete).mockResolvedValue({ data: {} });
|
||||
await deleteProxyHost('123');
|
||||
expect(client.delete).toHaveBeenCalledWith('/proxy-hosts/123');
|
||||
});
|
||||
|
||||
it('testProxyHostConnection calls client.post', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({ data: {} });
|
||||
await testProxyHostConnection('localhost', 8080);
|
||||
expect(client.post).toHaveBeenCalledWith('/proxy-hosts/test', {
|
||||
forward_host: 'localhost',
|
||||
forward_port: 8080,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,146 +0,0 @@
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import {
|
||||
getRemoteServers,
|
||||
getRemoteServer,
|
||||
createRemoteServer,
|
||||
updateRemoteServer,
|
||||
deleteRemoteServer,
|
||||
testRemoteServerConnection,
|
||||
testCustomRemoteServerConnection,
|
||||
} from '../remoteServers';
|
||||
import client from '../client';
|
||||
|
||||
vi.mock('../client', () => ({
|
||||
default: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('remoteServers API', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockServer = {
|
||||
uuid: 'server-123',
|
||||
name: 'Test Server',
|
||||
provider: 'docker',
|
||||
host: '192.168.1.100',
|
||||
port: 2375,
|
||||
username: 'admin',
|
||||
enabled: true,
|
||||
reachable: true,
|
||||
last_check: '2024-01-01T12:00:00Z',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T12:00:00Z',
|
||||
};
|
||||
|
||||
describe('getRemoteServers', () => {
|
||||
it('fetches all servers', async () => {
|
||||
vi.mocked(client.get).mockResolvedValue({ data: [mockServer] });
|
||||
|
||||
const result = await getRemoteServers();
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/remote-servers', { params: {} });
|
||||
expect(result).toEqual([mockServer]);
|
||||
});
|
||||
|
||||
it('fetches enabled servers only', async () => {
|
||||
vi.mocked(client.get).mockResolvedValue({ data: [mockServer] });
|
||||
|
||||
const result = await getRemoteServers(true);
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/remote-servers', { params: { enabled: true } });
|
||||
expect(result).toEqual([mockServer]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRemoteServer', () => {
|
||||
it('fetches a single server by UUID', async () => {
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockServer });
|
||||
|
||||
const result = await getRemoteServer('server-123');
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/remote-servers/server-123');
|
||||
expect(result).toEqual(mockServer);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createRemoteServer', () => {
|
||||
it('creates a new server', async () => {
|
||||
const newServer = {
|
||||
name: 'New Server',
|
||||
provider: 'docker',
|
||||
host: '10.0.0.1',
|
||||
port: 2375,
|
||||
};
|
||||
vi.mocked(client.post).mockResolvedValue({ data: { ...mockServer, ...newServer } });
|
||||
|
||||
const result = await createRemoteServer(newServer);
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/remote-servers', newServer);
|
||||
expect(result.name).toBe('New Server');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateRemoteServer', () => {
|
||||
it('updates an existing server', async () => {
|
||||
const updates = { name: 'Updated Server', enabled: false };
|
||||
vi.mocked(client.put).mockResolvedValue({ data: { ...mockServer, ...updates } });
|
||||
|
||||
const result = await updateRemoteServer('server-123', updates);
|
||||
|
||||
expect(client.put).toHaveBeenCalledWith('/remote-servers/server-123', updates);
|
||||
expect(result.name).toBe('Updated Server');
|
||||
expect(result.enabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteRemoteServer', () => {
|
||||
it('deletes a server', async () => {
|
||||
vi.mocked(client.delete).mockResolvedValue({});
|
||||
|
||||
await deleteRemoteServer('server-123');
|
||||
|
||||
expect(client.delete).toHaveBeenCalledWith('/remote-servers/server-123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('testRemoteServerConnection', () => {
|
||||
it('tests connection to an existing server', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({ data: { address: '192.168.1.100:2375' } });
|
||||
|
||||
const result = await testRemoteServerConnection('server-123');
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/remote-servers/server-123/test');
|
||||
expect(result.address).toBe('192.168.1.100:2375');
|
||||
});
|
||||
});
|
||||
|
||||
describe('testCustomRemoteServerConnection', () => {
|
||||
it('tests connection to a custom host and port', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({
|
||||
data: { address: '10.0.0.1:2375', reachable: true },
|
||||
});
|
||||
|
||||
const result = await testCustomRemoteServerConnection('10.0.0.1', 2375);
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/remote-servers/test', { host: '10.0.0.1', port: 2375 });
|
||||
expect(result.reachable).toBe(true);
|
||||
});
|
||||
|
||||
it('handles unreachable server', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({
|
||||
data: { address: '10.0.0.1:2375', reachable: false, error: 'Connection refused' },
|
||||
});
|
||||
|
||||
const result = await testCustomRemoteServerConnection('10.0.0.1', 2375);
|
||||
|
||||
expect(result.reachable).toBe(false);
|
||||
expect(result.error).toBe('Connection refused');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,244 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import * as security from '../security'
|
||||
import client from '../client'
|
||||
|
||||
vi.mock('../client')
|
||||
|
||||
describe('security API', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('getSecurityStatus', () => {
|
||||
it('should call GET /security/status', async () => {
|
||||
const mockData: security.SecurityStatus = {
|
||||
cerberus: { enabled: true },
|
||||
crowdsec: { mode: 'local', api_url: 'http://localhost:8080', enabled: true },
|
||||
waf: { mode: 'enabled', enabled: true },
|
||||
rate_limit: { mode: 'enabled', enabled: true },
|
||||
acl: { enabled: true }
|
||||
}
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await security.getSecurityStatus()
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/security/status')
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSecurityConfig', () => {
|
||||
it('should call GET /security/config', async () => {
|
||||
const mockData = { config: { admin_whitelist: '10.0.0.0/8' } }
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await security.getSecurityConfig()
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/security/config')
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateSecurityConfig', () => {
|
||||
it('should call POST /security/config with payload', async () => {
|
||||
const payload: security.SecurityConfigPayload = {
|
||||
name: 'test',
|
||||
enabled: true,
|
||||
admin_whitelist: '10.0.0.0/8'
|
||||
}
|
||||
const mockData = { success: true }
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await security.updateSecurityConfig(payload)
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/security/config', payload)
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
|
||||
it('should handle all payload fields', async () => {
|
||||
const payload: security.SecurityConfigPayload = {
|
||||
name: 'test',
|
||||
enabled: true,
|
||||
admin_whitelist: '10.0.0.0/8',
|
||||
crowdsec_mode: 'local',
|
||||
crowdsec_api_url: 'http://localhost:8080',
|
||||
waf_mode: 'enabled',
|
||||
waf_rules_source: 'coreruleset',
|
||||
waf_learning: true,
|
||||
rate_limit_enable: true,
|
||||
rate_limit_burst: 10,
|
||||
rate_limit_requests: 100,
|
||||
rate_limit_window_sec: 60
|
||||
}
|
||||
const mockData = { success: true }
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await security.updateSecurityConfig(payload)
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/security/config', payload)
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
|
||||
describe('generateBreakGlassToken', () => {
|
||||
it('should call POST /security/breakglass/generate', async () => {
|
||||
const mockData = { token: 'abc123' }
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await security.generateBreakGlassToken()
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/security/breakglass/generate')
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
|
||||
describe('enableCerberus', () => {
|
||||
it('should call POST /security/enable with payload', async () => {
|
||||
const payload = { mode: 'full' }
|
||||
const mockData = { success: true }
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await security.enableCerberus(payload)
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/security/enable', payload)
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
|
||||
it('should call POST /security/enable with empty object when no payload', async () => {
|
||||
const mockData = { success: true }
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await security.enableCerberus()
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/security/enable', {})
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
|
||||
describe('disableCerberus', () => {
|
||||
it('should call POST /security/disable with payload', async () => {
|
||||
const payload = { reason: 'maintenance' }
|
||||
const mockData = { success: true }
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await security.disableCerberus(payload)
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/security/disable', payload)
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
|
||||
it('should call POST /security/disable with empty object when no payload', async () => {
|
||||
const mockData = { success: true }
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await security.disableCerberus()
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/security/disable', {})
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getDecisions', () => {
|
||||
it('should call GET /security/decisions with default limit', async () => {
|
||||
const mockData = { decisions: [] }
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await security.getDecisions()
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/security/decisions?limit=50')
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
|
||||
it('should call GET /security/decisions with custom limit', async () => {
|
||||
const mockData = { decisions: [] }
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await security.getDecisions(100)
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/security/decisions?limit=100')
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
|
||||
describe('createDecision', () => {
|
||||
it('should call POST /security/decisions with payload', async () => {
|
||||
const payload = { value: '1.2.3.4', duration: '4h', type: 'ban' }
|
||||
const mockData = { success: true }
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await security.createDecision(payload)
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/security/decisions', payload)
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getRuleSets', () => {
|
||||
it('should call GET /security/rulesets', async () => {
|
||||
const mockData: security.RuleSetsResponse = {
|
||||
rulesets: [
|
||||
{
|
||||
id: 1,
|
||||
uuid: 'abc-123',
|
||||
name: 'OWASP CRS',
|
||||
source_url: 'https://example.com/rules',
|
||||
mode: 'blocking',
|
||||
last_updated: '2025-12-04T00:00:00Z',
|
||||
content: 'rule content'
|
||||
}
|
||||
]
|
||||
}
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await security.getRuleSets()
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/security/rulesets')
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
|
||||
describe('upsertRuleSet', () => {
|
||||
it('should call POST /security/rulesets with create payload', async () => {
|
||||
const payload: security.UpsertRuleSetPayload = {
|
||||
name: 'Custom Rules',
|
||||
content: 'rule content',
|
||||
mode: 'blocking'
|
||||
}
|
||||
const mockData = { success: true }
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await security.upsertRuleSet(payload)
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/security/rulesets', payload)
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
|
||||
it('should call POST /security/rulesets with update payload', async () => {
|
||||
const payload: security.UpsertRuleSetPayload = {
|
||||
id: 1,
|
||||
name: 'Updated Rules',
|
||||
source_url: 'https://example.com/rules',
|
||||
mode: 'detection'
|
||||
}
|
||||
const mockData = { success: true }
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await security.upsertRuleSet(payload)
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/security/rulesets', payload)
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteRuleSet', () => {
|
||||
it('should call DELETE /security/rulesets/:id', async () => {
|
||||
const mockData = { success: true }
|
||||
vi.mocked(client.delete).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await security.deleteRuleSet(1)
|
||||
|
||||
expect(client.delete).toHaveBeenCalledWith('/security/rulesets/1')
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,181 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import * as settings from '../settings'
|
||||
import client from '../client'
|
||||
|
||||
vi.mock('../client')
|
||||
|
||||
describe('settings API', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('getSettings', () => {
|
||||
it('should call GET /settings', async () => {
|
||||
const mockData: settings.SettingsMap = {
|
||||
'ui.theme': 'dark',
|
||||
'security.cerberus.enabled': 'true'
|
||||
}
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await settings.getSettings()
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/settings')
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateSetting', () => {
|
||||
it('should call POST /settings with key and value only', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({ data: {} })
|
||||
|
||||
await settings.updateSetting('ui.theme', 'light')
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/settings', {
|
||||
key: 'ui.theme',
|
||||
value: 'light',
|
||||
category: undefined,
|
||||
type: undefined
|
||||
})
|
||||
})
|
||||
|
||||
it('should call POST /settings with all parameters', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({ data: {} })
|
||||
|
||||
await settings.updateSetting('security.cerberus.enabled', 'true', 'security', 'bool')
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/settings', {
|
||||
key: 'security.cerberus.enabled',
|
||||
value: 'true',
|
||||
category: 'security',
|
||||
type: 'bool'
|
||||
})
|
||||
})
|
||||
|
||||
it('should call POST /settings with category but no type', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({ data: {} })
|
||||
|
||||
await settings.updateSetting('ui.theme', 'dark', 'ui')
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/settings', {
|
||||
key: 'ui.theme',
|
||||
value: 'dark',
|
||||
category: 'ui',
|
||||
type: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('validatePublicURL', () => {
|
||||
it('should call POST /settings/validate-url with URL', async () => {
|
||||
const mockResponse = { valid: true, normalized: 'https://example.com' }
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
|
||||
|
||||
const result = await settings.validatePublicURL('https://example.com')
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/settings/validate-url', { url: 'https://example.com' })
|
||||
expect(result).toEqual(mockResponse)
|
||||
})
|
||||
|
||||
it('should return valid: true for valid URL', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({ data: { valid: true } })
|
||||
|
||||
const result = await settings.validatePublicURL('https://valid.com')
|
||||
|
||||
expect(result.valid).toBe(true)
|
||||
})
|
||||
|
||||
it('should return valid: false for invalid URL', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({ data: { valid: false, error: 'Invalid URL format' } })
|
||||
|
||||
const result = await settings.validatePublicURL('not-a-url')
|
||||
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.error).toBe('Invalid URL format')
|
||||
})
|
||||
|
||||
it('should return normalized URL when provided', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({
|
||||
data: { valid: true, normalized: 'https://example.com/' }
|
||||
})
|
||||
|
||||
const result = await settings.validatePublicURL('https://example.com')
|
||||
|
||||
expect(result.normalized).toBe('https://example.com/')
|
||||
})
|
||||
|
||||
it('should handle validation errors', async () => {
|
||||
vi.mocked(client.post).mockRejectedValue(new Error('Network error'))
|
||||
|
||||
await expect(settings.validatePublicURL('https://example.com')).rejects.toThrow('Network error')
|
||||
})
|
||||
|
||||
it('should handle empty URL parameter', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({ data: { valid: false } })
|
||||
|
||||
const result = await settings.validatePublicURL('')
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/settings/validate-url', { url: '' })
|
||||
expect(result.valid).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('testPublicURL', () => {
|
||||
it('should call POST /settings/test-url with URL', async () => {
|
||||
const mockResponse = { reachable: true, latency: 42 }
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
|
||||
|
||||
const result = await settings.testPublicURL('https://example.com')
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/settings/test-url', { url: 'https://example.com' })
|
||||
expect(result).toEqual(mockResponse)
|
||||
})
|
||||
|
||||
it('should return reachable: true with latency for successful test', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({
|
||||
data: { reachable: true, latency: 123, message: 'URL is reachable' }
|
||||
})
|
||||
|
||||
const result = await settings.testPublicURL('https://example.com')
|
||||
|
||||
expect(result.reachable).toBe(true)
|
||||
expect(result.latency).toBe(123)
|
||||
expect(result.message).toBe('URL is reachable')
|
||||
})
|
||||
|
||||
it('should return reachable: false with error for failed test', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({
|
||||
data: { reachable: false, error: 'Connection timeout' }
|
||||
})
|
||||
|
||||
const result = await settings.testPublicURL('https://unreachable.com')
|
||||
|
||||
expect(result.reachable).toBe(false)
|
||||
expect(result.error).toBe('Connection timeout')
|
||||
})
|
||||
|
||||
it('should return message field when provided', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({
|
||||
data: { reachable: true, latency: 50, message: 'Custom success message' }
|
||||
})
|
||||
|
||||
const result = await settings.testPublicURL('https://example.com')
|
||||
|
||||
expect(result.message).toBe('Custom success message')
|
||||
})
|
||||
|
||||
it('should handle request errors', async () => {
|
||||
vi.mocked(client.post).mockRejectedValue(new Error('Request failed'))
|
||||
|
||||
await expect(settings.testPublicURL('https://example.com')).rejects.toThrow('Request failed')
|
||||
})
|
||||
|
||||
it('should handle empty URL parameter', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({ data: { reachable: false } })
|
||||
|
||||
const result = await settings.testPublicURL('')
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/settings/test-url', { url: '' })
|
||||
expect(result.reachable).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,23 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import client from '../../api/client'
|
||||
import { getSetupStatus, performSetup } from '../setup'
|
||||
|
||||
describe('setup api', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('getSetupStatus returns status', async () => {
|
||||
const data = { setupRequired: true }
|
||||
vi.spyOn(client, 'get').mockResolvedValueOnce({ data })
|
||||
const res = await getSetupStatus()
|
||||
expect(res).toEqual(data)
|
||||
})
|
||||
|
||||
it('performSetup posts data to setup endpoint', async () => {
|
||||
const spy = vi.spyOn(client, 'post').mockResolvedValueOnce({ data: {} })
|
||||
const payload = { name: 'Admin', email: 'admin@example.com', password: 'secret' }
|
||||
await performSetup(payload)
|
||||
expect(spy).toHaveBeenCalledWith('/setup', payload)
|
||||
})
|
||||
})
|
||||
@@ -1,62 +0,0 @@
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest'
|
||||
import client from '../client'
|
||||
import { checkUpdates, getNotifications, markNotificationRead, markAllNotificationsRead } from '../system'
|
||||
|
||||
vi.mock('../client', () => ({
|
||||
default: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('System API', () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('checkUpdates calls /system/updates', async () => {
|
||||
const mockData = { available: true, latest_version: '1.0.0', changelog_url: 'url' }
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await checkUpdates()
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/system/updates')
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
|
||||
it('getNotifications calls /notifications', async () => {
|
||||
const mockData = [{ id: '1', title: 'Test' }]
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await getNotifications()
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/notifications', { params: { unread: false } })
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
|
||||
it('getNotifications calls /notifications with unreadOnly=true', async () => {
|
||||
const mockData = [{ id: '1', title: 'Test' }]
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await getNotifications(true)
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/notifications', { params: { unread: true } })
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
|
||||
it('markNotificationRead calls /notifications/:id/read', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({})
|
||||
|
||||
await markNotificationRead('123')
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/notifications/123/read')
|
||||
})
|
||||
|
||||
it('markAllNotificationsRead calls /notifications/read-all', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({})
|
||||
|
||||
await markAllNotificationsRead()
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/notifications/read-all')
|
||||
})
|
||||
})
|
||||
@@ -1,135 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import * as uptime from '../uptime'
|
||||
import client from '../client'
|
||||
import type { UptimeMonitor, UptimeHeartbeat } from '../uptime'
|
||||
|
||||
vi.mock('../client')
|
||||
|
||||
describe('uptime API', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('getMonitors', () => {
|
||||
it('should call GET /uptime/monitors', async () => {
|
||||
const mockData: UptimeMonitor[] = [
|
||||
{
|
||||
id: 'mon-1',
|
||||
name: 'Test Monitor',
|
||||
type: 'http',
|
||||
url: 'https://example.com',
|
||||
interval: 60,
|
||||
enabled: true,
|
||||
status: 'up',
|
||||
latency: 100,
|
||||
max_retries: 3
|
||||
}
|
||||
]
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await uptime.getMonitors()
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/uptime/monitors')
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getMonitorHistory', () => {
|
||||
it('should call GET /uptime/monitors/:id/history with default limit', async () => {
|
||||
const mockData: UptimeHeartbeat[] = [
|
||||
{
|
||||
id: 1,
|
||||
monitor_id: 'mon-1',
|
||||
status: 'up',
|
||||
latency: 100,
|
||||
message: 'OK',
|
||||
created_at: '2025-12-04T00:00:00Z'
|
||||
}
|
||||
]
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await uptime.getMonitorHistory('mon-1')
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/uptime/monitors/mon-1/history?limit=50')
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
|
||||
it('should call GET /uptime/monitors/:id/history with custom limit', async () => {
|
||||
const mockData: UptimeHeartbeat[] = []
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await uptime.getMonitorHistory('mon-1', 100)
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/uptime/monitors/mon-1/history?limit=100')
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateMonitor', () => {
|
||||
it('should call PUT /uptime/monitors/:id', async () => {
|
||||
const mockMonitor: UptimeMonitor = {
|
||||
id: 'mon-1',
|
||||
name: 'Updated Monitor',
|
||||
type: 'http',
|
||||
url: 'https://example.com',
|
||||
interval: 120,
|
||||
enabled: false,
|
||||
status: 'down',
|
||||
latency: 0,
|
||||
max_retries: 5
|
||||
}
|
||||
vi.mocked(client.put).mockResolvedValue({ data: mockMonitor })
|
||||
|
||||
const result = await uptime.updateMonitor('mon-1', { enabled: false, interval: 120 })
|
||||
|
||||
expect(client.put).toHaveBeenCalledWith('/uptime/monitors/mon-1', { enabled: false, interval: 120 })
|
||||
expect(result).toEqual(mockMonitor)
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteMonitor', () => {
|
||||
it('should call DELETE /uptime/monitors/:id', async () => {
|
||||
vi.mocked(client.delete).mockResolvedValue({ data: undefined })
|
||||
|
||||
const result = await uptime.deleteMonitor('mon-1')
|
||||
|
||||
expect(client.delete).toHaveBeenCalledWith('/uptime/monitors/mon-1')
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('syncMonitors', () => {
|
||||
it('should call POST /uptime/sync with empty body when no params', async () => {
|
||||
const mockData = { synced: 5 }
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await uptime.syncMonitors()
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/uptime/sync', {})
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
|
||||
it('should call POST /uptime/sync with provided parameters', async () => {
|
||||
const mockData = { synced: 5 }
|
||||
const body = { interval: 120, max_retries: 5 }
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await uptime.syncMonitors(body)
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/uptime/sync', body)
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkMonitor', () => {
|
||||
it('should call POST /uptime/monitors/:id/check', async () => {
|
||||
const mockData = { message: 'Check initiated' }
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockData })
|
||||
|
||||
const result = await uptime.checkMonitor('mon-1')
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/uptime/monitors/mon-1/check')
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,189 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import client from '../client'
|
||||
import {
|
||||
listUsers,
|
||||
getUser,
|
||||
createUser,
|
||||
inviteUser,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
updateUserPermissions,
|
||||
validateInvite,
|
||||
acceptInvite,
|
||||
} from '../users'
|
||||
|
||||
vi.mock('../client', () => ({
|
||||
default: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('users api', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('lists, reads, creates, updates, and deletes users', async () => {
|
||||
vi.mocked(client.get).mockResolvedValueOnce({ data: [{ id: 1, email: 'a' }] })
|
||||
const users = await listUsers()
|
||||
expect(users[0].id).toBe(1)
|
||||
expect(client.get).toHaveBeenCalledWith('/users')
|
||||
|
||||
vi.mocked(client.get).mockResolvedValueOnce({ data: { id: 2 } })
|
||||
await getUser(2)
|
||||
expect(client.get).toHaveBeenCalledWith('/users/2')
|
||||
|
||||
vi.mocked(client.post).mockResolvedValueOnce({ data: { id: 3 } })
|
||||
await createUser({ email: 'e', name: 'n', password: 'p' })
|
||||
expect(client.post).toHaveBeenCalledWith('/users', { email: 'e', name: 'n', password: 'p' })
|
||||
|
||||
vi.mocked(client.put).mockResolvedValueOnce({ data: { message: 'ok' } })
|
||||
await updateUser(2, { enabled: false })
|
||||
expect(client.put).toHaveBeenCalledWith('/users/2', { enabled: false })
|
||||
|
||||
vi.mocked(client.delete).mockResolvedValueOnce({ data: { message: 'deleted' } })
|
||||
await deleteUser(2)
|
||||
expect(client.delete).toHaveBeenCalledWith('/users/2')
|
||||
})
|
||||
|
||||
it('invites users and updates permissions', async () => {
|
||||
vi.mocked(client.post).mockResolvedValueOnce({ data: { invite_token: 't' } })
|
||||
await inviteUser({ email: 'i', permission_mode: 'allow_all' })
|
||||
expect(client.post).toHaveBeenCalledWith('/users/invite', { email: 'i', permission_mode: 'allow_all' })
|
||||
|
||||
vi.mocked(client.put).mockResolvedValueOnce({ data: { message: 'saved' } })
|
||||
await updateUserPermissions(1, { permission_mode: 'deny_all', permitted_hosts: [1, 2] })
|
||||
expect(client.put).toHaveBeenCalledWith('/users/1/permissions', { permission_mode: 'deny_all', permitted_hosts: [1, 2] })
|
||||
})
|
||||
|
||||
it('validates and accepts invites with params', async () => {
|
||||
vi.mocked(client.get).mockResolvedValueOnce({ data: { valid: true, email: 'a' } })
|
||||
await validateInvite('token-1')
|
||||
expect(client.get).toHaveBeenCalledWith('/invite/validate', { params: { token: 'token-1' } })
|
||||
|
||||
vi.mocked(client.post).mockResolvedValueOnce({ data: { message: 'accepted', email: 'a' } })
|
||||
await acceptInvite({ token: 't', name: 'n', password: 'p' })
|
||||
expect(client.post).toHaveBeenCalledWith('/invite/accept', { token: 't', name: 'n', password: 'p' })
|
||||
})
|
||||
|
||||
describe('previewInviteURL', () => {
|
||||
it('should call POST /users/preview-invite-url with email', async () => {
|
||||
const mockResponse = {
|
||||
preview_url: 'https://example.com/accept-invite?token=SAMPLE_TOKEN_PREVIEW',
|
||||
base_url: 'https://example.com',
|
||||
is_configured: true,
|
||||
email: 'test@example.com',
|
||||
warning: false,
|
||||
warning_message: ''
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
|
||||
|
||||
const result = await import('../users').then(m => m.previewInviteURL('test@example.com'))
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/users/preview-invite-url', { email: 'test@example.com' })
|
||||
expect(result).toEqual(mockResponse)
|
||||
})
|
||||
|
||||
it('should return complete PreviewInviteURLResponse structure', async () => {
|
||||
const mockResponse = {
|
||||
preview_url: 'https://charon.example.com/accept-invite?token=SAMPLE_TOKEN_PREVIEW',
|
||||
base_url: 'https://charon.example.com',
|
||||
is_configured: true,
|
||||
email: 'user@test.com',
|
||||
warning: false,
|
||||
warning_message: ''
|
||||
}
|
||||
vi.mocked(client.post).mockResolvedValue({ data: mockResponse })
|
||||
|
||||
const result = await import('../users').then(m => m.previewInviteURL('user@test.com'))
|
||||
|
||||
expect(result.preview_url).toBeDefined()
|
||||
expect(result.base_url).toBeDefined()
|
||||
expect(result.is_configured).toBeDefined()
|
||||
expect(result.email).toBeDefined()
|
||||
expect(result.warning).toBeDefined()
|
||||
expect(result.warning_message).toBeDefined()
|
||||
})
|
||||
|
||||
it('should return preview_url with sample token', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({
|
||||
data: {
|
||||
preview_url: 'http://localhost:8080/accept-invite?token=SAMPLE_TOKEN_PREVIEW',
|
||||
base_url: 'http://localhost:8080',
|
||||
is_configured: false,
|
||||
email: 'test@example.com',
|
||||
warning: true,
|
||||
warning_message: 'Public URL not configured'
|
||||
}
|
||||
})
|
||||
|
||||
const result = await import('../users').then(m => m.previewInviteURL('test@example.com'))
|
||||
|
||||
expect(result.preview_url).toContain('SAMPLE_TOKEN_PREVIEW')
|
||||
})
|
||||
|
||||
it('should return is_configured flag', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({
|
||||
data: {
|
||||
preview_url: 'https://example.com/accept-invite?token=SAMPLE_TOKEN_PREVIEW',
|
||||
base_url: 'https://example.com',
|
||||
is_configured: true,
|
||||
email: 'test@example.com',
|
||||
warning: false,
|
||||
warning_message: ''
|
||||
}
|
||||
})
|
||||
|
||||
const result = await import('../users').then(m => m.previewInviteURL('test@example.com'))
|
||||
|
||||
expect(result.is_configured).toBe(true)
|
||||
})
|
||||
|
||||
it('should return warning flag when public URL not configured', async () => {
|
||||
vi.mocked(client.post).mockResolvedValue({
|
||||
data: {
|
||||
preview_url: 'http://localhost:8080/accept-invite?token=SAMPLE_TOKEN_PREVIEW',
|
||||
base_url: 'http://localhost:8080',
|
||||
is_configured: false,
|
||||
email: 'admin@test.com',
|
||||
warning: true,
|
||||
warning_message: 'Using default localhost URL'
|
||||
}
|
||||
})
|
||||
|
||||
const result = await import('../users').then(m => m.previewInviteURL('admin@test.com'))
|
||||
|
||||
expect(result.warning).toBe(true)
|
||||
expect(result.warning_message).toBe('Using default localhost URL')
|
||||
})
|
||||
|
||||
it('should return the provided email in response', async () => {
|
||||
const testEmail = 'specific@email.com'
|
||||
vi.mocked(client.post).mockResolvedValue({
|
||||
data: {
|
||||
preview_url: 'https://example.com/accept-invite?token=SAMPLE_TOKEN_PREVIEW',
|
||||
base_url: 'https://example.com',
|
||||
is_configured: true,
|
||||
email: testEmail,
|
||||
warning: false,
|
||||
warning_message: ''
|
||||
}
|
||||
})
|
||||
|
||||
const result = await import('../users').then(m => m.previewInviteURL(testEmail))
|
||||
|
||||
expect(result.email).toBe(testEmail)
|
||||
})
|
||||
|
||||
it('should handle request errors', async () => {
|
||||
vi.mocked(client.post).mockRejectedValue(new Error('Network error'))
|
||||
|
||||
await expect(
|
||||
import('../users').then(m => m.previewInviteURL('test@example.com'))
|
||||
).rejects.toThrow('Network error')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,112 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { getWebSocketConnections, getWebSocketStats } from '../websocket';
|
||||
import client from '../client';
|
||||
|
||||
vi.mock('../client');
|
||||
|
||||
describe('WebSocket API', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getWebSocketConnections', () => {
|
||||
it('should fetch WebSocket connections', async () => {
|
||||
const mockResponse = {
|
||||
connections: [
|
||||
{
|
||||
id: 'test-conn-1',
|
||||
type: 'logs',
|
||||
connected_at: '2024-01-15T10:00:00Z',
|
||||
last_activity_at: '2024-01-15T10:05:00Z',
|
||||
remote_addr: '192.168.1.1:12345',
|
||||
user_agent: 'Mozilla/5.0',
|
||||
filters: 'level=error',
|
||||
},
|
||||
{
|
||||
id: 'test-conn-2',
|
||||
type: 'cerberus',
|
||||
connected_at: '2024-01-15T10:02:00Z',
|
||||
last_activity_at: '2024-01-15T10:06:00Z',
|
||||
remote_addr: '192.168.1.2:54321',
|
||||
user_agent: 'Chrome/90.0',
|
||||
filters: 'source=waf',
|
||||
},
|
||||
],
|
||||
count: 2,
|
||||
};
|
||||
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const result = await getWebSocketConnections();
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/websocket/connections');
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(result.count).toBe(2);
|
||||
expect(result.connections).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should handle empty connections', async () => {
|
||||
const mockResponse = {
|
||||
connections: [],
|
||||
count: 0,
|
||||
};
|
||||
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const result = await getWebSocketConnections();
|
||||
|
||||
expect(result.connections).toHaveLength(0);
|
||||
expect(result.count).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle API errors', async () => {
|
||||
vi.mocked(client.get).mockRejectedValue(new Error('Network error'));
|
||||
|
||||
await expect(getWebSocketConnections()).rejects.toThrow('Network error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWebSocketStats', () => {
|
||||
it('should fetch WebSocket statistics', async () => {
|
||||
const mockResponse = {
|
||||
total_active: 3,
|
||||
logs_connections: 2,
|
||||
cerberus_connections: 1,
|
||||
oldest_connection: '2024-01-15T09:55:00Z',
|
||||
last_updated: '2024-01-15T10:10:00Z',
|
||||
};
|
||||
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const result = await getWebSocketStats();
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith('/websocket/stats');
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(result.total_active).toBe(3);
|
||||
expect(result.logs_connections).toBe(2);
|
||||
expect(result.cerberus_connections).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle stats with no connections', async () => {
|
||||
const mockResponse = {
|
||||
total_active: 0,
|
||||
logs_connections: 0,
|
||||
cerberus_connections: 0,
|
||||
last_updated: '2024-01-15T10:10:00Z',
|
||||
};
|
||||
|
||||
vi.mocked(client.get).mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const result = await getWebSocketStats();
|
||||
|
||||
expect(result.total_active).toBe(0);
|
||||
expect(result.oldest_connection).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle API errors', async () => {
|
||||
vi.mocked(client.get).mockRejectedValue(new Error('Server error'));
|
||||
|
||||
await expect(getWebSocketStats()).rejects.toThrow('Server error');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,126 +0,0 @@
|
||||
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 = {
|
||||
/**
|
||||
* Fetches all access lists.
|
||||
* @returns Promise resolving to array of AccessList objects
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
async list(): Promise<AccessList[]> {
|
||||
const response = await client.get<AccessList[]>('/access-lists');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets a single access list by ID.
|
||||
* @param id - The access list ID
|
||||
* @returns Promise resolving to the AccessList object
|
||||
* @throws {AxiosError} If the request fails or access list not found
|
||||
*/
|
||||
async get(id: number): Promise<AccessList> {
|
||||
const response = await client.get<AccessList>(`/access-lists/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new access list.
|
||||
* @param data - CreateAccessListRequest with access list configuration
|
||||
* @returns Promise resolving to the created AccessList
|
||||
* @throws {AxiosError} If creation fails or validation errors occur
|
||||
*/
|
||||
async create(data: CreateAccessListRequest): Promise<AccessList> {
|
||||
const response = await client.post<AccessList>('/access-lists', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates an existing access list.
|
||||
* @param id - The access list ID to update
|
||||
* @param data - Partial CreateAccessListRequest with fields to update
|
||||
* @returns Promise resolving to the updated AccessList
|
||||
* @throws {AxiosError} If update fails or access list not found
|
||||
*/
|
||||
async update(id: number, data: Partial<CreateAccessListRequest>): Promise<AccessList> {
|
||||
const response = await client.put<AccessList>(`/access-lists/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes an access list.
|
||||
* @param id - The access list ID to delete
|
||||
* @throws {AxiosError} If deletion fails or access list not found
|
||||
*/
|
||||
async delete(id: number): Promise<void> {
|
||||
await client.delete(`/access-lists/${id}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Tests if an IP address would be allowed or blocked by an access list.
|
||||
* @param id - The access list ID to test against
|
||||
* @param ipAddress - The IP address to test
|
||||
* @returns Promise resolving to TestIPResponse with allowed status and reason
|
||||
* @throws {AxiosError} If test fails or access list not found
|
||||
*/
|
||||
async testIP(id: number, ipAddress: string): Promise<TestIPResponse> {
|
||||
const response = await client.post<TestIPResponse>(`/access-lists/${id}/test`, {
|
||||
ip_address: ipAddress,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets predefined access list templates.
|
||||
* @returns Promise resolving to array of AccessListTemplate objects
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
async getTemplates(): Promise<AccessListTemplate[]> {
|
||||
const response = await client.get<AccessListTemplate[]>('/access-lists/templates');
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
@@ -1,267 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import client from './client'
|
||||
import {
|
||||
getAuditLogs,
|
||||
getAuditLog,
|
||||
getAuditLogsByProvider,
|
||||
exportAuditLogsCSV,
|
||||
type AuditLog,
|
||||
type AuditLogFilters,
|
||||
} from './auditLogs'
|
||||
|
||||
vi.mock('./client', () => ({
|
||||
default: {
|
||||
get: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
const mockedClient = client as unknown as {
|
||||
get: ReturnType<typeof vi.fn>
|
||||
}
|
||||
|
||||
describe('auditLogs api', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('getAuditLogs', () => {
|
||||
it('fetches audit logs with default pagination', async () => {
|
||||
const mockResponse = {
|
||||
logs: [
|
||||
{
|
||||
id: 1,
|
||||
uuid: 'log-1',
|
||||
actor: 'admin',
|
||||
action: 'user_login',
|
||||
event_category: 'user',
|
||||
details: 'User logged in',
|
||||
ip_address: '192.168.1.1',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 50,
|
||||
}
|
||||
mockedClient.get.mockResolvedValueOnce({ data: mockResponse })
|
||||
|
||||
const result = await getAuditLogs()
|
||||
|
||||
expect(mockedClient.get).toHaveBeenCalledWith('/audit-logs?page=1&limit=50')
|
||||
expect(result).toEqual(mockResponse)
|
||||
expect(result.logs).toHaveLength(1)
|
||||
expect(result.logs[0].uuid).toBe('log-1')
|
||||
})
|
||||
|
||||
it('fetches audit logs with custom pagination', async () => {
|
||||
const mockResponse = {
|
||||
logs: [],
|
||||
total: 100,
|
||||
page: 3,
|
||||
limit: 25,
|
||||
}
|
||||
mockedClient.get.mockResolvedValueOnce({ data: mockResponse })
|
||||
|
||||
const result = await getAuditLogs(undefined, 3, 25)
|
||||
|
||||
expect(mockedClient.get).toHaveBeenCalledWith('/audit-logs?page=3&limit=25')
|
||||
expect(result.page).toBe(3)
|
||||
expect(result.limit).toBe(25)
|
||||
})
|
||||
|
||||
it('fetches audit logs with all filters', async () => {
|
||||
const filters: AuditLogFilters = {
|
||||
event_category: 'dns_provider',
|
||||
actor: 'admin',
|
||||
action: 'dns_provider_create',
|
||||
start_date: '2024-01-01',
|
||||
end_date: '2024-12-31',
|
||||
resource_uuid: 'resource-123',
|
||||
}
|
||||
const mockResponse = {
|
||||
logs: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
limit: 50,
|
||||
}
|
||||
mockedClient.get.mockResolvedValueOnce({ data: mockResponse })
|
||||
|
||||
await getAuditLogs(filters)
|
||||
|
||||
expect(mockedClient.get).toHaveBeenCalledWith(
|
||||
'/audit-logs?page=1&limit=50&event_category=dns_provider&actor=admin&action=dns_provider_create&start_date=2024-01-01&end_date=2024-12-31&resource_uuid=resource-123'
|
||||
)
|
||||
})
|
||||
|
||||
it('fetches audit logs with partial filters', async () => {
|
||||
const filters: AuditLogFilters = {
|
||||
event_category: 'certificate',
|
||||
start_date: '2024-01-01',
|
||||
}
|
||||
const mockResponse = {
|
||||
logs: [],
|
||||
total: 5,
|
||||
page: 1,
|
||||
limit: 50,
|
||||
}
|
||||
mockedClient.get.mockResolvedValueOnce({ data: mockResponse })
|
||||
|
||||
await getAuditLogs(filters, 1, 50)
|
||||
|
||||
expect(mockedClient.get).toHaveBeenCalledWith(
|
||||
'/audit-logs?page=1&limit=50&event_category=certificate&start_date=2024-01-01'
|
||||
)
|
||||
})
|
||||
|
||||
it('handles errors when fetching audit logs', async () => {
|
||||
const error = new Error('Network error')
|
||||
mockedClient.get.mockRejectedValueOnce(error)
|
||||
|
||||
await expect(getAuditLogs()).rejects.toThrow('Network error')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getAuditLog', () => {
|
||||
it('fetches a single audit log by UUID', async () => {
|
||||
const mockLog: AuditLog = {
|
||||
id: 42,
|
||||
uuid: 'log-uuid-123',
|
||||
actor: 'admin',
|
||||
action: 'certificate_issue',
|
||||
event_category: 'certificate',
|
||||
resource_id: 10,
|
||||
resource_uuid: 'cert-uuid',
|
||||
details: 'Certificate issued successfully',
|
||||
ip_address: '10.0.0.1',
|
||||
user_agent: 'Mozilla/5.0',
|
||||
created_at: '2024-06-15T12:30:00Z',
|
||||
}
|
||||
mockedClient.get.mockResolvedValueOnce({ data: mockLog })
|
||||
|
||||
const result = await getAuditLog('log-uuid-123')
|
||||
|
||||
expect(mockedClient.get).toHaveBeenCalledWith('/audit-logs/log-uuid-123')
|
||||
expect(result).toEqual(mockLog)
|
||||
expect(result.uuid).toBe('log-uuid-123')
|
||||
expect(result.action).toBe('certificate_issue')
|
||||
})
|
||||
|
||||
it('handles 404 when audit log not found', async () => {
|
||||
const error = new Error('Not found')
|
||||
mockedClient.get.mockRejectedValueOnce(error)
|
||||
|
||||
await expect(getAuditLog('nonexistent')).rejects.toThrow('Not found')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getAuditLogsByProvider', () => {
|
||||
it('fetches audit logs for a specific DNS provider with default pagination', async () => {
|
||||
const mockResponse = {
|
||||
logs: [
|
||||
{
|
||||
id: 5,
|
||||
uuid: 'log-5',
|
||||
actor: 'system',
|
||||
action: 'dns_provider_update',
|
||||
event_category: 'dns_provider',
|
||||
resource_id: 123,
|
||||
details: 'DNS provider updated',
|
||||
created_at: '2024-03-15T10:00:00Z',
|
||||
},
|
||||
],
|
||||
total: 10,
|
||||
page: 1,
|
||||
limit: 50,
|
||||
}
|
||||
mockedClient.get.mockResolvedValueOnce({ data: mockResponse })
|
||||
|
||||
const result = await getAuditLogsByProvider(123)
|
||||
|
||||
expect(mockedClient.get).toHaveBeenCalledWith('/dns-providers/123/audit-logs?page=1&limit=50')
|
||||
expect(result.logs).toHaveLength(1)
|
||||
expect(result.logs[0].action).toBe('dns_provider_update')
|
||||
})
|
||||
|
||||
it('fetches audit logs for a provider with custom pagination', async () => {
|
||||
const mockResponse = {
|
||||
logs: [],
|
||||
total: 25,
|
||||
page: 2,
|
||||
limit: 10,
|
||||
}
|
||||
mockedClient.get.mockResolvedValueOnce({ data: mockResponse })
|
||||
|
||||
const result = await getAuditLogsByProvider(456, 2, 10)
|
||||
|
||||
expect(mockedClient.get).toHaveBeenCalledWith('/dns-providers/456/audit-logs?page=2&limit=10')
|
||||
expect(result.page).toBe(2)
|
||||
expect(result.limit).toBe(10)
|
||||
})
|
||||
|
||||
it('handles errors when fetching provider audit logs', async () => {
|
||||
const error = new Error('Provider not found')
|
||||
mockedClient.get.mockRejectedValueOnce(error)
|
||||
|
||||
await expect(getAuditLogsByProvider(999)).rejects.toThrow('Provider not found')
|
||||
})
|
||||
})
|
||||
|
||||
describe('exportAuditLogsCSV', () => {
|
||||
it('exports audit logs to CSV without filters', async () => {
|
||||
const mockCSV = 'id,actor,action,created_at\n1,admin,user_login,2024-01-01'
|
||||
mockedClient.get.mockResolvedValueOnce({ data: mockCSV })
|
||||
|
||||
const result = await exportAuditLogsCSV()
|
||||
|
||||
expect(mockedClient.get).toHaveBeenCalledWith(
|
||||
'/audit-logs/export?',
|
||||
{ headers: { Accept: 'text/csv' } }
|
||||
)
|
||||
expect(result).toBe(mockCSV)
|
||||
})
|
||||
|
||||
it('exports audit logs to CSV with all filters', async () => {
|
||||
const filters: AuditLogFilters = {
|
||||
event_category: 'proxy_host',
|
||||
actor: 'operator',
|
||||
action: 'proxy_host_delete',
|
||||
start_date: '2024-01-01',
|
||||
end_date: '2024-06-30',
|
||||
resource_uuid: 'host-uuid-456',
|
||||
}
|
||||
const mockCSV = 'id,actor,action,created_at\n'
|
||||
mockedClient.get.mockResolvedValueOnce({ data: mockCSV })
|
||||
|
||||
const result = await exportAuditLogsCSV(filters)
|
||||
|
||||
expect(mockedClient.get).toHaveBeenCalledWith(
|
||||
'/audit-logs/export?event_category=proxy_host&actor=operator&action=proxy_host_delete&start_date=2024-01-01&end_date=2024-06-30&resource_uuid=host-uuid-456',
|
||||
{ headers: { Accept: 'text/csv' } }
|
||||
)
|
||||
expect(result).toBe(mockCSV)
|
||||
})
|
||||
|
||||
it('exports audit logs with partial filters', async () => {
|
||||
const filters: AuditLogFilters = {
|
||||
action: 'settings_update',
|
||||
end_date: '2024-12-31',
|
||||
}
|
||||
const mockCSV = 'header,data\n'
|
||||
mockedClient.get.mockResolvedValueOnce({ data: mockCSV })
|
||||
|
||||
await exportAuditLogsCSV(filters)
|
||||
|
||||
expect(mockedClient.get).toHaveBeenCalledWith(
|
||||
'/audit-logs/export?action=settings_update&end_date=2024-12-31',
|
||||
{ headers: { Accept: 'text/csv' } }
|
||||
)
|
||||
})
|
||||
|
||||
it('handles errors when exporting audit logs', async () => {
|
||||
const error = new Error('Export failed')
|
||||
mockedClient.get.mockRejectedValueOnce(error)
|
||||
|
||||
await expect(exportAuditLogsCSV()).rejects.toThrow('Export failed')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,144 +0,0 @@
|
||||
import client from './client'
|
||||
|
||||
/** Audit log event category */
|
||||
export type EventCategory = 'dns_provider' | 'certificate' | 'proxy_host' | 'user' | 'system'
|
||||
|
||||
/** Audit log action type */
|
||||
export type AuditAction =
|
||||
| 'dns_provider_create'
|
||||
| 'dns_provider_update'
|
||||
| 'dns_provider_delete'
|
||||
| 'credential_test'
|
||||
| 'credential_decrypt'
|
||||
| 'certificate_issue'
|
||||
| 'certificate_renew'
|
||||
| 'proxy_host_create'
|
||||
| 'proxy_host_update'
|
||||
| 'proxy_host_delete'
|
||||
| 'user_login'
|
||||
| 'user_logout'
|
||||
| 'settings_update'
|
||||
|
||||
/** Represents a single audit log entry */
|
||||
export interface AuditLog {
|
||||
id: number
|
||||
uuid: string
|
||||
actor: string
|
||||
action: AuditAction
|
||||
event_category: EventCategory
|
||||
resource_id?: number
|
||||
resource_uuid?: string
|
||||
details: string
|
||||
ip_address?: string
|
||||
user_agent?: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
/** Filters for querying audit logs */
|
||||
export interface AuditLogFilters {
|
||||
event_category?: EventCategory
|
||||
actor?: string
|
||||
action?: AuditAction
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
resource_uuid?: string
|
||||
}
|
||||
|
||||
/** Response for list endpoint */
|
||||
interface ListAuditLogsResponse {
|
||||
logs: AuditLog[]
|
||||
total: number
|
||||
page: number
|
||||
limit: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches audit logs with pagination and filtering.
|
||||
* @param filters - Optional filters to apply
|
||||
* @param page - Page number (1-indexed)
|
||||
* @param limit - Number of records per page
|
||||
* @returns Promise resolving to paginated audit logs
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export async function getAuditLogs(
|
||||
filters?: AuditLogFilters,
|
||||
page: number = 1,
|
||||
limit: number = 50
|
||||
): Promise<ListAuditLogsResponse> {
|
||||
const params = new URLSearchParams()
|
||||
params.append('page', page.toString())
|
||||
params.append('limit', limit.toString())
|
||||
|
||||
if (filters) {
|
||||
if (filters.event_category) params.append('event_category', filters.event_category)
|
||||
if (filters.actor) params.append('actor', filters.actor)
|
||||
if (filters.action) params.append('action', filters.action)
|
||||
if (filters.start_date) params.append('start_date', filters.start_date)
|
||||
if (filters.end_date) params.append('end_date', filters.end_date)
|
||||
if (filters.resource_uuid) params.append('resource_uuid', filters.resource_uuid)
|
||||
}
|
||||
|
||||
const response = await client.get<ListAuditLogsResponse>(`/audit-logs?${params.toString()}`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a single audit log by UUID.
|
||||
* @param uuid - The audit log UUID
|
||||
* @returns Promise resolving to the audit log
|
||||
* @throws {AxiosError} If not found or request fails
|
||||
*/
|
||||
export async function getAuditLog(uuid: string): Promise<AuditLog> {
|
||||
const response = await client.get<AuditLog>(`/audit-logs/${uuid}`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches audit logs for a specific DNS provider.
|
||||
* @param providerId - The DNS provider ID
|
||||
* @param page - Page number (1-indexed)
|
||||
* @param limit - Number of records per page
|
||||
* @returns Promise resolving to paginated audit logs
|
||||
* @throws {AxiosError} If not found or request fails
|
||||
*/
|
||||
export async function getAuditLogsByProvider(
|
||||
providerId: number,
|
||||
page: number = 1,
|
||||
limit: number = 50
|
||||
): Promise<ListAuditLogsResponse> {
|
||||
const params = new URLSearchParams()
|
||||
params.append('page', page.toString())
|
||||
params.append('limit', limit.toString())
|
||||
|
||||
const response = await client.get<ListAuditLogsResponse>(
|
||||
`/dns-providers/${providerId}/audit-logs?${params.toString()}`
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports audit logs to CSV format.
|
||||
* @param filters - Optional filters to apply
|
||||
* @returns Promise resolving to CSV string
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export async function exportAuditLogsCSV(filters?: AuditLogFilters): Promise<string> {
|
||||
const params = new URLSearchParams()
|
||||
|
||||
if (filters) {
|
||||
if (filters.event_category) params.append('event_category', filters.event_category)
|
||||
if (filters.actor) params.append('actor', filters.actor)
|
||||
if (filters.action) params.append('action', filters.action)
|
||||
if (filters.start_date) params.append('start_date', filters.start_date)
|
||||
if (filters.end_date) params.append('end_date', filters.end_date)
|
||||
if (filters.resource_uuid) params.append('resource_uuid', filters.resource_uuid)
|
||||
}
|
||||
|
||||
const response = await client.get<string>(
|
||||
`/audit-logs/export?${params.toString()}`,
|
||||
{
|
||||
headers: { Accept: 'text/csv' },
|
||||
}
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import client from './client';
|
||||
|
||||
/** Represents a backup file stored on the server. */
|
||||
export interface BackupFile {
|
||||
filename: string;
|
||||
size: number;
|
||||
time: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all available backup files.
|
||||
* @returns Promise resolving to array of BackupFile objects
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export const getBackups = async (): Promise<BackupFile[]> => {
|
||||
const response = await client.get<BackupFile[]>('/backups');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new backup of the current configuration.
|
||||
* @returns Promise resolving to object containing the new backup filename
|
||||
* @throws {AxiosError} If backup creation fails
|
||||
*/
|
||||
export const createBackup = async (): Promise<{ filename: string }> => {
|
||||
const response = await client.post<{ filename: string }>('/backups');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Restores configuration from a backup file.
|
||||
* @param filename - The name of the backup file to restore
|
||||
* @throws {AxiosError} If restoration fails or file not found
|
||||
*/
|
||||
export const restoreBackup = async (filename: string): Promise<void> => {
|
||||
await client.post(`/backups/${filename}/restore`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes a backup file.
|
||||
* @param filename - The name of the backup file to delete
|
||||
* @throws {AxiosError} If deletion fails or file not found
|
||||
*/
|
||||
export const deleteBackup = async (filename: string): Promise<void> => {
|
||||
await client.delete(`/backups/${filename}`);
|
||||
};
|
||||
@@ -1,53 +0,0 @@
|
||||
import client from './client'
|
||||
|
||||
/** Represents an SSL/TLS certificate. */
|
||||
export interface Certificate {
|
||||
id?: number
|
||||
name?: string
|
||||
domain: string
|
||||
issuer: string
|
||||
expires_at: string
|
||||
status: 'valid' | 'expiring' | 'expired' | 'untrusted'
|
||||
provider: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all SSL certificates.
|
||||
* @returns Promise resolving to array of Certificate objects
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export async function getCertificates(): Promise<Certificate[]> {
|
||||
const response = await client.get<Certificate[]>('/certificates')
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a new SSL certificate with its private key.
|
||||
* @param name - Display name for the certificate
|
||||
* @param certFile - The certificate file (PEM format)
|
||||
* @param keyFile - The private key file (PEM format)
|
||||
* @returns Promise resolving to the created Certificate
|
||||
* @throws {AxiosError} If upload fails or certificate is invalid
|
||||
*/
|
||||
export async function uploadCertificate(name: string, certFile: File, keyFile: File): Promise<Certificate> {
|
||||
const formData = new FormData()
|
||||
formData.append('name', name)
|
||||
formData.append('certificate_file', certFile)
|
||||
formData.append('key_file', keyFile)
|
||||
|
||||
const response = await client.post<Certificate>('/certificates', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an SSL certificate.
|
||||
* @param id - The ID of the certificate to delete
|
||||
* @throws {AxiosError} If deletion fails or certificate not found
|
||||
*/
|
||||
export async function deleteCertificate(id: number): Promise<void> {
|
||||
await client.delete(`/certificates/${id}`)
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
* Pre-configured Axios instance for API communication.
|
||||
* Includes base URL, credentials, and timeout settings.
|
||||
*/
|
||||
const client = axios.create({
|
||||
baseURL: '/api/v1',
|
||||
withCredentials: true, // Required for HttpOnly cookie transmission
|
||||
timeout: 30000, // 30 second timeout
|
||||
});
|
||||
|
||||
/**
|
||||
* Sets or clears the Authorization header for API requests.
|
||||
* @param token - JWT token to set, or null to clear authentication
|
||||
*/
|
||||
export const setAuthToken = (token: string | null) => {
|
||||
if (token) {
|
||||
client.defaults.headers.common.Authorization = `Bearer ${token}`;
|
||||
} else {
|
||||
delete client.defaults.headers.common.Authorization;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback function invoked when a 401 authentication error occurs.
|
||||
* Set via setAuthErrorHandler to allow AuthContext to handle session expiry.
|
||||
*/
|
||||
let onAuthError: (() => void) | null = null;
|
||||
|
||||
/**
|
||||
* Registers a callback to handle authentication errors (401 responses).
|
||||
* @param handler - Function to call when authentication fails
|
||||
*/
|
||||
export const setAuthErrorHandler = (handler: () => void) => {
|
||||
onAuthError = handler;
|
||||
};
|
||||
|
||||
// Global 401 error handling - triggers auth error callback for session expiry
|
||||
client.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
console.warn('Authentication failed:', error.config?.url);
|
||||
// Skip auth error handling for login/auth endpoints to avoid redirect loops
|
||||
const url = error.config?.url || '';
|
||||
const isAuthEndpoint = url.includes('/auth/login') || url.includes('/auth/me');
|
||||
if (onAuthError && !isAuthEndpoint) {
|
||||
onAuthError();
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default client;
|
||||
@@ -1,57 +0,0 @@
|
||||
import client from './client'
|
||||
|
||||
/** CrowdSec Console enrollment status. */
|
||||
export interface ConsoleEnrollmentStatus {
|
||||
status: string
|
||||
tenant?: string
|
||||
agent_name?: string
|
||||
last_error?: string
|
||||
last_attempt_at?: string
|
||||
enrolled_at?: string
|
||||
last_heartbeat_at?: string
|
||||
key_present: boolean
|
||||
correlation_id?: string
|
||||
}
|
||||
|
||||
/** Payload for enrolling with CrowdSec Console. */
|
||||
export interface ConsoleEnrollPayload {
|
||||
enrollment_key: string
|
||||
tenant?: string
|
||||
agent_name: string
|
||||
force?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current CrowdSec Console enrollment status.
|
||||
* @returns Promise resolving to ConsoleEnrollmentStatus
|
||||
* @throws {AxiosError} If status check fails
|
||||
*/
|
||||
export async function getConsoleStatus(): Promise<ConsoleEnrollmentStatus> {
|
||||
const resp = await client.get<ConsoleEnrollmentStatus>('/admin/crowdsec/console/status')
|
||||
return resp.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrolls the instance with CrowdSec Console.
|
||||
* @param payload - Enrollment configuration including key and agent name
|
||||
* @returns Promise resolving to the new enrollment status
|
||||
* @throws {AxiosError} If enrollment fails
|
||||
*/
|
||||
export async function enrollConsole(payload: ConsoleEnrollPayload): Promise<ConsoleEnrollmentStatus> {
|
||||
const resp = await client.post<ConsoleEnrollmentStatus>('/admin/crowdsec/console/enroll', payload)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the current CrowdSec Console enrollment.
|
||||
* @throws {AxiosError} If clearing enrollment fails
|
||||
*/
|
||||
export async function clearConsoleEnrollment(): Promise<void> {
|
||||
await client.delete('/admin/crowdsec/console/enrollment')
|
||||
}
|
||||
|
||||
export default {
|
||||
getConsoleStatus,
|
||||
enrollConsole,
|
||||
clearConsoleEnrollment,
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
import client from './client'
|
||||
|
||||
/** Represents a zone-specific credential set */
|
||||
export interface DNSProviderCredential {
|
||||
id: number
|
||||
uuid: string
|
||||
dns_provider_id: number
|
||||
label: string
|
||||
zone_filter: string
|
||||
enabled: boolean
|
||||
propagation_timeout: number
|
||||
polling_interval: number
|
||||
key_version: number
|
||||
last_used_at?: string
|
||||
success_count: number
|
||||
failure_count: number
|
||||
last_error?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/** Request payload for creating/updating credentials */
|
||||
export interface CredentialRequest {
|
||||
label: string
|
||||
zone_filter: string
|
||||
credentials: Record<string, string>
|
||||
propagation_timeout?: number
|
||||
polling_interval?: number
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
/** Credential test result */
|
||||
export interface CredentialTestResult {
|
||||
success: boolean
|
||||
message?: string
|
||||
error?: string
|
||||
propagation_time_ms?: number
|
||||
}
|
||||
|
||||
/** Response for list endpoint */
|
||||
interface ListCredentialsResponse {
|
||||
credentials: DNSProviderCredential[]
|
||||
total: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all credentials for a DNS provider.
|
||||
* @param providerId - The DNS provider ID
|
||||
* @returns Promise resolving to array of credentials
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export async function getCredentials(providerId: number): Promise<DNSProviderCredential[]> {
|
||||
const response = await client.get<ListCredentialsResponse>(
|
||||
`/dns-providers/${providerId}/credentials`
|
||||
)
|
||||
return response.data.credentials
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a single credential by ID.
|
||||
* @param providerId - The DNS provider ID
|
||||
* @param credentialId - The credential ID
|
||||
* @returns Promise resolving to the credential
|
||||
* @throws {AxiosError} If not found or request fails
|
||||
*/
|
||||
export async function getCredential(
|
||||
providerId: number,
|
||||
credentialId: number
|
||||
): Promise<DNSProviderCredential> {
|
||||
const response = await client.get<DNSProviderCredential>(
|
||||
`/dns-providers/${providerId}/credentials/${credentialId}`
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new credential for a DNS provider.
|
||||
* @param providerId - The DNS provider ID
|
||||
* @param data - Credential configuration
|
||||
* @returns Promise resolving to the created credential
|
||||
* @throws {AxiosError} If validation fails or request fails
|
||||
*/
|
||||
export async function createCredential(
|
||||
providerId: number,
|
||||
data: CredentialRequest
|
||||
): Promise<DNSProviderCredential> {
|
||||
const response = await client.post<DNSProviderCredential>(
|
||||
`/dns-providers/${providerId}/credentials`,
|
||||
data
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing credential.
|
||||
* @param providerId - The DNS provider ID
|
||||
* @param credentialId - The credential ID
|
||||
* @param data - Updated configuration
|
||||
* @returns Promise resolving to the updated credential
|
||||
* @throws {AxiosError} If not found, validation fails, or request fails
|
||||
*/
|
||||
export async function updateCredential(
|
||||
providerId: number,
|
||||
credentialId: number,
|
||||
data: CredentialRequest
|
||||
): Promise<DNSProviderCredential> {
|
||||
const response = await client.put<DNSProviderCredential>(
|
||||
`/dns-providers/${providerId}/credentials/${credentialId}`,
|
||||
data
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a credential.
|
||||
* @param providerId - The DNS provider ID
|
||||
* @param credentialId - The credential ID
|
||||
* @throws {AxiosError} If not found or in use
|
||||
*/
|
||||
export async function deleteCredential(providerId: number, credentialId: number): Promise<void> {
|
||||
await client.delete(`/dns-providers/${providerId}/credentials/${credentialId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests a credential's connectivity.
|
||||
* @param providerId - The DNS provider ID
|
||||
* @param credentialId - The credential ID
|
||||
* @returns Promise resolving to test result
|
||||
* @throws {AxiosError} If not found or request fails
|
||||
*/
|
||||
export async function testCredential(
|
||||
providerId: number,
|
||||
credentialId: number
|
||||
): Promise<CredentialTestResult> {
|
||||
const response = await client.post<CredentialTestResult>(
|
||||
`/dns-providers/${providerId}/credentials/${credentialId}/test`
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables multi-credential mode for a DNS provider.
|
||||
* @param providerId - The DNS provider ID
|
||||
* @throws {AxiosError} If provider not found or already enabled
|
||||
*/
|
||||
export async function enableMultiCredentials(providerId: number): Promise<void> {
|
||||
await client.post(`/dns-providers/${providerId}/enable-multi-credentials`)
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
import client from './client'
|
||||
|
||||
/** Represents a CrowdSec decision (ban/captcha). */
|
||||
export interface CrowdSecDecision {
|
||||
id: string
|
||||
ip: string
|
||||
reason: string
|
||||
duration: string
|
||||
created_at: string
|
||||
source: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the CrowdSec security service.
|
||||
* @returns Promise resolving to status with process ID and LAPI readiness
|
||||
* @throws {AxiosError} If the service fails to start
|
||||
*/
|
||||
export async function startCrowdsec(): Promise<{ status: string; pid: number; lapi_ready?: boolean }> {
|
||||
const resp = await client.post('/admin/crowdsec/start')
|
||||
return resp.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the CrowdSec security service.
|
||||
* @returns Promise resolving to stop status
|
||||
* @throws {AxiosError} If the service fails to stop
|
||||
*/
|
||||
export async function stopCrowdsec() {
|
||||
const resp = await client.post('/admin/crowdsec/stop')
|
||||
return resp.data
|
||||
}
|
||||
|
||||
/** CrowdSec service status information. */
|
||||
export interface CrowdSecStatus {
|
||||
running: boolean
|
||||
pid: number
|
||||
lapi_ready: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current status of the CrowdSec service.
|
||||
* @returns Promise resolving to CrowdSecStatus
|
||||
* @throws {AxiosError} If status check fails
|
||||
*/
|
||||
export async function statusCrowdsec(): Promise<CrowdSecStatus> {
|
||||
const resp = await client.get<CrowdSecStatus>('/admin/crowdsec/status')
|
||||
return resp.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a CrowdSec configuration file.
|
||||
* @param file - The configuration file to import
|
||||
* @returns Promise resolving to import result
|
||||
* @throws {AxiosError} If import fails or file is invalid
|
||||
*/
|
||||
export async function importCrowdsecConfig(file: File) {
|
||||
const fd = new FormData()
|
||||
fd.append('file', file)
|
||||
const resp = await client.post('/admin/crowdsec/import', fd, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
return resp.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports the current CrowdSec configuration.
|
||||
* @returns Promise resolving to configuration blob for download
|
||||
* @throws {AxiosError} If export fails
|
||||
*/
|
||||
export async function exportCrowdsecConfig() {
|
||||
const resp = await client.get('/admin/crowdsec/export', { responseType: 'blob' })
|
||||
return resp.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all CrowdSec configuration files.
|
||||
* @returns Promise resolving to object containing file list
|
||||
* @throws {AxiosError} If listing fails
|
||||
*/
|
||||
export async function listCrowdsecFiles() {
|
||||
const resp = await client.get<{ files: string[] }>('/admin/crowdsec/files')
|
||||
return resp.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the content of a CrowdSec configuration file.
|
||||
* @param path - The file path to read
|
||||
* @returns Promise resolving to object containing file content
|
||||
* @throws {AxiosError} If file cannot be read
|
||||
*/
|
||||
export async function readCrowdsecFile(path: string) {
|
||||
const resp = await client.get<{ content: string }>(`/admin/crowdsec/file?path=${encodeURIComponent(path)}`)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes content to a CrowdSec configuration file.
|
||||
* @param path - The file path to write
|
||||
* @param content - The content to write
|
||||
* @returns Promise resolving to write result
|
||||
* @throws {AxiosError} If file cannot be written
|
||||
*/
|
||||
export async function writeCrowdsecFile(path: string, content: string) {
|
||||
const resp = await client.post('/admin/crowdsec/file', { path, content })
|
||||
return resp.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all active CrowdSec decisions (bans).
|
||||
* @returns Promise resolving to object containing decisions array
|
||||
* @throws {AxiosError} If listing fails
|
||||
*/
|
||||
export async function listCrowdsecDecisions(): Promise<{ decisions: CrowdSecDecision[] }> {
|
||||
const resp = await client.get<{ decisions: CrowdSecDecision[] }>('/admin/crowdsec/decisions')
|
||||
return resp.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Bans an IP address via CrowdSec.
|
||||
* @param ip - The IP address to ban
|
||||
* @param duration - Ban duration (e.g., "24h", "7d")
|
||||
* @param reason - Reason for the ban
|
||||
* @throws {AxiosError} If ban fails
|
||||
*/
|
||||
export async function banIP(ip: string, duration: string, reason: string): Promise<void> {
|
||||
await client.post('/admin/crowdsec/ban', { ip, duration, reason })
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a ban for an IP address.
|
||||
* @param ip - The IP address to unban
|
||||
* @throws {AxiosError} If unban fails
|
||||
*/
|
||||
export async function unbanIP(ip: string): Promise<void> {
|
||||
await client.delete(`/admin/crowdsec/ban/${encodeURIComponent(ip)}`)
|
||||
}
|
||||
|
||||
export default { startCrowdsec, stopCrowdsec, statusCrowdsec, importCrowdsecConfig, exportCrowdsecConfig, listCrowdsecFiles, readCrowdsecFile, writeCrowdsecFile, listCrowdsecDecisions, banIP, unbanIP }
|
||||
@@ -1,40 +0,0 @@
|
||||
import client from './client'
|
||||
import type { DNSProvider } from './dnsProviders'
|
||||
|
||||
/** DNS provider detection result */
|
||||
export interface DetectionResult {
|
||||
domain: string
|
||||
detected: boolean
|
||||
provider_type?: string
|
||||
nameservers: string[]
|
||||
confidence: 'high' | 'medium' | 'low' | 'none'
|
||||
suggested_provider?: DNSProvider
|
||||
error?: string
|
||||
}
|
||||
|
||||
/** Nameserver pattern used for detection */
|
||||
export interface NameserverPattern {
|
||||
pattern: string
|
||||
provider_type: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects DNS provider for a domain by analyzing nameservers.
|
||||
* @param domain - Domain name to detect provider for
|
||||
* @returns Promise resolving to detection result
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export async function detectDNSProvider(domain: string): Promise<DetectionResult> {
|
||||
const response = await client.post<DetectionResult>('/dns-providers/detect', { domain })
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches built-in nameserver patterns used for detection.
|
||||
* @returns Promise resolving to array of patterns
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export async function getDetectionPatterns(): Promise<NameserverPattern[]> {
|
||||
const response = await client.get<{ patterns: NameserverPattern[] }>('/dns-providers/patterns')
|
||||
return response.data.patterns
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
import client from './client'
|
||||
|
||||
/** Supported DNS provider types */
|
||||
export type DNSProviderType =
|
||||
| 'cloudflare'
|
||||
| 'route53'
|
||||
| 'digitalocean'
|
||||
| 'googleclouddns'
|
||||
| 'namecheap'
|
||||
| 'godaddy'
|
||||
| 'azure'
|
||||
| 'hetzner'
|
||||
| 'vultr'
|
||||
| 'dnsimple'
|
||||
// Custom plugin types
|
||||
| 'manual'
|
||||
| 'webhook'
|
||||
| 'rfc2136'
|
||||
| 'script'
|
||||
|
||||
/** Represents a configured DNS provider */
|
||||
export interface DNSProvider {
|
||||
id: number
|
||||
uuid: string
|
||||
name: string
|
||||
provider_type: DNSProviderType
|
||||
enabled: boolean
|
||||
is_default: boolean
|
||||
has_credentials: boolean
|
||||
propagation_timeout: number
|
||||
polling_interval: number
|
||||
last_used_at?: string
|
||||
success_count: number
|
||||
failure_count: number
|
||||
last_error?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/** Request payload for creating/updating DNS providers */
|
||||
export interface DNSProviderRequest {
|
||||
name: string
|
||||
provider_type: DNSProviderType
|
||||
credentials: Record<string, string>
|
||||
propagation_timeout?: number
|
||||
polling_interval?: number
|
||||
is_default?: boolean
|
||||
}
|
||||
|
||||
/** DNS provider test result */
|
||||
export interface DNSTestResult {
|
||||
success: boolean
|
||||
message?: string
|
||||
error?: string
|
||||
code?: string
|
||||
propagation_time_ms?: number
|
||||
}
|
||||
|
||||
/** Field definition for DNS provider credentials */
|
||||
export interface DNSProviderField {
|
||||
name: string
|
||||
label: string
|
||||
type: 'text' | 'password' | 'textarea' | 'select'
|
||||
required: boolean
|
||||
default?: string
|
||||
hint?: string
|
||||
placeholder?: string
|
||||
options?: Array<{
|
||||
value: string
|
||||
label: string
|
||||
}>
|
||||
}
|
||||
|
||||
/** DNS provider type information with field definitions */
|
||||
export interface DNSProviderTypeInfo {
|
||||
type: DNSProviderType
|
||||
name: string
|
||||
description?: string
|
||||
documentation_url?: string
|
||||
is_built_in?: boolean
|
||||
fields: DNSProviderField[]
|
||||
}
|
||||
|
||||
/** Response for list endpoint */
|
||||
interface ListDNSProvidersResponse {
|
||||
providers: DNSProvider[]
|
||||
total: number
|
||||
}
|
||||
|
||||
/** Response for types endpoint */
|
||||
interface DNSProviderTypesResponse {
|
||||
types: DNSProviderTypeInfo[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all configured DNS providers.
|
||||
* @returns Promise resolving to array of DNS providers
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export async function getDNSProviders(): Promise<DNSProvider[]> {
|
||||
const response = await client.get<ListDNSProvidersResponse>('/dns-providers')
|
||||
return response.data.providers
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a single DNS provider by ID.
|
||||
* @param id - The DNS provider ID
|
||||
* @returns Promise resolving to the DNS provider
|
||||
* @throws {AxiosError} If not found or request fails
|
||||
*/
|
||||
export async function getDNSProvider(id: number): Promise<DNSProvider> {
|
||||
const response = await client.get<DNSProvider>(`/dns-providers/${id}`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new DNS provider.
|
||||
* @param data - DNS provider configuration
|
||||
* @returns Promise resolving to the created provider
|
||||
* @throws {AxiosError} If validation fails or request fails
|
||||
*/
|
||||
export async function createDNSProvider(data: DNSProviderRequest): Promise<DNSProvider> {
|
||||
const response = await client.post<DNSProvider>('/dns-providers', data)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing DNS provider.
|
||||
* @param id - The DNS provider ID
|
||||
* @param data - Updated configuration
|
||||
* @returns Promise resolving to the updated provider
|
||||
* @throws {AxiosError} If not found, validation fails, or request fails
|
||||
*/
|
||||
export async function updateDNSProvider(id: number, data: DNSProviderRequest): Promise<DNSProvider> {
|
||||
const response = await client.put<DNSProvider>(`/dns-providers/${id}`, data)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a DNS provider.
|
||||
* @param id - The DNS provider ID
|
||||
* @throws {AxiosError} If not found or in use by proxy hosts
|
||||
*/
|
||||
export async function deleteDNSProvider(id: number): Promise<void> {
|
||||
await client.delete(`/dns-providers/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests connectivity of a saved DNS provider.
|
||||
* @param id - The DNS provider ID
|
||||
* @returns Promise resolving to test result
|
||||
* @throws {AxiosError} If not found or request fails
|
||||
*/
|
||||
export async function testDNSProvider(id: number): Promise<DNSTestResult> {
|
||||
const response = await client.post<DNSTestResult>(`/dns-providers/${id}/test`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests DNS provider credentials before saving.
|
||||
* @param data - Provider configuration to test
|
||||
* @returns Promise resolving to test result
|
||||
* @throws {AxiosError} If validation fails or request fails
|
||||
*/
|
||||
export async function testDNSProviderCredentials(data: DNSProviderRequest): Promise<DNSTestResult> {
|
||||
const response = await client.post<DNSTestResult>('/dns-providers/test', data)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches supported DNS provider types with field definitions.
|
||||
* @returns Promise resolving to array of provider type info
|
||||
* @throws {AxiosError} If request fails
|
||||
*/
|
||||
export async function getDNSProviderTypes(): Promise<DNSProviderTypeInfo[]> {
|
||||
const response = await client.get<DNSProviderTypesResponse>('/dns-providers/types')
|
||||
return response.data.types
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import client from './client'
|
||||
|
||||
/** Docker port mapping information. */
|
||||
export interface DockerPort {
|
||||
private_port: number
|
||||
public_port: number
|
||||
type: string
|
||||
}
|
||||
|
||||
/** Docker container information. */
|
||||
export interface DockerContainer {
|
||||
id: string
|
||||
names: string[]
|
||||
image: string
|
||||
state: string
|
||||
status: string
|
||||
network: string
|
||||
ip: string
|
||||
ports: DockerPort[]
|
||||
}
|
||||
|
||||
/** Docker API client for container operations. */
|
||||
export const dockerApi = {
|
||||
/**
|
||||
* Lists Docker containers from a local or remote host.
|
||||
* @param host - Optional Docker host address
|
||||
* @param serverId - Optional remote server ID
|
||||
* @returns Promise resolving to array of DockerContainer objects
|
||||
* @throws {AxiosError} If listing fails or host unreachable
|
||||
*/
|
||||
listContainers: async (host?: string, serverId?: string): Promise<DockerContainer[]> => {
|
||||
const params: Record<string, string> = {}
|
||||
if (host) params.host = host
|
||||
if (serverId) params.server_id = serverId
|
||||
|
||||
const response = await client.get<DockerContainer[]>('/docker/containers', { params })
|
||||
return response.data
|
||||
},
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import client from './client'
|
||||
|
||||
/** Represents a managed domain. */
|
||||
export interface Domain {
|
||||
id: number
|
||||
uuid: string
|
||||
name: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all managed domains.
|
||||
* @returns Promise resolving to array of Domain objects
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export const getDomains = async (): Promise<Domain[]> => {
|
||||
const { data } = await client.get<Domain[]>('/domains')
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new managed domain.
|
||||
* @param name - The domain name to create
|
||||
* @returns Promise resolving to the created Domain
|
||||
* @throws {AxiosError} If creation fails or domain is invalid
|
||||
*/
|
||||
export const createDomain = async (name: string): Promise<Domain> => {
|
||||
const { data } = await client.post<Domain>('/domains', { name })
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a managed domain.
|
||||
* @param uuid - The unique identifier of the domain to delete
|
||||
* @throws {AxiosError} If deletion fails or domain not found
|
||||
*/
|
||||
export const deleteDomain = async (uuid: string): Promise<void> => {
|
||||
await client.delete(`/domains/${uuid}`)
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import client from './client'
|
||||
|
||||
/** Rotation status for key management */
|
||||
export interface RotationStatus {
|
||||
current_version: number
|
||||
next_key_configured: boolean
|
||||
legacy_key_count: number
|
||||
providers_on_current_version: number
|
||||
providers_on_older_versions: number
|
||||
}
|
||||
|
||||
/** Result of a key rotation operation */
|
||||
export interface RotationResult {
|
||||
total_providers: number
|
||||
success_count: number
|
||||
failure_count: number
|
||||
failed_providers?: number[]
|
||||
duration: string
|
||||
new_key_version: number
|
||||
}
|
||||
|
||||
/** Audit log entry for key rotation history */
|
||||
export interface RotationHistoryEntry {
|
||||
id: number
|
||||
uuid: string
|
||||
actor: string
|
||||
action: string
|
||||
event_category: string
|
||||
details: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
/** Response for history endpoint */
|
||||
interface RotationHistoryResponse {
|
||||
history: RotationHistoryEntry[]
|
||||
total: number
|
||||
}
|
||||
|
||||
/** Validation result for key configuration */
|
||||
export interface KeyValidationResult {
|
||||
valid: boolean
|
||||
message?: string
|
||||
errors?: string[]
|
||||
warnings?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches current encryption key status and rotation information.
|
||||
* @returns Promise resolving to rotation status
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export async function getEncryptionStatus(): Promise<RotationStatus> {
|
||||
const response = await client.get<RotationStatus>('/admin/encryption/status')
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers rotation of all DNS provider credentials to a new encryption key.
|
||||
* @returns Promise resolving to rotation result
|
||||
* @throws {AxiosError} If rotation fails or request fails
|
||||
*/
|
||||
export async function rotateEncryptionKey(): Promise<RotationResult> {
|
||||
const response = await client.post<RotationResult>('/admin/encryption/rotate')
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches key rotation audit history.
|
||||
* @returns Promise resolving to array of rotation history entries
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export async function getRotationHistory(): Promise<RotationHistoryEntry[]> {
|
||||
const response = await client.get<RotationHistoryResponse>('/admin/encryption/history')
|
||||
return response.data.history
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the current key configuration.
|
||||
* @returns Promise resolving to validation result
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export async function validateKeyConfiguration(): Promise<KeyValidationResult> {
|
||||
const response = await client.post<KeyValidationResult>('/admin/encryption/validate')
|
||||
return response.data
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { vi, describe, it, expect } from 'vitest'
|
||||
|
||||
// Mock the client module which is an axios instance wrapper
|
||||
vi.mock('./client', () => ({
|
||||
default: {
|
||||
get: vi.fn(() => Promise.resolve({ data: { 'feature.cerberus.enabled': true } })),
|
||||
put: vi.fn(() => Promise.resolve({ data: { status: 'ok' } })),
|
||||
},
|
||||
}))
|
||||
|
||||
import { getFeatureFlags, updateFeatureFlags } from './featureFlags'
|
||||
import client from './client'
|
||||
|
||||
describe('featureFlags API', () => {
|
||||
it('fetches feature flags', async () => {
|
||||
const flags = await getFeatureFlags()
|
||||
expect(flags['feature.cerberus.enabled']).toBe(true)
|
||||
expect(vi.mocked(client.get)).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('updates feature flags', async () => {
|
||||
const resp = await updateFeatureFlags({ 'feature.cerberus.enabled': false })
|
||||
expect(resp).toEqual({ status: 'ok' })
|
||||
expect(vi.mocked(client.put)).toHaveBeenCalledWith('/feature-flags', { 'feature.cerberus.enabled': false })
|
||||
})
|
||||
})
|
||||
@@ -1,27 +0,0 @@
|
||||
import client from './client'
|
||||
|
||||
/**
|
||||
* Fetches all feature flags and their current states.
|
||||
* @returns Promise resolving to a record of flag names to boolean values
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export async function getFeatureFlags(): Promise<Record<string, boolean>> {
|
||||
const resp = await client.get<Record<string, boolean>>('/feature-flags')
|
||||
return resp.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates one or more feature flags.
|
||||
* @param payload - Record of flag names to new boolean values
|
||||
* @returns Promise resolving to the update result
|
||||
* @throws {AxiosError} If the update fails
|
||||
*/
|
||||
export async function updateFeatureFlags(payload: Record<string, boolean>) {
|
||||
const resp = await client.put('/feature-flags', payload)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
export default {
|
||||
getFeatureFlags,
|
||||
updateFeatureFlags,
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import client from './client';
|
||||
|
||||
/** Health check response with version and build information. */
|
||||
export interface HealthResponse {
|
||||
status: string;
|
||||
service: string;
|
||||
version: string;
|
||||
git_commit: string;
|
||||
build_time: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the health status of the API server.
|
||||
* @returns Promise resolving to HealthResponse with version info
|
||||
* @throws {AxiosError} If the health check fails
|
||||
*/
|
||||
export const checkHealth = async (): Promise<HealthResponse> => {
|
||||
const { data } = await client.get<HealthResponse>('/health');
|
||||
return data;
|
||||
};
|
||||
@@ -1,127 +0,0 @@
|
||||
import client from './client';
|
||||
|
||||
/** Represents an active import session. */
|
||||
export interface ImportSession {
|
||||
id: string;
|
||||
state: 'pending' | 'reviewing' | 'completed' | 'failed' | 'transient';
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
source_file?: string;
|
||||
}
|
||||
|
||||
/** Preview of a Caddyfile import with hosts and conflicts. */
|
||||
export interface ImportPreview {
|
||||
session: ImportSession;
|
||||
preview: {
|
||||
hosts: Array<{ domain_names: string; [key: string]: unknown }>;
|
||||
conflicts: string[];
|
||||
errors: string[];
|
||||
};
|
||||
caddyfile_content?: string;
|
||||
conflict_details?: Record<string, {
|
||||
existing: {
|
||||
forward_scheme: string;
|
||||
forward_host: string;
|
||||
forward_port: number;
|
||||
ssl_forced: boolean;
|
||||
websocket: boolean;
|
||||
enabled: boolean;
|
||||
};
|
||||
imported: {
|
||||
forward_scheme: string;
|
||||
forward_host: string;
|
||||
forward_port: number;
|
||||
ssl_forced: boolean;
|
||||
websocket: boolean;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a Caddyfile content for import preview.
|
||||
* @param content - The Caddyfile content as a string
|
||||
* @returns Promise resolving to ImportPreview with parsed hosts
|
||||
* @throws {AxiosError} If parsing fails or content is invalid
|
||||
*/
|
||||
export const uploadCaddyfile = async (content: string): Promise<ImportPreview> => {
|
||||
const { data } = await client.post<ImportPreview>('/import/upload', { content });
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Uploads multiple Caddyfile contents for batch import.
|
||||
* @param contents - Array of Caddyfile content strings
|
||||
* @returns Promise resolving to combined ImportPreview
|
||||
* @throws {AxiosError} If parsing fails
|
||||
*/
|
||||
export const uploadCaddyfilesMulti = async (contents: string[]): Promise<ImportPreview> => {
|
||||
const { data } = await client.post<ImportPreview>('/import/upload-multi', { contents });
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the current import preview for the active session.
|
||||
* @returns Promise resolving to ImportPreview
|
||||
* @throws {AxiosError} If no active session or request fails
|
||||
*/
|
||||
export const getImportPreview = async (): Promise<ImportPreview> => {
|
||||
const { data } = await client.get<ImportPreview>('/import/preview');
|
||||
return data;
|
||||
};
|
||||
|
||||
/** Result of committing an import operation. */
|
||||
export interface ImportCommitResult {
|
||||
created: number;
|
||||
updated: number;
|
||||
skipped: number;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits the import, creating/updating proxy hosts.
|
||||
* @param sessionUUID - The import session UUID
|
||||
* @param resolutions - Map of conflict resolutions (domain -> 'keep'|'replace'|'skip')
|
||||
* @param names - Map of custom names for imported hosts
|
||||
* @returns Promise resolving to ImportCommitResult with counts
|
||||
* @throws {AxiosError} If commit fails
|
||||
*/
|
||||
export const commitImport = async (
|
||||
sessionUUID: string,
|
||||
resolutions: Record<string, string>,
|
||||
names: Record<string, string>
|
||||
): Promise<ImportCommitResult> => {
|
||||
const { data } = await client.post<ImportCommitResult>('/import/commit', {
|
||||
session_uuid: sessionUUID,
|
||||
resolutions,
|
||||
names,
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancels the current import session.
|
||||
* @throws {AxiosError} If cancellation fails
|
||||
*/
|
||||
export const cancelImport = async (): Promise<void> => {
|
||||
await client.post('/import/cancel');
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the current import session status.
|
||||
* @returns Promise resolving to object with pending status and optional session
|
||||
*/
|
||||
export const getImportStatus = async (): Promise<{ has_pending: boolean; session?: ImportSession }> => {
|
||||
// Note: Assuming there might be a status endpoint or we infer from preview.
|
||||
// If no dedicated status endpoint exists in backend, we might rely on preview returning 404 or empty.
|
||||
// Based on previous context, there wasn't an explicit status endpoint mentioned in the simple API,
|
||||
// but the hook used `importAPI.status()`. I'll check the backend routes if needed.
|
||||
// For now, I'll implement it assuming /import/preview can serve as status check or there is a /import/status.
|
||||
// Let's check the backend routes to be sure.
|
||||
try {
|
||||
const { data } = await client.get<{ has_pending: boolean; session?: ImportSession }>('/import/status');
|
||||
return data;
|
||||
} catch {
|
||||
// Fallback if status endpoint doesn't exist, though the hook used it.
|
||||
return { has_pending: false };
|
||||
}
|
||||
};
|
||||
@@ -1,90 +0,0 @@
|
||||
import client from './client';
|
||||
|
||||
/** Represents a host parsed from a JSON export. */
|
||||
export interface JSONHost {
|
||||
domain_names: string;
|
||||
forward_scheme: string;
|
||||
forward_host: string;
|
||||
forward_port: number;
|
||||
ssl_forced: boolean;
|
||||
websocket_support: boolean;
|
||||
}
|
||||
|
||||
/** Preview of a JSON import with hosts and conflicts. */
|
||||
export interface JSONImportPreview {
|
||||
session: {
|
||||
id: string;
|
||||
state: string;
|
||||
source: string;
|
||||
};
|
||||
preview: {
|
||||
hosts: JSONHost[];
|
||||
conflicts: string[];
|
||||
errors: string[];
|
||||
};
|
||||
conflict_details: Record<string, {
|
||||
existing: {
|
||||
forward_scheme: string;
|
||||
forward_host: string;
|
||||
forward_port: number;
|
||||
ssl_forced: boolean;
|
||||
websocket: boolean;
|
||||
enabled: boolean;
|
||||
};
|
||||
imported: {
|
||||
forward_scheme: string;
|
||||
forward_host: string;
|
||||
forward_port: number;
|
||||
ssl_forced: boolean;
|
||||
websocket: boolean;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
/** Result of committing a JSON import operation. */
|
||||
export interface JSONImportCommitResult {
|
||||
created: number;
|
||||
updated: number;
|
||||
skipped: number;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads JSON export content for import preview.
|
||||
* @param content - The JSON export content as a string
|
||||
* @returns Promise resolving to JSONImportPreview with parsed hosts
|
||||
* @throws {AxiosError} If parsing fails or content is invalid
|
||||
*/
|
||||
export const uploadJSONExport = async (content: string): Promise<JSONImportPreview> => {
|
||||
const { data } = await client.post<JSONImportPreview>('/import/json/upload', { content });
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Commits the JSON import, creating/updating proxy hosts.
|
||||
* @param sessionUuid - The import session UUID
|
||||
* @param resolutions - Map of conflict resolutions (domain -> 'keep'|'replace'|'skip')
|
||||
* @param names - Map of custom names for imported hosts
|
||||
* @returns Promise resolving to JSONImportCommitResult with counts
|
||||
* @throws {AxiosError} If commit fails
|
||||
*/
|
||||
export const commitJSONImport = async (
|
||||
sessionUuid: string,
|
||||
resolutions: Record<string, string>,
|
||||
names: Record<string, string>
|
||||
): Promise<JSONImportCommitResult> => {
|
||||
const { data } = await client.post<JSONImportCommitResult>('/import/json/commit', {
|
||||
session_uuid: sessionUuid,
|
||||
resolutions,
|
||||
names,
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancels the current JSON import session.
|
||||
* @throws {AxiosError} If cancellation fails
|
||||
*/
|
||||
export const cancelJSONImport = async (): Promise<void> => {
|
||||
await client.post('/import/json/cancel');
|
||||
};
|
||||
@@ -1,339 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import client from './client'
|
||||
import { getLogs, getLogContent, downloadLog, connectLiveLogs, connectSecurityLogs } from './logs'
|
||||
import type { LiveLogEntry, SecurityLogEntry } from './logs'
|
||||
|
||||
vi.mock('./client', () => ({
|
||||
default: {
|
||||
get: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
const mockedClient = client as unknown as {
|
||||
get: ReturnType<typeof vi.fn>
|
||||
}
|
||||
|
||||
class MockWebSocket {
|
||||
static CONNECTING = 0
|
||||
static OPEN = 1
|
||||
static CLOSED = 3
|
||||
static instances: MockWebSocket[] = []
|
||||
|
||||
url: string
|
||||
readyState = MockWebSocket.CONNECTING
|
||||
onopen: (() => void) | null = null
|
||||
onmessage: ((event: { data: string }) => void) | null = null
|
||||
onerror: ((event: Event) => void) | null = null
|
||||
onclose: ((event: CloseEvent) => void) | null = null
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url
|
||||
MockWebSocket.instances.push(this)
|
||||
}
|
||||
|
||||
open() {
|
||||
this.readyState = MockWebSocket.OPEN
|
||||
this.onopen?.()
|
||||
}
|
||||
|
||||
sendMessage(data: string) {
|
||||
this.onmessage?.({ data })
|
||||
}
|
||||
|
||||
triggerError(event: Event) {
|
||||
this.onerror?.(event)
|
||||
}
|
||||
|
||||
close() {
|
||||
this.readyState = MockWebSocket.CLOSED
|
||||
this.onclose?.({ code: 1000, reason: '', wasClean: true } as CloseEvent)
|
||||
}
|
||||
}
|
||||
|
||||
const originalWebSocket = globalThis.WebSocket
|
||||
const originalLocation = { ...window.location }
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
;(globalThis as unknown as { WebSocket: typeof WebSocket }).WebSocket = MockWebSocket as unknown as typeof WebSocket
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { ...originalLocation, protocol: 'http:', host: 'localhost', href: '' },
|
||||
writable: true,
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
;(globalThis as unknown as { WebSocket: typeof WebSocket }).WebSocket = originalWebSocket
|
||||
Object.defineProperty(window, 'location', { value: originalLocation })
|
||||
MockWebSocket.instances.length = 0
|
||||
})
|
||||
|
||||
describe('logs api', () => {
|
||||
it('lists log files', async () => {
|
||||
mockedClient.get.mockResolvedValue({ data: [{ name: 'access.log', size: 10, mod_time: 'now' }] })
|
||||
|
||||
const logs = await getLogs()
|
||||
|
||||
expect(mockedClient.get).toHaveBeenCalledWith('/logs')
|
||||
expect(logs[0].name).toBe('access.log')
|
||||
})
|
||||
|
||||
it('fetches log content with filters applied', async () => {
|
||||
mockedClient.get.mockResolvedValue({ data: { filename: 'access.log', logs: [], total: 0, limit: 50, offset: 0 } })
|
||||
|
||||
await getLogContent('access.log', {
|
||||
search: 'error',
|
||||
host: 'example.com',
|
||||
status: '500',
|
||||
level: 'error',
|
||||
limit: 50,
|
||||
offset: 10,
|
||||
sort: 'asc',
|
||||
})
|
||||
|
||||
expect(mockedClient.get).toHaveBeenCalledWith(
|
||||
'/logs/access.log?search=error&host=example.com&status=500&level=error&limit=50&offset=10&sort=asc'
|
||||
)
|
||||
})
|
||||
|
||||
it('sets window location when downloading logs', () => {
|
||||
downloadLog('access.log')
|
||||
expect(window.location.href).toBe('/api/v1/logs/access.log/download')
|
||||
})
|
||||
|
||||
it('connects to live logs websocket and handles lifecycle events', () => {
|
||||
const received: LiveLogEntry[] = []
|
||||
const onOpen = vi.fn()
|
||||
const onError = vi.fn()
|
||||
const onClose = vi.fn()
|
||||
|
||||
const disconnect = connectLiveLogs({ level: 'error', source: 'cerberus' }, (log) => received.push(log), onOpen, onError, onClose)
|
||||
|
||||
const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
|
||||
expect(socket.url).toContain('level=error')
|
||||
expect(socket.url).toContain('source=cerberus')
|
||||
|
||||
socket.open()
|
||||
expect(onOpen).toHaveBeenCalled()
|
||||
|
||||
socket.sendMessage(JSON.stringify({ level: 'info', timestamp: 'now', message: 'hello' }))
|
||||
expect(received).toHaveLength(1)
|
||||
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
socket.sendMessage('not-json')
|
||||
expect(consoleError).toHaveBeenCalled()
|
||||
consoleError.mockRestore()
|
||||
|
||||
const errorEvent = new Event('error')
|
||||
socket.triggerError(errorEvent)
|
||||
expect(onError).toHaveBeenCalledWith(errorEvent)
|
||||
|
||||
socket.close()
|
||||
expect(onClose).toHaveBeenCalled()
|
||||
|
||||
disconnect()
|
||||
})
|
||||
})
|
||||
|
||||
describe('connectSecurityLogs', () => {
|
||||
it('connects to cerberus logs websocket endpoint', () => {
|
||||
const received: SecurityLogEntry[] = []
|
||||
const onOpen = vi.fn()
|
||||
|
||||
connectSecurityLogs({}, (log) => received.push(log), onOpen)
|
||||
|
||||
const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
|
||||
expect(socket.url).toContain('/api/v1/cerberus/logs/ws')
|
||||
})
|
||||
|
||||
it('passes source filter to websocket url', () => {
|
||||
connectSecurityLogs({ source: 'waf' }, () => {})
|
||||
|
||||
const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
|
||||
expect(socket.url).toContain('source=waf')
|
||||
})
|
||||
|
||||
it('passes level filter to websocket url', () => {
|
||||
connectSecurityLogs({ level: 'error' }, () => {})
|
||||
|
||||
const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
|
||||
expect(socket.url).toContain('level=error')
|
||||
})
|
||||
|
||||
it('passes ip filter to websocket url', () => {
|
||||
connectSecurityLogs({ ip: '192.168' }, () => {})
|
||||
|
||||
const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
|
||||
expect(socket.url).toContain('ip=192.168')
|
||||
})
|
||||
|
||||
it('passes host filter to websocket url', () => {
|
||||
connectSecurityLogs({ host: 'example.com' }, () => {})
|
||||
|
||||
const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
|
||||
expect(socket.url).toContain('host=example.com')
|
||||
})
|
||||
|
||||
it('passes blocked_only filter to websocket url', () => {
|
||||
connectSecurityLogs({ blocked_only: true }, () => {})
|
||||
|
||||
const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
|
||||
expect(socket.url).toContain('blocked_only=true')
|
||||
})
|
||||
|
||||
it('receives and parses security log entries', () => {
|
||||
const received: SecurityLogEntry[] = []
|
||||
connectSecurityLogs({}, (log) => received.push(log))
|
||||
|
||||
const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
|
||||
socket.open()
|
||||
|
||||
const securityLogEntry: SecurityLogEntry = {
|
||||
timestamp: '2025-12-12T10:30:00Z',
|
||||
level: 'info',
|
||||
logger: 'http.log.access',
|
||||
client_ip: '192.168.1.100',
|
||||
method: 'GET',
|
||||
uri: '/api/test',
|
||||
status: 200,
|
||||
duration: 0.05,
|
||||
size: 1024,
|
||||
user_agent: 'TestAgent/1.0',
|
||||
host: 'example.com',
|
||||
source: 'normal',
|
||||
blocked: false,
|
||||
}
|
||||
|
||||
socket.sendMessage(JSON.stringify(securityLogEntry))
|
||||
|
||||
expect(received).toHaveLength(1)
|
||||
expect(received[0].client_ip).toBe('192.168.1.100')
|
||||
expect(received[0].source).toBe('normal')
|
||||
expect(received[0].blocked).toBe(false)
|
||||
})
|
||||
|
||||
it('receives blocked security log entries', () => {
|
||||
const received: SecurityLogEntry[] = []
|
||||
connectSecurityLogs({}, (log) => received.push(log))
|
||||
|
||||
const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
|
||||
socket.open()
|
||||
|
||||
const blockedEntry: SecurityLogEntry = {
|
||||
timestamp: '2025-12-12T10:30:00Z',
|
||||
level: 'warn',
|
||||
logger: 'http.handlers.waf',
|
||||
client_ip: '10.0.0.1',
|
||||
method: 'POST',
|
||||
uri: '/admin',
|
||||
status: 403,
|
||||
duration: 0.001,
|
||||
size: 0,
|
||||
user_agent: 'Attack/1.0',
|
||||
host: 'example.com',
|
||||
source: 'waf',
|
||||
blocked: true,
|
||||
block_reason: 'SQL injection detected',
|
||||
}
|
||||
|
||||
socket.sendMessage(JSON.stringify(blockedEntry))
|
||||
|
||||
expect(received).toHaveLength(1)
|
||||
expect(received[0].blocked).toBe(true)
|
||||
expect(received[0].block_reason).toBe('SQL injection detected')
|
||||
expect(received[0].source).toBe('waf')
|
||||
})
|
||||
|
||||
it('handles onOpen callback', () => {
|
||||
const onOpen = vi.fn()
|
||||
connectSecurityLogs({}, () => {}, onOpen)
|
||||
|
||||
const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
|
||||
socket.open()
|
||||
|
||||
expect(onOpen).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('handles onError callback', () => {
|
||||
const onError = vi.fn()
|
||||
connectSecurityLogs({}, () => {}, undefined, onError)
|
||||
|
||||
const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
|
||||
const errorEvent = new Event('error')
|
||||
socket.triggerError(errorEvent)
|
||||
|
||||
expect(onError).toHaveBeenCalledWith(errorEvent)
|
||||
})
|
||||
|
||||
it('handles onClose callback', () => {
|
||||
const onClose = vi.fn()
|
||||
connectSecurityLogs({}, () => {}, undefined, undefined, onClose)
|
||||
|
||||
const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
|
||||
socket.close()
|
||||
|
||||
expect(onClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('returns disconnect function that closes websocket', () => {
|
||||
const disconnect = connectSecurityLogs({}, () => {})
|
||||
|
||||
const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
|
||||
socket.open()
|
||||
|
||||
expect(socket.readyState).toBe(MockWebSocket.OPEN)
|
||||
|
||||
disconnect()
|
||||
|
||||
expect(socket.readyState).toBe(MockWebSocket.CLOSED)
|
||||
})
|
||||
|
||||
it('handles JSON parse errors gracefully', () => {
|
||||
const received: SecurityLogEntry[] = []
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
connectSecurityLogs({}, (log) => received.push(log))
|
||||
|
||||
const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
|
||||
socket.open()
|
||||
socket.sendMessage('invalid-json')
|
||||
|
||||
expect(received).toHaveLength(0)
|
||||
expect(consoleError).toHaveBeenCalled()
|
||||
|
||||
consoleError.mockRestore()
|
||||
})
|
||||
|
||||
it('uses wss protocol when on https', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { protocol: 'https:', host: 'secure.example.com', href: '' },
|
||||
writable: true,
|
||||
})
|
||||
|
||||
connectSecurityLogs({}, () => {})
|
||||
|
||||
const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
|
||||
expect(socket.url).toContain('wss://')
|
||||
expect(socket.url).toContain('secure.example.com')
|
||||
})
|
||||
|
||||
it('combines multiple filters in websocket url', () => {
|
||||
connectSecurityLogs(
|
||||
{
|
||||
source: 'waf',
|
||||
level: 'warn',
|
||||
ip: '10.0.0',
|
||||
host: 'example.com',
|
||||
blocked_only: true,
|
||||
},
|
||||
() => {}
|
||||
)
|
||||
|
||||
const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
|
||||
expect(socket.url).toContain('source=waf')
|
||||
expect(socket.url).toContain('level=warn')
|
||||
expect(socket.url).toContain('ip=10.0.0')
|
||||
expect(socket.url).toContain('host=example.com')
|
||||
expect(socket.url).toContain('blocked_only=true')
|
||||
})
|
||||
})
|
||||
@@ -1,262 +0,0 @@
|
||||
import client from './client';
|
||||
|
||||
/** Represents a log file on the server. */
|
||||
export interface LogFile {
|
||||
name: string;
|
||||
size: number;
|
||||
mod_time: string;
|
||||
}
|
||||
|
||||
/** Parsed Caddy access log entry. */
|
||||
export interface CaddyAccessLog {
|
||||
level: string;
|
||||
ts: number;
|
||||
logger: string;
|
||||
msg: string;
|
||||
request: {
|
||||
remote_ip: string;
|
||||
method: string;
|
||||
host: string;
|
||||
uri: string;
|
||||
proto: string;
|
||||
};
|
||||
status: number;
|
||||
duration: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
/** Paginated log response. */
|
||||
export interface LogResponse {
|
||||
filename: string;
|
||||
logs: CaddyAccessLog[];
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
/** Filter options for log queries. */
|
||||
export interface LogFilter {
|
||||
search?: string;
|
||||
host?: string;
|
||||
status?: string;
|
||||
level?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
sort?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the list of available log files.
|
||||
* @returns Promise resolving to array of LogFile objects
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export const getLogs = async (): Promise<LogFile[]> => {
|
||||
const response = await client.get<LogFile[]>('/logs');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches paginated and filtered log entries from a specific file.
|
||||
* @param filename - The log file name to read
|
||||
* @param filter - Optional filter and pagination options
|
||||
* @returns Promise resolving to LogResponse with entries and metadata
|
||||
* @throws {AxiosError} If the request fails or file not found
|
||||
*/
|
||||
export const getLogContent = async (filename: string, filter: LogFilter = {}): Promise<LogResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
if (filter.search) params.append('search', filter.search);
|
||||
if (filter.host) params.append('host', filter.host);
|
||||
if (filter.status) params.append('status', filter.status);
|
||||
if (filter.level) params.append('level', filter.level);
|
||||
if (filter.limit) params.append('limit', filter.limit.toString());
|
||||
if (filter.offset) params.append('offset', filter.offset.toString());
|
||||
if (filter.sort) params.append('sort', filter.sort);
|
||||
|
||||
const response = await client.get<LogResponse>(`/logs/${filename}?${params.toString()}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initiates a log file download by redirecting the browser.
|
||||
* @param filename - The log file name to download
|
||||
*/
|
||||
export const downloadLog = (filename: string) => {
|
||||
// Direct window location change to trigger download
|
||||
// We need to use the base URL from the client config if possible,
|
||||
// but for now we assume relative path works with the proxy setup
|
||||
window.location.href = `/api/v1/logs/${filename}/download`;
|
||||
};
|
||||
|
||||
/** Live log entry from WebSocket stream. */
|
||||
export interface LiveLogEntry {
|
||||
level: string;
|
||||
timestamp: string;
|
||||
message: string;
|
||||
source?: string;
|
||||
data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** Filter options for live log streaming. */
|
||||
export interface LiveLogFilter {
|
||||
level?: string;
|
||||
source?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* SecurityLogEntry represents a security-relevant log entry from Cerberus.
|
||||
* This matches the backend SecurityLogEntry struct from /api/v1/cerberus/logs/ws
|
||||
*/
|
||||
export interface SecurityLogEntry {
|
||||
timestamp: string;
|
||||
level: string;
|
||||
logger: string;
|
||||
client_ip: string;
|
||||
method: string;
|
||||
uri: string;
|
||||
status: number;
|
||||
duration: number;
|
||||
size: number;
|
||||
user_agent: string;
|
||||
host: string;
|
||||
source: 'waf' | 'crowdsec' | 'ratelimit' | 'acl' | 'normal';
|
||||
blocked: boolean;
|
||||
block_reason?: string;
|
||||
details?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters for the Cerberus security logs WebSocket endpoint.
|
||||
*/
|
||||
export interface SecurityLogFilter {
|
||||
source?: string; // Filter by security module: waf, crowdsec, ratelimit, acl, normal
|
||||
level?: string; // Filter by log level: info, warn, error
|
||||
ip?: string; // Filter by client IP (partial match)
|
||||
host?: string; // Filter by host (partial match)
|
||||
blocked_only?: boolean; // Only show blocked requests
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to the live logs WebSocket endpoint for real-time log streaming.
|
||||
* Returns a cleanup function to close the connection.
|
||||
* @param filters - LiveLogFilter options for level and source filtering
|
||||
* @param onMessage - Callback invoked for each received LiveLogEntry
|
||||
* @param onOpen - Optional callback when WebSocket connection is established
|
||||
* @param onError - Optional callback on WebSocket error
|
||||
* @param onClose - Optional callback when WebSocket connection closes
|
||||
* @returns Function to close the WebSocket connection
|
||||
*/
|
||||
export const connectLiveLogs = (
|
||||
filters: LiveLogFilter,
|
||||
onMessage: (log: LiveLogEntry) => void,
|
||||
onOpen?: () => void,
|
||||
onError?: (error: Event) => void,
|
||||
onClose?: () => void
|
||||
): (() => void) => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters.level) params.append('level', filters.level);
|
||||
if (filters.source) params.append('source', filters.source);
|
||||
|
||||
// Authentication is handled via HttpOnly cookies sent automatically by the browser
|
||||
// This prevents tokens from being logged in access logs or exposed to XSS attacks
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/api/v1/logs/live?${params.toString()}`;
|
||||
|
||||
console.log('Connecting to WebSocket:', wsUrl);
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket connection established');
|
||||
onOpen?.();
|
||||
};
|
||||
|
||||
ws.onmessage = (event: MessageEvent) => {
|
||||
try {
|
||||
const log = JSON.parse(event.data) as LiveLogEntry;
|
||||
onMessage(log);
|
||||
} catch (err) {
|
||||
console.error('Failed to parse log message:', err);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error: Event) => {
|
||||
console.error('WebSocket error:', error);
|
||||
onError?.(error);
|
||||
};
|
||||
|
||||
ws.onclose = (event: CloseEvent) => {
|
||||
console.log('WebSocket connection closed', { code: event.code, reason: event.reason, wasClean: event.wasClean });
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
return () => {
|
||||
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
||||
ws.close();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Connects to the Cerberus security logs WebSocket endpoint.
|
||||
* This streams parsed Caddy access logs with security event annotations.
|
||||
*
|
||||
* @param filters - Optional filters for source, level, IP, host, and blocked_only
|
||||
* @param onMessage - Callback for each received SecurityLogEntry
|
||||
* @param onOpen - Callback when connection is established
|
||||
* @param onError - Callback on connection error
|
||||
* @param onClose - Callback when connection closes
|
||||
* @returns A function to close the WebSocket connection
|
||||
*/
|
||||
export const connectSecurityLogs = (
|
||||
filters: SecurityLogFilter,
|
||||
onMessage: (log: SecurityLogEntry) => void,
|
||||
onOpen?: () => void,
|
||||
onError?: (error: Event) => void,
|
||||
onClose?: () => void
|
||||
): (() => void) => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters.source) params.append('source', filters.source);
|
||||
if (filters.level) params.append('level', filters.level);
|
||||
if (filters.ip) params.append('ip', filters.ip);
|
||||
if (filters.host) params.append('host', filters.host);
|
||||
if (filters.blocked_only) params.append('blocked_only', 'true');
|
||||
|
||||
// Authentication is handled via HttpOnly cookies sent automatically by the browser
|
||||
// This prevents tokens from being logged in access logs or exposed to XSS attacks
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/api/v1/cerberus/logs/ws?${params.toString()}`;
|
||||
|
||||
console.log('Connecting to Cerberus logs WebSocket:', wsUrl);
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('Cerberus logs WebSocket connection established');
|
||||
onOpen?.();
|
||||
};
|
||||
|
||||
ws.onmessage = (event: MessageEvent) => {
|
||||
try {
|
||||
const log = JSON.parse(event.data) as SecurityLogEntry;
|
||||
onMessage(log);
|
||||
} catch (err) {
|
||||
console.error('Failed to parse security log message:', err);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error: Event) => {
|
||||
console.error('Cerberus logs WebSocket error:', error);
|
||||
onError?.(error);
|
||||
};
|
||||
|
||||
ws.onclose = (event: CloseEvent) => {
|
||||
console.log('Cerberus logs WebSocket closed', { code: event.code, reason: event.reason, wasClean: event.wasClean });
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
return () => {
|
||||
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
||||
ws.close();
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,115 +0,0 @@
|
||||
import client from './client'
|
||||
|
||||
/** Status of a manual DNS challenge */
|
||||
export type ChallengeStatus = 'created' | 'pending' | 'verifying' | 'verified' | 'expired' | 'failed'
|
||||
|
||||
/** Manual DNS challenge response from API */
|
||||
export interface ManualChallenge {
|
||||
id: string
|
||||
status: ChallengeStatus
|
||||
fqdn: string
|
||||
value: string
|
||||
ttl: number
|
||||
created_at: string
|
||||
expires_at: string
|
||||
last_check_at?: string
|
||||
dns_propagated: boolean
|
||||
error_message?: string
|
||||
}
|
||||
|
||||
/** Polling response for challenge status */
|
||||
export interface ChallengePollResponse {
|
||||
status: ChallengeStatus
|
||||
dns_propagated: boolean
|
||||
time_remaining_seconds: number
|
||||
last_check_at: string
|
||||
error_message?: string
|
||||
}
|
||||
|
||||
/** Challenge verification result */
|
||||
export interface ChallengeVerifyResponse {
|
||||
success: boolean
|
||||
dns_found: boolean
|
||||
message: string
|
||||
}
|
||||
|
||||
/** Request to create a new manual challenge */
|
||||
export interface CreateChallengeRequest {
|
||||
domain: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a manual challenge by ID.
|
||||
* @param providerId - The DNS provider ID
|
||||
* @param challengeId - The challenge UUID
|
||||
* @returns Promise resolving to the challenge details
|
||||
* @throws {AxiosError} If not found or request fails
|
||||
*/
|
||||
export async function getChallenge(providerId: number, challengeId: string): Promise<ManualChallenge> {
|
||||
const response = await client.get<ManualChallenge>(
|
||||
`/dns-providers/${providerId}/manual-challenge/${challengeId}`
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new manual DNS challenge.
|
||||
* @param providerId - The DNS provider ID
|
||||
* @param data - Challenge creation data
|
||||
* @returns Promise resolving to the created challenge
|
||||
* @throws {AxiosError} If validation fails or request fails
|
||||
*/
|
||||
export async function createChallenge(
|
||||
providerId: number,
|
||||
data: CreateChallengeRequest
|
||||
): Promise<ManualChallenge> {
|
||||
const response = await client.post<ManualChallenge>(
|
||||
`/dns-providers/${providerId}/manual-challenge`,
|
||||
data
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers verification of a manual challenge.
|
||||
* @param providerId - The DNS provider ID
|
||||
* @param challengeId - The challenge UUID
|
||||
* @returns Promise resolving to verification result
|
||||
* @throws {AxiosError} If not found or request fails
|
||||
*/
|
||||
export async function verifyChallenge(
|
||||
providerId: number,
|
||||
challengeId: string
|
||||
): Promise<ChallengeVerifyResponse> {
|
||||
const response = await client.post<ChallengeVerifyResponse>(
|
||||
`/dns-providers/${providerId}/manual-challenge/${challengeId}/verify`
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Polls for challenge status updates.
|
||||
* @param providerId - The DNS provider ID
|
||||
* @param challengeId - The challenge UUID
|
||||
* @returns Promise resolving to poll response
|
||||
* @throws {AxiosError} If not found or request fails
|
||||
*/
|
||||
export async function pollChallenge(
|
||||
providerId: number,
|
||||
challengeId: string
|
||||
): Promise<ChallengePollResponse> {
|
||||
const response = await client.get<ChallengePollResponse>(
|
||||
`/dns-providers/${providerId}/manual-challenge/${challengeId}/poll`
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes/cancels a manual challenge.
|
||||
* @param providerId - The DNS provider ID
|
||||
* @param challengeId - The challenge UUID
|
||||
* @throws {AxiosError} If not found or request fails
|
||||
*/
|
||||
export async function deleteChallenge(providerId: number, challengeId: string): Promise<void> {
|
||||
await client.delete(`/dns-providers/${providerId}/manual-challenge/${challengeId}`)
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import client from './client'
|
||||
import {
|
||||
getProviders,
|
||||
createProvider,
|
||||
updateProvider,
|
||||
deleteProvider,
|
||||
testProvider,
|
||||
getTemplates,
|
||||
previewProvider,
|
||||
getExternalTemplates,
|
||||
createExternalTemplate,
|
||||
updateExternalTemplate,
|
||||
deleteExternalTemplate,
|
||||
previewExternalTemplate,
|
||||
getSecurityNotificationSettings,
|
||||
updateSecurityNotificationSettings,
|
||||
} from './notifications'
|
||||
|
||||
vi.mock('./client', () => ({
|
||||
default: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
const mockedClient = client as unknown as {
|
||||
get: ReturnType<typeof vi.fn>
|
||||
post: ReturnType<typeof vi.fn>
|
||||
put: ReturnType<typeof vi.fn>
|
||||
delete: ReturnType<typeof vi.fn>
|
||||
}
|
||||
|
||||
describe('notifications api', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('fetches providers list', async () => {
|
||||
mockedClient.get.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'PagerDuty',
|
||||
type: 'webhook',
|
||||
url: 'https://hooks.example.com',
|
||||
enabled: true,
|
||||
notify_proxy_hosts: true,
|
||||
notify_remote_servers: false,
|
||||
notify_domains: false,
|
||||
notify_certs: false,
|
||||
notify_uptime: true,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const result = await getProviders()
|
||||
|
||||
expect(mockedClient.get).toHaveBeenCalledWith('/notifications/providers')
|
||||
expect(result[0].name).toBe('PagerDuty')
|
||||
})
|
||||
|
||||
it('creates, updates, tests, and deletes a provider', async () => {
|
||||
mockedClient.post.mockResolvedValue({ data: { id: 'new', name: 'Slack' } })
|
||||
mockedClient.put.mockResolvedValue({ data: { id: 'new', name: 'Slack v2' } })
|
||||
|
||||
const created = await createProvider({ name: 'Slack' })
|
||||
expect(mockedClient.post).toHaveBeenCalledWith('/notifications/providers', { name: 'Slack' })
|
||||
expect(created.id).toBe('new')
|
||||
|
||||
const updated = await updateProvider('new', { enabled: false })
|
||||
expect(mockedClient.put).toHaveBeenCalledWith('/notifications/providers/new', { enabled: false })
|
||||
expect(updated.name).toBe('Slack v2')
|
||||
|
||||
await testProvider({ id: 'new', name: 'Slack', enabled: true })
|
||||
expect(mockedClient.post).toHaveBeenCalledWith('/notifications/providers/test', {
|
||||
id: 'new',
|
||||
name: 'Slack',
|
||||
enabled: true,
|
||||
})
|
||||
|
||||
mockedClient.delete.mockResolvedValue({})
|
||||
await deleteProvider('new')
|
||||
expect(mockedClient.delete).toHaveBeenCalledWith('/notifications/providers/new')
|
||||
})
|
||||
|
||||
it('fetches templates and previews provider payloads with data', async () => {
|
||||
mockedClient.get.mockResolvedValueOnce({ data: [{ id: 'tpl', name: 'default' }] })
|
||||
mockedClient.post.mockResolvedValue({ data: { preview: 'ok' } })
|
||||
|
||||
const templates = await getTemplates()
|
||||
expect(mockedClient.get).toHaveBeenCalledWith('/notifications/templates')
|
||||
expect(templates[0].id).toBe('tpl')
|
||||
|
||||
const preview = await previewProvider({ id: 'p1', name: 'Provider' }, { foo: 'bar' })
|
||||
expect(mockedClient.post).toHaveBeenCalledWith('/notifications/providers/preview', {
|
||||
id: 'p1',
|
||||
name: 'Provider',
|
||||
data: { foo: 'bar' },
|
||||
})
|
||||
expect(preview).toEqual({ preview: 'ok' })
|
||||
})
|
||||
|
||||
it('handles external templates lifecycle and previews', async () => {
|
||||
mockedClient.get.mockResolvedValueOnce({ data: [{ id: 'ext', name: 'External' }] })
|
||||
mockedClient.post.mockResolvedValueOnce({ data: { id: 'ext', name: 'created' } })
|
||||
mockedClient.put.mockResolvedValueOnce({ data: { id: 'ext', name: 'updated' } })
|
||||
mockedClient.post.mockResolvedValueOnce({ data: { preview: 'rendered' } })
|
||||
|
||||
const list = await getExternalTemplates()
|
||||
expect(mockedClient.get).toHaveBeenCalledWith('/notifications/external-templates')
|
||||
expect(list[0].id).toBe('ext')
|
||||
|
||||
const created = await createExternalTemplate({ name: 'External' })
|
||||
expect(mockedClient.post).toHaveBeenCalledWith('/notifications/external-templates', { name: 'External' })
|
||||
expect(created.name).toBe('created')
|
||||
|
||||
const updated = await updateExternalTemplate('ext', { description: 'desc' })
|
||||
expect(mockedClient.put).toHaveBeenCalledWith('/notifications/external-templates/ext', { description: 'desc' })
|
||||
expect(updated.name).toBe('updated')
|
||||
|
||||
await deleteExternalTemplate('ext')
|
||||
expect(mockedClient.delete).toHaveBeenCalledWith('/notifications/external-templates/ext')
|
||||
|
||||
const preview = await previewExternalTemplate('ext', '<tpl>', { a: 1 })
|
||||
expect(mockedClient.post).toHaveBeenCalledWith('/notifications/external-templates/preview', {
|
||||
template_id: 'ext',
|
||||
template: '<tpl>',
|
||||
data: { a: 1 },
|
||||
})
|
||||
expect(preview).toEqual({ preview: 'rendered' })
|
||||
})
|
||||
|
||||
it('reads and updates security notification settings', async () => {
|
||||
mockedClient.get.mockResolvedValueOnce({ data: { enabled: true, min_log_level: 'info', notify_waf_blocks: true, notify_acl_denials: false, notify_rate_limit_hits: true } })
|
||||
mockedClient.put.mockResolvedValueOnce({ data: { enabled: false, min_log_level: 'error', notify_waf_blocks: false, notify_acl_denials: true, notify_rate_limit_hits: false } })
|
||||
|
||||
const settings = await getSecurityNotificationSettings()
|
||||
expect(settings.enabled).toBe(true)
|
||||
expect(mockedClient.get).toHaveBeenCalledWith('/notifications/settings/security')
|
||||
|
||||
const updated = await updateSecurityNotificationSettings({ enabled: false, min_log_level: 'error' })
|
||||
expect(mockedClient.put).toHaveBeenCalledWith('/notifications/settings/security', { enabled: false, min_log_level: 'error' })
|
||||
expect(updated.enabled).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,204 +0,0 @@
|
||||
import client from './client';
|
||||
|
||||
/** Notification provider configuration. */
|
||||
export interface NotificationProvider {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
url: string;
|
||||
config?: string;
|
||||
template?: string;
|
||||
enabled: boolean;
|
||||
notify_proxy_hosts: boolean;
|
||||
notify_remote_servers: boolean;
|
||||
notify_domains: boolean;
|
||||
notify_certs: boolean;
|
||||
notify_uptime: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all notification providers.
|
||||
* @returns Promise resolving to array of NotificationProvider objects
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export const getProviders = async () => {
|
||||
const response = await client.get<NotificationProvider[]>('/notifications/providers');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new notification provider.
|
||||
* @param data - Partial NotificationProvider configuration
|
||||
* @returns Promise resolving to the created NotificationProvider
|
||||
* @throws {AxiosError} If creation fails
|
||||
*/
|
||||
export const createProvider = async (data: Partial<NotificationProvider>) => {
|
||||
const response = await client.post<NotificationProvider>('/notifications/providers', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates an existing notification provider.
|
||||
* @param id - The provider ID to update
|
||||
* @param data - Partial NotificationProvider with fields to update
|
||||
* @returns Promise resolving to the updated NotificationProvider
|
||||
* @throws {AxiosError} If update fails or provider not found
|
||||
*/
|
||||
export const updateProvider = async (id: string, data: Partial<NotificationProvider>) => {
|
||||
const response = await client.put<NotificationProvider>(`/notifications/providers/${id}`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes a notification provider.
|
||||
* @param id - The provider ID to delete
|
||||
* @throws {AxiosError} If deletion fails or provider not found
|
||||
*/
|
||||
export const deleteProvider = async (id: string) => {
|
||||
await client.delete(`/notifications/providers/${id}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests a notification provider by sending a test message.
|
||||
* @param provider - Provider configuration to test
|
||||
* @throws {AxiosError} If test fails
|
||||
*/
|
||||
export const testProvider = async (provider: Partial<NotificationProvider>) => {
|
||||
await client.post('/notifications/providers/test', provider);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches all available notification templates.
|
||||
* @returns Promise resolving to array of NotificationTemplate objects
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export const getTemplates = async () => {
|
||||
const response = await client.get<NotificationTemplate[]>('/notifications/templates');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/** Notification template definition. */
|
||||
export interface NotificationTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Previews a notification with sample data.
|
||||
* @param provider - Provider configuration for preview
|
||||
* @param data - Optional sample data for template rendering
|
||||
* @returns Promise resolving to preview result
|
||||
* @throws {AxiosError} If preview fails
|
||||
*/
|
||||
export const previewProvider = async (provider: Partial<NotificationProvider>, data?: Record<string, unknown>) => {
|
||||
const payload: Record<string, unknown> = { ...provider } as Record<string, unknown>;
|
||||
if (data) payload.data = data;
|
||||
const response = await client.post('/notifications/providers/preview', payload);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// External (saved) templates API
|
||||
/** External notification template configuration. */
|
||||
export interface ExternalTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
config?: string;
|
||||
template?: string;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all external notification templates.
|
||||
* @returns Promise resolving to array of ExternalTemplate objects
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export const getExternalTemplates = async () => {
|
||||
const response = await client.get<ExternalTemplate[]>('/notifications/external-templates');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new external notification template.
|
||||
* @param data - Partial ExternalTemplate configuration
|
||||
* @returns Promise resolving to the created ExternalTemplate
|
||||
* @throws {AxiosError} If creation fails
|
||||
*/
|
||||
export const createExternalTemplate = async (data: Partial<ExternalTemplate>) => {
|
||||
const response = await client.post<ExternalTemplate>('/notifications/external-templates', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates an existing external notification template.
|
||||
* @param id - The template ID to update
|
||||
* @param data - Partial ExternalTemplate with fields to update
|
||||
* @returns Promise resolving to the updated ExternalTemplate
|
||||
* @throws {AxiosError} If update fails or template not found
|
||||
*/
|
||||
export const updateExternalTemplate = async (id: string, data: Partial<ExternalTemplate>) => {
|
||||
const response = await client.put<ExternalTemplate>(`/notifications/external-templates/${id}`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes an external notification template.
|
||||
* @param id - The template ID to delete
|
||||
* @throws {AxiosError} If deletion fails or template not found
|
||||
*/
|
||||
export const deleteExternalTemplate = async (id: string) => {
|
||||
await client.delete(`/notifications/external-templates/${id}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Previews an external template with sample data.
|
||||
* @param templateId - Optional existing template ID to preview
|
||||
* @param template - Optional template content string
|
||||
* @param data - Optional sample data for rendering
|
||||
* @returns Promise resolving to preview result
|
||||
* @throws {AxiosError} If preview fails
|
||||
*/
|
||||
export const previewExternalTemplate = async (templateId?: string, template?: string, data?: Record<string, unknown>) => {
|
||||
const payload: Record<string, unknown> = {};
|
||||
if (templateId) payload.template_id = templateId;
|
||||
if (template) payload.template = template;
|
||||
if (data) payload.data = data;
|
||||
const response = await client.post('/notifications/external-templates/preview', payload);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// Security Notification Settings
|
||||
/** Security notification configuration. */
|
||||
export interface SecurityNotificationSettings {
|
||||
enabled: boolean;
|
||||
min_log_level: string;
|
||||
notify_waf_blocks: boolean;
|
||||
notify_acl_denials: boolean;
|
||||
notify_rate_limit_hits: boolean;
|
||||
webhook_url?: string;
|
||||
email_recipients?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches security notification settings.
|
||||
* @returns Promise resolving to SecurityNotificationSettings
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export const getSecurityNotificationSettings = async (): Promise<SecurityNotificationSettings> => {
|
||||
const response = await client.get<SecurityNotificationSettings>('/notifications/settings/security');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates security notification settings.
|
||||
* @param settings - Partial settings to update
|
||||
* @returns Promise resolving to the updated SecurityNotificationSettings
|
||||
* @throws {AxiosError} If update fails
|
||||
*/
|
||||
export const updateSecurityNotificationSettings = async (
|
||||
settings: Partial<SecurityNotificationSettings>
|
||||
): Promise<SecurityNotificationSettings> => {
|
||||
const response = await client.put<SecurityNotificationSettings>('/notifications/settings/security', settings);
|
||||
return response.data;
|
||||
};
|
||||
@@ -1,90 +0,0 @@
|
||||
import client from './client';
|
||||
|
||||
/** Represents a host parsed from an NPM export. */
|
||||
export interface NPMHost {
|
||||
domain_names: string;
|
||||
forward_scheme: string;
|
||||
forward_host: string;
|
||||
forward_port: number;
|
||||
ssl_forced: boolean;
|
||||
websocket_support: boolean;
|
||||
}
|
||||
|
||||
/** Preview of an NPM import with hosts and conflicts. */
|
||||
export interface NPMImportPreview {
|
||||
session: {
|
||||
id: string;
|
||||
state: string;
|
||||
source: string;
|
||||
};
|
||||
preview: {
|
||||
hosts: NPMHost[];
|
||||
conflicts: string[];
|
||||
errors: string[];
|
||||
};
|
||||
conflict_details: Record<string, {
|
||||
existing: {
|
||||
forward_scheme: string;
|
||||
forward_host: string;
|
||||
forward_port: number;
|
||||
ssl_forced: boolean;
|
||||
websocket: boolean;
|
||||
enabled: boolean;
|
||||
};
|
||||
imported: {
|
||||
forward_scheme: string;
|
||||
forward_host: string;
|
||||
forward_port: number;
|
||||
ssl_forced: boolean;
|
||||
websocket: boolean;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
/** Result of committing an NPM import operation. */
|
||||
export interface NPMImportCommitResult {
|
||||
created: number;
|
||||
updated: number;
|
||||
skipped: number;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads NPM export content for import preview.
|
||||
* @param content - The NPM export JSON content as a string
|
||||
* @returns Promise resolving to NPMImportPreview with parsed hosts
|
||||
* @throws {AxiosError} If parsing fails or content is invalid
|
||||
*/
|
||||
export const uploadNPMExport = async (content: string): Promise<NPMImportPreview> => {
|
||||
const { data } = await client.post<NPMImportPreview>('/import/npm/upload', { content });
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Commits the NPM import, creating/updating proxy hosts.
|
||||
* @param sessionUuid - The import session UUID
|
||||
* @param resolutions - Map of conflict resolutions (domain -> 'keep'|'replace'|'skip')
|
||||
* @param names - Map of custom names for imported hosts
|
||||
* @returns Promise resolving to NPMImportCommitResult with counts
|
||||
* @throws {AxiosError} If commit fails
|
||||
*/
|
||||
export const commitNPMImport = async (
|
||||
sessionUuid: string,
|
||||
resolutions: Record<string, string>,
|
||||
names: Record<string, string>
|
||||
): Promise<NPMImportCommitResult> => {
|
||||
const { data } = await client.post<NPMImportCommitResult>('/import/npm/commit', {
|
||||
session_uuid: sessionUuid,
|
||||
resolutions,
|
||||
names,
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancels the current NPM import session.
|
||||
* @throws {AxiosError} If cancellation fails
|
||||
*/
|
||||
export const cancelNPMImport = async (): Promise<void> => {
|
||||
await client.post('/import/npm/cancel');
|
||||
};
|
||||
@@ -1,109 +0,0 @@
|
||||
import client from './client'
|
||||
|
||||
/** Plugin status types */
|
||||
export type PluginStatus = 'pending' | 'loaded' | 'error'
|
||||
|
||||
/** Plugin information */
|
||||
export interface PluginInfo {
|
||||
id: number
|
||||
uuid: string
|
||||
name: string
|
||||
type: string
|
||||
enabled: boolean
|
||||
status: PluginStatus
|
||||
error?: string
|
||||
version?: string
|
||||
author?: string
|
||||
is_built_in: boolean
|
||||
description?: string
|
||||
documentation_url?: string
|
||||
loaded_at?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/** Credential field specification */
|
||||
export interface CredentialFieldSpec {
|
||||
name: string
|
||||
label: string
|
||||
type: 'text' | 'password' | 'textarea' | 'select'
|
||||
placeholder?: string
|
||||
hint?: string
|
||||
required?: boolean
|
||||
options?: Array<{
|
||||
value: string
|
||||
label: string
|
||||
}>
|
||||
}
|
||||
|
||||
/** Provider metadata response */
|
||||
export interface ProviderFieldsResponse {
|
||||
type: string
|
||||
name: string
|
||||
required_fields: CredentialFieldSpec[]
|
||||
optional_fields: CredentialFieldSpec[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all plugins (built-in and external).
|
||||
* @returns Promise resolving to array of plugin info
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export async function getPlugins(): Promise<PluginInfo[]> {
|
||||
const response = await client.get<PluginInfo[]>('/admin/plugins')
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a single plugin by ID.
|
||||
* @param id - The plugin ID
|
||||
* @returns Promise resolving to the plugin info
|
||||
* @throws {AxiosError} If not found or request fails
|
||||
*/
|
||||
export async function getPlugin(id: number): Promise<PluginInfo> {
|
||||
const response = await client.get<PluginInfo>(`/admin/plugins/${id}`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables a disabled plugin.
|
||||
* @param id - The plugin ID
|
||||
* @returns Promise resolving to success message
|
||||
* @throws {AxiosError} If not found or request fails
|
||||
*/
|
||||
export async function enablePlugin(id: number): Promise<{ message: string }> {
|
||||
const response = await client.post<{ message: string }>(`/admin/plugins/${id}/enable`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables an active plugin.
|
||||
* @param id - The plugin ID
|
||||
* @returns Promise resolving to success message
|
||||
* @throws {AxiosError} If not found, in use, or request fails
|
||||
*/
|
||||
export async function disablePlugin(id: number): Promise<{ message: string }> {
|
||||
const response = await client.post<{ message: string }>(`/admin/plugins/${id}/disable`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads all plugins from the plugin directory.
|
||||
* @returns Promise resolving to success message and count
|
||||
* @throws {AxiosError} If request fails
|
||||
*/
|
||||
export async function reloadPlugins(): Promise<{ message: string; count: number }> {
|
||||
const response = await client.post<{ message: string; count: number }>('/admin/plugins/reload')
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches credential field definitions for a DNS provider type.
|
||||
* @param providerType - The provider type (e.g., "cloudflare", "powerdns")
|
||||
* @returns Promise resolving to field specifications
|
||||
* @throws {AxiosError} If provider type not found or request fails
|
||||
*/
|
||||
export async function getProviderFields(providerType: string): Promise<ProviderFieldsResponse> {
|
||||
const response = await client.get<ProviderFieldsResponse>(`/dns-providers/types/${providerType}/fields`)
|
||||
return response.data
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
import client from './client'
|
||||
|
||||
/** Summary of an available CrowdSec preset. */
|
||||
export interface CrowdsecPresetSummary {
|
||||
slug: string
|
||||
title: string
|
||||
summary: string
|
||||
source: string
|
||||
tags?: string[]
|
||||
requires_hub: boolean
|
||||
available: boolean
|
||||
cached: boolean
|
||||
cache_key?: string
|
||||
etag?: string
|
||||
retrieved_at?: string
|
||||
}
|
||||
|
||||
/** Response from pulling a CrowdSec preset. */
|
||||
export interface PullCrowdsecPresetResponse {
|
||||
status: string
|
||||
slug: string
|
||||
preview: string
|
||||
cache_key: string
|
||||
etag?: string
|
||||
retrieved_at?: string
|
||||
source?: string
|
||||
}
|
||||
|
||||
/** Response from applying a CrowdSec preset. */
|
||||
export interface ApplyCrowdsecPresetResponse {
|
||||
status: string
|
||||
backup?: string
|
||||
reload_hint?: boolean
|
||||
used_cscli?: boolean
|
||||
cache_key?: string
|
||||
slug?: string
|
||||
}
|
||||
|
||||
/** Cached CrowdSec preset preview data. */
|
||||
export interface CachedCrowdsecPresetPreview {
|
||||
preview: string
|
||||
cache_key: string
|
||||
etag?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all available CrowdSec presets.
|
||||
* @returns Promise resolving to object containing presets array
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export async function listCrowdsecPresets() {
|
||||
const resp = await client.get<{ presets: CrowdsecPresetSummary[] }>('/admin/crowdsec/presets')
|
||||
return resp.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all CrowdSec presets (alias for listCrowdsecPresets).
|
||||
* @returns Promise resolving to object containing presets array
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export async function getCrowdsecPresets() {
|
||||
return listCrowdsecPresets()
|
||||
}
|
||||
|
||||
/**
|
||||
* Pulls a CrowdSec preset from the remote source.
|
||||
* @param slug - The preset slug identifier
|
||||
* @returns Promise resolving to PullCrowdsecPresetResponse with preview
|
||||
* @throws {AxiosError} If pull fails or preset not found
|
||||
*/
|
||||
export async function pullCrowdsecPreset(slug: string) {
|
||||
const resp = await client.post<PullCrowdsecPresetResponse>('/admin/crowdsec/presets/pull', { slug })
|
||||
return resp.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a CrowdSec preset to the configuration.
|
||||
* @param payload - Object with preset slug and optional cache_key
|
||||
* @returns Promise resolving to ApplyCrowdsecPresetResponse
|
||||
* @throws {AxiosError} If application fails
|
||||
*/
|
||||
export async function applyCrowdsecPreset(payload: { slug: string; cache_key?: string }) {
|
||||
const resp = await client.post<ApplyCrowdsecPresetResponse>('/admin/crowdsec/presets/apply', payload)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a cached CrowdSec preset preview.
|
||||
* @param slug - The preset slug identifier
|
||||
* @returns Promise resolving to CachedCrowdsecPresetPreview
|
||||
* @throws {AxiosError} If not cached or request fails
|
||||
*/
|
||||
export async function getCrowdsecPresetCache(slug: string) {
|
||||
const resp = await client.get<CachedCrowdsecPresetPreview>(`/admin/crowdsec/presets/cache/${encodeURIComponent(slug)}`)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
export default {
|
||||
listCrowdsecPresets,
|
||||
getCrowdsecPresets,
|
||||
pullCrowdsecPreset,
|
||||
applyCrowdsecPreset,
|
||||
getCrowdsecPresetCache,
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
import client from './client';
|
||||
|
||||
export interface Location {
|
||||
uuid?: string;
|
||||
path: string;
|
||||
forward_scheme: string;
|
||||
forward_host: string;
|
||||
forward_port: number;
|
||||
}
|
||||
|
||||
export interface Certificate {
|
||||
id: number;
|
||||
uuid: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
domains: string;
|
||||
expires_at: string;
|
||||
}
|
||||
|
||||
export type ApplicationPreset = 'none' | 'plex' | 'jellyfin' | 'emby' | 'homeassistant' | 'nextcloud' | 'vaultwarden';
|
||||
|
||||
export interface ProxyHost {
|
||||
uuid: string;
|
||||
name: string;
|
||||
domain_names: string;
|
||||
forward_scheme: string;
|
||||
forward_host: string;
|
||||
forward_port: number;
|
||||
ssl_forced: boolean;
|
||||
http2_support: boolean;
|
||||
hsts_enabled: boolean;
|
||||
hsts_subdomains: boolean;
|
||||
block_exploits: boolean;
|
||||
websocket_support: boolean;
|
||||
enable_standard_headers?: boolean;
|
||||
forward_auth_enabled?: boolean;
|
||||
waf_disabled?: boolean;
|
||||
application: ApplicationPreset;
|
||||
locations: Location[];
|
||||
advanced_config?: string;
|
||||
advanced_config_backup?: string;
|
||||
enabled: boolean;
|
||||
certificate_id?: number | null;
|
||||
certificate?: Certificate | null;
|
||||
access_list_id?: number | null;
|
||||
security_header_profile_id?: number | null;
|
||||
dns_provider_id?: number | null;
|
||||
security_header_profile?: {
|
||||
id: number;
|
||||
uuid: string;
|
||||
name: string;
|
||||
description: string;
|
||||
security_score: number;
|
||||
is_preset: boolean;
|
||||
} | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all proxy hosts from the API.
|
||||
* @returns Promise resolving to array of ProxyHost objects
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export const getProxyHosts = async (): Promise<ProxyHost[]> => {
|
||||
const { data } = await client.get<ProxyHost[]>('/proxy-hosts');
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches a single proxy host by UUID.
|
||||
* @param uuid - The unique identifier of the proxy host
|
||||
* @returns Promise resolving to the ProxyHost object
|
||||
* @throws {AxiosError} If the request fails or host not found
|
||||
*/
|
||||
export const getProxyHost = async (uuid: string): Promise<ProxyHost> => {
|
||||
const { data } = await client.get<ProxyHost>(`/proxy-hosts/${uuid}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new proxy host.
|
||||
* @param host - Partial ProxyHost object with configuration
|
||||
* @returns Promise resolving to the created ProxyHost
|
||||
* @throws {AxiosError} If the request fails or validation errors occur
|
||||
*/
|
||||
export const createProxyHost = async (host: Partial<ProxyHost>): Promise<ProxyHost> => {
|
||||
const { data } = await client.post<ProxyHost>('/proxy-hosts', host);
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates an existing proxy host.
|
||||
* @param uuid - The unique identifier of the proxy host to update
|
||||
* @param host - Partial ProxyHost object with fields to update
|
||||
* @returns Promise resolving to the updated ProxyHost
|
||||
* @throws {AxiosError} If the request fails or host not found
|
||||
*/
|
||||
export const updateProxyHost = async (uuid: string, host: Partial<ProxyHost>): Promise<ProxyHost> => {
|
||||
const { data } = await client.put<ProxyHost>(`/proxy-hosts/${uuid}`, host);
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes a proxy host.
|
||||
* @param uuid - The unique identifier of the proxy host to delete
|
||||
* @param deleteUptime - Optional flag to also delete associated uptime monitors
|
||||
* @throws {AxiosError} If the request fails or host not found
|
||||
*/
|
||||
export const deleteProxyHost = async (uuid: string, deleteUptime?: boolean): Promise<void> => {
|
||||
const url = `/proxy-hosts/${uuid}${deleteUptime ? '?delete_uptime=true' : ''}`
|
||||
await client.delete(url);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests connectivity to a backend host.
|
||||
* @param host - The hostname or IP address to test
|
||||
* @param port - The port number to test
|
||||
* @throws {AxiosError} If the connection test fails
|
||||
*/
|
||||
export const testProxyHostConnection = async (host: string, port: number): Promise<void> => {
|
||||
await client.post('/proxy-hosts/test', { forward_host: host, forward_port: port });
|
||||
};
|
||||
|
||||
export interface BulkUpdateACLRequest {
|
||||
host_uuids: string[];
|
||||
access_list_id: number | null;
|
||||
}
|
||||
|
||||
export interface BulkUpdateACLResponse {
|
||||
updated: number;
|
||||
errors: { uuid: string; error: string }[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk updates access control list assignments for multiple proxy hosts.
|
||||
* @param hostUUIDs - Array of proxy host UUIDs to update
|
||||
* @param accessListID - The access list ID to assign, or null to remove
|
||||
* @returns Promise resolving to the bulk update result with success/error counts
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export const bulkUpdateACL = async (
|
||||
hostUUIDs: string[],
|
||||
accessListID: number | null
|
||||
): Promise<BulkUpdateACLResponse> => {
|
||||
const { data } = await client.put<BulkUpdateACLResponse>('/proxy-hosts/bulk-update-acl', {
|
||||
host_uuids: hostUUIDs,
|
||||
access_list_id: accessListID,
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
export interface BulkUpdateSecurityHeadersRequest {
|
||||
host_uuids: string[];
|
||||
security_header_profile_id: number | null;
|
||||
}
|
||||
|
||||
export interface BulkUpdateSecurityHeadersResponse {
|
||||
updated: number;
|
||||
errors: { uuid: string; error: string }[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk updates security header profile assignments for multiple proxy hosts.
|
||||
* @param hostUUIDs - Array of proxy host UUIDs to update
|
||||
* @param securityHeaderProfileId - The security header profile ID to assign, or null to remove
|
||||
* @returns Promise resolving to the bulk update result with success/error counts
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export const bulkUpdateSecurityHeaders = async (
|
||||
hostUUIDs: string[],
|
||||
securityHeaderProfileId: number | null
|
||||
): Promise<BulkUpdateSecurityHeadersResponse> => {
|
||||
const { data } = await client.put<BulkUpdateSecurityHeadersResponse>(
|
||||
'/proxy-hosts/bulk-update-security-headers',
|
||||
{
|
||||
host_uuids: hostUUIDs,
|
||||
security_header_profile_id: securityHeaderProfileId,
|
||||
}
|
||||
);
|
||||
return data;
|
||||
};
|
||||
@@ -1,94 +0,0 @@
|
||||
import client from './client';
|
||||
|
||||
/** Remote server configuration for Docker host connections. */
|
||||
export interface RemoteServer {
|
||||
uuid: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
host: string;
|
||||
port: number;
|
||||
username?: string;
|
||||
enabled: boolean;
|
||||
reachable: boolean;
|
||||
last_check?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all remote servers.
|
||||
* @param enabledOnly - If true, only returns enabled servers
|
||||
* @returns Promise resolving to array of RemoteServer objects
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export const getRemoteServers = async (enabledOnly = false): Promise<RemoteServer[]> => {
|
||||
const params = enabledOnly ? { enabled: true } : {};
|
||||
const { data } = await client.get<RemoteServer[]>('/remote-servers', { params });
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches a single remote server by UUID.
|
||||
* @param uuid - The unique identifier of the remote server
|
||||
* @returns Promise resolving to the RemoteServer object
|
||||
* @throws {AxiosError} If the request fails or server not found
|
||||
*/
|
||||
export const getRemoteServer = async (uuid: string): Promise<RemoteServer> => {
|
||||
const { data } = await client.get<RemoteServer>(`/remote-servers/${uuid}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new remote server.
|
||||
* @param server - Partial RemoteServer configuration
|
||||
* @returns Promise resolving to the created RemoteServer
|
||||
* @throws {AxiosError} If creation fails
|
||||
*/
|
||||
export const createRemoteServer = async (server: Partial<RemoteServer>): Promise<RemoteServer> => {
|
||||
const { data } = await client.post<RemoteServer>('/remote-servers', server);
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates an existing remote server.
|
||||
* @param uuid - The unique identifier of the server to update
|
||||
* @param server - Partial RemoteServer with fields to update
|
||||
* @returns Promise resolving to the updated RemoteServer
|
||||
* @throws {AxiosError} If update fails or server not found
|
||||
*/
|
||||
export const updateRemoteServer = async (uuid: string, server: Partial<RemoteServer>): Promise<RemoteServer> => {
|
||||
const { data } = await client.put<RemoteServer>(`/remote-servers/${uuid}`, server);
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes a remote server.
|
||||
* @param uuid - The unique identifier of the server to delete
|
||||
* @throws {AxiosError} If deletion fails or server not found
|
||||
*/
|
||||
export const deleteRemoteServer = async (uuid: string): Promise<void> => {
|
||||
await client.delete(`/remote-servers/${uuid}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests connectivity to an existing remote server.
|
||||
* @param uuid - The unique identifier of the server to test
|
||||
* @returns Promise resolving to object with server address
|
||||
* @throws {AxiosError} If connection test fails
|
||||
*/
|
||||
export const testRemoteServerConnection = async (uuid: string): Promise<{ address: string }> => {
|
||||
const { data } = await client.post<{ address: string }>(`/remote-servers/${uuid}/test`);
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests connectivity to a custom host and port.
|
||||
* @param host - The hostname or IP to test
|
||||
* @param port - The port number to test
|
||||
* @returns Promise resolving to connection result with reachable status
|
||||
* @throws {AxiosError} If request fails
|
||||
*/
|
||||
export const testCustomRemoteServerConnection = async (host: string, port: number): Promise<{ address: string; reachable: boolean; error?: string }> => {
|
||||
const { data } = await client.post<{ address: string; reachable: boolean; error?: string }>('/remote-servers/test', { host, port });
|
||||
return data;
|
||||
};
|
||||
@@ -1,189 +0,0 @@
|
||||
import client from './client'
|
||||
|
||||
/** Security module status information. */
|
||||
export interface SecurityStatus {
|
||||
cerberus?: { enabled: boolean }
|
||||
crowdsec: {
|
||||
mode: 'disabled' | 'local'
|
||||
api_url: string
|
||||
enabled: boolean
|
||||
}
|
||||
waf: {
|
||||
mode: 'disabled' | 'enabled'
|
||||
enabled: boolean
|
||||
}
|
||||
rate_limit: {
|
||||
mode?: 'disabled' | 'enabled'
|
||||
enabled: boolean
|
||||
}
|
||||
acl: {
|
||||
enabled: boolean
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current security status for all modules.
|
||||
* @returns Promise resolving to SecurityStatus
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export const getSecurityStatus = async (): Promise<SecurityStatus> => {
|
||||
const response = await client.get<SecurityStatus>('/security/status')
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** Security configuration payload. */
|
||||
export interface SecurityConfigPayload {
|
||||
name?: string
|
||||
enabled?: boolean
|
||||
admin_whitelist?: string
|
||||
crowdsec_mode?: string
|
||||
crowdsec_api_url?: string
|
||||
waf_mode?: string
|
||||
waf_rules_source?: string
|
||||
waf_learning?: boolean
|
||||
rate_limit_enable?: boolean
|
||||
rate_limit_burst?: number
|
||||
rate_limit_requests?: number
|
||||
rate_limit_window_sec?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current security configuration.
|
||||
* @returns Promise resolving to the security configuration
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export const getSecurityConfig = async () => {
|
||||
const response = await client.get('/security/config')
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates security configuration.
|
||||
* @param payload - SecurityConfigPayload with settings to update
|
||||
* @returns Promise resolving to the updated configuration
|
||||
* @throws {AxiosError} If update fails
|
||||
*/
|
||||
export const updateSecurityConfig = async (payload: SecurityConfigPayload) => {
|
||||
const response = await client.post('/security/config', payload)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a break-glass token for emergency access.
|
||||
* @returns Promise resolving to object containing the token
|
||||
* @throws {AxiosError} If generation fails
|
||||
*/
|
||||
export const generateBreakGlassToken = async () => {
|
||||
const response = await client.post('/security/breakglass/generate')
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the Cerberus security module.
|
||||
* @param payload - Optional configuration for enabling
|
||||
* @returns Promise resolving to enable result
|
||||
* @throws {AxiosError} If enabling fails
|
||||
*/
|
||||
export const enableCerberus = async (payload?: Record<string, unknown>) => {
|
||||
const response = await client.post('/security/enable', payload || {})
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the Cerberus security module.
|
||||
* @param payload - Optional configuration for disabling
|
||||
* @returns Promise resolving to disable result
|
||||
* @throws {AxiosError} If disabling fails
|
||||
*/
|
||||
export const disableCerberus = async (payload?: Record<string, unknown>) => {
|
||||
const response = await client.post('/security/disable', payload || {})
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets security decisions (bans, captchas) with optional limit.
|
||||
* @param limit - Maximum number of decisions to return (default: 50)
|
||||
* @returns Promise resolving to decisions list
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export const getDecisions = async (limit = 50) => {
|
||||
const response = await client.get(`/security/decisions?limit=${limit}`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** Payload for creating a security decision. */
|
||||
export interface CreateDecisionPayload {
|
||||
type: string
|
||||
value: string
|
||||
duration: string
|
||||
reason?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new security decision (e.g., ban an IP).
|
||||
* @param payload - Decision configuration
|
||||
* @returns Promise resolving to the created decision
|
||||
* @throws {AxiosError} If creation fails
|
||||
*/
|
||||
export const createDecision = async (payload: CreateDecisionPayload) => {
|
||||
const response = await client.post('/security/decisions', payload)
|
||||
return response.data
|
||||
}
|
||||
|
||||
// WAF Ruleset types
|
||||
/** WAF security ruleset configuration. */
|
||||
export interface SecurityRuleSet {
|
||||
id: number
|
||||
uuid: string
|
||||
name: string
|
||||
source_url: string
|
||||
mode: string
|
||||
last_updated: string
|
||||
content: string
|
||||
}
|
||||
|
||||
/** Response containing WAF rulesets. */
|
||||
export interface RuleSetsResponse {
|
||||
rulesets: SecurityRuleSet[]
|
||||
}
|
||||
|
||||
/** Payload for creating/updating a WAF ruleset. */
|
||||
export interface UpsertRuleSetPayload {
|
||||
id?: number
|
||||
name: string
|
||||
content?: string
|
||||
source_url?: string
|
||||
mode?: 'blocking' | 'detection'
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all WAF rulesets.
|
||||
* @returns Promise resolving to RuleSetsResponse
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export const getRuleSets = async (): Promise<RuleSetsResponse> => {
|
||||
const response = await client.get<RuleSetsResponse>('/security/rulesets')
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or updates a WAF ruleset.
|
||||
* @param payload - Ruleset configuration
|
||||
* @returns Promise resolving to the upserted ruleset
|
||||
* @throws {AxiosError} If upsert fails
|
||||
*/
|
||||
export const upsertRuleSet = async (payload: UpsertRuleSetPayload) => {
|
||||
const response = await client.post('/security/rulesets', payload)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a WAF ruleset.
|
||||
* @param id - The ruleset ID to delete
|
||||
* @returns Promise resolving to delete result
|
||||
* @throws {AxiosError} If deletion fails or ruleset not found
|
||||
*/
|
||||
export const deleteRuleSet = async (id: number) => {
|
||||
const response = await client.delete(`/security/rulesets/${id}`)
|
||||
return response.data
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
import client from './client';
|
||||
|
||||
// Types
|
||||
export interface SecurityHeaderProfile {
|
||||
id: number;
|
||||
uuid: string;
|
||||
name: string;
|
||||
hsts_enabled: boolean;
|
||||
hsts_max_age: number;
|
||||
hsts_include_subdomains: boolean;
|
||||
hsts_preload: boolean;
|
||||
csp_enabled: boolean;
|
||||
csp_directives: string;
|
||||
csp_report_only: boolean;
|
||||
csp_report_uri: string;
|
||||
x_frame_options: string;
|
||||
x_content_type_options: boolean;
|
||||
referrer_policy: string;
|
||||
permissions_policy: string;
|
||||
cross_origin_opener_policy: string;
|
||||
cross_origin_resource_policy: string;
|
||||
cross_origin_embedder_policy: string;
|
||||
xss_protection: boolean;
|
||||
cache_control_no_store: boolean;
|
||||
security_score: number;
|
||||
is_preset: boolean;
|
||||
preset_type: string;
|
||||
description: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface SecurityHeaderPreset {
|
||||
preset_type: 'basic' | 'strict' | 'paranoid';
|
||||
name: string;
|
||||
description: string;
|
||||
security_score: number;
|
||||
config: Partial<SecurityHeaderProfile>;
|
||||
}
|
||||
|
||||
export interface ScoreBreakdown {
|
||||
score: number;
|
||||
max_score: number;
|
||||
breakdown: Record<string, number>;
|
||||
suggestions: string[];
|
||||
}
|
||||
|
||||
export interface CSPDirective {
|
||||
directive: string;
|
||||
values: string[];
|
||||
}
|
||||
|
||||
export interface CreateProfileRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
hsts_enabled?: boolean;
|
||||
hsts_max_age?: number;
|
||||
hsts_include_subdomains?: boolean;
|
||||
hsts_preload?: boolean;
|
||||
csp_enabled?: boolean;
|
||||
csp_directives?: string;
|
||||
csp_report_only?: boolean;
|
||||
csp_report_uri?: string;
|
||||
x_frame_options?: string;
|
||||
x_content_type_options?: boolean;
|
||||
referrer_policy?: string;
|
||||
permissions_policy?: string;
|
||||
cross_origin_opener_policy?: string;
|
||||
cross_origin_resource_policy?: string;
|
||||
cross_origin_embedder_policy?: string;
|
||||
xss_protection?: boolean;
|
||||
cache_control_no_store?: boolean;
|
||||
}
|
||||
|
||||
export interface ApplyPresetRequest {
|
||||
preset_type: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// API Functions
|
||||
export const securityHeadersApi = {
|
||||
/**
|
||||
* Lists all security header profiles.
|
||||
* @returns Promise resolving to array of SecurityHeaderProfile objects
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
async listProfiles(): Promise<SecurityHeaderProfile[]> {
|
||||
const response = await client.get<{profiles: SecurityHeaderProfile[]}>('/security/headers/profiles');
|
||||
return response.data.profiles;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets a single security header profile by ID or UUID.
|
||||
* @param id - The profile ID (number) or UUID (string)
|
||||
* @returns Promise resolving to the SecurityHeaderProfile object
|
||||
* @throws {AxiosError} If the request fails or profile not found
|
||||
*/
|
||||
async getProfile(id: number | string): Promise<SecurityHeaderProfile> {
|
||||
const response = await client.get<{profile: SecurityHeaderProfile}>(`/security/headers/profiles/${id}`);
|
||||
return response.data.profile;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new security header profile.
|
||||
* @param data - CreateProfileRequest with profile configuration
|
||||
* @returns Promise resolving to the created SecurityHeaderProfile
|
||||
* @throws {AxiosError} If creation fails or validation errors occur
|
||||
*/
|
||||
async createProfile(data: CreateProfileRequest): Promise<SecurityHeaderProfile> {
|
||||
const response = await client.post<{profile: SecurityHeaderProfile}>('/security/headers/profiles', data);
|
||||
return response.data.profile;
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates an existing security header profile.
|
||||
* @param id - The profile ID to update
|
||||
* @param data - Partial CreateProfileRequest with fields to update
|
||||
* @returns Promise resolving to the updated SecurityHeaderProfile
|
||||
* @throws {AxiosError} If update fails or profile not found
|
||||
*/
|
||||
async updateProfile(id: number, data: Partial<CreateProfileRequest>): Promise<SecurityHeaderProfile> {
|
||||
const response = await client.put<{profile: SecurityHeaderProfile}>(`/security/headers/profiles/${id}`, data);
|
||||
return response.data.profile;
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes a security header profile.
|
||||
* @param id - The profile ID to delete (cannot delete preset profiles)
|
||||
* @throws {AxiosError} If deletion fails, profile not found, or is a preset
|
||||
*/
|
||||
async deleteProfile(id: number): Promise<void> {
|
||||
await client.delete(`/security/headers/profiles/${id}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets all built-in security header presets.
|
||||
* @returns Promise resolving to array of SecurityHeaderPreset objects
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
async getPresets(): Promise<SecurityHeaderPreset[]> {
|
||||
const response = await client.get<{presets: SecurityHeaderPreset[]}>('/security/headers/presets');
|
||||
return response.data.presets;
|
||||
},
|
||||
|
||||
/**
|
||||
* Applies a preset to create or update a security header profile.
|
||||
* @param data - ApplyPresetRequest with preset type and profile name
|
||||
* @returns Promise resolving to the created/updated SecurityHeaderProfile
|
||||
* @throws {AxiosError} If preset application fails
|
||||
*/
|
||||
async applyPreset(data: ApplyPresetRequest): Promise<SecurityHeaderProfile> {
|
||||
const response = await client.post<{profile: SecurityHeaderProfile}>('/security/headers/presets/apply', data);
|
||||
return response.data.profile;
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculates the security score for given header settings.
|
||||
* @param config - Partial CreateProfileRequest with settings to evaluate
|
||||
* @returns Promise resolving to ScoreBreakdown with score, max, breakdown, and suggestions
|
||||
* @throws {AxiosError} If calculation fails
|
||||
*/
|
||||
async calculateScore(config: Partial<CreateProfileRequest>): Promise<ScoreBreakdown> {
|
||||
const response = await client.post<ScoreBreakdown>('/security/headers/score', config);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Validates a Content Security Policy string.
|
||||
* @param csp - The CSP string to validate
|
||||
* @returns Promise resolving to object with validity status and any errors
|
||||
* @throws {AxiosError} If validation request fails
|
||||
*/
|
||||
async validateCSP(csp: string): Promise<{ valid: boolean; errors: string[] }> {
|
||||
const response = await client.post<{ valid: boolean; errors: string[] }>('/security/headers/csp/validate', { csp });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Builds a Content Security Policy string from directives.
|
||||
* @param directives - Array of CSPDirective objects to combine
|
||||
* @returns Promise resolving to object containing the built CSP string
|
||||
* @throws {AxiosError} If build request fails
|
||||
*/
|
||||
async buildCSP(directives: CSPDirective[]): Promise<{ csp: string }> {
|
||||
const response = await client.post<{ csp: string }>('/security/headers/csp/build', { directives });
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
import client from './client'
|
||||
|
||||
/** Map of setting keys to string values. */
|
||||
export interface SettingsMap {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all application settings.
|
||||
* @returns Promise resolving to SettingsMap
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export const getSettings = async (): Promise<SettingsMap> => {
|
||||
const response = await client.get('/settings')
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a single application setting.
|
||||
* @param key - The setting key to update
|
||||
* @param value - The new value for the setting
|
||||
* @param category - Optional category for organization
|
||||
* @param type - Optional type hint for the setting
|
||||
* @throws {AxiosError} If the update fails
|
||||
*/
|
||||
export const updateSetting = async (key: string, value: string, category?: string, type?: string): Promise<void> => {
|
||||
await client.post('/settings', { key, value, category, type })
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a URL for use as the application URL.
|
||||
* @param url - The URL to validate
|
||||
* @returns Promise resolving to validation result
|
||||
*/
|
||||
export const validatePublicURL = async (url: string): Promise<{
|
||||
valid: boolean
|
||||
normalized?: string
|
||||
error?: string
|
||||
}> => {
|
||||
const response = await client.post('/settings/validate-url', { url })
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if a URL is reachable from the server with SSRF protection.
|
||||
* @param url - The URL to test
|
||||
* @returns Promise resolving to test result with reachability status and latency
|
||||
*/
|
||||
export const testPublicURL = async (url: string): Promise<{
|
||||
reachable: boolean
|
||||
latency?: number
|
||||
message?: string
|
||||
error?: string
|
||||
}> => {
|
||||
const response = await client.post('/settings/test-url', { url })
|
||||
return response.data
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import client from './client';
|
||||
|
||||
/** Status indicating if initial setup is required. */
|
||||
export interface SetupStatus {
|
||||
setupRequired: boolean;
|
||||
}
|
||||
|
||||
/** Request payload for initial setup. */
|
||||
export interface SetupRequest {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if initial setup is required.
|
||||
* @returns Promise resolving to SetupStatus
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export const getSetupStatus = async (): Promise<SetupStatus> => {
|
||||
const response = await client.get<SetupStatus>('/setup');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Performs initial application setup with admin user creation.
|
||||
* @param data - SetupRequest with admin user details
|
||||
* @throws {AxiosError} If setup fails or already completed
|
||||
*/
|
||||
export const performSetup = async (data: SetupRequest): Promise<void> => {
|
||||
await client.post('/setup', data);
|
||||
};
|
||||
@@ -1,76 +0,0 @@
|
||||
import client from './client'
|
||||
|
||||
/** SMTP server configuration. */
|
||||
export interface SMTPConfig {
|
||||
host: string
|
||||
port: number
|
||||
username: string
|
||||
password: string
|
||||
from_address: string
|
||||
encryption: 'none' | 'ssl' | 'starttls'
|
||||
configured: boolean
|
||||
}
|
||||
|
||||
/** Request payload for SMTP configuration. */
|
||||
export interface SMTPConfigRequest {
|
||||
host: string
|
||||
port: number
|
||||
username: string
|
||||
password: string
|
||||
from_address: string
|
||||
encryption: 'none' | 'ssl' | 'starttls'
|
||||
}
|
||||
|
||||
/** Request payload for sending a test email. */
|
||||
export interface TestEmailRequest {
|
||||
to: string
|
||||
}
|
||||
|
||||
/** Result of an SMTP test operation. */
|
||||
export interface SMTPTestResult {
|
||||
success: boolean
|
||||
message?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the current SMTP configuration.
|
||||
* @returns Promise resolving to SMTPConfig
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export const getSMTPConfig = async (): Promise<SMTPConfig> => {
|
||||
const response = await client.get<SMTPConfig>('/settings/smtp')
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the SMTP configuration.
|
||||
* @param config - SMTPConfigRequest with new settings
|
||||
* @returns Promise resolving to success message
|
||||
* @throws {AxiosError} If update fails
|
||||
*/
|
||||
export const updateSMTPConfig = async (config: SMTPConfigRequest): Promise<{ message: string }> => {
|
||||
const response = await client.post<{ message: string }>('/settings/smtp', config)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the SMTP connection with current settings.
|
||||
* @returns Promise resolving to SMTPTestResult
|
||||
* @throws {AxiosError} If test request fails
|
||||
*/
|
||||
export const testSMTPConnection = async (): Promise<SMTPTestResult> => {
|
||||
const response = await client.post<SMTPTestResult>('/settings/smtp/test')
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a test email to verify SMTP configuration.
|
||||
* @param request - TestEmailRequest with recipient address
|
||||
* @returns Promise resolving to SMTPTestResult
|
||||
* @throws {AxiosError} If sending fails
|
||||
*/
|
||||
export const sendTestEmail = async (request: TestEmailRequest): Promise<SMTPTestResult> => {
|
||||
const response = await client.post<SMTPTestResult>('/settings/smtp/test-email', request)
|
||||
return response.data
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import client from './client';
|
||||
|
||||
/** Update availability information. */
|
||||
export interface UpdateInfo {
|
||||
available: boolean;
|
||||
latest_version: string;
|
||||
changelog_url: string;
|
||||
}
|
||||
|
||||
/** System notification entry. */
|
||||
export interface Notification {
|
||||
id: string;
|
||||
type: 'info' | 'success' | 'warning' | 'error';
|
||||
title: string;
|
||||
message: string;
|
||||
read: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for available application updates.
|
||||
* @returns Promise resolving to UpdateInfo
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export const checkUpdates = async (): Promise<UpdateInfo> => {
|
||||
const response = await client.get('/system/updates');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches system notifications.
|
||||
* @param unreadOnly - If true, only returns unread notifications
|
||||
* @returns Promise resolving to array of Notification objects
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export const getNotifications = async (unreadOnly = false): Promise<Notification[]> => {
|
||||
const response = await client.get('/notifications', { params: { unread: unreadOnly } });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Marks a notification as read.
|
||||
* @param id - The notification ID to mark as read
|
||||
* @throws {AxiosError} If marking fails or notification not found
|
||||
*/
|
||||
export const markNotificationRead = async (id: string): Promise<void> => {
|
||||
await client.post(`/notifications/${id}/read`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Marks all notifications as read.
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export const markAllNotificationsRead = async (): Promise<void> => {
|
||||
await client.post('/notifications/read-all');
|
||||
};
|
||||
|
||||
/** Response containing the client's public IP address. */
|
||||
export interface MyIPResponse {
|
||||
ip: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the client's public IP address as seen by the server.
|
||||
* @returns Promise resolving to MyIPResponse with IP address
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export const getMyIP = async (): Promise<MyIPResponse> => {
|
||||
const response = await client.get<MyIPResponse>('/system/my-ip');
|
||||
return response.data;
|
||||
};
|
||||
@@ -1,112 +0,0 @@
|
||||
import client from './client';
|
||||
|
||||
/** Uptime monitor configuration. */
|
||||
export interface UptimeMonitor {
|
||||
id: string;
|
||||
upstream_host?: string;
|
||||
proxy_host_id?: number;
|
||||
remote_server_id?: number;
|
||||
name: string;
|
||||
type: string;
|
||||
url: string;
|
||||
interval: number;
|
||||
enabled: boolean;
|
||||
status: string;
|
||||
last_check?: string | null;
|
||||
latency: number;
|
||||
max_retries: number;
|
||||
}
|
||||
|
||||
/** Uptime heartbeat (check result) entry. */
|
||||
export interface UptimeHeartbeat {
|
||||
id: number;
|
||||
monitor_id: string;
|
||||
status: string;
|
||||
latency: number;
|
||||
message: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all uptime monitors.
|
||||
* @returns Promise resolving to array of UptimeMonitor objects
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export const getMonitors = async () => {
|
||||
const response = await client.get<UptimeMonitor[]>('/uptime/monitors');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches heartbeat history for a monitor.
|
||||
* @param id - The monitor ID
|
||||
* @param limit - Maximum number of heartbeats to return (default: 50)
|
||||
* @returns Promise resolving to array of UptimeHeartbeat objects
|
||||
* @throws {AxiosError} If the request fails or monitor not found
|
||||
*/
|
||||
export const getMonitorHistory = async (id: string, limit: number = 50) => {
|
||||
const response = await client.get<UptimeHeartbeat[]>(`/uptime/monitors/${id}/history?limit=${limit}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates an uptime monitor configuration.
|
||||
* @param id - The monitor ID to update
|
||||
* @param data - Partial UptimeMonitor with fields to update
|
||||
* @returns Promise resolving to the updated UptimeMonitor
|
||||
* @throws {AxiosError} If update fails or monitor not found
|
||||
*/
|
||||
export const updateMonitor = async (id: string, data: Partial<UptimeMonitor>) => {
|
||||
const response = await client.put<UptimeMonitor>(`/uptime/monitors/${id}`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes an uptime monitor.
|
||||
* @param id - The monitor ID to delete
|
||||
* @returns Promise resolving to void
|
||||
* @throws {AxiosError} If deletion fails or monitor not found
|
||||
*/
|
||||
export const deleteMonitor = async (id: string) => {
|
||||
const response = await client.delete<void>(`/uptime/monitors/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new uptime monitor.
|
||||
* @param data - Monitor configuration (name, url, type, interval, max_retries)
|
||||
* @returns Promise resolving to the created UptimeMonitor
|
||||
* @throws {AxiosError} If creation fails
|
||||
*/
|
||||
export const createMonitor = async (data: {
|
||||
name: string;
|
||||
url: string;
|
||||
type: string;
|
||||
interval?: number;
|
||||
max_retries?: number;
|
||||
}): Promise<UptimeMonitor> => {
|
||||
const response = await client.post<UptimeMonitor>('/uptime/monitors', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Syncs monitors with proxy hosts and remote servers.
|
||||
* @param body - Optional configuration for sync (interval, max_retries)
|
||||
* @returns Promise resolving to sync result with message
|
||||
* @throws {AxiosError} If sync fails
|
||||
*/
|
||||
export async function syncMonitors(body?: { interval?: number; max_retries?: number }): Promise<{ message: string }> {
|
||||
const res = await client.post<{ message: string }>('/uptime/sync', body || {});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers an immediate check for a monitor.
|
||||
* @param id - The monitor ID to check
|
||||
* @returns Promise resolving to object with result message
|
||||
* @throws {AxiosError} If check fails or monitor not found
|
||||
*/
|
||||
export const checkMonitor = async (id: string) => {
|
||||
const response = await client.post<{ message: string }>(`/uptime/monitors/${id}/check`);
|
||||
return response.data;
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
import client from './client'
|
||||
|
||||
/** Current user profile information. */
|
||||
export interface UserProfile {
|
||||
id: number
|
||||
email: string
|
||||
name: string
|
||||
role: string
|
||||
api_key: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the current user's profile.
|
||||
* @returns Promise resolving to UserProfile
|
||||
* @throws {AxiosError} If the request fails or not authenticated
|
||||
*/
|
||||
export const getProfile = async (): Promise<UserProfile> => {
|
||||
const response = await client.get('/user/profile')
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerates the current user's API key.
|
||||
* @returns Promise resolving to object containing the new API key
|
||||
* @throws {AxiosError} If regeneration fails
|
||||
*/
|
||||
export const regenerateApiKey = async (): Promise<{ api_key: string }> => {
|
||||
const response = await client.post('/user/api-key')
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current user's profile.
|
||||
* @param data - Object with name, email, and optional current_password for verification
|
||||
* @returns Promise resolving to success message
|
||||
* @throws {AxiosError} If update fails or password verification fails
|
||||
*/
|
||||
export const updateProfile = async (data: { name: string; email: string; current_password?: string }): Promise<{ message: string }> => {
|
||||
const response = await client.post('/user/profile', data)
|
||||
return response.data
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import client from './client'
|
||||
import {
|
||||
listUsers,
|
||||
getUser,
|
||||
createUser,
|
||||
inviteUser,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
updateUserPermissions,
|
||||
validateInvite,
|
||||
acceptInvite,
|
||||
} from './users'
|
||||
|
||||
vi.mock('./client', () => ({
|
||||
default: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
const mockedClient = client as unknown as {
|
||||
get: ReturnType<typeof vi.fn>
|
||||
post: ReturnType<typeof vi.fn>
|
||||
put: ReturnType<typeof vi.fn>
|
||||
delete: ReturnType<typeof vi.fn>
|
||||
}
|
||||
|
||||
describe('users api', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('lists and fetches users', async () => {
|
||||
mockedClient.get
|
||||
.mockResolvedValueOnce({ data: [{ id: 1, uuid: 'u1', email: 'a@example.com', name: 'A', role: 'admin', enabled: true, permission_mode: 'allow_all', created_at: '', updated_at: '' }] })
|
||||
.mockResolvedValueOnce({ data: { id: 2, uuid: 'u2', email: 'b@example.com', name: 'B', role: 'user', enabled: true, permission_mode: 'allow_all', created_at: '', updated_at: '' } })
|
||||
|
||||
const users = await listUsers()
|
||||
expect(mockedClient.get).toHaveBeenCalledWith('/users')
|
||||
expect(users[0].email).toBe('a@example.com')
|
||||
|
||||
const user = await getUser(2)
|
||||
expect(mockedClient.get).toHaveBeenCalledWith('/users/2')
|
||||
expect(user.uuid).toBe('u2')
|
||||
})
|
||||
|
||||
it('creates, invites, updates, and deletes users', async () => {
|
||||
mockedClient.post
|
||||
.mockResolvedValueOnce({ data: { id: 3, uuid: 'u3', email: 'c@example.com', name: 'C', role: 'user', enabled: true, permission_mode: 'allow_all', created_at: '', updated_at: '' } })
|
||||
.mockResolvedValueOnce({ data: { id: 4, uuid: 'u4', email: 'invite@example.com', role: 'user', invite_token: 'token', email_sent: true, expires_at: '' } })
|
||||
|
||||
mockedClient.put.mockResolvedValueOnce({ data: { message: 'updated' } })
|
||||
mockedClient.delete.mockResolvedValueOnce({ data: { message: 'deleted' } })
|
||||
|
||||
const created = await createUser({ email: 'c@example.com', name: 'C', password: 'pw' })
|
||||
expect(mockedClient.post).toHaveBeenCalledWith('/users', { email: 'c@example.com', name: 'C', password: 'pw' })
|
||||
expect(created.id).toBe(3)
|
||||
|
||||
const invite = await inviteUser({ email: 'invite@example.com', role: 'user' })
|
||||
expect(mockedClient.post).toHaveBeenCalledWith('/users/invite', { email: 'invite@example.com', role: 'user' })
|
||||
expect(invite.invite_token).toBe('token')
|
||||
|
||||
await updateUser(3, { enabled: false })
|
||||
expect(mockedClient.put).toHaveBeenCalledWith('/users/3', { enabled: false })
|
||||
|
||||
await deleteUser(3)
|
||||
expect(mockedClient.delete).toHaveBeenCalledWith('/users/3')
|
||||
})
|
||||
|
||||
it('updates permissions and validates/accepts invites', async () => {
|
||||
mockedClient.put.mockResolvedValueOnce({ data: { message: 'perms updated' } })
|
||||
mockedClient.get.mockResolvedValueOnce({ data: { valid: true, email: 'invite@example.com' } })
|
||||
mockedClient.post.mockResolvedValueOnce({ data: { message: 'accepted', email: 'invite@example.com' } })
|
||||
|
||||
const perms = await updateUserPermissions(5, { permission_mode: 'deny_all', permitted_hosts: [1, 2] })
|
||||
expect(mockedClient.put).toHaveBeenCalledWith('/users/5/permissions', {
|
||||
permission_mode: 'deny_all',
|
||||
permitted_hosts: [1, 2],
|
||||
})
|
||||
expect(perms.message).toBe('perms updated')
|
||||
|
||||
const validation = await validateInvite('token-abc')
|
||||
expect(mockedClient.get).toHaveBeenCalledWith('/invite/validate', { params: { token: 'token-abc' } })
|
||||
expect(validation.valid).toBe(true)
|
||||
|
||||
const accept = await acceptInvite({ token: 'token-abc', name: 'New', password: 'pw' })
|
||||
expect(mockedClient.post).toHaveBeenCalledWith('/invite/accept', { token: 'token-abc', name: 'New', password: 'pw' })
|
||||
expect(accept.message).toBe('accepted')
|
||||
})
|
||||
})
|
||||
@@ -1,213 +0,0 @@
|
||||
import client from './client'
|
||||
|
||||
/** User permission mode type. */
|
||||
export type PermissionMode = 'allow_all' | 'deny_all'
|
||||
|
||||
/** User account information. */
|
||||
export interface User {
|
||||
id: number
|
||||
uuid: string
|
||||
email: string
|
||||
name: string
|
||||
role: 'admin' | 'user' | 'viewer'
|
||||
enabled: boolean
|
||||
last_login?: string
|
||||
invite_status?: 'pending' | 'accepted' | 'expired'
|
||||
invited_at?: string
|
||||
permission_mode: PermissionMode
|
||||
permitted_hosts?: number[]
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/** Request payload for creating a user. */
|
||||
export interface CreateUserRequest {
|
||||
email: string
|
||||
name: string
|
||||
password: string
|
||||
role?: string
|
||||
permission_mode?: PermissionMode
|
||||
permitted_hosts?: number[]
|
||||
}
|
||||
|
||||
/** Request payload for inviting a user. */
|
||||
export interface InviteUserRequest {
|
||||
email: string
|
||||
role?: string
|
||||
permission_mode?: PermissionMode
|
||||
permitted_hosts?: number[]
|
||||
}
|
||||
|
||||
/** Response from user invitation. */
|
||||
export interface InviteUserResponse {
|
||||
id: number
|
||||
uuid: string
|
||||
email: string
|
||||
role: string
|
||||
invite_token: string
|
||||
email_sent: boolean
|
||||
expires_at: string
|
||||
}
|
||||
|
||||
/** Request payload for updating a user. */
|
||||
export interface UpdateUserRequest {
|
||||
name?: string
|
||||
email?: string
|
||||
role?: string
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
/** Request payload for updating user permissions. */
|
||||
export interface UpdateUserPermissionsRequest {
|
||||
permission_mode: PermissionMode
|
||||
permitted_hosts: number[]
|
||||
}
|
||||
|
||||
/** Response from invite validation. */
|
||||
export interface ValidateInviteResponse {
|
||||
valid: boolean
|
||||
email: string
|
||||
}
|
||||
|
||||
/** Request payload for accepting an invitation. */
|
||||
export interface AcceptInviteRequest {
|
||||
token: string
|
||||
name: string
|
||||
password: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all users.
|
||||
* @returns Promise resolving to array of User objects
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export const listUsers = async (): Promise<User[]> => {
|
||||
const response = await client.get<User[]>('/users')
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a single user by ID.
|
||||
* @param id - The user ID
|
||||
* @returns Promise resolving to the User object
|
||||
* @throws {AxiosError} If the request fails or user not found
|
||||
*/
|
||||
export const getUser = async (id: number): Promise<User> => {
|
||||
const response = await client.get<User>(`/users/${id}`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new user.
|
||||
* @param data - CreateUserRequest with user details
|
||||
* @returns Promise resolving to the created User
|
||||
* @throws {AxiosError} If creation fails or email already exists
|
||||
*/
|
||||
export const createUser = async (data: CreateUserRequest): Promise<User> => {
|
||||
const response = await client.post<User>('/users', data)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Invites a new user via email.
|
||||
* @param data - InviteUserRequest with invitation details
|
||||
* @returns Promise resolving to InviteUserResponse with token
|
||||
* @throws {AxiosError} If invitation fails
|
||||
*/
|
||||
export const inviteUser = async (data: InviteUserRequest): Promise<InviteUserResponse> => {
|
||||
const response = await client.post<InviteUserResponse>('/users/invite', data)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing user.
|
||||
* @param id - The user ID to update
|
||||
* @param data - UpdateUserRequest with fields to update
|
||||
* @returns Promise resolving to success message
|
||||
* @throws {AxiosError} If update fails or user not found
|
||||
*/
|
||||
export const updateUser = async (id: number, data: UpdateUserRequest): Promise<{ message: string }> => {
|
||||
const response = await client.put<{ message: string }>(`/users/${id}`, data)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a user.
|
||||
* @param id - The user ID to delete
|
||||
* @returns Promise resolving to success message
|
||||
* @throws {AxiosError} If deletion fails or user not found
|
||||
*/
|
||||
export const deleteUser = async (id: number): Promise<{ message: string }> => {
|
||||
const response = await client.delete<{ message: string }>(`/users/${id}`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a user's permissions.
|
||||
* @param id - The user ID to update
|
||||
* @param data - UpdateUserPermissionsRequest with new permissions
|
||||
* @returns Promise resolving to success message
|
||||
* @throws {AxiosError} If update fails or user not found
|
||||
*/
|
||||
export const updateUserPermissions = async (
|
||||
id: number,
|
||||
data: UpdateUserPermissionsRequest
|
||||
): Promise<{ message: string }> => {
|
||||
const response = await client.put<{ message: string }>(`/users/${id}/permissions`, data)
|
||||
return response.data
|
||||
}
|
||||
|
||||
// Public endpoints (no auth required)
|
||||
/**
|
||||
* Validates an invitation token.
|
||||
* @param token - The invitation token to validate
|
||||
* @returns Promise resolving to ValidateInviteResponse
|
||||
* @throws {AxiosError} If validation fails
|
||||
*/
|
||||
export const validateInvite = async (token: string): Promise<ValidateInviteResponse> => {
|
||||
const response = await client.get<ValidateInviteResponse>('/invite/validate', {
|
||||
params: { token }
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts an invitation and creates the user account.
|
||||
* @param data - AcceptInviteRequest with token and user details
|
||||
* @returns Promise resolving to success message and email
|
||||
* @throws {AxiosError} If acceptance fails or token invalid/expired
|
||||
*/
|
||||
export const acceptInvite = async (data: AcceptInviteRequest): Promise<{ message: string; email: string }> => {
|
||||
const response = await client.post<{ message: string; email: string }>('/invite/accept', data)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** Response from invite URL preview. */
|
||||
export interface PreviewInviteURLResponse {
|
||||
preview_url: string
|
||||
base_url: string
|
||||
is_configured: boolean
|
||||
email: string
|
||||
warning: boolean
|
||||
warning_message: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Previews what the invite URL will look like for a given email.
|
||||
* @param email - The email to preview
|
||||
* @returns Promise resolving to PreviewInviteURLResponse
|
||||
*/
|
||||
export const previewInviteURL = async (email: string): Promise<PreviewInviteURLResponse> => {
|
||||
const response = await client.post<PreviewInviteURLResponse>('/users/preview-invite-url', { email })
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Resends an invitation email to a pending user.
|
||||
* @param id - The user ID to resend invite to
|
||||
* @returns Promise resolving to InviteUserResponse with new token
|
||||
*/
|
||||
export const resendInvite = async (id: number): Promise<InviteUserResponse> => {
|
||||
const response = await client.post<InviteUserResponse>(`/users/${id}/resend-invite`)
|
||||
return response.data
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import client from './client';
|
||||
|
||||
/** Information about a WebSocket connection. */
|
||||
export interface ConnectionInfo {
|
||||
id: string;
|
||||
type: 'logs' | 'cerberus';
|
||||
connected_at: string;
|
||||
last_activity_at: string;
|
||||
remote_addr?: string;
|
||||
user_agent?: string;
|
||||
filters?: string;
|
||||
}
|
||||
|
||||
/** Aggregate statistics for WebSocket connections. */
|
||||
export interface ConnectionStats {
|
||||
total_active: number;
|
||||
logs_connections: number;
|
||||
cerberus_connections: number;
|
||||
oldest_connection?: string;
|
||||
last_updated: string;
|
||||
}
|
||||
|
||||
/** Response containing WebSocket connections list. */
|
||||
export interface ConnectionsResponse {
|
||||
connections: ConnectionInfo[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all active WebSocket connections.
|
||||
* @returns Promise resolving to ConnectionsResponse with connections list
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export const getWebSocketConnections = async (): Promise<ConnectionsResponse> => {
|
||||
const response = await client.get('/websocket/connections');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets aggregate WebSocket connection statistics.
|
||||
* @returns Promise resolving to ConnectionStats
|
||||
* @throws {AxiosError} If the request fails
|
||||
*/
|
||||
export const getWebSocketStats = async (): Promise<ConnectionStats> => {
|
||||
const response = await client.get('/websocket/stats');
|
||||
return response.data;
|
||||
};
|
||||
Reference in New Issue
Block a user