package middleware import ( "bookhoard/internal/database" "regexp" "sync" "github.com/go-playground/validator/v10" ) // specialCharRegex matches the historical "special character" set used by the // password complexity rules. const specialCharRegex = `[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]` // PasswordValidator validates password complexity against the configured rules. // When a database.SettingsRegistry is wired via SetSettings, rules are read live and the // regex set is recompiled under a mutex on each validation. Without a registry // the historical hardcoded defaults (8+ chars, upper/lower/number/special) apply. type PasswordValidator struct { settings *database.SettingsRegistry } // SetSettings wires the tunable settings registry. func (v *PasswordValidator) SetSettings(s *database.SettingsRegistry) { v.settings = s } // compileSpecialRegex isolates the regexp compile (which is safe to call // concurrently, but we keep it behind a cached var for the no-registry path). var ( specialOnce sync.Once specialRe *regexp.Regexp ) func specialRegex() *regexp.Regexp { specialOnce.Do(func() { specialRe = regexp.MustCompile(specialCharRegex) }) return specialRe } func (v *PasswordValidator) rules() database.PasswordRules { if v.settings != nil { return v.settings.PasswordRules() } return database.PasswordRules{MinLength: 8, Upper: true, Lower: true, Number: true, Special: true} } // CheckPassword applies the active rules to a single password. func (v *PasswordValidator) CheckPassword(password string) bool { r := v.rules() if len(password) < r.MinLength { return false } if r.Upper && !regexp.MustCompile(`[A-Z]`).MatchString(password) { return false } if r.Lower && !regexp.MustCompile(`[a-z]`).MatchString(password) { return false } if r.Number && !regexp.MustCompile(`[0-9]`).MatchString(password) { return false } if r.Special && !specialRegex().MatchString(password) { return false } return true } // defaultPasswordValidator backs the passwordcomplex struct tag registered by // RegisterPasswordValidation. Callers that want live rule updates should // construct their own PasswordValidator and call SetSettings. var defaultPasswordValidator = &PasswordValidator{} // RegisterPasswordValidation registers the password validator with the // validator instance. The registered func re-evaluates rules on every call, so // changes to the wired registry take effect immediately. func RegisterPasswordValidation(v *validator.Validate) error { return v.RegisterValidation("passwordcomplex", func(fl validator.FieldLevel) bool { return defaultPasswordValidator.CheckPassword(fl.Field().String()) }) } // SetDefaultPasswordSettings wires the settings registry into the package-level // default validator so that the passwordcomplex struct-tag validator (used by // echo's CustomValidator) follows live configuration. Intended to be called // once at startup. func SetDefaultPasswordSettings(s *database.SettingsRegistry) { defaultPasswordValidator.SetSettings(s) }