feat(storage): support deleting backends and improve admin UI
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m41s

- Implement storage backend deletion, which automatically resets default storage settings and user-specific overrides when a backend is removed.
- Add unit tests covering the delete action and its cleanup side effects.
- Improve admin UI responsiveness, fixing table scrolling, flex wrapping, and text truncation for long storage backend names.
- Update security documentation to clarify trusted proxy configurations and explain how trusted proxies are protected from automatic bans.
This commit is contained in:
2026-06-01 02:24:51 +03:00
parent 4eacb4cde2
commit 73bd14572d
27 changed files with 1124 additions and 128 deletions

View File

@@ -21,19 +21,19 @@ func ClientIPFromContext(r *http.Request) (string, bool) {
// ClientIP resolves the effective client IP. When trustedProxies is empty,
// forwarded headers are trusted for easy reverse-proxy/container defaults.
func ClientIP(remoteAddr, forwardedFor, realIP string, trustedProxies []string) string {
remoteIP := remoteIPOnly(remoteAddr)
remoteIP := IPOnly(remoteAddr)
if len(trustedProxies) == 0 || remoteTrusted(remoteIP, trustedProxies) {
if ip := firstForwardedIP(forwardedFor); ip != "" {
return ip
return IPOnly(ip)
}
if ip := strings.TrimSpace(realIP); ip != "" {
return ip
return IPOnly(ip)
}
}
return remoteIP
}
func remoteIPOnly(remoteAddr string) string {
func IPOnly(remoteAddr string) string {
host := strings.TrimSpace(remoteAddr)
if splitHost, _, err := net.SplitHostPort(remoteAddr); err == nil {
host = splitHost
@@ -41,14 +41,65 @@ func remoteIPOnly(remoteAddr string) string {
return strings.Trim(host, "[]")
}
func firstForwardedIP(forwardedFor string) string {
for _, part := range strings.Split(forwardedFor, ",") {
ip := strings.TrimSpace(part)
if ip != "" {
return strings.Trim(ip, "[]")
func IsProtectedProxyIP(ip string, trustedProxies []string) bool {
parsed := net.ParseIP(IPOnly(ip))
if parsed == nil {
return false
}
if parsed.IsLoopback() {
return true
}
return remoteTrusted(parsed.String(), trustedProxies)
}
func ProtectedBanTarget(target string, trustedProxies []string) bool {
normalized, err := NormalizeBanTarget(target)
if err != nil {
return false
}
if !strings.Contains(normalized, "/") {
return IsProtectedProxyIP(normalized, trustedProxies)
}
_, targetNet, err := net.ParseCIDR(normalized)
if err != nil {
return false
}
if targetNet.Contains(net.ParseIP("127.0.0.1")) || targetNet.Contains(net.ParseIP("::1")) {
return true
}
for _, trusted := range trustedProxies {
trusted = strings.TrimSpace(trusted)
if trusted == "" {
continue
}
if strings.Contains(trusted, "/") {
if _, trustedNet, err := net.ParseCIDR(trusted); err == nil && networksOverlap(targetNet, trustedNet) {
return true
}
continue
}
if ip := net.ParseIP(IPOnly(trusted)); ip != nil && targetNet.Contains(ip) {
return true
}
}
return ""
return false
}
func firstForwardedIP(forwardedFor string) string {
var fallback string
for _, part := range strings.Split(forwardedFor, ",") {
ip := IPOnly(part)
if net.ParseIP(ip) == nil {
continue
}
if fallback == "" {
fallback = ip
}
if isExternalIP(ip) {
return ip
}
}
return fallback
}
func remoteTrusted(remoteIP string, trustedProxies []string) bool {
@@ -73,3 +124,17 @@ func remoteTrusted(remoteIP string, trustedProxies []string) bool {
}
return false
}
func isExternalIP(ip string) bool {
parsed := net.ParseIP(IPOnly(ip))
return parsed != nil &&
!parsed.IsLoopback() &&
!parsed.IsPrivate() &&
!parsed.IsLinkLocalUnicast() &&
!parsed.IsLinkLocalMulticast() &&
!parsed.IsUnspecified()
}
func networksOverlap(a, b *net.IPNet) bool {
return a.Contains(b.IP) || b.Contains(a.IP)
}