Skip to content

Commit

Permalink
feat: service methods for changing user name (see #739)
Browse files Browse the repository at this point in the history
  • Loading branch information
muety committed Feb 2, 2025
1 parent 7c3d4c5 commit 2fef990
Show file tree
Hide file tree
Showing 10 changed files with 1,490 additions and 1,355 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ pkged.go
package-lock.json
node_modules
.DS_Store
.venv
venv
2,746 changes: 1,404 additions & 1,342 deletions coverage/coverage.out

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"embed"
"flag"
"github.com/muety/wakapi/models"
"io/fs"
"log"
"log/slog"
Expand Down Expand Up @@ -175,14 +176,14 @@ func main() {
// Services
mailService = mail.NewMailService()
aliasService = services.NewAliasService(aliasRepository)
userService = services.NewUserService(mailService, userRepository)
keyValueService = services.NewKeyValueService(keyValueRepository)
userService = services.NewUserService(keyValueService, mailService, userRepository)
languageMappingService = services.NewLanguageMappingService(languageMappingRepository)
projectLabelService = services.NewProjectLabelService(projectLabelRepository)
heartbeatService = services.NewHeartbeatService(heartbeatRepository, languageMappingService)
durationService = services.NewDurationService(heartbeatService)
summaryService = services.NewSummaryService(summaryRepository, heartbeatService, durationService, aliasService, projectLabelService)
aggregationService = services.NewAggregationService(userService, summaryService, heartbeatService)
keyValueService = services.NewKeyValueService(keyValueRepository)
reportService = services.NewReportService(summaryService, userService, mailService)
activityService = services.NewActivityService(summaryService)
diagnosticsService = services.NewDiagnosticsService(diagnosticsRepository)
Expand Down
5 changes: 5 additions & 0 deletions mocks/key_value_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,8 @@ func (m *KeyValueServiceMock) DeleteString(s string) error {
args := m.Called(s)
return args.Error(0)
}

func (m *KeyValueServiceMock) ReplaceKeySuffix(s1, s2 string) error {
args := m.Called(s1, s2)
return args.Error(0)
}
5 changes: 5 additions & 0 deletions mocks/user_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ func (m *UserServiceMock) GenerateResetToken(user *models.User) (*models.User, e
return args.Get(0).(*models.User), args.Error(1)
}

func (m *UserServiceMock) ChangeUserId(user *models.User, s1 string) (*models.User, error) {
args := m.Called(user, s1)
return args.Get(0).(*models.User), args.Error(1)
}

func (m *UserServiceMock) FlushCache() {
m.Called()
}
Expand Down
25 changes: 25 additions & 0 deletions repositories/key_value.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package repositories

import (
"errors"
"fmt"

"github.com/muety/wakapi/models"
"github.com/muety/wakapi/utils"
Expand Down Expand Up @@ -77,3 +78,27 @@ func (r *KeyValueRepository) DeleteString(key string) error {

return nil
}

// 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" {
patternOld := fmt.Sprintf("(.+)%s$", suffixOld)
patternNew := fmt.Sprintf("$1%s", suffixNew) // mysql group replace style
if dialector == "postgres" {
patternNew = fmt.Sprintf("\\1%s", suffixNew) // postgres group replace style
}

return r.db.Model(&models.KeyStringValue{}).
Where(utils.QuoteSql(r.db, "%s like ?", "key"), "%"+suffixOld).
Update("key", gorm.Expr(
utils.QuoteSql(r.db, "regexp_replace(%s, ?, ?)", "key"),
patternOld,
patternNew,
)).Error
} else {
// a bit less safe, because not only replacing suffixes
return r.db.Model(&models.KeyStringValue{}).
Where("key like ?", "%"+suffixOld).
Update("key", gorm.Expr("replace(key, ?, ?)", suffixOld, suffixNew)).Error
}
}
1 change: 1 addition & 0 deletions repositories/repositories.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type IKeyValueRepository interface {
PutString(*models.KeyStringValue) error
DeleteString(string) error
Search(string) ([]*models.KeyStringValue, error)
ReplaceKeySuffix(string, string) error
}

type ILanguageMappingRepository interface {
Expand Down
4 changes: 4 additions & 0 deletions services/key_value.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,7 @@ func (srv *KeyValueService) PutString(kv *models.KeyStringValue) error {
func (srv *KeyValueService) DeleteString(key string) error {
return srv.repository.DeleteString(key)
}

func (srv *KeyValueService) ReplaceKeySuffix(suffixOld, suffixNew string) error {
return srv.repository.ReplaceKeySuffix(suffixOld, suffixNew)
}
2 changes: 2 additions & 0 deletions services/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type IKeyValueService interface {
GetByPrefix(string) ([]*models.KeyStringValue, error)
PutString(*models.KeyStringValue) error
DeleteString(string) error
ReplaceKeySuffix(string, string) error
}

type ILanguageMappingService interface {
Expand Down Expand Up @@ -145,6 +146,7 @@ type IUserService interface {
CreateOrGet(*models.Signup, bool) (*models.User, bool, error)
Update(*models.User) (*models.User, error)
Delete(*models.User) error
ChangeUserId(*models.User, string) (*models.User, error)
ResetApiKey(*models.User) (*models.User, error)
SetWakatimeApiCredentials(*models.User, string, string) (*models.User, error)
GenerateResetToken(*models.User) (*models.User, error)
Expand Down
50 changes: 39 additions & 11 deletions services/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,22 @@ import (
)

type UserService struct {
config *config.Config
cache *cache.Cache
eventBus *hub.Hub
mailService IMailService
repository repositories.IUserRepository
config *config.Config
cache *cache.Cache
eventBus *hub.Hub
keyValueService IKeyValueService
mailService IMailService
repository repositories.IUserRepository
}

func NewUserService(mailService IMailService, userRepo repositories.IUserRepository) *UserService {
func NewUserService(keyValueService IKeyValueService, mailService IMailService, userRepo repositories.IUserRepository) *UserService {
srv := &UserService{
config: config.Get(),
eventBus: config.EventBus(),
cache: cache.New(1*time.Hour, 2*time.Hour),
mailService: mailService,
repository: userRepo,
config: config.Get(),
eventBus: config.EventBus(),
cache: cache.New(1*time.Hour, 2*time.Hour),
keyValueService: keyValueService,
mailService: mailService,
repository: userRepo,
}

sub1 := srv.eventBus.Subscribe(0, config.EventWakatimeFailure)
Expand Down Expand Up @@ -197,6 +199,32 @@ func (srv *UserService) Update(user *models.User) (*models.User, error) {
return srv.repository.Update(user)
}

func (srv *UserService) ChangeUserId(user *models.User, newUserId string) (*models.User, error) {
// https://github.com/muety/wakapi/issues/739
oldUserId := user.ID
defer srv.FlushUserCache(oldUserId)

// TODO: make this transactional somehow
userNew, err := srv.repository.UpdateField(user, "id", newUserId)
if err != nil {
return nil, err
}

err = srv.keyValueService.ReplaceKeySuffix(fmt.Sprintf("_%s", oldUserId), fmt.Sprintf("_%s", newUserId))
if err != nil {
// try roll back "manually"
config.Log().Error("failed to update key string values during user id change, trying to roll back manually", "userID", oldUserId, "newUserID", newUserId)
if _, err := srv.repository.UpdateField(userNew, "id", oldUserId); err != nil {
config.Log().Error("manual user id rollback failed", "userID", oldUserId, "newUserID", newUserId)
}
return nil, err
}

config.Log().Info("user changed their user id", "userID", oldUserId, "newUserID", newUserId)

return userNew, err
}

func (srv *UserService) ResetApiKey(user *models.User) (*models.User, error) {
srv.FlushUserCache(user.ID)
user.ApiKey = uuid.Must(uuid.NewV4()).String()
Expand Down

0 comments on commit 2fef990

Please sign in to comment.