fix: delete user cache keys upon user deletion (resolve #853)

This commit is contained in:
Ferdinand Mütsch
2025-10-14 08:18:02 +02:00
parent 6a0ffe54e9
commit b62629b515
12 changed files with 2220 additions and 2132 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@ package mocks
import (
"github.com/stretchr/testify/mock"
"gorm.io/gorm"
)
type BaseRepositoryMock struct {
@@ -23,5 +24,10 @@ func (m *BaseRepositoryMock) GetTableDDLSqlite(s string) (string, error) {
return args.Get(0).(string), args.Error(1)
}
func (m *BaseRepositoryMock) RunInTx(f func(db *gorm.DB) error) error {
args := m.Called(f)
return args.Error(0)
}
func (m *BaseRepositoryMock) VacuumOrOptimize() {
}

View File

@@ -3,6 +3,7 @@ package mocks
import (
"github.com/muety/wakapi/models"
"github.com/stretchr/testify/mock"
"gorm.io/gorm"
)
type KeyValueServiceMock struct {
@@ -34,6 +35,21 @@ func (m *KeyValueServiceMock) DeleteString(s string) error {
return args.Error(0)
}
func (m *KeyValueServiceMock) DeleteStringTx(s string, d *gorm.DB) error {
args := m.Called(s, d)
return args.Error(0)
}
func (m *KeyValueServiceMock) DeleteWildcard(s string) error {
args := m.Called(s)
return args.Error(0)
}
func (m *KeyValueServiceMock) DeleteWildcardTx(s string, d *gorm.DB) error {
args := m.Called(s, d)
return args.Error(0)
}
func (m *KeyValueServiceMock) ReplaceKeySuffix(s1, s2 string) error {
args := m.Called(s1, s2)
return args.Error(0)

View File

@@ -110,11 +110,6 @@ func (m *UserServiceMock) ResetApiKey(user *models.User) (*models.User, error) {
return args.Get(0).(*models.User), args.Error(1)
}
func (m *UserServiceMock) ToggleBadges(user *models.User) (*models.User, error) {
args := m.Called(user)
return args.Get(0).(*models.User), args.Error(1)
}
func (m *UserServiceMock) SetWakatimeApiCredentials(user *models.User, s1, s2 string) (*models.User, error) {
args := m.Called(user, s1, s2)
return args.Get(0).(*models.User), args.Error(1)

View File

@@ -49,6 +49,10 @@ func (r *BaseRepository) GetTableDDLSqlite(tableName string) (result string, err
return result, err
}
func (r *BaseRepository) RunInTx(f func(tx *gorm.DB) error) error {
return r.db.Transaction(f)
}
func (r *BaseRepository) VacuumOrOptimize() {
// sqlite and postgres require manual vacuuming regularly to reclaim free storage from deleted records
// see https://www.postgresql.org/docs/current/sql-vacuum.html and https://www.sqlite.org/lang_vacuum.html

View File

@@ -3,6 +3,7 @@ package repositories
import (
"errors"
"fmt"
"strings"
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/utils"
@@ -65,13 +66,15 @@ func (r *KeyValueRepository) PutString(kv *models.KeyStringValue) error {
}
func (r *KeyValueRepository) DeleteString(key string) error {
result := r.db.
Delete(&models.KeyStringValue{}, &models.KeyStringValue{Key: key})
return r.DeleteStringTx(key, r.db)
}
func (r *KeyValueRepository) DeleteStringTx(key string, tx *gorm.DB) error {
result := tx.Delete(&models.KeyStringValue{}, &models.KeyStringValue{Key: key})
if err := result.Error; err != nil {
return err
}
if result.RowsAffected != 1 {
return errors.New("nothing deleted")
}
@@ -79,6 +82,16 @@ func (r *KeyValueRepository) DeleteString(key string) error {
return nil
}
func (r *KeyValueRepository) DeleteWildcard(pattern string) error {
return r.DeleteWildcardTx(pattern, r.db)
}
func (r *KeyValueRepository) DeleteWildcardTx(pattern string, tx *gorm.DB) error {
return tx.
Where(utils.QuoteSql(r.db, "%s like ?", "key"), strings.ReplaceAll(pattern, "*", "%")).
Delete(&models.KeyStringValue{}).Error
}
// ReplaceKeySuffix will search for key-value pairs whose key ends with suffixOld and replace it with suffixNew instead.
func (r *KeyValueRepository) ReplaceKeySuffix(suffixOld, suffixNew string) error {
if dialector := r.db.Dialector.Name(); dialector == "mysql" || dialector == "postgres" {

View File

@@ -4,12 +4,14 @@ import (
"time"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
)
type IBaseRepository interface {
GetDialector() string
GetTableDDLMysql(string) (string, error)
GetTableDDLSqlite(string) (string, error)
RunInTx(func(*gorm.DB) error) error
VacuumOrOptimize()
}
@@ -75,6 +77,9 @@ type IKeyValueRepository interface {
GetString(string) (*models.KeyStringValue, error)
PutString(*models.KeyStringValue) error
DeleteString(string) error
DeleteStringTx(string, *gorm.DB) error
DeleteWildcard(string) error
DeleteWildcardTx(string, *gorm.DB) error
Search(string) ([]*models.KeyStringValue, error)
ReplaceKeySuffix(string, string) error
}
@@ -123,6 +128,7 @@ type IUserRepository interface {
Update(*models.User) (*models.User, error)
UpdateField(*models.User, string, interface{}) (*models.User, error)
Delete(*models.User) error
DeleteTx(*models.User, *gorm.DB) error
}
type ILeaderboardRepository interface {

View File

@@ -190,7 +190,11 @@ func (r *UserRepository) UpdateField(user *models.User, key string, value interf
}
func (r *UserRepository) Delete(user *models.User) error {
return r.db.Delete(user).Error
return r.DeleteTx(user, r.db)
}
func (r *UserRepository) DeleteTx(user *models.User, tx *gorm.DB) error {
return tx.Delete(user).Error
}
func (r *UserRepository) getByLoggedIn(t time.Time, after bool) ([]*models.User, error) {

View File

@@ -785,7 +785,7 @@ func (h *SettingsHandler) actionDeleteUser(w http.ResponseWriter, r *http.Reques
user := middlewares.GetPrincipal(r)
go func(user *models.User, r *http.Request) {
slog.Info("deleting user shortly", "userID", user.ID)
time.Sleep(5 * time.Minute)
//time.Sleep(5 * time.Minute)
if err := h.userSrvc.Delete(user); err != nil {
conf.Log().Request(r).Error("failed to delete user", "userID", user.ID, "error", err)
} else {

View File

@@ -4,6 +4,7 @@ import (
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/repositories"
"gorm.io/gorm"
)
type KeyValueService struct {
@@ -45,6 +46,18 @@ func (srv *KeyValueService) DeleteString(key string) error {
return srv.repository.DeleteString(key)
}
func (srv *KeyValueService) DeleteStringTx(key string, tx *gorm.DB) error {
return srv.repository.DeleteStringTx(key, tx)
}
func (srv *KeyValueService) DeleteWildcard(key string) error {
return srv.repository.DeleteWildcard(key)
}
func (srv *KeyValueService) DeleteWildcardTx(key string, tx *gorm.DB) error {
return srv.repository.DeleteWildcardTx(key, tx)
}
func (srv *KeyValueService) ReplaceKeySuffix(suffixOld, suffixNew string) error {
return srv.repository.ReplaceKeySuffix(suffixOld, suffixNew)
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/models/types"
"github.com/muety/wakapi/utils"
"gorm.io/gorm"
)
type IAggregationService interface {
@@ -68,6 +69,9 @@ type IKeyValueService interface {
GetByPrefix(string) ([]*models.KeyStringValue, error)
PutString(*models.KeyStringValue) error
DeleteString(string) error
DeleteStringTx(string, *gorm.DB) error
DeleteWildcard(string) error
DeleteWildcardTx(string, *gorm.DB) error
ReplaceKeySuffix(string, string) error
}

View File

@@ -17,6 +17,7 @@ import (
"github.com/muety/wakapi/repositories"
"github.com/muety/wakapi/utils"
"github.com/patrickmn/go-cache"
"gorm.io/gorm"
)
type UserService struct {
@@ -305,9 +306,18 @@ func (srv *UserService) Delete(user *models.User) error {
user.ReportsWeekly = false
srv.notifyUpdate(user)
srv.notifyDelete(user)
return srv.repository.Delete(user)
return srv.repository.RunInTx(func(tx *gorm.DB) error {
if err := srv.repository.DeleteTx(user, tx); err != nil {
return err
}
if err := srv.keyValueService.DeleteWildcardTx(fmt.Sprintf("*_%s", user.ID), tx); err != nil {
return err
}
srv.notifyDelete(user)
return nil
})
}
func (srv *UserService) MapUsersById(users []*models.User) map[string]*models.User {