feat: add security header profile assignment to proxy hosts

Implement complete workflow for assigning security header profiles
to proxy hosts via dropdown selector in ProxyHostForm.

Backend Changes:
- Add security_header_profile_id handling to proxy host update endpoint
- Add SecurityHeaderProfile preloading in service layer
- Add 5 comprehensive tests for profile CRUD operations

Frontend Changes:
- Add Security Headers section to ProxyHostForm with dropdown
- Group profiles: System Profiles (presets) vs Custom Profiles
- Remove confusing "Apply" button from SecurityHeaders page
- Rename section to "System Profiles (Read-Only)" for clarity
- Show security score inline when profile selected

UX Improvements:
- Clear workflow: Select profile → Assign to host → Caddy applies
- No more confusion about what "Apply" does
- Discoverable security header assignment
- Visual distinction between presets and custom profiles

Tests: Backend 85.6%, Frontend 87.21% coverage
Docs: Updated workflows in docs/features.md
This commit is contained in:
GitHub Actions
2025-12-18 15:56:47 +00:00
parent c039ef10cf
commit 555ab5e669
11 changed files with 878 additions and 97 deletions
@@ -164,44 +164,6 @@ describe('SecurityHeaders', () => {
});
});
it('should apply preset', async () => {
const mockProfiles = [
{
id: 1,
name: 'Basic Security',
description: 'Essential headers',
is_preset: true,
preset_type: 'basic',
security_score: 65,
updated_at: '2025-12-18T00:00:00Z',
},
];
vi.mocked(securityHeadersApi.listProfiles).mockResolvedValue(mockProfiles as any);
vi.mocked(securityHeadersApi.getPresets).mockResolvedValue([]);
vi.mocked(securityHeadersApi.applyPreset).mockResolvedValue({
id: 2,
name: 'Basic Security Profile',
security_score: 65,
} as any);
render(<SecurityHeaders />, { wrapper: createWrapper() });
await waitFor(() => {
expect(screen.getByText('Basic Security')).toBeInTheDocument();
});
const applyButton = screen.getByRole('button', { name: /Apply/ });
fireEvent.click(applyButton);
await waitFor(() => {
expect(securityHeadersApi.applyPreset).toHaveBeenCalledWith({
preset_type: 'basic',
name: 'Basic Security Profile',
});
});
});
it('should clone profile', async () => {
const mockProfiles = [
{
@@ -312,15 +274,15 @@ describe('SecurityHeaders', () => {
render(<SecurityHeaders />, { wrapper: createWrapper() });
await waitFor(() => {
expect(screen.getByText('Quick Presets')).toBeInTheDocument();
expect(screen.getByText('System Profiles (Read-Only)')).toBeInTheDocument();
expect(screen.getByText('Custom Profiles')).toBeInTheDocument();
});
// Quick preset should have preset_type badge and View/Apply/Clone buttons
// System profiles should have View and Clone buttons
const presetCard = screen.getByText('Basic Security').closest('div');
expect(presetCard?.textContent).toContain('basic');
expect(presetCard).toBeInTheDocument();
// Custom profile should have "Edit" and delete buttons
// Custom profile should have Edit button
const customCard = screen.getByText('Custom Profile').closest('div');
expect(customCard?.textContent).toContain('Custom Profile');
});