Address production-readiness review: clearer errors, safer votes, and ops hardening.
Distinguish auth/lookup failures, make votes idempotent on visible questions, bound shutdown, page admin users, LRU throttle, trusted-proxy CIDRs, avatar cleanup, versioned migrations, and session cleanup logging.
This commit is contained in:
+37
-10
@@ -43,7 +43,7 @@ func (m *Memory) CreateUser(_ context.Context, u *User) error {
|
||||
}
|
||||
u.Username = NormalizeUsername(u.Username)
|
||||
if _, ok := m.byName[u.Username]; ok {
|
||||
return fmt.Errorf("username taken")
|
||||
return ErrDuplicateUsername
|
||||
}
|
||||
if u.ID == "" {
|
||||
u.ID = uuid.NewString()
|
||||
@@ -94,18 +94,44 @@ func (m *Memory) UserByUsername(_ context.Context, username string) (*User, erro
|
||||
return &cp, nil
|
||||
}
|
||||
|
||||
func (m *Memory) ListUsers(_ context.Context) ([]User, error) {
|
||||
func (m *Memory) ListUsers(_ context.Context, q ListUsersQuery) ([]User, string, string, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
limit := q.Limit
|
||||
if limit <= 0 {
|
||||
limit = AdminUsersLimit
|
||||
}
|
||||
search := strings.ToLower(strings.TrimSpace(q.Search))
|
||||
out := make([]User, 0, len(m.users))
|
||||
for _, u := range m.users {
|
||||
if search != "" &&
|
||||
!strings.Contains(strings.ToLower(u.Username), search) &&
|
||||
!strings.Contains(strings.ToLower(u.Name), search) {
|
||||
continue
|
||||
}
|
||||
if q.CursorCreated != "" {
|
||||
if u.CreatedAt > q.CursorCreated {
|
||||
continue
|
||||
}
|
||||
if u.CreatedAt == q.CursorCreated && u.ID >= q.CursorID {
|
||||
continue
|
||||
}
|
||||
}
|
||||
out = append(out, *u)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].CreatedAt < out[j].CreatedAt })
|
||||
if len(out) > AdminUsersLimit {
|
||||
out = out[:AdminUsersLimit]
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
if out[i].CreatedAt != out[j].CreatedAt {
|
||||
return out[i].CreatedAt > out[j].CreatedAt
|
||||
}
|
||||
return out[i].ID > out[j].ID
|
||||
})
|
||||
var nextCreated, nextID string
|
||||
if len(out) > limit {
|
||||
last := out[limit-1]
|
||||
nextCreated, nextID = last.CreatedAt, last.ID
|
||||
out = out[:limit]
|
||||
}
|
||||
return out, nil
|
||||
return out, nextCreated, nextID, nil
|
||||
}
|
||||
|
||||
func (m *Memory) CountAdmins(_ context.Context) (int, error) {
|
||||
@@ -331,16 +357,17 @@ func (m *Memory) UpsertAnswer(_ context.Context, a *Answer) error {
|
||||
func (m *Memory) Vote(_ context.Context, userID, questionID string, value int) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if value != 1 && value != -1 {
|
||||
if value != 1 && value != -1 && value != 0 {
|
||||
return fmt.Errorf("invalid vote")
|
||||
}
|
||||
if _, ok := m.questions[questionID]; !ok {
|
||||
return sql.ErrNoRows
|
||||
q, ok := m.questions[questionID]
|
||||
if !ok || q.Hidden {
|
||||
return ErrHiddenOrMissing
|
||||
}
|
||||
if m.votes[questionID] == nil {
|
||||
m.votes[questionID] = map[string]int{}
|
||||
}
|
||||
if cur, ok := m.votes[questionID][userID]; ok && cur == value {
|
||||
if value == 0 {
|
||||
delete(m.votes[questionID], userID)
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user