feat: change email, export my data, delete account (with data anonymization)
This commit is contained in:
1 parent
bb191c389a
commit
10a66cd979
123 files changed
+10789
-76
No files matched your search
@@ -0,0 +1,90 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"RenewCMS/api/pkg/apperr"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type successSlots struct {
|
||||
mu sync.Mutex
|
||||
window time.Duration
|
||||
now func() time.Time
|
||||
taken map[string]time.Time
|
||||
}
|
||||
|
||||
func NewUserSuccessLimiter(window time.Duration) func(http.Handler) http.Handler {
|
||||
return NewUserSuccessLimiterAt(window, time.Now)
|
||||
}
|
||||
|
||||
func NewUserSuccessLimiterAt(window time.Duration, now func() time.Time) func(http.Handler) http.Handler {
|
||||
slots := &successSlots{window: window, now: now, taken: make(map[string]time.Time)}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
key := userRateLimitKey(r)
|
||||
claimed, retryAfter, granted := slots.claim(key)
|
||||
if !granted {
|
||||
w.Header().Set("Retry-After", strconv.Itoa(retryAfter))
|
||||
http.Error(w, apperr.TooManyRequestsErrorMessage, http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
|
||||
recorder := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
|
||||
succeeded := false
|
||||
defer func() {
|
||||
if !succeeded {
|
||||
slots.release(key, claimed)
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(recorder, r)
|
||||
succeeded = recorder.status < http.StatusBadRequest
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *successSlots) claim(key string) (time.Time, int, bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
now := s.now()
|
||||
s.sweepExpired(now)
|
||||
if claimedAt, held := s.taken[key]; held {
|
||||
return time.Time{}, retryAfterSeconds(s.window - now.Sub(claimedAt)), false
|
||||
}
|
||||
s.taken[key] = now
|
||||
return now, 0, true
|
||||
}
|
||||
|
||||
func (s *successSlots) release(key string, claimedAt time.Time) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if current, held := s.taken[key]; held && current.Equal(claimedAt) {
|
||||
delete(s.taken, key)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *successSlots) sweepExpired(now time.Time) {
|
||||
for key, claimedAt := range s.taken {
|
||||
if now.Sub(claimedAt) >= s.window {
|
||||
delete(s.taken, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type statusRecorder struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
}
|
||||
|
||||
func (s *statusRecorder) WriteHeader(status int) {
|
||||
s.status = status
|
||||
s.ResponseWriter.WriteHeader(status)
|
||||
}
|
||||
|
||||
func (s *statusRecorder) Unwrap() http.ResponseWriter {
|
||||
return s.ResponseWriter
|
||||
}
|
||||
Reference in new issue
Block a user