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:
2026-08-22 12:16:59 -07:00
parent 5bdaa8977f
commit 29b0536215
26 changed files with 612 additions and 146 deletions
+37 -10
View File
@@ -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
}