feat: SSL wildcard certificate picker from NPM
- NPM client: ListCertificates endpoint - API: GET /api/settings/npm-certificates (wildcard-only filter) - Settings UI: EntityPicker for selecting wildcard certs - Deployer: applies certificate_id + ssl_forced to proxy hosts - Uses HTTPS subdomain URLs when SSL cert is configured
This commit is contained in:
@@ -131,6 +131,7 @@ func (s *Server) Router() chi.Router {
|
||||
r.Get("/images", s.listRegistryImages)
|
||||
})
|
||||
r.Get("/settings", s.getSettings)
|
||||
r.Get("/settings/npm-certificates", s.listNpmCertificates)
|
||||
|
||||
// Admin-only routes: require admin role.
|
||||
r.Group(func(r chi.Router) {
|
||||
|
||||
+74
-10
@@ -3,8 +3,10 @@ package api
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/alexei/docker-watcher/internal/crypto"
|
||||
"github.com/alexei/docker-watcher/internal/npm"
|
||||
"github.com/alexei/docker-watcher/internal/webhook"
|
||||
)
|
||||
|
||||
@@ -19,6 +21,7 @@ type settingsRequest struct {
|
||||
NpmEmail string `json:"npm_email"`
|
||||
NpmPassword string `json:"npm_password"`
|
||||
PollingInterval string `json:"polling_interval"`
|
||||
SSLCertificateID *int `json:"ssl_certificate_id,omitempty"`
|
||||
}
|
||||
|
||||
// getSettings handles GET /api/settings.
|
||||
@@ -31,16 +34,17 @@ func (s *Server) getSettings(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Return settings without sensitive fields.
|
||||
respondJSON(w, http.StatusOK, map[string]any{
|
||||
"domain": settings.Domain,
|
||||
"server_ip": settings.ServerIP,
|
||||
"network": settings.Network,
|
||||
"subdomain_pattern": settings.SubdomainPattern,
|
||||
"notification_url": settings.NotificationURL,
|
||||
"npm_url": settings.NpmURL,
|
||||
"npm_email": settings.NpmEmail,
|
||||
"has_npm_password": settings.NpmPassword != "",
|
||||
"polling_interval": settings.PollingInterval,
|
||||
"updated_at": settings.UpdatedAt,
|
||||
"domain": settings.Domain,
|
||||
"server_ip": settings.ServerIP,
|
||||
"network": settings.Network,
|
||||
"subdomain_pattern": settings.SubdomainPattern,
|
||||
"notification_url": settings.NotificationURL,
|
||||
"npm_url": settings.NpmURL,
|
||||
"npm_email": settings.NpmEmail,
|
||||
"has_npm_password": settings.NpmPassword != "",
|
||||
"polling_interval": settings.PollingInterval,
|
||||
"ssl_certificate_id": settings.SSLCertificateID,
|
||||
"updated_at": settings.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -89,6 +93,9 @@ func (s *Server) updateSettings(w http.ResponseWriter, r *http.Request) {
|
||||
if req.PollingInterval != "" {
|
||||
updated.PollingInterval = req.PollingInterval
|
||||
}
|
||||
if req.SSLCertificateID != nil {
|
||||
updated.SSLCertificateID = *req.SSLCertificateID
|
||||
}
|
||||
|
||||
if err := s.store.UpdateSettings(updated); err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "failed to update settings: "+err.Error())
|
||||
@@ -140,3 +147,60 @@ func (s *Server) regenerateWebhookSecret(w http.ResponseWriter, r *http.Request)
|
||||
})
|
||||
}
|
||||
|
||||
// listNpmCertificates handles GET /api/settings/npm-certificates.
|
||||
// It authenticates to NPM using the stored credentials and returns only wildcard certificates.
|
||||
func (s *Server) listNpmCertificates(w http.ResponseWriter, r *http.Request) {
|
||||
settings, err := s.store.GetSettings()
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "failed to get settings: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if settings.NpmURL == "" || settings.NpmEmail == "" || settings.NpmPassword == "" {
|
||||
respondError(w, http.StatusBadRequest, "NPM credentials not configured")
|
||||
return
|
||||
}
|
||||
|
||||
npmPassword, err := crypto.Decrypt(s.encKey, settings.NpmPassword)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "failed to decrypt npm password: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
client := npm.New(settings.NpmURL)
|
||||
if err := client.Authenticate(r.Context(), settings.NpmEmail, npmPassword); err != nil {
|
||||
respondError(w, http.StatusBadGateway, "failed to authenticate to NPM: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
certs, err := client.ListCertificates(r.Context())
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadGateway, "failed to list certificates: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Filter to wildcard certificates only.
|
||||
var wildcards []npm.Certificate
|
||||
for _, cert := range certs {
|
||||
if isWildcardCert(cert) {
|
||||
wildcards = append(wildcards, cert)
|
||||
}
|
||||
}
|
||||
|
||||
if wildcards == nil {
|
||||
wildcards = []npm.Certificate{}
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, wildcards)
|
||||
}
|
||||
|
||||
// isWildcardCert returns true if any of the certificate's domain names contains "*".
|
||||
func isWildcardCert(cert npm.Certificate) bool {
|
||||
for _, d := range cert.DomainNames {
|
||||
if strings.Contains(d, "*") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user