package store import "git.t-juice.club/torjus/apiary/models" var _ LoginAttemptStore = &CachingStore{} type CachingStore struct { backend LoginAttemptStore totalStatsCache []StatsResult usernameQueryCache map[string][]models.LoginAttempt passwordQueryCache map[string][]models.LoginAttempt ipQueryCache map[string][]models.LoginAttempt uniqueUsernames map[string]struct{} uniquePasswords map[string]struct{} uniqueIPs map[string]struct{} uniqueCountries map[string]struct{} totalLoginsCount int } func NewCachingStore(backend LoginAttemptStore) *CachingStore { cs := &CachingStore{ backend: backend, usernameQueryCache: make(map[string][]models.LoginAttempt), passwordQueryCache: make(map[string][]models.LoginAttempt), ipQueryCache: make(map[string][]models.LoginAttempt), uniqueUsernames: make(map[string]struct{}), uniquePasswords: make(map[string]struct{}), uniqueIPs: make(map[string]struct{}), uniqueCountries: make(map[string]struct{}), } all, err := backend.All() if err != nil { //TODO: Handle better maybe? panic(err) } var loginCount int for attempt := range all { cs.uniqueUsernames[attempt.Username] = struct{}{} cs.uniquePasswords[attempt.Password] = struct{}{} cs.uniqueIPs[attempt.RemoteIP.String()] = struct{}{} cs.uniqueCountries[attempt.Country] = struct{}{} loginCount++ } cs.totalLoginsCount = loginCount return cs } func (s *CachingStore) AddAttempt(l *models.LoginAttempt) error { s.totalStatsCache = nil delete(s.ipQueryCache, l.RemoteIP.String()) delete(s.passwordQueryCache, l.Password) delete(s.usernameQueryCache, l.Username) s.totalLoginsCount++ s.uniqueUsernames[l.Username] = struct{}{} s.uniquePasswords[l.Password] = struct{}{} s.uniqueIPs[l.RemoteIP.String()] = struct{}{} s.uniqueCountries[l.Country] = struct{}{} return s.backend.AddAttempt(l) } func (s *CachingStore) All() (<-chan models.LoginAttempt, error) { return s.backend.All() } func (s *CachingStore) Stats(statType LoginStats, limit int) ([]StatsResult, error) { // Only cache totals for now, as they are the most queried if statType == LoginStatsTotals { return []StatsResult{ {Name: "UniquePasswords", Count: len(s.uniquePasswords)}, {Name: "UniqueUsernames", Count: len(s.uniqueUsernames)}, {Name: "UniqueIPs", Count: len(s.uniqueIPs)}, {Name: "UniqueCountries", Count: len(s.uniqueCountries)}, {Name: "TotalLoginAttempts", Count: s.totalLoginsCount}, }, nil } return s.backend.Stats(statType, limit) } func (s *CachingStore) Query(query AttemptQuery) ([]models.LoginAttempt, error) { switch query.QueryType { case AttemptQueryTypeIP: if attempts, ok := s.ipQueryCache[query.Query]; ok { return attempts, nil } case AttemptQueryTypePassword: if attempts, ok := s.passwordQueryCache[query.Query]; ok { return attempts, nil } case AttemptQueryTypeUsername: if attempts, ok := s.usernameQueryCache[query.Query]; ok { return attempts, nil } } attempts, err := s.backend.Query(query) if err != nil { return attempts, err } switch query.QueryType { case AttemptQueryTypeIP: s.ipQueryCache[query.Query] = attempts case AttemptQueryTypeUsername: s.ipQueryCache[query.Query] = attempts case AttemptQueryTypePassword: s.ipQueryCache[query.Query] = attempts } return attempts, err } func (s *CachingStore) IsHealthy() error { return s.backend.IsHealthy() }