package model import ( "crypto/sha256" "encoding/hex" "fmt" "sort" "strings" ) // HashForwards returns a stable, order-independent digest of a Forward set, // used to detect whether a user's already-delivered client config still // matches their current access. Sorted before hashing so that reordering // the underlying groups/hosts/services (without actually changing what's // granted) never produces a spurious "stale" result. func HashForwards(forwards []Forward) string { lines := make([]string, len(forwards)) for i, f := range forwards { lines[i] = fmt.Sprintf("%s|%s|%d|%s|%s", f.HostName, f.Address, f.Port, f.ServiceName, f.Description) } sort.Strings(lines) sum := sha256.Sum256([]byte(strings.Join(lines, "\n"))) return hex.EncodeToString(sum[:]) } // IsConfigStale reports whether u's live AllowedForwards no longer matches // the set they were handed in their last config (full export or // config-only update). Users who never had a key issued are not "stale" — // that's a distinct, more basic state (see State.NeverExportedUsernames). func (s *State) IsConfigStale(u *User) bool { if u.PublicKey == "" { return false } return u.ForwardsHash != HashForwards(s.AllowedForwards(u)) } // NeverExportedUsernames returns active users who have never had a key // issued at all (empty PublicKey) — created but forgotten before "Экспорт // архивов". func (s *State) NeverExportedUsernames() []string { var out []string for _, u := range s.Users { if u.Status == StatusActive && u.PublicKey == "" { out = append(out, u.Username) } } return out } // StaleConfigUsernames returns active, already-issued users whose local // config no longer matches their real access (see IsConfigStale) — they // need "Обновить конфиги", not a full re-export. func (s *State) StaleConfigUsernames() []string { var out []string for i := range s.Users { u := &s.Users[i] if u.Status == StatusActive && u.PublicKey != "" && s.IsConfigStale(u) { out = append(out, u.Username) } } return out }