fix: ConfirmDialog accessibility and standardize destructive action confirmations

- Add focus trap, Escape key handling, ARIA attributes to ConfirmDialog
- Replace native confirm() with ConfirmDialog for stage, registry, user deletion
- Add confirmation dialogs for env variable and volume deletion
This commit is contained in:
2026-03-29 13:11:21 +03:00
parent 9f284932a1
commit f6f758c4e7
5 changed files with 173 additions and 8 deletions
+101 -1
View File
@@ -1,12 +1,15 @@
package api
import (
"context"
"fmt"
"log/slog"
"net/http"
"strings"
"github.com/alexei/docker-watcher/internal/crypto"
"github.com/alexei/docker-watcher/internal/npm"
"github.com/alexei/docker-watcher/internal/store"
"github.com/alexei/docker-watcher/internal/webhook"
)
@@ -93,14 +96,22 @@ func (s *Server) updateSettings(w http.ResponseWriter, r *http.Request) {
if req.PollingInterval != "" {
updated.PollingInterval = req.PollingInterval
}
if req.SSLCertificateID != nil {
sslChanged := false
if req.SSLCertificateID != nil && *req.SSLCertificateID != updated.SSLCertificateID {
updated.SSLCertificateID = *req.SSLCertificateID
sslChanged = true
}
if err := s.store.UpdateSettings(updated); err != nil {
respondError(w, http.StatusInternalServerError, "failed to update settings: "+err.Error())
return
}
// If SSL cert changed, update all existing NPM proxy hosts in the background.
if sslChanged {
go s.reapplySSLToAllProxies(updated)
}
respondJSON(w, http.StatusOK, map[string]string{"status": "updated"})
}
@@ -204,3 +215,92 @@ func isWildcardCert(cert npm.Certificate) bool {
return false
}
// reapplySSLToAllProxies updates all existing NPM proxy hosts managed by Docker Watcher
// to use the new SSL certificate. Runs in the background after settings change.
func (s *Server) reapplySSLToAllProxies(settings store.Settings) {
ctx := context.Background()
npmPassword, err := crypto.Decrypt(s.encKey, settings.NpmPassword)
if err != nil {
slog.Error("reapply SSL: decrypt npm password", "error", err)
return
}
npmClient := npm.New(settings.NpmURL)
if err := npmClient.Authenticate(ctx, settings.NpmEmail, npmPassword); err != nil {
slog.Error("reapply SSL: authenticate to NPM", "error", err)
return
}
// Get all proxy hosts from NPM.
hosts, err := npmClient.ListProxyHosts(ctx)
if err != nil {
slog.Error("reapply SSL: list proxy hosts", "error", err)
return
}
// Get all our managed instances to identify which proxy hosts are ours.
projects, err := s.store.GetAllProjects()
if err != nil {
slog.Error("reapply SSL: get projects", "error", err)
return
}
// Build a set of NPM proxy IDs that belong to our instances.
managedProxyIDs := make(map[int]bool)
for _, p := range projects {
stages, err := s.store.GetStagesByProjectID(p.ID)
if err != nil {
continue
}
for _, st := range stages {
instances, err := s.store.GetInstancesByStageID(st.ID)
if err != nil {
continue
}
for _, inst := range instances {
if inst.NpmProxyID > 0 {
managedProxyIDs[inst.NpmProxyID] = true
}
}
}
}
updated := 0
for _, host := range hosts {
if !managedProxyIDs[host.ID] {
continue
}
config := npm.ProxyHostConfig{
DomainNames: host.DomainNames,
ForwardScheme: host.ForwardScheme,
ForwardHost: host.ForwardHost,
ForwardPort: host.ForwardPort,
BlockExploits: true,
AllowWebsocket: true,
HTTP2Support: true,
Meta: npm.Meta{},
Locations: []any{},
}
if settings.SSLCertificateID > 0 {
config.CertificateID = settings.SSLCertificateID
config.SSLForced = true
config.HSTSEnabled = true
} else {
config.CertificateID = 0
config.SSLForced = false
config.HSTSEnabled = false
}
if _, err := npmClient.UpdateProxyHost(ctx, host.ID, config); err != nil {
slog.Warn("reapply SSL: update proxy host failed", "host_id", host.ID, "error", err)
continue
}
updated++
}
slog.Info("reapply SSL: completed", "updated", updated, "total_managed", len(managedProxyIDs))
}