diff --git a/backend/internal/caddy/client_test.go b/backend/internal/caddy/client_test.go index 7701465c..a4bfaf40 100644 --- a/backend/internal/caddy/client_test.go +++ b/backend/internal/caddy/client_test.go @@ -30,7 +30,7 @@ func TestClient_Load_Success(t *testing.T) { ForwardPort: 8080, Enabled: true, }, - }, "/tmp/caddy-data", "admin@example.com", "") + }, "/tmp/caddy-data", "admin@example.com", "", "") err := client.Load(context.Background(), config) require.NoError(t, err) diff --git a/backend/internal/caddy/config.go b/backend/internal/caddy/config.go index ed7478a7..6402d82e 100644 --- a/backend/internal/caddy/config.go +++ b/backend/internal/caddy/config.go @@ -10,7 +10,7 @@ import ( // GenerateConfig creates a Caddy JSON configuration from proxy hosts. // This is the core transformation layer from our database model to Caddy config. -func GenerateConfig(hosts []models.ProxyHost, storageDir string, acmeEmail string, frontendDir string) (*Config, error) { +func GenerateConfig(hosts []models.ProxyHost, storageDir string, acmeEmail string, frontendDir string, sslProvider string) (*Config, error) { // Define log file paths // We assume storageDir is like ".../data/caddy/data", so we go up to ".../data/logs" // storageDir is .../data/caddy/data @@ -51,19 +51,34 @@ func GenerateConfig(hosts []models.ProxyHost, storageDir string, acmeEmail strin } if acmeEmail != "" { + var issuers []interface{} + + // Configure issuers based on provider preference + switch sslProvider { + case "letsencrypt": + issuers = append(issuers, map[string]interface{}{ + "module": "acme", + "email": acmeEmail, + }) + case "zerossl": + issuers = append(issuers, map[string]interface{}{ + "module": "zerossl", + }) + default: // "both" or empty + issuers = append(issuers, map[string]interface{}{ + "module": "acme", + "email": acmeEmail, + }) + issuers = append(issuers, map[string]interface{}{ + "module": "zerossl", + }) + } + config.Apps.TLS = &TLSApp{ Automation: &AutomationConfig{ Policies: []*AutomationPolicy{ { - IssuersRaw: []interface{}{ - map[string]interface{}{ - "module": "acme", - "email": acmeEmail, - }, - map[string]interface{}{ - "module": "zerossl", - }, - }, + IssuersRaw: issuers, }, }, }, diff --git a/backend/internal/caddy/config_test.go b/backend/internal/caddy/config_test.go index 37fe2a74..77b37f54 100644 --- a/backend/internal/caddy/config_test.go +++ b/backend/internal/caddy/config_test.go @@ -9,7 +9,7 @@ import ( ) func TestGenerateConfig_Empty(t *testing.T) { - config, err := GenerateConfig([]models.ProxyHost{}, "/tmp/caddy-data", "admin@example.com", "") + config, err := GenerateConfig([]models.ProxyHost{}, "/tmp/caddy-data", "admin@example.com", "", "") require.NoError(t, err) require.NotNil(t, config) require.NotNil(t, config.Apps.HTTP) @@ -31,7 +31,7 @@ func TestGenerateConfig_SingleHost(t *testing.T) { }, } - config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "") + config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "") require.NoError(t, err) require.NotNil(t, config) require.NotNil(t, config.Apps.HTTP) @@ -71,7 +71,7 @@ func TestGenerateConfig_MultipleHosts(t *testing.T) { }, } - config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "") + config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "") require.NoError(t, err) require.Len(t, config.Apps.HTTP.Servers["cpm_server"].Routes, 2) } @@ -88,7 +88,7 @@ func TestGenerateConfig_WebSocketEnabled(t *testing.T) { }, } - config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "") + config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "") require.NoError(t, err) route := config.Apps.HTTP.Servers["cpm_server"].Routes[0] @@ -109,7 +109,7 @@ func TestGenerateConfig_EmptyDomain(t *testing.T) { }, } - config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "") + config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "") require.NoError(t, err) // Should produce empty routes (or just catch-all if frontendDir was set, but it's empty here) require.Empty(t, config.Apps.HTTP.Servers["cpm_server"].Routes) @@ -117,7 +117,7 @@ func TestGenerateConfig_EmptyDomain(t *testing.T) { func TestGenerateConfig_Logging(t *testing.T) { hosts := []models.ProxyHost{} - config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "") + config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "") require.NoError(t, err) // Verify logging configuration @@ -155,7 +155,7 @@ func TestGenerateConfig_Advanced(t *testing.T) { }, } - config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "") + config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "") require.NoError(t, err) require.NotNil(t, config) diff --git a/backend/internal/caddy/manager.go b/backend/internal/caddy/manager.go index 70096d7f..7b99fbc8 100644 --- a/backend/internal/caddy/manager.go +++ b/backend/internal/caddy/manager.go @@ -48,8 +48,15 @@ func (m *Manager) ApplyConfig(ctx context.Context) error { acmeEmail = acmeEmailSetting.Value } + // Fetch SSL Provider setting + var sslProviderSetting models.Setting + var sslProvider string + if err := m.db.Where("key = ?", "caddy.ssl_provider").First(&sslProviderSetting).Error; err == nil { + sslProvider = sslProviderSetting.Value + } + // Generate Caddy config - config, err := GenerateConfig(hosts, filepath.Join(m.configDir, "data"), acmeEmail, m.frontendDir) + config, err := GenerateConfig(hosts, filepath.Join(m.configDir, "data"), acmeEmail, m.frontendDir, sslProvider) if err != nil { return fmt.Errorf("generate config: %w", err) } diff --git a/backend/internal/caddy/validator_test.go b/backend/internal/caddy/validator_test.go index 72a928c9..c5eb9648 100644 --- a/backend/internal/caddy/validator_test.go +++ b/backend/internal/caddy/validator_test.go @@ -25,7 +25,7 @@ func TestValidate_ValidConfig(t *testing.T) { }, } - config, _ := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "") + config, _ := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "") err := Validate(config) require.NoError(t, err) } diff --git a/frontend/src/pages/SystemSettings.tsx b/frontend/src/pages/SystemSettings.tsx index ed977b02..2b77ac61 100644 --- a/frontend/src/pages/SystemSettings.tsx +++ b/frontend/src/pages/SystemSettings.tsx @@ -26,6 +26,7 @@ interface UpdateInfo { export default function SystemSettings() { const queryClient = useQueryClient() const [caddyAdminAPI, setCaddyAdminAPI] = useState('http://localhost:2019') + const [sslProvider, setSslProvider] = useState('letsencrypt') // Fetch Settings const { data: settings } = useQuery({ @@ -37,6 +38,7 @@ export default function SystemSettings() { useEffect(() => { if (settings) { if (settings['caddy.admin_api']) setCaddyAdminAPI(settings['caddy.admin_api']) + if (settings['caddy.ssl_provider']) setSslProvider(settings['caddy.ssl_provider']) } }, [settings]) @@ -66,6 +68,7 @@ export default function SystemSettings() { const saveSettingsMutation = useMutation({ mutationFn: async () => { await updateSetting('caddy.admin_api', caddyAdminAPI, 'caddy', 'string') + await updateSetting('caddy.ssl_provider', sslProvider, 'caddy', 'string') }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['settings'] }) @@ -97,6 +100,24 @@ export default function SystemSettings() {

URL to the Caddy admin API (usually on port 2019)

+ +
+ + +

+ Choose the default Certificate Authority for SSL certificates. +

+
+